Double-buffer contact events (#6295)

* Add

* a

* Fix test

* note

* Fix reflection
This commit is contained in:
metalgearsloth
2025-11-13 21:28:13 +11:00
committed by GitHub
parent 015bf3318b
commit cd59027089
6 changed files with 138 additions and 8 deletions

View File

@@ -35,7 +35,7 @@ END TEMPLATE-->
### Breaking changes
*None yet*
* StartCollide and EndCollide events are now buffered until the end of physics substeps instead of being raised during the CollideContacts step. EndCollide events are double-buffered and any new ones raised while the events are being dispatched will now go out on the next tick / substep.
### New features

View File

@@ -106,6 +106,7 @@ public sealed partial class PhysicsSystem
}
UpdateIsTouching(contacts);
DispatchEvents();
}
/// <summary>

View File

@@ -353,8 +353,8 @@ public abstract partial class SharedPhysicsSystem
{
var ev1 = new EndCollideEvent(aUid, bUid, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB);
var ev2 = new EndCollideEvent(bUid, aUid, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA);
RaiseLocalEvent(aUid, ref ev1);
RaiseLocalEvent(bUid, ref ev2);
_endCollideEvents[_endEventIndex].Add(ev1);
_endCollideEvents[_endEventIndex].Add(ev2);
}
if (contact.Manifold.PointCount > 0 && contact.FixtureA?.Hard == true && contact.FixtureB?.Hard == true)
@@ -603,8 +603,9 @@ public abstract partial class SharedPhysicsSystem
var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal);
var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal);
RaiseLocalEvent(uidA, ref ev1, true);
RaiseLocalEvent(uidB, ref ev2, true);
_startCollideEvents.Add(ev1);
_startCollideEvents.Add(ev2);
break;
}
case ContactStatus.Touching:
@@ -626,8 +627,8 @@ public abstract partial class SharedPhysicsSystem
var ev1 = new EndCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB);
var ev2 = new EndCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA);
RaiseLocalEvent(uidA, ref ev1);
RaiseLocalEvent(uidB, ref ev2);
_endCollideEvents[_endEventIndex].Add(ev1);
_endCollideEvents[_endEventIndex].Add(ev2);
break;
}
case ContactStatus.NoContact:

View File

@@ -0,0 +1,30 @@
namespace Robust.Shared.Physics.Systems;
public abstract partial class SharedPhysicsSystem
{
protected void DispatchEvents()
{
// This will raise events even if the contact is gone which is fine I think
// because otherwise we may get issues with events not getting raised in some cases.
// Swap the end index over so new events happen next tick.
_endEventIndex = 1 - _endEventIndex;
// Raises all the buffered events once physics step is done.
foreach (var ev in _startCollideEvents)
{
var elem = ev;
RaiseLocalEvent(ev.OurEntity, ref elem);
}
_startCollideEvents.Clear();
foreach (var ev in _endCollideEvents[1 - _endEventIndex])
{
var elem = ev;
RaiseLocalEvent(ev.OurEntity, ref elem);
}
_endCollideEvents[1 - _endEventIndex].Clear();
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Prometheus;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
@@ -50,6 +51,25 @@ namespace Robust.Shared.Physics.Systems
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly CollisionWakeSystem _wakeSystem = default!;
/*
* Events
*/
// Buffer events to avoid enumeration issues.
private readonly List<StartCollideEvent> _startCollideEvents = new();
// Double-buffer end-collide events like box2d-v3 because we can raise these while destroying contacts
// whereas with start events they only ever get made during the collision step.
private readonly List<EndCollideEvent>[]
_endCollideEvents =
[
new List<EndCollideEvent>(),
new List<EndCollideEvent>()
];
private int _endEventIndex = 0;
private int _substeps;
/// <summary>
@@ -302,6 +322,8 @@ namespace Robust.Shared.Physics.Systems
Step(frameTime, prediction);
DispatchEvents();
var updateAfterSolve = new PhysicsUpdateAfterSolveEvent(prediction, frameTime);
RaiseLocalEvent(ref updateAfterSolve);

View File

@@ -30,14 +30,90 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
public sealed class Collision_Test
public sealed partial class Collision_Test
{
/// <summary>
/// Asserts that collision events even go out.
/// </summary>
[Test]
public void CollisionEvents()
{
var sim = RobustServerSimulation.NewSimulation()
.RegisterEntitySystems(fac =>
{
fac.LoadExtraSystemType<CollisionSubSystem>();
})
.RegisterComponents(fac =>
{
fac.RegisterClass<CollisionSubComponent>();
})
.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var broadphase = entManager.System<SharedBroadphaseSystem>();
var fixtures = entManager.System<FixtureSystem>();
var physics = entManager.System<SharedPhysicsSystem>();
var sub = entManager.System<CollisionSubSystem>();
var map = sim.CreateMap();
var bodyAUid = entManager.SpawnAttachedTo(null, new EntityCoordinates(map.Uid, Vector2.Zero));
var bodyBUid = entManager.SpawnAttachedTo(null, new EntityCoordinates(map.Uid, Vector2.Zero));
var bodyA = entManager.AddComponent<PhysicsComponent>(bodyAUid);
var bodyB = entManager.AddComponent<PhysicsComponent>(bodyBUid);
entManager.AddComponent<CollisionSubComponent>(bodyAUid);
physics.SetBodyType(bodyAUid, BodyType.Dynamic);
physics.SetBodyType(bodyBUid, BodyType.Dynamic);
fixtures.CreateFixture(bodyAUid, "fix1", new Fixture(new PhysShapeCircle(0.5f), 1, 1, true));
fixtures.CreateFixture(bodyBUid, "fix1", new Fixture(new PhysShapeCircle(0.5f), 1, 1, true));
physics.SetCanCollide(bodyAUid, true, body: bodyA);
physics.SetCanCollide(bodyBUid, true, body: bodyB);
physics.WakeBody(bodyAUid);
Assert.That(physics.IsHardCollidable(bodyAUid, bodyBUid));
broadphase.FindNewContacts();
Assert.That(bodyA.ContactCount, Is.EqualTo(1));
Assert.That(bodyB.ContactCount, Is.EqualTo(1));
physics.Update(0.016f);
Assert.That(sub._counter, Is.GreaterThan(0));
}
[Reflect(false)]
private sealed class CollisionSubSystem : EntitySystem
{
public int _counter;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CollisionSubComponent, StartCollideEvent>(OnStartCollide);
}
private void OnStartCollide(Entity<CollisionSubComponent> ent, ref StartCollideEvent args)
{
_counter++;
return;
}
}
private sealed partial class CollisionSubComponent : Component
{
}
[Test]
public void TestHardCollidable()
{