Command Pattern - Implementing a Replay System

Implementing a Replay System

The Command Pattern â¯ī¸

CSCI 3213 - Game Programming

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.

Developer's Note: "Never Panic Early"

âš ī¸ What to Expect During Implementation

As we implement this pattern, we'll be creating multiple files in a specific order. You will see errors in Unity and your IDE until all files are complete.

How to Handle Development Errors

  • Don't Ignore: Note the errors - they're telling you something
  • Don't Panic: These are expected until all files are created
  • Stay Calm: Follow the implementation order and errors will resolve

As Apollo 13's Fred Haise said: "Never Panic Early"

This is a critical skill for software development. Note problems, but stay focused on the implementation path.

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 (TestPanel)
│──────────────────│
│ + Execute()      │
└────────â”Ŧ─────────┘
         │ uses
         â–ŧ
┌──────────────────┐         ┌─────────────────┐
│    ICommand      │◄────────│  BikeController │  ← Receiver
│──────────────────│         │─────────────────│
│ + Execute()      │         │ + TurnLeft()    │
└────────â”Ŧ─────────┘         │ + TurnRight()   │
         │ implements        └─────────────────┘
         │
    ┌────┴────────────────┐
    â–ŧ                     â–ŧ
┌──────────────┐  ┌──────────────┐
│ TurnLeft     │  │ TurnRight    │  ← Concrete Commands
│──────────────│  │──────────────│
│ + Execute()  │  │ + Execute()  │
└──────────────┘  └──────────────┘
                
Flow: TestPanel 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

📁 File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/Command/ICommand.cs
Base interface that all command classes must implement.
âš ī¸ This code goes into your Unity project for Blade Racer.

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

Why an Interface?

Contract: Any class implementing ICommand must have an Execute() method. The Invoker doesn't care what the command does - just that it can be executed.

Polymorphism: Store TurnLeft, TurnRight, Jump, Fire - all in one List<ICommand>. The list doesn't know or care about specific types!

Decoupling: The Invoker only depends on ICommand, not on concrete classes. Add new commands without changing Invoker code.

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

Implementation Step 2: TurnLeft Command

📁 File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/Command/TurnLeft.cs
Command that turns the bike left when executed.
âš ī¸ This code goes into your Unity project for Blade Racer.

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(Direction.Left); } }

Command Pattern Flow

┌─────────────┐
│  ICommand   │ ← Interface (contract)
│─────────────│
│ + Execute() │
└──────â”Ŧ──────┘
       │ implements
       â–ŧ
┌─────────────┐    ┌─────────────┐
│  TurnLeft   │    │  TurnRight  │
│─────────────│    │─────────────│
│ _controller │    │ _controller │
│ + Execute() │    │ + Execute() │
└──────â”Ŧ──────┘    └─────────────┘
       │ calls
       â–ŧ
┌─────────────────┐
│ BikeController  │ ← Receiver
│─────────────────│
│ + Turn(dir)     │
└─────────────────┘

TurnLeft implements ICommand: This makes TurnLeft interchangeable with any other command (TurnRight, Accelerate, etc.).

Stores receiver reference: The command "knows" which BikeController to act on, passed via constructor.

Execute() delegates: When called, the command tells the receiver what to do. The caller never touches BikeController directly!

Implementation Step 3: TurnRight Command

📁 File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/Command/TurnRight.cs
Command that turns the bike right when executed.
âš ī¸ This code goes into your Unity project for Blade Racer.

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

Copy TurnLeft, Change 3 Things

New commands follow the same structure. Copy TurnLeft.cs and update the highlighted spots:

1. Class name:
TurnLeft → TurnRight

2. Constructor name:
TurnLeft(...) → TurnRight(...)

3. Direction parameter:
Direction.Left → Direction.Right

This consistency is the power of the Command Pattern - adding new commands is predictable!

Implementation Step 4: Invoker Recording

📁 File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Patterns/Command/Invoker.cs
Manages command execution, recording, and replay functionality.
âš ī¸ This code goes into your Unity project for Blade Racer.

using UnityEngine; using System.Collections.Generic; 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)>(); } public void StartRecording() { _recordedCommands.Clear(); _isRecording = true; } public void ExecuteCommand(ICommand command) { command.Execute(); if (_isRecording) _recordedCommands.Add((command, Time.time)); } }

Invoker's Role

The Invoker is the "command manager" - it doesn't know what commands do, just how to execute and store them.

State Fields: Track if we're recording, replaying, and where we are in the replay sequence.

Tuple List: List<(ICommand, float)> stores pairs of command + timestamp. C# tuples let us group related data without creating a new class.

ExecuteCommand(): Always runs the command immediately, then saves it to the list if recording is active.

The Invoker accepts any ICommand - it doesn't know TurnLeft from TurnRight!

Implementation Step 5: Invoker Playback

// Continuing Invoker class... public void StopRecording() { _isRecording = false; } public void StartReplay() { if (_recordedCommands.Count == 0) return; _isReplaying = true; _replayIndex = 0; _replayTime = Time.time; } 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; } }

How Methods Work Together

StopRecording() - Sets flag to stop saving commands
StartReplay() - Resets to beginning, stores start time

Breaking Down the Highlighted Line

var - Type inference. Lets the compiler figure out the types automatically instead of writing them explicitly.

(command, timestamp) - Tuple deconstruction. Unpacks a tuple into two separate variables in one line. Same as writing:
ICommand command = tuple.Item1;
float timestamp = tuple.Item2;

= _recordedCommands[_replayIndex] - Gets the tuple at the current index. Each entry is (ICommand, float).

C# 7+ feature - cleaner than accessing .Item1 and .Item2!

Implementation Step 6: Direction Enum

📁 File Structure Note - PRODUCTION CODE

Create a new file: Assets/Scripts/Enums/Direction.cs
Enum for turn directions (may already exist from State Pattern).
âš ī¸ This code goes into your Unity project for Blade Racer.

public enum Direction { Left = -1, Right = 1 }

Why These Values?

Left = -1, Right = 1: These values map directly to movement direction. Cast to float and multiply by distance for instant lateral movement!

Shared Across Patterns: Used by State Pattern (BikeTurnState) and Command Pattern (TurnLeft/TurnRight). One definition, many uses.

Note: If you already created this from the State Pattern lecture, you're good to go - no changes needed!

Implementation Step 7: Update BikeController

📁 File Structure Note - PRODUCTION CODE

Update existing file: Assets/Scripts/Controllers/BikeController.cs
Add Turn method if not already present from State Pattern.
âš ī¸ This code goes into your Unity project for Blade Racer.

using UnityEngine; public class BikeController : MonoBehaviour { public float turnDistance = 2.0f; // Called by TurnLeft/TurnRight commands public void Turn(Direction direction) { if (direction == Direction.Left) { transform.Translate(Vector3.left * turnDistance); Debug.Log("[BikeController] Turned LEFT"); } else if (direction == Direction.Right) { transform.Translate(Vector3.right * turnDistance); Debug.Log("[BikeController] Turned RIGHT"); } } // ... rest of BikeController from State Pattern }

The Receiver

In the Command Pattern, BikeController is the Receiver - the object that actually performs the work.

Turn(Direction): A simple public method. Commands call this - BikeController doesn't know or care that commands exist!

Why This Matters: BikeController can be used with or without the Command Pattern. Other systems can call Turn() directly if needed.

The Receiver is "dumb" about the pattern - it just does its job when asked!

Testing the Replay System

✅ Reusing Your Work: You already created 5 scenes (Scene1-Scene5) from the Singleton lecture! We'll use those and add one more for practice.

Test Setup Steps

  1. Open Scene1 (from Singleton lecture)
  2. Find your bike GameObject (should already have BikeController from State Pattern)
  3. Attach Invoker script to the bike
  4. Update TestPanel with Command Pattern support (next slides)
  5. Duplicate Scene1 → Scene6 for extra practice (File → Save As)
  6. Ensure all 6 scenes are in Build Settings

Test Sequence

Press 1: Start recording
Press A/D: Turn left/right (watch bike move)
Press 2: Stop recording
Press 3: Play back recording!

Key Mapping Note: We use number keys (1, 2, 3) for recording to avoid conflicts with Event Bus keys (R=Restart, S=Stop, P=Pause). This lets you test all systems together!

TestPanel: Fields & Keyboard Input

📁 Update TestPanel.cs

Add these fields and keyboard shortcuts to your existing TestPanel.cs.

// Add to TestPanel fields private bool _commandExpanded = true; private Invoker _invoker; private BikeController _bikeController; private ICommand _turnLeft, _turnRight; void Start() { _invoker = FindFirstObjectByType<Invoker>(); _bikeController = FindFirstObjectByType<BikeController>(); if (_bikeController != null) { _turnLeft = new TurnLeft(_bikeController); _turnRight = new TurnRight(_bikeController); } } // Add to Update() - Command Pattern shortcuts if (_invoker != null) { if (Input.GetKeyDown(KeyCode.A)) _invoker.ExecuteCommand(_turnLeft); if (Input.GetKeyDown(KeyCode.D)) _invoker.ExecuteCommand(_turnRight); if (Input.GetKeyDown(KeyCode.Alpha1)) _invoker.StartRecording(); if (Input.GetKeyDown(KeyCode.Alpha2)) _invoker.StopRecording(); if (Input.GetKeyDown(KeyCode.Alpha3)) _invoker.StartReplay(); }

Code Breakdown

New Fields: Store references to Invoker, BikeController, and pre-created command objects. Commands are created once in Start().

Start(): Find components in scene and create command instances. We reuse the same TurnLeft/TurnRight objects for all inputs.

Update(): Check for keypresses and route to Invoker. A/D execute turn commands, 1/2/3 control recording.

Note: Null checks prevent errors if Invoker isn't in scene.

TestPanel: GUI Section

Add DrawCommandSection() and call it from DrawWindow().

void DrawCommandSection() { if (_invoker == null) return; GUI.backgroundColor = Color.green; _commandExpanded = GUILayout.Toggle( _commandExpanded, "â–ŧ Command Pattern", "button"); GUI.backgroundColor = Color.white; if (_commandExpanded) { GUILayout.BeginVertical("box"); if (GUILayout.Button("Turn Left (A)")) _invoker.ExecuteCommand(_turnLeft); if (GUILayout.Button("Turn Right (D)")) _invoker.ExecuteCommand(_turnRight); if (GUILayout.Button("Start Recording (1)")) _invoker.StartRecording(); if (GUILayout.Button("Stop Recording (2)")) _invoker.StopRecording(); if (GUILayout.Button("Play Replay (3)")) _invoker.StartReplay(); GUILayout.EndVertical(); } } // In DrawWindow(), add: DrawCommandSection();

The 3 Elements of Each Input

Every TestPanel action has three parts:

1. GUI Button - GUILayout.Button("Turn Left (A)")
Visual click target in the TestPanel window

2. Key Command - Input.GetKeyDown(KeyCode.A)
Keyboard shortcut in Update() (previous slide)

3. Invoker Logic - _invoker.ExecuteCommand(_turnLeft)
Both button AND key trigger the same Invoker call!

Code Details

Collapsible Header: Green toggle button expands/collapses section.

Don't forget: Call DrawCommandSection() in DrawWindow()!

Updating the Keymap

Update the DrawKeymapWindow() method in TestPanel to include Command Pattern shortcuts.

void DrawKeymapWindow(int windowID) { GUILayout.Label("--- Event Bus ---"); GUILayout.Label("C = Countdown"); GUILayout.Label("S = Stop"); GUILayout.Label("R = Restart"); GUILayout.Label("F = Finish"); GUILayout.Label("P = Pause"); GUILayout.Label("Q = Quit"); GUILayout.Space(10); // NEW: Command Pattern shortcuts GUILayout.Label("--- Command Pattern ---"); GUILayout.Label("A = Turn Left"); GUILayout.Label("D = Turn Right"); GUILayout.Label("1 = Start Recording"); GUILayout.Label("2 = Stop Recording"); GUILayout.Label("3 = Play Replay"); GUILayout.Space(10); GUILayout.Label("--- General ---"); GUILayout.Label("K = Toggle this keymap"); GUILayout.Space(10); if (GUILayout.Button("Close")) _showKeymap = false; GUI.DragWindow(); }

TestPanel Evolution

As you progress through lectures, TestPanel grows:

  • Lecture 4: Event Bus section + keymap
  • Lecture 5: + Command Pattern section
  • Lecture 7: + Observer Pattern section
  • Future: Add sections as needed!
Pattern: One unified TestPanel with collapsible sections for each pattern you've implemented!

Implementation Step 8: Update TestPanel

Adding Command Pattern Controls

Evolving TestPanel.cs

Remember TestPanel.cs from the Event Bus lecture? Now we'll add Command Pattern support - keyboard shortcuts and GUI buttons for recording/replay.

What We Added

  • New Fields: Invoker, BikeController, and ICommand references
  • Keyboard Shortcuts: A/D for turning, 1/2/3 for record/stop/replay
  • GUI Section: Collapsible "Command Pattern" button group
  • Updated Keymap: Show all shortcuts in the help window

One TestPanel, Many Patterns

TestPanel grows with each pattern you implement. By the end of the course, you'll have a comprehensive debug tool with collapsible sections for every system!

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(Direction.Left); } public void Undo() { // Undo left turn = turn right! _controller.Turn(Direction.Right); } }

How Undo Works

Interface Change: Add Undo() to ICommand. Every command must now know how to reverse itself.

Reverse Logic: TurnLeft's Undo turns right! Each command's Undo does the opposite of Execute.

Replay Rewind: To play backwards, iterate the command list in reverse order calling Undo() on each.

Homework: 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. Update TestPanel with Command Pattern support
  5. Implement BikeController receiver
  6. Test recording and replay
Goal: Record a sequence of turns, then watch it replay automatically!

Gaming History Moment đŸ•šī¸

Competitive Arcade Culture & High Scores (1981-Present)

In 1981, Walter Day founded Twin Galaxies, the world's first official video game scoreboard. For the first time, players could submit their high scores and be recognized globally. Life Magazine published the first official high score list in 1982, legitimizing competitive gaming.

But there was a problem: How do you prove a high score is real? Without recording systems, players had to submit photographs or have witnesses. This led to famous controversies like the Billy Mitchell vs. Steve Wiebe Donkey Kong rivalry (documented in "The King of Kong").

Connection to Command Pattern

Modern competitive games like StarCraft use the Command Pattern to record every input as proof of skill. The replay file is just a list of commands executed with timestamps - exactly what we're building today!

Learn More: Twin Galaxies History (Gaming Historian) | The King of Kong (Documentary)

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 (Left Arrow key during replay)
    • Implement Undo() method in commands
    • Execute commands backwards through the list
  3. Playback Speed: Add 2x fast-forward option (Right Arrow 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 1, perform 5-10 turns with A/D
  • Stop Recording: Press 2
  • Playback: Press 3, show replay executing
  • Scene Test: Load different scene, 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

25 pts: Invoker with recording and playback works correctly
25 pts: Rewind feature with Undo() implementation
15 pts: ICommand interface and concrete commands implemented
15 pts: Pause/Resume functionality implemented
10 pts: Replay system works across 3 scenes
10 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! 🚀