What Is It?
The Command pattern turns a request into a standalone object. Instead of calling unit.Move(Vector2Int.up) directly, you create a MoveCommand that remembers who to act on and what to do, then hand it to an invoker that decides when to run it.
Every command implements the same tiny interface — usually Execute() and Undo(). Because the invoker only sees ICommand, it can push commands onto a history stack, pop them to undo, and push them onto a redo stack without knowing anything about movement, building or inventory.
Think of a tactics game on a grid: each tile step the player takes becomes a MoveCommand. Undo walks the unit back exactly one tile, redo replays it, and the same history can drive a replay system or a network log of player inputs.
When Is It Used?
Use it when actions must be reversible: level editors, puzzle games, turn-based tactics, and anything with an undo button.
It is also a natural fit for input remapping, macro recording, deterministic replays and sending player intent over the network, since a command is just data plus a method.
Skip it for continuous, per-frame actions such as analog movement in an action game. Wrapping every Update tick in an object adds garbage and complexity with little payoff.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
namespace Patterns.Behavioral.Command
{
/// <summary>
/// Client: turns key presses into command objects and hands them
/// to the invoker. It never moves the unit itself.
/// </summary>
public class InputHandler : MonoBehaviour
{
[SerializeField] private Transform unit;
[SerializeField] private RectInt board = new(0, 0, 5, 5);
private readonly CommandInvoker invoker = new();
private void Update()
{
if (Input.GetKeyDown(KeyCode.W)) Move(Vector2Int.up);
if (Input.GetKeyDown(KeyCode.S)) Move(Vector2Int.down);
if (Input.GetKeyDown(KeyCode.A)) Move(Vector2Int.left);
if (Input.GetKeyDown(KeyCode.D)) Move(Vector2Int.right);
if (Input.GetKeyDown(KeyCode.Z)) invoker.Undo();
if (Input.GetKeyDown(KeyCode.Y)) invoker.Redo();
}
private void Move(Vector2Int direction)
{
invoker.Execute(new MoveCommand(unit, direction, board));
}
}
}namespace Patterns.Behavioral.Command
{
/// <summary>
/// A single, reversible action. The invoker only knows this interface,
/// never the concrete work being done.
/// </summary>
public interface ICommand
{
/// <summary>Performs the action. Returns false if it could not run.</summary>
bool Execute();
/// <summary>Reverts exactly what Execute did.</summary>
void Undo();
}
}using UnityEngine;
namespace Patterns.Behavioral.Command
{
/// <summary>
/// Moves a unit one tile on a grid. Remembers where the unit started
/// so Undo can put it back precisely.
/// </summary>
public class MoveCommand : ICommand
{
private readonly Transform unit;
private readonly Vector2Int direction;
private readonly RectInt bounds;
private Vector3 previousPosition;
public MoveCommand(Transform unit, Vector2Int direction, RectInt bounds)
{
this.unit = unit;
this.direction = direction;
this.bounds = bounds;
}
public bool Execute()
{
Vector3 target = unit.position + new Vector3(direction.x, direction.y, 0f);
var cell = Vector2Int.RoundToInt(target);
if (!bounds.Contains(cell))
return false; // walking off the board is not allowed
previousPosition = unit.position;
unit.position = target;
return true;
}
public void Undo() => unit.position = previousPosition;
public override string ToString() => $"Move {direction}";
}
}using System.Collections.Generic;
using UnityEngine;
namespace Patterns.Behavioral.Command
{
/// <summary>
/// Runs commands and keeps the history needed for undo and redo.
/// It has no idea what any command actually does.
/// </summary>
public class CommandInvoker
{
private readonly Stack<ICommand> undoStack = new();
private readonly Stack<ICommand> redoStack = new();
public int UndoCount => undoStack.Count;
public int RedoCount => redoStack.Count;
public void Execute(ICommand command)
{
if (!command.Execute())
{
Debug.Log($"{command} blocked");
return;
}
undoStack.Push(command);
redoStack.Clear(); // a new action invalidates the old future
}
public void Undo()
{
if (undoStack.Count == 0) return;
ICommand command = undoStack.Pop();
command.Undo();
redoStack.Push(command);
}
public void Redo()
{
if (redoStack.Count == 0) return;
ICommand command = redoStack.Pop();
command.Execute();
undoStack.Push(command);
}
}
}Advantages & Disadvantages
+ Advantages
- Undo and redo become a matter of two stacks instead of special-case code everywhere.
- Decouples the code that triggers an action (input, AI, UI) from the code that performs it.
- Commands are data: they can be queued, delayed, serialized, logged or replayed.
− Disadvantages
- Each action needs its own class, which can mean a lot of small files.
- Undo must restore state exactly; commands that touch physics, randomness or other systems are hard to reverse correctly.
- Long histories hold references to objects, so you may need to cap the stack or clear it when objects are destroyed.
Tips
- 01Clear the redo stack whenever a brand-new command is executed — otherwise redo can replay a branch of history that no longer makes sense.
- 02Store the result of an action in the command (e.g. the position before moving) so
Undodoes not have to recompute anything. - 03Validate before executing: if a move is blocked, do not push it onto the history at all.
- 04For replays, record the frame number alongside each command and re-execute them in order from a known starting state.
- 05Keep commands plain C# classes rather than MonoBehaviours so they are cheap to allocate and easy to test.