diff --git a/src/LitMotion/Assets/LitMotion/Runtime/ManualMotionDispatcher.cs b/src/LitMotion/Assets/LitMotion/Runtime/ManualMotionDispatcher.cs index e7708667..036134f0 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/ManualMotionDispatcher.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/ManualMotionDispatcher.cs @@ -39,6 +39,14 @@ public sealed class ManualMotionDispatcher readonly ManualMotionDispatcherScheduler scheduler; readonly Dictionary runners = new(); + // Reused buffer so Update/Reset iterate a snapshot of the runners instead of the live + // dictionary. A motion callback fired during iteration can schedule the first motion of a + // new type, which lazily adds a runner (see GetOrCreateRunner) and would otherwise throw + // "InvalidOperationException: Collection was modified" mid-enumeration. Reused rather than + // allocated per call; AddRange(runners.Values) copies via ICollection.CopyTo and the + // Dictionary caches its ValueCollection, so this stays allocation-free after warmup. + readonly List runnerBuffer = new(); + public ManualMotionDispatcher() { scheduler = new(this); @@ -79,10 +87,13 @@ public void Update(double deltaTime) { time += deltaTime; - foreach (var kv in runners) + runnerBuffer.Clear(); + runnerBuffer.AddRange(runners.Values); + for (int i = 0; i < runnerBuffer.Count; i++) { - kv.Value.Update(time, time, time); + runnerBuffer[i].Update(time, time, time); } + runnerBuffer.Clear(); } /// @@ -90,10 +101,13 @@ public void Update(double deltaTime) /// public void Reset() { - foreach (var kv in runners) + runnerBuffer.Clear(); + runnerBuffer.AddRange(runners.Values); + for (int i = 0; i < runnerBuffer.Count; i++) { - kv.Value.Reset(); + runnerBuffer[i].Reset(); } + runnerBuffer.Clear(); time = 0; }