Command Pattern - Implementing a Replay System

Implementing a Replay System

The Command Pattern â¯ī¸

CSCI 3213 - Game Programming

Spring '26 - Week 5, Class 1

Encapsulating actions as objects - recording the past!

Today's Content

📚 Based on Chapter 7

Implementing a Replay System with the Command Pattern

From: Game Development Patterns with Unity 2021 (2nd Edition)
By David Baron

Available: Dulaney Browne Library or major book retailers. This chapter teaches how to record and replay player inputs!

Today's Learning Objectives

What We'll Master

  • đŸŽ¯ Understand the Command Pattern structure
  • đŸŽ¯ Encapsulate player actions as commands
  • đŸŽ¯ Implement a replay recording system
  • đŸŽ¯ Build undo/redo functionality
  • đŸŽ¯ Test replay with multiple scenes

Goal: Build a replay system that records bike movements and plays them back perfectly, frame by frame.

The Problem: Direct Input Handling

❌ Without Command Pattern

void Update() { // Direct input handling - not replayable! if (Input.GetKey(KeyCode.W)) { transform.Translate(Vector3.forward); } if (Input.GetKey(KeyCode.A)) { transform.Rotate(Vector3.up, -90); } if (Input.GetKey(KeyCode.D)) { transform.Rotate(Vector3.up, 90); } }

Problems: No recording capability, can't undo, can't replay, tightly coupled to Input system

What We Need

A way to capture player actions, store them, and replay them exactly as they happened!

What is the Command Pattern?

Command Pattern

A behavioral pattern that encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.

✅ Key Concept

Turn actions into objects! Instead of calling methods directly, create command objects that can be stored, queued, and executed later.

In Simple Terms: Commands are like recording button presses on a DVR - you can play them back, rewind, fast-forward, or delete them!

Command Pattern: Benefits & Drawbacks

✅ Benefits

  • Decoupling: Separates invoker from receiver
  • Undo/Redo: Store command history
  • Macro Commands: Combine multiple commands
  • Replay Systems: Record and playback
  • Queuing: Execute commands in sequence

âš ī¸ Drawbacks

  • Complexity: More classes to manage
  • Memory: Storing command history uses RAM
  • Overhead: Extra layer of abstraction
  • Synchronization: Replay timing can be tricky
Best For: Games with replay systems, undo functionality, input recording, or turn-based mechanics.

Command Pattern UML Diagram

┌──────────────────┐
│     Invoker      │  ← Triggers commands (InputHandler)
│──────────────────│
│ + Execute()      │
└────────â”Ŧ─────────┘
         │ uses
         â–ŧ
┌──────────────────┐         ┌─────────────────┐
│    ICommand      │◄────────│  BikeController │  ← Receiver
│──────────────────│         │─────────────────│
│ + Execute()      │         │ + TurnLeft()    │
└────────â”Ŧ─────────┘         │ + TurnRight()   │
         │ implements        └─────────────────┘
         │
    ┌────┴────────────────┐
    â–ŧ                     â–ŧ
┌──────────────┐  ┌──────────────┐
│ TurnLeft     │  │ TurnRight    │  ← Concrete Commands
│──────────────│  │──────────────│
│ + Execute()  │  │ + Execute()  │
└──────────────┘  └──────────────┘
                
Flow: InputHandler creates command → Command stores receiver reference → Execute() calls receiver's method → Command gets stored for replay

Replay System Architecture

RECORD
→
STORE
→
PLAYBACK

How It Works

  1. RECORD: Capture player input as command objects with timestamps
  2. STORE: Add commands to a List<Command> history
  3. PLAYBACK: Execute stored commands in sequence at original timestamps

Key Components

Invoker: Handles recording/playback state

Commands: Store action + timestamp

Receiver: BikeController executes actions

Implementation Step 1: Command Interface

namespace Chapter.Command { /// Base interface for all commands public interface ICommand { // Execute the command action void Execute(); } }

Simple But Powerful

Every command implements this interface. The Execute() method contains the actual action to perform. This uniformity allows us to store different command types in the same collection!

Design Note: We keep the interface simple. Commands will hold their own data (receiver reference, parameters, timestamp).

Implementation Step 2: TurnLeft Command

namespace Chapter.Command { public class TurnLeft : ICommand { // Reference to the receiver (bike controller) private BikeController _controller; // Constructor receives the bike controller public TurnLeft(BikeController controller) { _controller = controller; } // Execute the turn left action public void Execute() { _controller.Turn(BikeController.Direction.Left); } } }
Key Pattern: Command holds reference to receiver, Execute() delegates to receiver's method. This decouples the caller from the receiver!

Implementation Step 3: TurnRight Command

namespace Chapter.Command { public class TurnRight : ICommand { private BikeController _controller; public TurnRight(BikeController controller) { _controller = controller; } public void Execute() { _controller.Turn(BikeController.Direction.Right); } } }

Pattern Repetition

Notice the similarity? Each command follows the same structure: hold receiver, execute action. This consistency makes the pattern powerful!

Implementation Step 4: Invoker Recording

using UnityEngine; using System.Collections.Generic; namespace Chapter.Command { public class Invoker : MonoBehaviour { // Recording state private bool _isRecording; private bool _isReplaying; private float _replayTime; private int _replayIndex; // Command storage with timestamps private List<(ICommand command, float timestamp)> _recordedCommands; void Start() { _recordedCommands = new List<(ICommand, float)>(); } // Start recording commands public void StartRecording() { _recordedCommands.Clear(); _isRecording = true; Debug.Log("[Invoker] Recording started"); } // Record a command with current timestamp public void ExecuteCommand(ICommand command) { command.Execute(); if (_isRecording) { _recordedCommands.Add((command, Time.time)); } } } }

Implementation Step 5: Invoker Playback

// Continuing Invoker class... public void StopRecording() { _isRecording = false; Debug.Log($"[Invoker] Recording stopped. {_recordedCommands.Count} commands recorded"); } public void StartReplay() { if (_recordedCommands.Count == 0) { Debug.LogWarning("[Invoker] No commands to replay!"); return; } _isReplaying = true; _replayIndex = 0; _replayTime = Time.time; Debug.Log("[Invoker] Replay started"); } void Update() { if (_isReplaying && _replayIndex < _recordedCommands.Count) { var (command, timestamp) = _recordedCommands[_replayIndex]; float elapsedTime = Time.time - _replayTime; if (elapsedTime >= timestamp - _recordedCommands[0].timestamp) { command.Execute(); _replayIndex++; } if (_replayIndex >= _recordedCommands.Count) { _isReplaying = false; Debug.Log("[Invoker] Replay completed"); } } }

Implementation Step 6: Input Handler

using UnityEngine; namespace Chapter.Command { public class InputHandler : MonoBehaviour { private Invoker _invoker; private BikeController _bikeController; private ICommand _buttonA, _buttonD; void Start() { _invoker = FindObjectOfType<Invoker>(); _bikeController = FindObjectOfType<BikeController>(); // Create command objects _buttonA = new TurnLeft(_bikeController); _buttonD = new TurnRight(_bikeController); } void Update() { // Input handling if (Input.GetKeyDown(KeyCode.A)) _invoker.ExecuteCommand(_buttonA); if (Input.GetKeyDown(KeyCode.D)) _invoker.ExecuteCommand(_buttonD); // Recording controls if (Input.GetKeyDown(KeyCode.R)) _invoker.StartRecording(); if (Input.GetKeyDown(KeyCode.S)) _invoker.StopRecording(); if (Input.GetKeyDown(KeyCode.P)) _invoker.StartReplay(); } } }

Implementation Step 7: BikeController

using UnityEngine; namespace Chapter.Command { public class BikeController : MonoBehaviour { public enum Direction { Left = -1, Right = 1 } public float distance = 1.0f; public float turnSpeed = 2.0f; // Execute turn command public void Turn(Direction direction) { if (direction == Direction.Left) { transform.Translate(Vector3.left * distance); Debug.Log("[BikeController] Turned LEFT"); } else if (direction == Direction.Right) { transform.Translate(Vector3.right * distance); Debug.Log("[BikeController] Turned RIGHT"); } } } }
Receiver Pattern: BikeController knows nothing about commands - it just provides methods that commands can call!

Testing the Replay System

Test Setup Steps

  1. Create 3 new Unity scenes (Scene1, Scene2, Scene3)
  2. In each scene: Add Cube GameObject (the bike)
  3. Attach BikeController, Invoker, InputHandler scripts
  4. Add different background colors (visual verification)
  5. Add all scenes to Build Settings

Test Sequence

Press R: Start recording
Press A/D: Turn left/right (watch cube move)
Press S: Stop recording
Press P: Play back recording!

Optional: GUI Test Client

using UnityEngine; namespace Chapter.Command { public class GUITestClient : MonoBehaviour { private Invoker _invoker; private bool _isRecording; void Start() { _invoker = FindObjectOfType<Invoker>(); } void OnGUI() { GUILayout.BeginVertical(); if (!_isRecording) { if (GUILayout.Button("Start Recording")) { _invoker.StartRecording(); _isRecording = true; } } else { if (GUILayout.Button("Stop Recording")) { _invoker.StopRecording(); _isRecording = false; } } if (GUILayout.Button("Play Replay")) _invoker.StartReplay(); GUILayout.EndVertical(); } } }

Bonus: Undo/Redo Implementation

// Add to ICommand interface public interface ICommand { void Execute(); void Undo(); // New method! } // Implement in TurnLeft public class TurnLeft : ICommand { private BikeController _controller; public void Execute() { _controller.Turn(BikeController.Direction.Left); } public void Undo() { // Undo left turn = turn right! _controller.Turn(BikeController.Direction.Right); } }
Homework Extension: You'll implement full undo/redo with a command stack!

Hands-On Implementation đŸ’ģ

30-Minute Implementation Challenge

Implement the Command Pattern for replay:

  1. Create ICommand interface
  2. Implement TurnLeft and TurnRight commands
  3. Create Invoker with recording/playback
  4. Create InputHandler for user controls
  5. Implement BikeController receiver
  6. Test recording and replay
Goal: Record a sequence of turns, then watch it replay automatically!

Real-World Uses of Command Pattern

🎮 Game Replays

StarCraft, League of Legends - record entire matches for playback

â†Šī¸ Undo Systems

Photoshop, Unity Editor - undo/redo any action

🤖 AI Training

Record player actions to train AI opponents

đŸŽŦ Cutscenes

Record player movements to create in-game cutscenes

📊 Analytics

Log player actions for heatmaps and behavior analysis

🎲 Turn-Based Games

Queue and execute actions in specific order

Command Pattern Variations

Macro Commands

Combine multiple commands into one. Example: "Turbo Boost" = [SpeedUp, PlaySound, EmitParticles] all executed together!

Queued Commands

Store commands in a queue, execute one per frame. Perfect for turn-based games or animation sequencing!

Networked Commands

Serialize commands to send over network. Multiplayer games send command objects instead of raw input!

Performance & Memory Management

✅ Optimization Tips

  • Use Object Pooling for command objects
  • Set maximum recording length (time limit)
  • Compress timestamps (delta time)
  • Clear old replays to free memory
  • Consider binary serialization for storage

âš ī¸ Watch Out For

  • Memory leaks from uncleaned command lists
  • Performance hit from large command counts
  • Floating-point timestamp drift over time
  • Circular references in command objects
  • Thread safety in networked scenarios
Rule of Thumb: For a 5-minute replay at 60 FPS with input every 0.5 seconds, you'll store ~600 commands. Plan accordingly!

Homework Assignment 📝

Assignment: Replay System with Pause & Rewind

Due: Next class
Submission: Unity project + video to D2L

Part 1: Core Implementation (50 points)

  1. Implement complete Command Pattern (all classes from today)
  2. Create 3 test scenes with different backgrounds
  3. Test replay system works across scene transitions
  4. Add visual recording indicator (● REC)

Homework Assignment (Continued)

Part 2: Advanced Features (50 points)

  1. Pause Replay: Add ability to pause/resume playback (Space key)
  2. Rewind System: Play commands in reverse (R key during replay)
    • Implement Undo() method in commands
    • Execute commands backwards through the list
  3. Playback Speed: Add 2x fast-forward option (F key)
  4. UI Display: Show current command index during replay

Video Requirements (3-5 minutes):

  • Show recording input sequence
  • Demonstrate normal replay
  • Show pause/resume functionality
  • Demonstrate rewind feature
  • Test across all 3 scenes

Video Submission Guidelines

Recording Checklist

  • Show Code: Briefly show your command classes
  • Show Hierarchy: Display your scene setup
  • Record Session: Press R, perform 5-10 turns
  • Playback: Press P, show normal replay
  • Pause: Press Space during replay
  • Rewind: Press R during replay
  • Scene Test: Load Scene2, replay still works
  • Narration: Explain what you're demonstrating
Pro Tip: Use OBS Studio (free) or Xbox Game Bar for clean recordings!

Grading Rubric (100 points)

Point Breakdown

15 pts: ICommand interface and concrete commands implemented
20 pts: Invoker with recording and playback works correctly
15 pts: Replay system works across 3 scenes
15 pts: Pause/Resume functionality implemented
20 pts: Rewind feature with Undo() implementation
15 pts: Video clearly demonstrates all features

Additional Resources

📚 Further Reading

Office Hours: Stuck on rewind implementation? Come see me! The Undo() logic can be tricky.

Questions & Discussion đŸ’Ŧ

Open Floor

  • Command Pattern vs direct method calls?
  • How does timestamp-based replay work?
  • Best practices for command storage?
  • Implementing the rewind feature?
  • Homework clarifications?

Replay System Complete! â¯ī¸

Today's Achievements:

Homework Due Next Class:

Replay System + Pause + Rewind + 3-Scene Test

Video submission to D2L

Next: Object Pool Pattern for performance optimization! 🚀