Quickstart - CommandEngine

Quick Start: CommandEngine

The CommandEngine is a complete system for deterministic turn/tick-based games with built-in replay, recording, scheduling, and event broadcasting.

When to Use CommandEngine

  • Turn-based games (card games, strategy games)
  • Tick-based simulations
  • Deterministic gameplay with seeded randomness
  • Full command recording and playback

For simple undo/redo without these features, see the CommandHandler Quick Start.

Core Components

The CommandEngine combines four powerful subsystems:

  • CommandHandler - Executes commands with undo/redo
  • CommandScheduler - Schedules commands for specific ticks
  • CommandRecorder - Records all commands for replay
  • SeededRandomSource - Deterministic random generation

Installation

Import the DCE asset from the Unity Asset Store into your project.

Step 1: Choose Execution Mode

    public enum ExecutionMode
    {
        TurnBased,  // Commands execute immediately, tick advances per command
        TickBased   // Commands scheduled by tick, manual Tick() calls required
    }

TurnBased: Ideal for card games, board games, RPG battles

TickBased: Ideal for real-time simulations, physics-based games

Step 2: Initialize CommandEngine

    using JFCreative.DCE.Core;
    using UnityEngine;
 
    public class GameManager : MonoBehaviour
    {
        private CommandEngine _engine;
 
        void Awake()
        {
            int seed = CommandEngine.GenerateRandomSeed();
            _engine = new CommandEngine(
                mode: CommandEngine.ExecutionMode.TurnBased,
                seed: seed,
                maxCommandHistory: -1,
                debug: true
            );
        }
 
        void FixedUpdate()
        {
            if (_engine.Mode == CommandEngine.ExecutionMode.TickBased)
            {
                _engine.Tick();
            }
        }
    }

Step 3: Create Commands

Commands are identical to CommandHandler examples:
 
    using JFCreative.DCE.Core;
    using System;
 
    public class PlayCardCommand : ICommand
    {
        private CardGamePlayer _player;
        private Card _card;
        private int _previousHealth;
 
        public PlayCardCommand(CardGamePlayer player, Card card)
        {
            _player = player;
            _card = card;
        }
 
        public void Execute(Action executedCallback)
        {
            _previousHealth = _player.Health;
            _card.ApplyEffect(_player);
            executedCallback?.Invoke();
        }
 
        public void Undo()
        {
            _player.Health = _previousHealth;
        }
 
        public void Redo() => Execute(null);
    }

Step 4: Execute Commands

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _engine.Execute(new PlayCardCommand(currentPlayer, selectedCard));
        }
    }

Important: The engine automatically records all executed commands for replay.

Deterministic Randomness

Always use the engine's Random property for deterministic behavior:

    var damage = _engine.Random.Range(1, 10);
    var chance = _engine.Random.Value();

DO NOT use UnityEngine.Random - it breaks replay determinism!

Replay System

Start Replay (Async)

    public async void StartReplay()
    {
        _engine.SetReplaySpeed(CommandEngine.ReplaySpeed.Fast);
        
        bool success = await _engine.StartReplayAsync();
        
        if (success)
        {
            Debug.Log("Replay completed!");
        }
    }

Replay Speeds

  • CommandEngine.ReplaySpeed.Instant - No delay (0ms)
  • CommandEngine.ReplaySpeed.Fast - 4x speed (250ms)
  • CommandEngine.ReplaySpeed.Normal - 1x speed (1000ms)
  • CommandEngine.ReplaySpeed.Slow - 0.5x speed (2000ms)

Stop Replay

_engine.StopReplay();

Event System

Publish and subscribe to game events without tight coupling:

    public struct TurnEndedEvent
    {
        public CardGamePlayer CurrentPlayer;
        public CardGamePlayer NextPlayer;
    }
 
    void Awake()
    {
        _engine.Events.Subscribe<TurnEndedEvent>(OnTurnEnded);
    }
 
    void OnTurnEnded(TurnEndedEvent evt)
    {
        Debug.Log($"{evt.NextPlayer.Name}'s turn!");
    }
 
    public void Execute(Action executedCallback)
    {
        _engine.Events.Publish(new TurnEndedEvent 
        { 
            CurrentPlayer = _current,
            NextPlayer = _next
        });
        executedCallback?.Invoke();
    }
 
    void OnDestroy()
    {
        _engine.Events.Unsubscribe<TurnEndedEvent>(OnTurnEnded);
    }

Undo/Redo During Gameplay

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Z))
        {
            _engine.Undo();
        }
 
        if (Input.GetKeyDown(KeyCode.Y))
        {
            _engine.Redo();
        }
    }

Note: Undo/redo is disabled during replay mode.

Reset and Clear

    _engine.Reset(CommandEngine.GenerateRandomSeed());
 
    _engine.Reset();
 
    _engine.ClearRecording();

Sample: Card Game

Check out the included Card Game Sample for a complete example:

    void Awake()
    {
        var seed = CommandEngine.GenerateRandomSeed();
        _commandEngine = new CommandEngine(
            CommandEngine.ExecutionMode.TurnBased,
            seed,
            -1,
            false
        );
    }
 
    public void PlayerPlaysCard(CardGamePlayer owner, Card card)
    {
        _commandEngine.Execute(new PlayCardCommand(owner, GetOpponent(owner), card, this));
    }
 
    public void PlayerEndsTurn(CardGamePlayer player)
    {
        _commandEngine.Execute(new EndTurnCommand(this, _commandEngine.Events, player));
    }

Best Practices

Do's

  • Always use _engine.Random for randomness
  • Call executedCallback?.Invoke() when execution completes
  • Use events for decoupled system communication
  • Test replays frequently during development

Don'ts

  • Don't run other commands from inside a command's Execute() method (breaks replay)
  • Don't use UnityEngine.Random (breaks determinism)
  • Don't store references to Unity objects that might be destroyed (store id's or indexes)

Composite Commands Pattern

For multi-step operations, create composite commands:

    var setPositionCommand = new SetPropertyCommand<PuzzleBox, Vector3>(
        this,
        x => x.transform.position,
        transform.position + direction
    );
 
    var setRotationCommand = new SetPropertyCommand<PuzzleBox, Quaternion>(
        this,
        x => x.transform.rotation,
        Quaternion.LookRotation(direction)
    );
 
    var cmd = new CompositeCommand(setPositionCommand, setRotationCommand);
 
    engine.Execute(cmd);

Next Steps

  • Explore the Card Game Sample for a complete implementation and TickBased scheduling
  • Check out the Puzzle Game Sample for TurnBased scheduling
  • Implement your own commands for game-specific logic
  • Set up replay UI and controls
  • Test determinism by comparing multiple replays

Need simpler undo/redo? Check out the CommandHandler Quick Start for lightweight command execution without replay features.