Recursive AI

Crowds in a rollback game

All onepath recipes

Run a OnePath crowd with a fixed time step, save its state, roll back and resimulate, and check with a state hash that the second run reaches the same bits.

Eight agents cross a 40 × 40 floor in two rows, through each other:

C#
using RecursiveAI.OneMath;
using RecursiveAI.OnePath;
 
var floor = new[]
{
    new Triangle3D(new Vector3D(0, 0, 0), new Vector3D(0, 0, 40), new Vector3D(40, 0, 0)),
    new Triangle3D(new Vector3D(40, 0, 0), new Vector3D(0, 0, 40), new Vector3D(40, 0, 40)),
};
NavMesh level = NavMesh.Build(floor, NavSettings.Humanoid);
Crowd crowd = level.CreateCrowd();
 
// Eight agents in two rows, each walking to the other side.
for (int i = 0; i < 4; i++)
{
    crowd.Add(new Vector3D(5 + 8 * i, 0, 5), Agent.Humanoid).SetDestination(new Vector3D(35 - 8 * i, 0, 35));
    crowd.Add(new Vector3D(5 + 8 * i, 0, 35), Agent.Humanoid).SetDestination(new Vector3D(35 - 8 * i, 0, 5));
}
 
for (int i = 0; i < 100; i++) crowd.Step(1.0 / 30);
byte[] saved = crowd.SaveState();                 // frame 100
Console.WriteLine($"frame {crowd.StepCount}: saved {saved.Length} bytes, hash {crowd.StateHash():X16}");
 
for (int i = 0; i < 200; i++) crowd.Step(1.0 / 30);
ulong ahead = crowd.StateHash();
Console.WriteLine($"frame {crowd.StepCount}: hash {ahead:X16}");
 
crowd.RestoreState(saved);                        // roll back to frame 100 and simulate again
Console.WriteLine($"restored: frame {crowd.StepCount}, hash {crowd.StateHash():X16}");
for (int i = 0; i < 200; i++) crowd.Step(1.0 / 30);
Console.WriteLine($"frame {crowd.StepCount}: hash {crowd.StateHash():X16}, same as before: {crowd.StateHash() == ahead}");
 
Console.WriteLine($"{crowd.Count(a => a.ReachedTarget)} of {crowd.Count} agents have arrived");
Text
frame 100: saved 3320 bytes, hash E7D9E5CD5E787CD2
frame 300: hash 75F1194801CC93C2
restored: frame 100, hash E7D9E5CD5E787CD2
frame 300: hash 75F1194801CC93C2, same as before: True
4 of 8 agents have arrived

The pieces

  • crowd.Step(dt) takes the time step from you. The crowd has no clock, and nothing in it depends on threads or hash order, so the same steps give the same state on every machine.
  • SaveState() writes the crowd’s state to bytes and RestoreState(bytes) puts it back, so the next step continues exactly where the saved one left off. The same state writes the same bytes on every platform.
  • StateHash() is a cheap fingerprint to exchange with peers every few frames: a mismatch is a desync.
  • SaveStateWithMesh also saves a dynamic mesh (rebuilt tiles, obstacles) with the crowd.

ORCA and the corridor follower are in ORCA solver and corridors and following.