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
git repository
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();
When building a Local Service, default History Limit can be set in Project Setting.
In case of specified history limit:
minimun History Limit is 4
it cannot be changed after service built
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.
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.
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.
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();
}
}
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