Recursive AI

Publish live crowd replay records in C#

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

Stream crowd changes and step hashes—checksums of the crowd state after each step—to a consumer, the code receiving the records. This is a callback, not a network transport.

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 setup = File.ReadAllBytes("setup.bin");
var crowd = new CrowdPlayer(setup).Crowd;
var recorder =
    new CrowdRecorder(crowd, 1.0 / 30);
var records = new List<byte[]>();
recorder.Record = bytes =>
    records.Add(bytes.ToArray());
recorder.Step();
Console.WriteLine(records.Count);
Text
1

Unzip the example, then dotnet run.

setup.bin in the zip contains the square-floor crowd after step 1. The snippet publishes one step record to an in-memory consumer; it does not open a network connection.

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));
 
// Attach at a completed step, on the same thread as all recorder calls.
recorder.Step();
var snapshot = new ArrayBufferWriter<byte>();
recorder.WriteSnapshot(snapshot);
File.WriteAllBytes("setup.bin", snapshot.WrittenSpan.ToArray());
var records = new List<byte[]>();
recorder.Record = record => records.Add(record.ToArray());
recorder.Stop(walker);
recorder.Step();
var player = new CrowdPlayer(snapshot.WrittenSpan);
foreach (byte[] record in records)
{
    if (record[0] == 1) player.Apply(record.AsSpan(1));
    else if (record[0] == 2)
    {
        player.Step();
        ulong expected = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(record.AsSpan(1));
        if (player.Crowd.StateHash() != expected) throw new InvalidOperationException("Live hash mismatch");
    }
    else throw new InvalidDataException("Unknown record kind");
}
Console.WriteLine($"Delivered records: {records.Count}");
Console.WriteLine($"Consumer step: {player.Crowd.StepCount}");
Console.WriteLine($"Hash matches: {player.Crowd.StateHash() == crowd.StateHash()}");

dotnet run prints:

Text
Delivered records: 2
Consumer step: 2
Hash matches: True

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

Variations

Detach the consumer. Future records are dropped; the already copied list still has 2 entries.

C#
// After the delivery loop in the full program:
recorder.Record = null;
recorder.Step();
Console.WriteLine(records.Count);

The short answer

Assign CrowdRecorder.Record a handler. It receives one record after each recorded change or step. Copy the supplied bytes if the consumer needs to retain them: the span is valid only during the callback. Give a new consumer a current setup snapshot before delivering subsequent records. The full example uses a local queue and a second crowd, with no network service.

How do I stream live crowd simulation records in C#?

The recorder calls the handler synchronously after changes. A kind-1 record contains one operation after its leading byte. A kind-2 record contains the state hash after a simulation step. The consumer applies each operation, then advances once when it receives the corresponding step record and compares the resulting hash.

The full program attaches after a completed step. It captures a setup, installs the handler, stops the moving crowd member through the recorder, and steps. The consumer receives the stop operation and then the step hash. That preserves the distinction between a command issued between steps and a completed simulation step. Read the record layout when implementing another consumer.

How do I start a live consumer from a snapshot?

WriteSnapshot must describe exactly the state after the last record the consumer has received. In the example there are no prior delivered records, so the snapshot establishes the initial boundary. The callback is installed before the next operation. The snapshot and subsequent recorder calls all run on one thread; the recorder and player are documented for one thread at a time.

Do not capture a snapshot, allow the simulation to advance, and only then attach the handler. The consumer would miss the intervening commands or steps. Do not send old queued operations on top of a snapshot that already contains their effects. Starting with the right mesh but the wrong boundary is not enough to reproduce the host state.

Can live crowd records be watched later?

Live delivery and retaining a replay are independent choices. Construct the recorder with keepSteps: true if you also need ToReplay() for later playback. With the default false value, the recorder does not retain a growing history of steps. The example’s list does retain its delivered records, so it is a demonstration queue, not an unbounded production streaming design.

The callback is not a network transport. It does not provide message acknowledgments, reconnection or backpressure. If you send records through a byte stream, preserve each callback’s message boundary in your own framing and retain their order; a stream read can split or combine writes. Copying to a byte array in the example makes the lifetime explicit. A handler must not call back into the recorder. For late attachment or reconnect, establish a new consistent snapshot boundary instead of silently skipping missing records. Changes that bypass the recorder, mesh edits and query filters cannot be reconstructed from this live stream.

All onepath recipes