Command Pattern for implementing undo/redo systems in Unreal Engine.
The basic idea behind using the command pattern for undo / redo is to wrap any function that needs to be undoable with a command. Inside that command we can define what happens when the command is executed / done and what happens when the command is unexecuted or undone. We can then store a list of these commands in some sort of history that allows us to step backwards and forwards through the commands when undoing and redoing.
For more information on the command pattern see: refactoring guru - command pattern
Note: This Unreal Engine implementation uses "Do" instead of "Execute". This change was made to avoid confusion when calling UINTERFACE functions in C++, which use a generated Execute_ function: ICommand::Execute_Do();
Hard-ish rules to be aware of. Be careful if your use case needs to break one.
-
Commands should be used for actions that change the state of the application.
-
Every ICommand should implement
ICommand.Do()
ANDICommand.Undo()
where:ICommand.Do()
sets some state to a new valueICommand.Undo()
restores that state to the original value
-
ICommand.Do()
should only ever be called in theICommandStack.Push()
function or theICommandStack.Redo()
functions. Similarly,ICommand.Undo()
should only ever be called in theICommandStack.Undo()
functions. -
ICommand.GetDisplayString()
is not required. But if used, should return a user freindly message. This message should reflect the new value, which is set by callingICommand.Do()
Here is an example of how to turn a function into a command:
Let's start with an example object ExampleState
with a function SetX()
that we want to track in the command history.
class ExampleState
{
private:
float X;
public:
float GetX() { return X; }
void SetX(float NewX) { X = NewX; }
}
In order to call SetX()
from a command we will need:
- a reference to the object it's being called on
- the float value we want to use
These will be assigned in the costructor of our command (exposed on spawn in blueprint)
class ExampleCommand : pubic ICommand
{
ExampleState* Target;
float NewX;
float OldX;
ExampleCommand(ExampleState* InTarget, float InNewX)
{
Target = InTarget;
NewX = InNewX;
OldX = Target->GetX(); // store the old value
}
void Do_Implementation() override { Target->SetX(NewX); }
void Undo_Implementation() override { Target->SetX(OldX); }
FString GetDisplayString_Implementation() overrid { return "updated X on target"; }
}
Notice that OldX
does not need to be passed in to the constructor. Since there is already have a reference to the state object, we can simply call GetX()
to cache the current state.