Skip to content

Commit 3f9b925

Browse files
authored
fix: Removal of self while running OnSimulationUpdate (#2954)
1 parent 5086f17 commit 3f9b925

File tree

2 files changed

+130
-7
lines changed

2 files changed

+130
-7
lines changed

sources/engine/Stride.BepuPhysics/Stride.BepuPhysics.Tests/BepuTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using BepuPhysics.Collidables;
88
using BepuPhysics.CollisionDetection;
9+
using Stride.BepuPhysics.Components;
910
using Stride.BepuPhysics.Constraints;
1011
using Stride.BepuPhysics.Definitions;
1112
using Stride.BepuPhysics.Definitions.Colliders;
@@ -397,6 +398,103 @@ int TestRemovalUnsafe(BepuSimulation simulation)
397398
}
398399
}
399400

401+
[Fact]
402+
public static void OnSimulationUpdateRemovalTest()
403+
{
404+
var game = new GameTest();
405+
game.Script.AddTask(async () =>
406+
{
407+
game.ScreenShotAutomationEnabled = false;
408+
409+
var listOfUpdate = new List<int>();
410+
var c2 = new StaticComponent { Collider = new CompoundCollider { Colliders = { new BoxCollider() } } };
411+
412+
var allEntities = game.SceneSystem.SceneInstance.RootScene.Entities;
413+
414+
Entity a, b, c, d, e;
415+
allEntities.AddRange(new[]
416+
{
417+
a = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(0); } } },
418+
b = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(1); } } },
419+
c = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(2); } } },
420+
d = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(3); } } },
421+
new Entity { c2 },
422+
});
423+
424+
var simulation = allEntities[0].GetSimulation();
425+
426+
427+
// First test, check if every component received its update
428+
{
429+
await simulation.AfterUpdate();
430+
431+
Assert.Equal([0, 1, 2, 3], listOfUpdate);
432+
}
433+
434+
435+
// Second test, check if removing works appropriately
436+
{
437+
listOfUpdate.Clear();
438+
allEntities.Remove(b);
439+
allEntities.Remove(c);
440+
441+
await simulation.AfterUpdate();
442+
443+
// We've removed the second and third before running sim,
444+
// so only the first and fourth should report as having received the update
445+
Assert.Equal([0, 3], listOfUpdate);
446+
}
447+
448+
449+
// Clearing multiple listeners while running a listener
450+
{
451+
listOfUpdate.Clear();
452+
allEntities.Remove(a);
453+
allEntities.Remove(d);
454+
455+
var toRemove = new List<Entity>();
456+
allEntities.AddRange(new[]
457+
{
458+
a = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(0); } } },
459+
b = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(1); } } },
460+
c = new Entity
461+
{
462+
new SimUpdateListener
463+
{
464+
SimUpdate = () =>
465+
{
466+
listOfUpdate.Add(2);
467+
foreach (var entity in toRemove)
468+
allEntities.Remove(entity);
469+
}
470+
}
471+
},
472+
d = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(3); } } },
473+
e = new Entity { new SimUpdateListener { SimUpdate = () => { listOfUpdate.Add(4); } } }
474+
});
475+
476+
toRemove.AddRange([a, b, c, e]);
477+
478+
await simulation.AfterUpdate();
479+
480+
// We've removed a, b and c right after running them, e before it could run and haven't removed d
481+
Assert.Equal([0, 1, 2, 3], listOfUpdate);
482+
}
483+
484+
game.Exit();
485+
});
486+
RunGameTest(game);
487+
}
488+
489+
private class SimUpdateListener : ScriptComponent, ISimulationUpdate
490+
{
491+
public Action? SimUpdate, AfterSimUpdate;
492+
493+
public void SimulationUpdate(BepuSimulation simulation, float simTimeStep) => SimUpdate?.Invoke();
494+
495+
public void AfterSimulationUpdate(BepuSimulation simulation, float simTimeStep) => AfterSimUpdate?.Invoke();
496+
}
497+
400498
private class ContactEvents : IContactHandler
401499
{
402500
public required bool NoContactResponse { get; init; }

sources/engine/Stride.BepuPhysics/Stride.BepuPhysics/BepuSimulation.cs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -909,24 +909,49 @@ public static void AfterSimulationUpdate(Dictionary<Type, Elider> handlers, Bepu
909909
private sealed class Handler<T> : Elider where T : ISimulationUpdate // This class get specialized to a concrete type
910910
{
911911
private List<T> _abstraction = [];
912-
protected override void Add(ISimulationUpdate obj) => _abstraction.Add((T)obj);
913-
protected override bool Remove(ISimulationUpdate obj) => _abstraction.Remove((T)obj);
912+
private int _processingIndex, _removedWhileProcessing;
913+
protected override void Add(ISimulationUpdate obj)
914+
{
915+
_abstraction.Add((T)obj);
916+
}
917+
918+
protected override bool Remove(ISimulationUpdate obj)
919+
{
920+
int idx = _abstraction.IndexOf((T)obj);
921+
if (idx < 0)
922+
return false;
923+
924+
// If this occured as part of a SimulationUpdate,
925+
// ensure execution of said update continues from the right component
926+
if (idx <= _processingIndex)
927+
{
928+
_removedWhileProcessing++;
929+
_processingIndex--;
930+
}
931+
932+
_abstraction.RemoveAt(idx);
933+
return true;
934+
}
935+
914936
protected override void SimulationUpdate(BepuSimulation sim, float deltaTime)
915937
{
916-
foreach (var abstraction in _abstraction)
917-
abstraction.SimulationUpdate(sim, deltaTime);
938+
_processingIndex = _removedWhileProcessing = 0;
939+
for (int i = 0; i < _abstraction.Count; i += 1 - _removedWhileProcessing, _removedWhileProcessing = 0, _processingIndex = i)
940+
_abstraction[i].SimulationUpdate(sim, deltaTime);
918941
}
942+
919943
protected override void AfterSimulationUpdate(BepuSimulation sim, float deltaTime)
920944
{
921-
foreach (var abstraction in _abstraction)
922-
abstraction.AfterSimulationUpdate(sim, deltaTime);
945+
_processingIndex = _removedWhileProcessing = 0;
946+
for (int i = 0; i < _abstraction.Count; i += 1 - _removedWhileProcessing, _removedWhileProcessing = 0, _processingIndex = i)
947+
_abstraction[i].AfterSimulationUpdate(sim, deltaTime);
923948
}
924949
}
925950
}
926951

927952
internal class AwaitRunner
928953
{
929-
private object _addLock = new();
954+
private Lock _addLock = new();
930955
private List<Action> _scheduled = new();
931956
private List<Action> _processed = new();
932957

0 commit comments

Comments
 (0)