Command Pattern

Summary

The Command pattern encapsulates a request or action as a self-contained object. Instead of calling a method directly, you create a command object that knows how to perform (and optionally reverse) the action. Commands can be stored, queued, logged, or undone — capabilities that are impossible when actions are direct method calls.

In Unity this pattern suits turn-based movement, level editors, input replay, and any system that needs undo/redo functionality (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).

Implementation

ICommand interface

All commands implement the same interface. Execute performs the action; Undo reverses it.

public interface ICommand
{
    void Execute();
    void Undo();
}

Concrete command

Each concrete command holds a reference to the receiver (the object being acted upon) and any data needed to perform or reverse the action.

public class MoveCommand : ICommand
{
    private readonly PlayerMover _player;
    private readonly Vector3 _direction;
 
    public MoveCommand(PlayerMover player, Vector3 direction)
    {
        _player = player;
        _direction = direction;
    }
 
    public void Execute() => _player.Move(_direction);
    public void Undo()    => _player.Move(-_direction);
}

CommandInvoker

The invoker is the only place that calls Execute and Undo. It maintains an undo stack so actions can be reversed in order (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).

public class CommandInvoker
{
    private static readonly Stack<ICommand> _undoStack = new Stack<ICommand>();
 
    public static void ExecuteCommand(ICommand command)
    {
        command.Execute();
        _undoStack.Push(command);
    }
 
    public static void UndoCommand()
    {
        if (_undoStack.Count > 0)
        {
            ICommand command = _undoStack.Pop();
            command.Undo();
        }
    }
}

Wiring it together

The input handler creates commands and passes them to the invoker.

public class InputHandler : MonoBehaviour
{
    [SerializeField] private PlayerMover player;
 
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.W))
            CommandInvoker.ExecuteCommand(new MoveCommand(player, Vector3.forward));
 
        if (Input.GetKeyDown(KeyCode.Z))
            CommandInvoker.UndoCommand();
    }
}

Extending to a redo stack

Add a redo stack alongside the undo stack. When undoing, push the command onto the redo stack; when redoing, pop from redo and call Execute again.

private static readonly Stack<ICommand> _redoStack = new Stack<ICommand>();
 
public static void UndoCommand()
{
    if (_undoStack.Count > 0)
    {
        ICommand command = _undoStack.Pop();
        command.Undo();
        _redoStack.Push(command);
    }
}
 
public static void RedoCommand()
{
    if (_redoStack.Count > 0)
    {
        ICommand command = _redoStack.Pop();
        command.Execute();
        _undoStack.Push(command);
    }
}

Trade-offs

When to use:

  • Undo/redo is required (level editors, turn-based games, drawing tools).
  • Actions need to be queued, scheduled, or replayed.
  • You want to decouple input handling from game logic — the input handler creates commands; the receiver executes them.
  • Logging or auditing of actions is needed.

When not to:

  • Actions are simple and single-directional with no need for reversal — a direct method call is cleaner.
  • The undo data would be expensive to store (e.g. full scene snapshots). In those cases, consider storing diffs or snapshots more selectively.

Limitations: Every action requires a new class, which can add many small files for complex systems. A macro-command (a command that executes a list of sub-commands) can help group related actions.

Examples

  • Level editor: Every placement or deletion action is a PlaceCommand or RemoveCommand. Ctrl+Z walks the undo stack to restore the previous state.
  • Turn-based movement: Player and AI moves are queued as commands and replayed for an instant-replay feature at the end of a round.
  • Input recording: Commands are serialised to disk; on playback, they are deserialised and executed in sequence without any live input.