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:
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");
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 andRestoreState(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.SaveStateWithMeshalso saves a dynamic mesh (rebuilt tiles, obstacles) with the crowd.
ORCA and the corridor follower are in ORCA solver and corridors and following.