Core Concepts

Core Concepts

Understanding these core concepts will help you make the most of DCE and avoid common pitfalls.

This page explains the foundational ideas behind command pattern architecture and deterministic execution.

What is a Command?

A Command is an object that encapsulates a single action or operation in your game.

Instead of directly modifying game state, you create a command object that knows how to:

Execute → Perform the action Undo → Reverse the action Redo → Perform the action again

Example: Moving a Character

❌ Without Commands (Direct Execution)

void MovePlayer(Vector3 newPosition)
{
    player.transform.position = newPosition;
    // No way to undo this!
}

✅ With Commands (Reversible Execution)

public class MoveCommand : ICommand
{
    private Transform _target;
    private Vector3 _oldPosition;
    private Vector3 _newPosition;
 
    public MoveCommand(Transform target, Vector3 newPosition)
    {
        _target = target;
        _oldPosition = target.position;
        _newPosition = newPosition;
    }
 
    public void Execute(Action executedCallback)
    {
        _target.position = _newPosition;
        executedCallback?.Invoke();
    }
 
    public void Undo()
    {
        _target.position = _oldPosition;
    }
 
    public void Redo()
    {
        _target.position = _newPosition;
    }
}

Key Benefits

Undo/Redo → Every action can be reversed Recording → All actions can be saved and replayed Scheduling → Commands can execute now or later Testing → Easy to test individual operations Networking → Commands can be sent over the network

What is a CommandHandler?

The CommandHandler is the system that manages command execution and history.

Think of it as a "traffic controller" for all your game actions.

Responsibilities

  • Execute commands
  • Track history
  • Undo/Redo
  • Limit history

Example Usage

private CommandHandler _handler;
 
void Awake()
{
    var tickSource = new SystemTickSource();
 
    _handler = new CommandHandler(
        tickSource: tickSource,
        recorder: null,
        maxCommands: 50,
        debug: true
    );
}
 
void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        _handler.Execute(new MoveCommand(player, targetPosition));
    }
 
    if (Input.GetKeyDown(KeyCode.Z))
    {
        _handler.Undo();
    }
 
    if (Input.GetKeyDown(KeyCode.Y))
    {
        _handler.Redo();
    }
}

What is Command History?

Command history is the ordered list of all executed commands.

[Command 1] → [Command 2] → [Command 3] → [Command 4] ↑ Current Position How Undo / Redo Works

Undo:

[Command 1] → [Command 2] → [Command 3] → [Command 4] ↑ Move backward

Redo:

[Command 1] → [Command 2] → [Command 3] → [Command 4] ↑ Move forward

History Limits

new CommandHandler(tickSource, null, maxCommands: 10, debug: false);

When the limit is reached, the oldest command is removed.

What is Determinism?

Determinism means:

The same inputs always produce the same outputs.

This is essential for:

  • Replay systems
  • Multiplayer synchronization
  • Bug reproduction
  • Automated testing

The Problem: Non-Deterministic Randomness

❌ Wrong (Breaks Replay)

public void Execute(Action executedCallback)
{
    int damage = UnityEngine.Random.Range(1, 10);
    player.TakeDamage(damage);
 
    executedCallback?.Invoke();
}

✅ Correct (Deterministic)

public void Execute(Action executedCallback)
{
    int damage = _engine.Random.Range(1, 10);
    player.TakeDamage(damage);
 
    executedCallback?.Invoke();
}

Seeded Randomness

// Game 1
var engine = new CommandEngine(ExecutionMode.TurnBased, seed: 12345);
 
var roll1 = engine.Random.Range(1, 6); // 4
var roll2 = engine.Random.Range(1, 6); // 2
var roll3 = engine.Random.Range(1, 6); // 5
 
// Game 2 (same seed)
var engine2 = new CommandEngine(ExecutionMode.TurnBased, seed: 12345);
 
var roll1b = engine2.Random.Range(1, 6); // 4
var roll2b = engine2.Random.Range(1, 6); // 2
var roll3b = engine2.Random.Range(1, 6); // 5

Same seed = same results.

Execution Modes

  1. Immediate (CommandHandler)
var handler = new CommandHandler(tickSource, null, -1, true);
handler.Execute(new MoveCommand(player, newPos));

✅ Undo/Redo ❌ No recording ❌ No replay

  1. Recorded (TurnBased)
var engine = new CommandEngine(ExecutionMode.TurnBased, seed: 12345);
engine.Execute(new PlayCardCommand(player, card));

✅ Undo/Redo ✅ Recording ✅ Replay

  1. Scheduled (TickBased)
var engine = new CommandEngine(ExecutionMode.TickBased, seed: 12345);
 
engine.Scheduler.Schedule(new SpawnEnemyCommand(), tick: 100);
 
void FixedUpdate()
{
    engine.Tick();
}

✅ Scheduled execution ✅ Replay ✅ Full control

Recording vs Replay

Recording

engine.Execute(new PlayCardCommand(player1, card1));
engine.Execute(new PlayCardCommand(player2, card2));
engine.Execute(new EndTurnCommand());

Replay

await engine.StartReplayAsync();

Plays all commands back Uses same seed Produces identical results

Common Pitfalls

❌ Using UnityEngine.Random

int damage = UnityEngine.Random.Range(1, 10);

✅ Fix

int damage = engine.Random.Range(1, 10);

❌ Storing Unity References

private GameObject _enemy;

✅ Fix

private int _enemyId;

❌ Executing Commands Inside Commands

_engine.Execute(new OtherCommand());

✅ Fix

Use composite commands instead.

❌ Forgetting Callback

public void Execute(Action executedCallback)
{
    player.Move(position);
}

✅ Fix

executedCallback?.Invoke();

Next Steps

Understanding these fundamentals will help you build robust, maintainable, and replay-ready systems with DCE 🚀