Recursive AI

Record and play crowd replay in C#

OnePath is a free, open-source pathfinding library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

A replay saves crowd commands and steps for later playback.

Terminal
dotnet add package RecursiveAI.OnePath

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

setup.bin comes from the example zip and is created by the full program below.

C#
using RecursiveAI.OnePath.Replay;
 
var crowd = new CrowdPlayer(
    File.ReadAllBytes("setup.bin")).Crowd;
var recorder =
    new CrowdRecorder(crowd, 1.0 / 30,
        keepSteps: true);
recorder.Step();
File.WriteAllBytes("replay.bin",
    recorder.ToReplay().ToBytes());
var replay = CrowdReplay.Read(
    File.ReadAllBytes("replay.bin"));
var player = new CrowdPlayer(replay.Setup.Span);
foreach (var step in replay.Steps)
{
    player.Apply(step.Span);
    player.Step();
}
Console.WriteLine(replay.FindMismatch() is null);
Text
True

Unzip the example, then dotnet run.

setup.bin in the zip contains the square floor and one moving crowd member used below, before the first step. The walker added before WriteSnapshot is already in this snapshot’s setup. In the full program’s retained replay, the recorder was constructed before the addition, so the addition is a command instead. The snippet writes replay.bin, plays its steps, and checks every recorded hash.

The full program

The program creates its own floor geometry and crowd in code; it needs no external assets. RecursiveAI.OneMath, used for the geometry types, is installed automatically as a dependency of OnePath. It regenerates the binary input used by the snippet at the top.

C#
using System.Buffers;
using RecursiveAI.OneMath;
using RecursiveAI.OnePath;
using RecursiveAI.OnePath.Replay;
 
var floor = new[]
{
    new Triangle3D(new(0, 0, 0), new(0, 0, 40), new(40, 0, 0)),
    new Triangle3D(new(40, 0, 0), new(0, 0, 40), new(40, 0, 40))
};
var crowd = NavMesh.Build(floor, NavSettings.Humanoid).CreateCrowd();
var recorder = new CrowdRecorder(crowd, 1.0 / 30, keepSteps: true);
var walker = recorder.Add(new(5, 0, 5));
recorder.SetDestination(walker, new(30, 0, 30));
 
var setup = new ArrayBufferWriter<byte>();
recorder.WriteSnapshot(setup);
File.WriteAllBytes("setup.bin", setup.WrittenSpan.ToArray());
for (int i = 0; i < 10; i++) recorder.Step();
File.WriteAllBytes("replay.bin", recorder.ToReplay().ToBytes());
var replay = CrowdReplay.Read(File.ReadAllBytes("replay.bin"));
var player = new CrowdPlayer(replay.Setup.Span);
foreach (var step in replay.Steps)
{
    player.Apply(step.Span);
    player.Step();
}
Console.WriteLine($"Played {player.Crowd.StepCount} steps");
Console.WriteLine($"Final hash matches: {player.Crowd.StateHash() == crowd.StateHash()}");
Console.WriteLine($"All hashes match: {replay.FindMismatch() is null}");

dotnet run prints:

Text
Played 10 steps
Final hash matches: True
All hashes match: True

Malformed setup or replay bytes throw CrowdReplayException; edits to the mesh while recording make recorder.Step() throw InvalidOperationException.

Variations

Route other changes through the recorder too. Insert these lines before the stepping loop; the printed checks remain true.

C#
// After adding walker in the full program, before the loop:
recorder.Stop(walker);
recorder.Teleport(walker, new(6, 0, 6));
recorder.SetDestination(walker, new(25, 0, 25));

The short answer

Use CrowdRecorder for changes and steps, keep the steps in memory, and save ToReplay().ToBytes(). Read that file with CrowdReplay.Read. CrowdPlayer restores its setup, applies each step’s operations, and advances by the recorded time step. This is a simulation replay, not a recording of rendered frames or a video file.

How do I record and replay a simulation in C#?

The complete program builds a flat square from two triangles. Both the navigation mesh and the crowd’s initial state are embedded in the setup, so playback does not rebuild the floor from your original geometry files. Each recorded step holds the commands issued before it and the state hash after it. Apply the commands before calling player.Step(), as the full program does.

Recording starts when the recorder is constructed. Existing crowd members belong to the setup; later additions belong to the command stream. The keepSteps: true argument is necessary for file export. The default is live-only recording, so calling ToReplay with that default throws InvalidOperationException.

Why does a simulation replay diverge?

Route additions, removals, destination changes, stops, teleports and time steps through the recorder. Calling the crowd member’s own methods changes the live simulation without adding those commands to the recording. That can leave the replay with valid bytes but different state hashes. FindMismatch() plays the recorded run and returns the first differing step index, or null if all recorded hashes match.

Mesh edits and query filters are not recorded. The recorder rejects a step if the mesh has changed since recording began. A query filter is a delegate, and the restored crowd uses none. Do not rely on an unrecorded filter to reproduce the run. Thread-count settings are not saved either; the documentation says results do not depend on them.

Should I record inputs or simulation events?

Record the commands that actually change this crowd, rather than keyboard state or screen coordinates. A destination command includes the destination position and the crowd member’s ID. That makes playback independent of whatever UI generated the command. Preserve command order: a destination change after an addition must not arrive before the addition.

The recording format specifies little-endian integers and doubles stored by bit pattern. The saved setup includes the time step, options, navigation mesh and crowd state. You do not supply a new frame delta during playback: CrowdPlayer.Step() uses the setup’s value. Changes after the last recorded step are not a completed replay step; step the recorder before exporting commands you want played.

All onepath recipes