Memento Service

A robust state management system for Unity that provides undo/redo functionality with flexible service levels

In One Line

State historians unite: Yesterday's code is just an undo away!

Overview

Memento Service implements the Memento pattern to manage state history, offering both local and background service modes. It's perfect for implementing undo/redo systems in editors, game states, or any scenario requiring state history management.

The Memento Service seamlessly integrates with both Unity Components and regular C# classes.


Package Info

display name

AceLand Menento Service

package name

com.aceland.mementoservice

latest version

1.0.11

namespace

AceLand.MementoService
AceLand.MementoService.Global

dependencies

com.aceland.library: 1.0.14

Key Features

  • Two service modes: Global & Local

  • Generic type support for any state type

  • Configurable history limit

  • Automatic resource cleanup on Global Service


Project Settings

Settings

---

Memento Service Mode

Select service supporting follow modes:

  • Local Only

  • Global and Local

default: Global and Local

Undo Limit

How many Undo record can be storaged in Memento per service.

mininum: 4 default: 32

Log Level

Level of Logging

default: BuildLevel.DevelopmentBuild


Global Service Mode

By Calling Memento API to use the global service.

Global Service save only data inherited GlobalMementoState. See Advance Global Use for details.

  • Create a Global Memento State

    using AceLand.MementoService;
    using AceLand.MementoService.Global;
    
    // customize your state by inheritted GlobalMementoState
    public MyGlobalState : GlobalMementoState
    {
        // data of state
        public int data1;
        public int data2;
        
        // default Clone is MemberwiseClone (ShallowCopy)
        //    that is supporting copying Unity Object Reference
        // do NOT override unless necessary
        public override IMementoState Clone()
        {
            return base.Clone();
        }
        
        public override void OnBeforeStateSave()
        {
            // called before saving the state
            // update the state here
        }
    
        public override void OnStateBeforeUndo()
        {
            // called before state undo
            // Do stuff with current state
        }
    
        public override void OnStateAfterUndo()
        {
            // called after state undo
            // Do stuff with undo state
        }
        
        public override void OnStateBeforeRedo()
        {
            // called before state redo
            // Do stuff with current state
        }
        
        public override void OnStateAfterRedo()
        {
            // called after state redo
            // Do stuff with redo state
        }
    }
  • Use the Global Memento Service

    using AceLand.MementoService;
    using AceLand.MementoService.Global;
    
    // your state
    private MyGlobalState _myState = new();
    
    // Save this state with Extension (recommended)
    _myState.SaveGlobalState();
    // or by Memento API
    Memento.SaveGlobalState(_myState);
    
    // how many undo or redo in this state
    Memento.GlobalUndoCount;
    Memento.GlobalRedoCount;
    
    // Undo the state
    Memento.UndoGlobalState();
    
    // Redo the state
    Memento.RedoGlobalState();
    
    // Clear all history of this type
    Memento.ClearGlobalHistory();

Local Service Mode

By building Memento Service to use the local service.

This mode is object-specific recommended for save memento state by object.

  • Create a Local Memento State as ref type

    using AceLand.MementoService;
    
    // Create your Memento state as class type
    public class MyMementoState : MementoState
    {
        // data of current state
        public int data1;
        public int data2;
        
        // default Clone is MemberwiseClone (ShallowCopy)
        //    that is supporting copying Unity Object Reference
        // do NOT override unless necessary
        public override IMementoState Clone()
        {
            return base.Clone();
        }
    }
  • Create a Local Memento State as data struct type

    using AceLand.MementoService;
    
    // Create your Memento state as struct data type
    public struct MyMementoState : IMementoState
    {
        // data of current state
        public int data1;
        public int data2;
        
        // required by IMementoState interface
        public IMementoState Clone() => this;
        // if there is other ref type in the state
        // it's highly recommended to use ref type on this case
        // public IMementoState Clone() => (IMementoState)MemberwiseClone();
    }
  • Create your Local Memento Service

    using AceLand.MementoService;
    
    // your state
    private MyMementoState _myState = new();
    
    // build Local Memento Service
    private MementoService<MyMementoState> _service;
    _service = Memento.BuildLocalService<MyMementoState>();
    // or build with specified history limit
    _service = Memento.BuildLocalService<MyMementoState>(historyLimit: 16);
  • Use the Service

    using AceLand.MementoService;
    
    // how many undo or redo in this state
    _service.UndoCount;
    _service.RedoCount;
    
    // Save this state
    _service.SaveState(_myState);
    
    // Undo the state (recommended)
    if (_service.UndoCount > 0)
        _myState = _service.Undo();
    
    // Redo the state (recommended)
    if (_service.RedoCount > 0)
        _myState = _service.Redo();
    
    // Clear all history of this type
    _service.ClearHistory();
    
    // Dispose the service
    _service.Dispose();

State History Behavior

Undo/Redo Stack Management

  • Undo/Redo Operations

    • Neither undo nor redo operations destroy state history

    • States remain in memory for potential future use

    • Switching between states preserves the full history

  • Adding New States

    • Saving a new state clears all redo history

Memento States: [S1] <- [S2] | [S3] | [S4] -> [S5]
Undo stack: [S1] <- [S2]
Current: [S3]
Redo stack: [S4] -> [S5]
TotalRecord: 5
UndoCount: 2
RedoCount: 2

After saving new state S6:
Memento States: [S1] <- [S2] <- [S3] | [S6] |
Undo stack: [S1] <- [S2] <- [S3]
Current: [S6]
Redo stack: (S4 and S5 are removed)
TotalRecord: 4
UndoCount: 3
RedoCount: 0

Best Practices

  • Memory Management

    // Always dispose local services when done
    var mementoService = Memento.BuildLocalService<MyState>();
    mementoService.Dispose();
  • State Design

    // Keep states lightweight
    public struct MyState : IMementoState
    {
        public int Value;
        public string Name;
        
        public MyState Clone() => this;
    }
  • Service Selection

    • Use Global Service for customized GlobalMementoState

    • Use Local Service for object-specific or component-specific states

    • Consider memory implications with large state histories


Usage in Unity

The workflow is the same. Here is an example to use Local Memento Service to record a Transform state.

  • Create a Local Memento State for Transform

    using AceLand.MementoService;
    using UnityEngine;
    
    // build a Memento State
    public class TransformState : MementoState
    {
        public TransformState(Transform transform) =>
            _transform = transform;
    
        private readonly Transform _transform;
        private Vector3 _position;
        private Quaternion _rotation;
        private Vector3 _localScale;
    
        public void UpdateState()
        {
            _position = _transform.position;
            _rotation = _transform.rotation;
            _localScale = _transform.localScale;
        }
    
        public void ApplyToTransform()
        {
            _transform.position = _position;
            _transform.rotation = _rotation;
            _transform.localScale = _localScale;
        }
    }
  • Create a MonoBehaviour component with Local Memento Service

    using AceLand.MementoService;
    using UnityEngine;
    
    // your component
    public class MyComponent : MonoBehaviour
    {
        [SerializeField, Range(4, 256)]
        private int historyLimit = 16;
    
        // a Local Memento Service of TransfromState
        private MementoService<TransformState> _service;
        private TransformState _state;
    
        private void Awake()
        {
            // build the service on awake (or start)
            _service = Memento.BuildLocalService<TransformState>(historyLimit);
            _state = new TransformState(transform);
            // init state
            _state.UpdateState();
            SaveState();
        }
    
        private void OnDestroy()
        {
            // dispose the service on destroy
            _service.Dispose();
        }
    
        // save current state
        public void SaveState()
        {
            _state.UpdateState();
            _service.SaveState(_state);
        }
    
        // undo a step of state
        public void UndoState()
        {
            if (_service.UndoCount == 0) return;
            _state = _service.Undo();
            _state.ApplyToTransform();
        }
    
        // redo a step of state
        public void RedoState()
        {
            if (_service.RedoCount == 0) return;
            _state = _service.Redo();
            _state.ApplyToTransform();
        }
        
        // clear all history
        public void ClearHistory() =>
            _service.ClearHistory();
    }

Advance Local Use

Example is using above TransformState.

DisposableObject is provided by AceLand Library.

Please read this page for details.

  • Create a class with Memento Service to make a state-controlable object.

    using System;
    using AceLand.Library.Disposable;
    using AceLand.MementoService;
    using UnityEngine;
    
    public class TransformStateMemento : DisposableObject
    {
        // build up stuff in constructor
        public TransformStateMemento(Transform transform, int historyLimit = 32)
        {
            _service = Memento.BuildLocalService<TransformState>(historyLimit );
            _state = new TransformState(transform);
            // init state
            _state.UpdateState();
            SaveState();
        }
    
        // dispose service on dispose
        protected override void DisposeManagedResources()
        {
            _service.Dispose();
        }
    
        private readonly MementoService<TransformState> _service;
        private TransformState _state;
    
        public void SaveState()
        {
            _state.UpdateState();
            _service.SaveState(_state);
        }
    
        public void Undo()
        {
            if (_service.UndoCount == 0) return;
            _state = _service.Undo();
            _state.ApplyToTransform();
        }
    
        public void Redo()
        {
            if (_service.RedoCount == 0) return;
            _state = _service.Redo();
            _state.ApplyToTransform();
        }
    
        public void Clear()
        {
            _service.ClearHistory();
        }
    }
  • Create a MonoBehaviour Component with the TransformStateMemento.

    public class YourComponent : MonoBehaviour
    {
        [SerializeField, Min(16)] private int history = 16;
    
        private TransformStateMemento _memento;
    
        // create the Memento and save init state.    
        private void Awake()
        {
            _memento = new TransformStateMemento(transform, history);
            _memento.SaveState();
        }
    
        // save state on every move
        private void Move()
        {
            Vector3 velocity = GetMoveVelocity();
            transform.Translate(velocity);
            _memento.SaveState();
        }
    
        // undo state
        public void Undo() => _memento.Undo();
    
        // redo state
        public void Redo() => _memento.Redo();
        
        // clear history
        public void ClearHistory() => _memento.Clear();
    
        private Vector3 GetMoveVelocity() => Vector3.up;
    }

Advance Global Use

Here is an example to make a Transfrom Memento State for global service.

  • Create a Global Memento State for Transform

    using AceLand.MementoService;
    using AceLand.MementoService.Global;
    using UnityEngine;
    
    namespace AceLand.Test
    {
        public class TransformMementoState : GlobalMementoState
        {
            public TransformMementoState(Transform transform) =>
                _transform = transform;
    
            // save the translation of current state
            private readonly Transform _transform;
            private Vector3 _translation;
    
            // apply translation
            public void Translate(Vector3 translation)
            {
                _transform.Translate(translation);
                _translation = translation;
            }
    
            public override void OnStateBeforeUndo() =>
                _transform.Translate(-_translation);
    
            public override void OnStateAfterRedo() =>
                _transform.Translate(_translation);
        }
    }
  • Create a Global Memento component to save the state.

    using AceLand.Library.Attribute;
    using AceLand.MementoService.Global;
    using UnityEngine;
    
    public class TransformMemento : MonoBehaviour
    {
        private TransformMementoState state;
    
        private void Awake()
        {
            state = new TransformMementoState(transform);
        }
        
        public void MoveAndSaveState(Vector3 translation)
        {
            state.Translate(translation);
            state.SaveGlobalState();
        }
    }
  • Create a Global Memento Events component to Undo, Redo and Clear History.

    using AceLand.Library.Attribute;
    using AceLand.MementoService;
    using UnityEngine;
    
    public class GlobalMementoEvents : MonoBehaviour
    {
        // connect with UI components
        public void OnUndoState() => Memento.UndoGlobalState();
        public void OnRedoState() => Memento.RedoGlobalState();
        public void OnClearHistory() => Memento.ClearGlobalHistory();
    }

With AceLand.Input events, set hotkeys to Undo and Redo in a second.

IButtonPressed is provided by AceLand Input.

Please read this page for details.

---

EventBus is provided by AceLand Event Drive.

please read this page for details.

using AceLand.EventDriven.Bus;
using AceLand.Input.Events;
using AceLand.Input.State;
using AceLand.MementoService;
using UnityEngine;

public class GlobalMementoEvents : MonoBehaviour
{
    private void OnEnable()
    {
        EventBus.Event<IButtonPressed>()
            .WithListener<BtnStatus>(OnButtonPressed)
            .Listen();
    }

    private void OnDisable()
    {
        EventBus.Event<IButtonPressed>()
            .Unlisten<BtnStatus>(OnButtonPressed);
    }

    private void OnButtonPressed(object sender, BtnStatus btnStatus)
    {
        switch (btnStatus.Name)
        {
            case "Undo":    // Ctrl+Z
                Memento.UndoGlobalState();
                break;
            case "Redo":    // Ctrl+Shift+Z or Ctrl+Y
                Memento.RedoGlobalState();
                break;
        }
    }
}

Simple Global Test

Test with above Transform Memento State with Inspector Buttons.

Inspector Button is provided by AceLand Library.

Please read this page for details.

using AceLand.Library.Attribute;
using AceLand.MementoService;
using UnityEngine;

// Global Service Wrapper
public class GlobalMementoService : MonoBehaviour
{
    [InspectorButton(Mode = InspectorButtonMode.EnabledInPlayMode)]
    private void UndoState() => Memento.UndoGlobalState();
    
    [InspectorButton(Mode = InspectorButtonMode.EnabledInPlayMode)]
    private void RedoState() => Memento.RedoGlobalState();
    
    [InspectorButton(Mode = InspectorButtonMode.EnabledInPlayMode)]
    private void ClearHistory() => Memento.ClearGlobalHistory();
}

// Memento State Tool
public class GlobalMementoTest : MonoBehaviour
{
    [SerializeField] private int multiplier = 1;
    
    private TransformMementoState state;

    private void Awake()
    {
        state = new TransformMementoState(transform);
    }

    [InspectorButton(Mode = InspectorButtonMode.EnabledInPlayMode)]
    private void MoveAndSaveState()
    {
        state.Translate(transform.up * multiplier);
        state.SaveGlobalState();
    }
}
test result

Technical Notes

  • History limit is enforced to prevent memory issues

  • Thread-safe operations for background service

  • Automatic cleanup of disposed states

  • Generic type support enables type-safe operations

  • Do not Save GameObject and Component to service


Last updated