The Problem
If you’re an editor developer like me you’ve likely already stumbled over Undo. It’s essential for basically every editor extension/tool. But implementing it can be a real pain, because of the nature of the Undo system - you have predefined common actions like creation and deletion of objects and a general purpose modification of objects. That’s it, and eg. recording list changes requires you to record the whole object containing the list, which is often total bullshit because there’s the greatly better, performance-wise faster and less complicated way: An action based Undo system.
There are solutions like VFW by vexe… But until now they were all seperate systems that required the developer to force their users into a new workflow, as the common shortcuts were used by the default Undo system.
for TL;DR:
The Solution
So I’ve tried hard and worked on an integration of the Command-Pattern (or Action-based) Undo system into the default Unity Undo system. It is in a mostly stable state, although new changes could at any point throw the tracking off - at which point the undo might trigger not at the original intended record, but a few off (but it will always trigger).
Features
- Extended Callbacks for Undo: Undo/Redo seperated, OnAddUndoRecord, …
- Add action-based undo records with both undo- and redoactions
- Includes a pretty solid and stable serializable action
→ Supports all serializable objects (of both UnityEngine.Object and System.Object) and unserializable objects partially (one layer serializable member serialization), all other objects get defaulted
→ Supports even most anonymous actions (no unserializable found yet)! You can fully use the context and reference nearly all local variables (conditions outlined above apply)!
Functionality behind
Provided about the default Undo system is only the current ID (not unique for a record, but a steadily increasing one), the current group name and with reflection the complete Undo/Redo stack only by name (not unique). As the behaviour of the default Undo system is nearly unpredictable (records may duplicate in certain conditions when undone/redone, or vanish), it is very hard, but a requirement to make a solid tracking algorithm. Additionally, the addition of new records has to be detected.
I’m making use of Update to check for new records and the UndoRedoPerformed callback to query the stacks for the undone/redone records. Then I’ll update the internal UndoPro records, represented by a dummy record in the default undo system, accordingly.
Usage
UndoPro gives two fundamental capabilities to enable an command-based undo pattern:
- It integrates custom undo records into the default undo system by creating dummies, and keeping track of them in order to trigger the provided undo/redo function whenever it’s associated dummy record is called.
This by itself could support a separate undo system through static function calls, but we want more than that - a command based system - so: - Instead of static functions, anonymous functions are also supported, so that each record can keep it’s own data encoded into the anonymous functions.
Why is this a big deal? Static functions only tell you ‘Undo that’, without context and associated data (e.g. undo node delete, but without knowing which node, what it was connected to, etc.).
Instead, you can choose data to be stored alongside the action.
Of course, conditions apply. That data has to be explicitly assigned to own local variables right before the anonymous action is defined (in the same command level, e.g. if-block, as well!) to ensure it is correctly serialized.
ConnectionPort port1 = this, port2 = port;
UndoPro.UndoProManager.RecordOperation(
() => port1.TryApplyConnection(port2, true),
() => port1.RemoveConnection(port2, true),
"Create Connection");
Would be valid (stored data: two SerializableObjects in this case),
ConnectionPort port1 = this, port2 = port;
{
UndoPro.UndoProManager.RecordOperation(
() => port1.TryApplyConnection(port2, true),
() => port1.RemoveConnection(port2, true),
"Create Connection");
}
would be invalid. It would result the variables to be stored elsewhere, which can give complications.
As for the TYPE of the variables, these are supported:
UnityEngine.Object, Serializable System.object, ICollections of aforementioned things, unserializable types with serializable members
And these are NOT supported:
Nested Lists, Non-serializable structures (e.g. Vector2)
Null-values are supported, if a type is NOT supported, it will be created to its default state.
Why is serialization important?
These actions have to survive script compilations and playmode change (not scene change), so they need to be serialized.
Note that your references WILL be broken, so if you care about references, use ScriptableObjects, which will retain their reference (Provided that they still exist after the reload, as for the NEF I had to change a bit to comply to this restriction).
Usage Example
A comprehensive example provides the implementation of Undo in the Node Editor Framework
Current Problems
As of now, there are no constellations that break the tracking system of UndoPro
Any feedback welcome!