Testland
Browse all skills & agents

gameplay-recording-replay

Build a deterministic gameplay record/replay test artefact for Unity, Unreal, or Godot - record a player session, save it to disk, replay it bit-for-bit, and assert that the resulting game state matches the original. Covers Unity Input System's InputEventTrace API (Enable / Disable / WriteTo / ReadFrom / Replay) for input-level capture, Unreal's Replay System (DemoRec / DemoPlay / DemoStop console commands plus DemoNetDriver + NetworkReplayStreamer, default storage at %LOCALAPPDATA%/{Project}/Saved/Demos) for replication-stream capture, and Godot's community-pattern deterministic-RNG + input-script replay since Godot ships no first-party replay system. Use when authoring a regression-test artefact for player-recorded sessions, building a netcode replay for spectator / esports, or producing reproducible bug repros for cert teams.

Install with skills.sh (any agent)

npx skills add testland/qa --skill gameplay-recording-replay
View source

gameplay-recording-replay

Overview

A deterministic replay artefact is a recorded session of player input or replicated state that can be played back to recreate the original game state. Three reasons games invest in replays:

  1. Regression tests. Record a scripted playthrough; replay it in CI; assert the final state matches the original. Catches physics drift, AI determinism breaks, save-format regressions.
  2. Bug repros. Players record a session; QA replays it inside the build with the bug; cert teams attach the replay to the ticket.
  3. Esports / spectator / clip sharing. Match replays surfaced in-product.

This is a workflow that produces, per engine, a working record/replay setup plus a CI regression test that runs it. The full per-engine record, replay, and CI code lives in references/engine-record-replay-apis.md; this file is the cross-engine decision surface.

The three engine surfaces differ:

EngineRecording surfaceStability of public docs
UnityInput System's InputEventTrace (input-level)Public - com.unity.inputsystem@1.8 API (opens in new window)
UnrealReplay System (replication-stream level) - DemoRec / DemoPlay / DemoStopPublic - Replays in Unreal Engine (opens in new window)
GodotNo first-party replay - community pattern: deterministic RNG seed + recorded InputEvent scriptCommunity - Godot documentation does not ship a replay subsystem [author opinion, per docs.godotengine.org (opens in new window)]

Composes with:

  • unity-test-framework, unreal-automation-system, godot-gut-tests - the host test framework the replay runs inside.
  • multiplayer-state-machine-coverage - Unreal's Replay system shares NetworkReplayStreamer with its multiplayer stack; multiplayer fixtures can replay captured sessions.
  • platform-cert-overview-reference - replays are evidence artefacts for cert (Xbox XR-003 title quality, attaching a repro to a CFR).

When to use

  • Producing a deterministic regression test for a story playthrough / level traversal / damage script.
  • Building an in-product replay feature (spectator mode, instant replay, clip sharing).
  • Producing reproducible repro artefacts for cert teams.
  • Investigating a determinism regression - "this build diverges from the previous build's replay at frame 4823."

How to use

  1. Lock determinism first for the capture level you need - fix the Step 1 sources of non-determinism (fixed timestep, seeded RNG, synchronous loads) before recording anything.
  2. Pick the capture level from the Step 2 per-engine contract table - Unity and Godot capture input (which needs determinism); Unreal captures the replication stream (which does not).
  3. Record and replay with the engine's API - full working code in references/engine-record-replay-apis.md.
  4. Choose the assertion model (final-state hash, per-checkpoint, or per-frame) and wire the replay into a CI regression test (Step 3).
  5. Embed a build hash + replay-format version in the replay header so CI can tell a real regression from a stale replay (Step 4).

Inputs

Gather before walking the workflow:

InputWhereWhy
Engine + versionProjectDetermines which API surface applies
Determinism baselineGame design - is fixed-tick physics on? RNG seeded? AI deterministic?Replays only work when the game's per-frame outputs are functions of (state + input)
Recording scopeInput-only? Full replication stream?Trades off file size against determinism guarantees
Target storage locationApplication.persistentDataPath, %LOCALAPPDATA%/<Project>/Saved/Demos, etc.Where replay files land
Replay length budgetSeconds / minutesInputEventTrace buffer sizing
Assertion modelFinal-state hash? Per-frame? Checkpoint-only?Drives the test harness shape

Workflow

Step 1 - Establish determinism

Replays are worthless if the game is non-deterministic. Before any recording work, lock down:

Source of non-determinismLock-down
Variable timestep / Time.deltaTimeRun physics on FixedUpdate (Unity) / TickGroup (Unreal) with a fixed delta
Unseeded RandomSeed every RNG at session start; record the seed in the replay header
Async load timingForce synchronous load during replay (SceneManager.LoadScene sync; AssetRegistry::Tick to drain)
Multithreaded game logicPin to one thread or commit to ordered consumption of results
Frame-rate-dependent FXTag visual-only systems and skip them in headless replay runs

If you can't lock determinism, replay can still be useful as a visual debug artefact, but it won't drive regression assertions.

Step 2 - Record and replay per engine

Each engine captures at a different level, with a different determinism contract. Pick the row that matches your engine, then lift the working record + replay code from references/engine-record-replay-apis.md.

EngineCapture levelDefault storageDeterminism contract
UnityInput events (InputEventTrace, buffer-sized)Application.persistentDataPath / repo test/Needs Step 1 - replay reproduces input; state matches only if the sim is a pure function of state + input
UnrealReplication stream (DemoNetDriver + NetworkReplayStreamer)%LOCALAPPDATA%/<Project>/Saved/DemosReconstructs the server's streamed view - does not require local determinism
GodotInput events + RNG seed (community pattern)FileAccess path of choiceSame contract as Unity - needs Step 1

The key split: input-level replay (Unity, Godot) depends on determinism; replication-stream replay (Unreal) does not, because the Unreal simulation already ran on the server and the replay just re-streams its output. This is why Unreal replays handle full multiplayer matches while Unity/Godot replays are for pinned single-player.

Step 3 - Wire replays as CI regression assertions

Per engine, the CI loop is:

  1. Check the replay file into the repo under test/replays/.
  2. CI job loads the build, plays the replay, hashes the final game state.
  3. Compare the hash against the baseline (also in repo).
  4. Mismatch → CI fails the build.

The per-engine harness code (Unity [UnityTest] PlayMode fixture, Unreal LatentIt latent command, Godot GUT fixture) is in references/engine-record-replay-apis.md.

Step 4 - Define a fail-safe for replay drift

Real-world replays drift when:

  • The game build version changed and the replay was recorded against an earlier version.
  • A non-deterministic source slipped in (new async loader, new particle system using Random).
  • The save format changed.

The harness must distinguish bug-in-build from replay-stale. Encode the build hash + replay format version in the replay header; CI rejects replays whose replay_format < current_format, then either re-records (acceptable for visual replays) or fails the build (regression test replays).

Worked example - Unity speedrun regression

Inputs:

  • Engine: Unity 6.0, Input System v1.8.
  • Determinism: Physics at FixedUpdate, 60 Hz; RNG seeded from replay header; AI scripted via deterministic state machines.
  • Recording scope: Input-only via InputEventTrace.
  • Storage: Assets/Tests/Replays/level1.trace (committed to the repo).
  • Length budget: 90 s - fits comfortably in 4 MB buffer (recordFrameMarkers = true per the API page (opens in new window)).
  • Assertion: SHA-256 hash of GameStateRoot.Serialize() at T=90 s.

Step 1 - physics on FixedUpdate, RNG seeded from replay header, async loads forced synchronous in headless mode.

Step 2 - recorder MonoBehaviour creates InputEventTrace, enables on level start, writes to level1.trace on first level completion.

Step 3 - CI fixture (UTF [UnityTest]) reads the trace, replays it, hashes the final GameStateRoot, compares to the baseline hash committed in Assets/Tests/Baselines/level1.hash.

Step 4 - replay header includes replay_format = 2 and build_hash = <git-sha>. CI fails the build with a clear "replay stale" message when format < 2 rather than silently passing.

Anti-patterns

Anti-patternWhy it failsFix
Recording input without seeding RNGReplay diverges by frame ~30Step 1 - every RNG seeded; seed in replay header
InputEventTrace without Dispose()Memory leak per API page (opens in new window) - "must be disposed of … or they will leak memory on the unmanaged (C++) memory heap"Always using / Dispose()
InputEventTrace without recordFrameMarkers = trueReplay reissues events back-to-back instead of respecting timing per API page (opens in new window)Set recordFrameMarkers = true before Enable()
Comparing per-frame state instead of checkpointsFalse-positive failures from float-precision driftHash at coarse-grained checkpoints (every N seconds or per level)
Treating Unreal replays as deterministic input replaysThey're replication-stream replays - different determinism contract per Replays page (opens in new window)Document the contract; don't assume input-level determinism
Storing replay format version implicitlyReplay-stale failures get misdiagnosed as bugsEmbed replay_format + build_hash in the header
Replays committed without their baselinesAsserting nothingCommit replay.trace + baseline.hash as a pair
Replays > 30 min in CIReplay run time dominates total job timeSplit into checkpointed mini-replays
Replay scrubbing in test fixturesMany engines don't expose deterministic scrubbing for testsLinear playback only in regression fixtures; scrubbing is a feature, not a test surface
Replays storing PIISaved player names / chat messages in attached reprosStrip PII from replay headers before sharing

Limitations

  • Input-level replay (Unity) and replication-stream replay (Unreal) are different contracts. Unity replays depend on determinism; Unreal replays reconstruct the server's view and do not. Don't conflate them in a multi-engine team.
  • Godot has no first-party replay system per the Godot documentation home (opens in new window); the community pattern in Step 2 is sufficient for deterministic single-player but doesn't generalise to multiplayer.
  • Unreal replays bloat with high-bandwidth replication. The .replay file under %LOCALAPPDATA%/<Project>/Saved/Demos (opens in new window) grows with replicated-property volume - large open-world games produce GB-scale replays. Use checkpoint-only replays for long sessions.
  • Build-version coupling. Replays recorded against build N rarely play correctly on build N+1 if the netcode / save format changed. Treat the replay file as a build-pinned artefact.
  • No public common exit-code definition for replay-failed states in either Unity or Unreal - parse the test framework's result file (UTF NUnit XML or Unreal automation JSON) for the failure detail, don't rely on engine exit code.
  • No assertion API on Unreal DemoNetDriver replay completion in public docs - the spec / latent-command harness must poll the demo time and time-out. See Replays in Unreal Engine (opens in new window).
  • PII in replays. Player chat, gamertags (per Microsoft XR-046 (opens in new window) modern-gamertag display rules), and inventory data can land inside replay attachments - strip before sharing with cert / publisher.

References

Per-engine record/replay APIs

View source (opens in new window)

Per-engine record/replay APIs

Deep reference for gameplay-recording-replay SKILL.md - the full working record, replay, and CI-assertion code for Unity, Unreal, and Godot. Consult after Step 1 (determinism) is locked, when wiring the actual capture for your engine.

Unity - InputEventTrace

Per the InputEventTrace API page (opens in new window), InputEventTrace "captures input events into an unmanaged memory buffer". Critically: traces "must be disposed of (by calling Dispose()) after use or they will leak memory on the unmanaged (C++) memory heap".

Recording:

using UnityEngine.InputSystem.LowLevel;

public class SessionRecorder : MonoBehaviour
{
    private InputEventTrace _trace;

    void Start()
    {
        // 4 MB buffer; grow up to 64 MB; default device = all
        _trace = new InputEventTrace(bufferSizeInBytes: 4 * 1024 * 1024,
                                     growBuffer: true,
                                     maxBufferSizeInBytes: 64 * 1024 * 1024);
        _trace.recordFrameMarkers = true;   // per the API page
        _trace.Enable();
    }

    public void StopAndSave(string path)
    {
        _trace.Disable();
        _trace.WriteTo(path);  // binary on-disk format
    }

    void OnDestroy()
    {
        _trace?.Dispose();  // required per the API page
    }
}

Per the same API page: "Enable recordFrameMarkers to insert boundary events between frames, allowing proper temporal spacing during playback" - without it, the replay will reissue events back-to-back rather than respecting original frame timing.

Replay:

public void Replay(string path)
{
    var trace = new InputEventTrace();
    trace.ReadFrom(path);                  // per the API page
    var controller = trace.Replay();       // returns ReplayController
    controller.PlayAllEventsAccordingToTimestamps();
    // or controller.PlayAllFramesOneByOne() for headless step
}

Per the API page, Replay() "begins event reconstruction, returning a ReplayController object" - the controller exposes play / pause / scrub / step methods.

Determinism caveat. InputEventTrace captures the input but not the game state. A replay reproduces the same input events in the same order - if the game's per-frame output is a pure function of state + input + fixed delta + seeded RNG (Step 1), the resulting state matches. If not, the replay diverges.

CI regression assertion (UTF [UnityTest] PlayMode fixture):

[UnityTest]
public IEnumerator Level1_Speedrun_Replay_MatchesBaseline()
{
    var trace = new InputEventTrace();
    trace.ReadFrom("Assets/Tests/Replays/level1.trace");
    var ctl = trace.Replay();
    ctl.PlayAllEventsAccordingToTimestamps();

    yield return new WaitForSeconds(60f);  // length of replay

    var hash = GameStateHasher.Compute(GameStateRoot);
    Assert.AreEqual("baseline-hash-here", hash);
    trace.Dispose();   // per the API page
}

Unreal - Replay System

Per Replays in Unreal Engine (opens in new window), Unreal's replay system captures the replicated state stream via the DemoNetDriver and persists via NetworkReplayStreamer. Default storage per the Recording Replays page (opens in new window): %LOCALAPPDATA%/<Project>/Saved/Demos.

Console commands (per the same page):

CommandEffect
DemoRec <FriendlyName>"Initiate replay recording" - emits a .replay file under Saved/Demos
DemoStopStop recording / playback
DemoPlay <FriendlyName>Play back a previously recorded replay

The <FriendlyName> argument is the replay's identifier - per the same page "helps distinguish between multiple recording sessions".

Programmatic recording (per the Recording Replays page (opens in new window)):

// In-game: start recording
GEngine->Exec(GetWorld(), TEXT("DemoRec MyMatch_v1"));

// Stop
GEngine->Exec(GetWorld(), TEXT("DemoStop"));

// Play back
GEngine->Exec(GetWorld(), TEXT("DemoPlay MyMatch_v1"));

For lower-level control, use FNetworkReplayStreamer directly (per the same page) - the streamer exposes start / stop / pause / goto-time methods and is the abstraction DemoNetDriver wraps.

Time-shifting and scrubbing are supported per the Playing Back Replays page (opens in new window) - playback isn't strictly forward-only; replays can be scrubbed to arbitrary timestamps.

Determinism semantics differ from Unity. Unreal records the output of the network replication layer, not raw input. The replay reconstructs the server's view as it was streamed to clients - it does not require the local game simulation to be deterministic (the simulation already happened on the server). This is why Unreal replays are practical for full multiplayer matches whereas Unity InputEventTrace replays require pinning deterministic single-player.

CI regression assertion (in an unreal-automation-system test):

LatentIt("Level1 replay reaches baseline checkpoint",
         [this](const FDoneDelegate& Done)
{
    GEngine->Exec(GetWorld(), TEXT("DemoPlay Level1_Baseline"));
    // poll for replay end via DemoNetDriver state, then
    // hash GameState and compare to baseline
    StartReplayWatchdog(Done);
});

Godot - community deterministic pattern

Godot has no first-party replay subsystem comparable to InputEventTrace or DemoNetDriver [author opinion, per the Godot documentation home (opens in new window) which does not surface a testing/replay section]. The community pattern is deterministic RNG seed + recorded InputEvent script:

# recorder.gd
extends Node

var _events: Array = []
var _rng_seed: int

func _ready():
    _rng_seed = randi()
    seed(_rng_seed)

func _input(event: InputEvent):
    _events.append({
        "tick": Engine.get_physics_frames(),
        "event": event.duplicate(),
    })

func save_to(path: String) -> void:
    var f := FileAccess.open(path, FileAccess.WRITE)
    f.store_var({"seed": _rng_seed, "events": _events})
    f.close()
# player.gd - replay
extends Node

var _events: Array
var _cursor := 0

func load_from(path: String) -> void:
    var f := FileAccess.open(path, FileAccess.READ)
    var data = f.get_var()
    seed(data["seed"])
    _events = data["events"]

func _physics_process(_dt):
    while _cursor < _events.size() \
            and _events[_cursor]["tick"] <= Engine.get_physics_frames():
        Input.parse_input_event(_events[_cursor]["event"])
        _cursor += 1

Replay reissues InputEvents on the same Engine.get_physics_frames() tick they were captured. Combined with seed(...) re-applied from the header and fixed physics/common/physics_ticks_per_second, this gives equivalent determinism to Unity's InputEventTrace pattern.

CI regression assertion - test harness in godot-gut-tests:

extends GutTest

func test_level_1_speedrun_replay_matches_baseline():
    var player := preload("res://src/replay_player.gd").new()
    player.load_from("res://test/fixtures/level1_baseline.replay")
    add_child(player)

    await get_tree().create_timer(10.0).timeout

    var final_state := %Game.get_state_hash()
    assert_eq(final_state,
              "abc123…baseline-hash…",
              "Replay produced same final state hash as baseline")

Related skills

game-perf-profiling

Profiles game builds against frame-time, memory, GPU draw-call, and GC-spike budgets using Unity Profiler + Profile Analyzer + Performance Testing package and Unreal Insights + stat commands. Establishes pass/fail thresholds (16.6 ms at 60 fps, 33.3 ms at 30 fps), writes automated performance regression tests that run in CI, and emits a structured budget report per SKU. Use when a title must hit a declared frame-time or memory budget before a milestone gate or platform-cert submission, or when a recent change needs a performance regression check.

game-test-categories-reference

Pure-reference catalog of the testing categories that apply to a video-game build before it ships. Defines the six canonical buckets the industry tests against - functional / compliance / compatibility / performance / localization / accessibility - plus the multiplayer and content-rating sub-axes. Cross-references each bucket to the platform-holder vocabulary that drives it (Microsoft Xbox Requirements / XR test cases, Sony TRC, Nintendo Lotcheck, Steam Direct review). Use as the taxonomy lookup when planning a game test pass, scoping QA effort, mapping platform-cert findings back to internal test categories, preparing a submission checklist, reviewing first-party certification requirements, or triaging cert testing failures against internal categories.

godot-gut-tests

Author and run GUT (Godot Unit Test) - the community-canonical GDScript test framework at github.com/bitwes/Gut and gut.readthedocs.io. Covers install (Godot Asset Library or manual `addons/gut/` copy + plugin enable), GUT panel inside the editor, writing tests that extend GutTest with `test_` prefix methods, the assertion family (assert_eq / assert_almost_eq / assert_true / assert_signal_emitted), lifecycle hooks (before_each / after_each / before_all / after_all), inner classes for grouping, parameterized tests via `params=[...]`, doubles / stubs / spies, async / coroutine tests, the command-line runner (`-d -s addons/gut/gut_cmdln.gd -gdir=res://test -gjunit_xml_file=... -gexit`), JUnit XML export, and CI integration. Godot 4.x uses GUT 9.x (current main branch supports 4.6.x; godot_4_7 branch for 4.7.x); Godot 3.x uses GUT 7.x. Use when the unit under test is GDScript code in a Godot project.

multiplayer-state-machine-coverage

Build a coverage matrix for a networked-game state machine that exercises connect / authority-handoff / disconnect / reconnect / host-migration paths across Unity Netcode for GameObjects, Unreal Engine replication, and Mirror Networking. Workflow: enumerate the engine's connection states + ownership states + replicated-property update rules, cross them against latency / loss / out-of-order packet injection, encode each combination as a test fixture, and emit a go / no-go gate. Use before submitting a multiplayer title to platform cert - Microsoft's cert guide lists 'Multiplayer does not work as expected' as one of the most common Hold reasons, and Xbox XR-067 (MPSD session state) is failed by uncovered state-machine paths.

platform-cert-overview-reference

Pure-reference catalog of the four platform-holder certification regimes a multi-platform title submits to before release: Microsoft Xbox Requirements (XR) / Xbox certification on learn.microsoft.com, Sony Technical Requirements Checklist (TRC) on the gated PlayStation DevNet portal, Nintendo Lotcheck on the gated Nintendo Developer Portal, and Steam Direct review on partner.steamgames.com. Documents the submission workflow, severity / pass-fail vocabulary, test-bench configurations, and known SLAs for each platform. Cites public sources inline; cites gated NDA portals by stable ID per PLUGIN_AUTHORING.md Step 4 fallback. Use when planning a cert calendar, mapping internal QA findings to the platform's vocabulary, or sequencing submissions across platforms.

unity-test-framework

Author and run the Unity game-engine Test Framework (`com.unity.test-framework`, currently v1.8). Distinct from the ThrowTheSwitch Unity C testing library at throwtheswitch.org/unity - the two tools share only a name. Covers package install via Package Manager, the EditMode vs PlayMode split, the [Test] / [UnityTest] / [SetUp] / [TearDown] / [UnityPlatform] attributes, assembly-definition setup (Editor folder vs asmdef with `includePlatforms` / `optionalUnityReferences: [TestAssemblies]`), Test Runner window, command-line batch invocation with `-runTests` / `-testPlatform` / `-testResults` / `-testFilter` / `-testCategory`, NUnit 3.5 assertion API, and CI integration. Use when the unit under test is C# Unity code that needs to exercise the Unity runtime or editor.

unreal-automation-system

Author and run Unreal Engine's Automation Test Framework - Epic's C++ test framework for UE 4.x / 5.x, documented at dev.epicgames.com/documentation/en-us/unreal-engine. Covers the five test categories Epic defines (Unit / Feature / Smoke / Content Stress / Screenshot Comparison), the IMPLEMENT_SIMPLE_AUTOMATION_TEST and IMPLEMENT_COMPLEX_AUTOMATION_TEST macros, the BDD-style Automation Spec API (DEFINE_SPEC / BEGIN_DEFINE_SPEC / Describe / It / BeforeEach / LatentIt / xIt), latent commands (ADD_LATENT_AUTOMATION_COMMAND), the Automation Driver for UI input simulation (IAutomationDriverModule::Get().CreateDriver(), By::Id / By::Path locators), running via Session Frontend (Window > Test Automation) and command line (-ExecCmds="Automation RunTests …"), and CI integration. Use when the unit under test is C++ Unreal code that needs the UE runtime, editor, or UMG UI surface.