// Copyright (c) 2017 Kastellanos Nikolaos /* Original source Farseer Physics Engine: * Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com * Microsoft Permissive License (Ms-PL) v1.1 */ /* * Farseer Physics Engine: * Copyright (c) 2012 Ian Qvist * * Original source Box2D: * Copyright (c) 2006-2011 Erin Catto http://www.box2d.org * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; using JetBrains.Annotations; using Microsoft.Extensions.ObjectPool; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Dynamics.Contacts; using Robust.Shared.Physics.Events; using Robust.Shared.Threading; using Robust.Shared.Utility; namespace Robust.Shared.Physics.Systems; public abstract partial class SharedPhysicsSystem { // TODO: Jesus we should really have a test for this /// /// Ordering is under /// uses enum to work out which collision evaluation to use. /// private static Contact.ContactType[,] _registers = { { // Circle register Contact.ContactType.Circle, Contact.ContactType.EdgeAndCircle, Contact.ContactType.PolygonAndCircle, Contact.ContactType.ChainAndCircle, }, { // Edge register Contact.ContactType.EdgeAndCircle, Contact.ContactType.NotSupported, // Edge Contact.ContactType.EdgeAndPolygon, Contact.ContactType.NotSupported, // Chain }, { // Polygon register Contact.ContactType.PolygonAndCircle, Contact.ContactType.EdgeAndPolygon, Contact.ContactType.Polygon, Contact.ContactType.ChainAndPolygon, }, { // Chain register Contact.ContactType.ChainAndCircle, Contact.ContactType.NotSupported, // Edge Contact.ContactType.ChainAndPolygon, Contact.ContactType.NotSupported, // Chain } }; private int ContactCount => _activeContacts.Count; private const int ContactPoolInitialSize = 128; private const int ContactsPerThread = 32; private ObjectPool _contactPool = default!; private readonly LinkedList _activeContacts = new(); private sealed class ContactPoolPolicy : IPooledObjectPolicy { private readonly SharedDebugPhysicsSystem _debugPhysicsSystem; private readonly IManifoldManager _manifoldManager; public ContactPoolPolicy(SharedDebugPhysicsSystem debugPhysicsSystem, IManifoldManager manifoldManager) { _debugPhysicsSystem = debugPhysicsSystem; _manifoldManager = manifoldManager; } public Contact Create() { var contact = new Contact(_manifoldManager); #if DEBUG contact._debugPhysics = _debugPhysicsSystem; #endif contact.Manifold = new Manifold(); return contact; } public bool Return(Contact obj) { DebugTools.Assert(obj.Flags is ContactFlags.None or ContactFlags.Deleted); SetContact(obj, false, new Entity(EntityUid.Invalid, null, null), new Entity(EntityUid.Invalid, null, null), string.Empty, string.Empty, null, 0, null, 0); return true; } } private static void SetContact(Contact contact, bool enabled, Entity entA, Entity entB, string fixtureAId, string fixtureBId, Fixture? fixtureA, int indexA, Fixture? fixtureB, int indexB) { var uidA = entA.Owner; var uidB = entB.Owner; contact.Enabled = enabled; contact.IsTouching = false; DebugTools.Assert(contact.Flags is ContactFlags.None or ContactFlags.PreInit or ContactFlags.Deleted); // TOIFlag = false; contact.EntityA = uidA; contact.EntityB = uidB; contact.FixtureAId = fixtureAId; contact.FixtureBId = fixtureBId; contact.FixtureA = fixtureA; contact.FixtureB = fixtureB; contact.BodyA = entA.Comp1; contact.BodyB = entB.Comp1; contact.XformA = entA.Comp2; contact.XformB = entB.Comp2; contact.ChildIndexA = indexA; contact.ChildIndexB = indexB; contact.Manifold.PointCount = 0; //FPE: We only set the friction and restitution if we are not destroying the contact if (fixtureA != null && fixtureB != null) { contact.Friction = MathF.Sqrt(fixtureA.Friction * fixtureB.Friction); contact.Restitution = MathF.Max(fixtureA.Restitution, fixtureB.Restitution); } contact.TangentSpeed = 0; } private void InitializeContacts() { _contactPool = new DefaultObjectPool( new ContactPoolPolicy(_debugPhysics, _manifoldManager), 4096); InitializePool(); EntityManager.EntityQueueDeleted += OnContactEntityQueueDel; } private void ShutdownContacts() { EntityManager.EntityQueueDeleted -= OnContactEntityQueueDel; } private void OnContactEntityQueueDel(EntityUid obj) { // If an entity is queuedeleted then we want to purge its contacts before SimulateWorld runs in the same tick. if (!TryComp(obj, out var physicsComp)) return; DestroyContacts(physicsComp); } private void InitializePool() { var dummy = new Contact[ContactPoolInitialSize]; for (var i = 0; i < ContactPoolInitialSize; i++) { dummy[i] = _contactPool.Get(); } for (var i = 0; i < ContactPoolInitialSize; i++) { _contactPool.Return(dummy[i]); } } private Contact CreateContact( Entity entA, Entity entB, string fixtureAId, string fixtureBId, Fixture fixtureA, int indexA, Fixture fixtureB, int indexB) { var type1 = fixtureA.Shape.ShapeType; var type2 = fixtureB.Shape.ShapeType; DebugTools.Assert(ShapeType.Unknown < type1 && type1 < ShapeType.TypeCount); DebugTools.Assert(ShapeType.Unknown < type2 && type2 < ShapeType.TypeCount); // Pull out a spare contact object var contact = _contactPool.Get(); DebugTools.Assert(contact.Flags is ContactFlags.None or ContactFlags.Deleted); contact.Flags = ContactFlags.PreInit; // Edge+Polygon is non-symmetrical due to the way Erin handles collision type registration. if ((type1 >= type2 || (type1 == ShapeType.Edge && type2 == ShapeType.Polygon)) && !(type2 == ShapeType.Edge && type1 == ShapeType.Polygon)) { SetContact(contact, true, entA, entB, fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB); } else { SetContact(contact, true, entB, entA, fixtureBId, fixtureAId, fixtureB, indexB, fixtureA, indexA); } contact.Type = _registers[(int)type1, (int)type2]; return contact; } /// /// Try to create a contact between these 2 fixtures. /// internal void AddPair( Entity entA, Entity entB, string fixtureAId, string fixtureBId, Fixture fixtureA, int indexA, Fixture fixtureB, int indexB, PhysicsComponent bodyA, PhysicsComponent bodyB, ContactFlags flags = ContactFlags.None) { // Broadphase has already done the faster check for collision mask / layers // so no point duplicating DebugTools.Assert(!fixtureA.Contacts.ContainsKey(fixtureB), $"{ToPrettyString(entB)} fixture {fixtureBId} was already in contact with {ToPrettyString(entA)} fixture {fixtureAId}"); DebugTools.Assert(!fixtureB.Contacts.ContainsKey(fixtureA), $"{ToPrettyString(entA)} fixture {fixtureAId} was already in contact with {ToPrettyString(entB)} fixture {fixtureBId}"); var xformA = entA.Comp2; var xformB = entB.Comp2; // Does a joint override collision? Is at least one body dynamic? if (!ShouldCollideSlow(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB)) return; // Call the factory. var contact = CreateContact((entA.Owner, entA.Comp1, entA.Comp2), (entB.Owner, entB.Comp1, entB.Comp2), fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB); contact.Flags = flags; // Contact creation may swap fixtures. var fixA = contact.FixtureA!; var fixB = contact.FixtureB!; var bodA = contact.BodyA!; var bodB = contact.BodyB!; // Insert into world _activeContacts.AddLast(contact.MapNode); // Connect to body A DebugTools.Assert(!fixA.Contacts.ContainsKey(fixB), $"{ToPrettyString(contact.EntityB)} fixture {contact.FixtureBId} was already added to contacts of {ToPrettyString(contact.EntityA)} fixture {contact.FixtureAId}"); fixA.Contacts.Add(fixB, contact); bodA.Contacts.AddLast(contact.BodyANode); // Connect to body B DebugTools.Assert(!fixB.Contacts.ContainsKey(fixA), $"{ToPrettyString(contact.EntityA)} fixture {contact.FixtureAId} was already added to contacts of {ToPrettyString(contact.EntityB)} fixture {contact.FixtureBId}"); fixB.Contacts.Add(fixA, contact); bodB.Contacts.AddLast(contact.BodyBNode); // If it's a spawned static ent then need to wake any contacting entities. // The issue is that static ents can never be awake and if it spawns on an asleep entity never gets a contact. // Checking only bodyA should be okay because if bodyA is the other ent (i.e. dynamic / kinematic) then it should already be awake. if (bodyA.BodyType == BodyType.Static && !bodyB.Awake) { WakeBody(entB.Owner, body: bodyB); } } /// /// Go through the cached broadphase movement and update contacts. /// internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB, ContactFlags flags = ContactFlags.None) { AddPair((proxyA.Entity, proxyA.Body, proxyA.Xform), (proxyB.Entity, proxyB.Body, proxyB.Xform), fixtureAId, fixtureBId, proxyA.Fixture, proxyA.ChildIndex, proxyB.Fixture, proxyB.ChildIndex, proxyA.Body, proxyB.Body, flags: flags); } internal static bool ShouldCollide(Fixture fixtureA, Fixture fixtureB) { return !((fixtureA.CollisionMask & fixtureB.CollisionLayer) == 0x0 && (fixtureB.CollisionMask & fixtureA.CollisionLayer) == 0x0); } public void DestroyContact(Contact contact) { DestroyContact(contact, null, out _); } internal void DestroyContact(Contact contact, LinkedListNode? node, out LinkedListNode? next) { // EndCollideEvent may cause knock on effects that cause contacts to be destroyed. // This check prevents us from trying to destroy a contact that is already being, or already has been, destroyed. if ((contact.Flags & (ContactFlags.Deleting | ContactFlags.Deleted)) != 0x0) { next = node?.Next; return; } DebugTools.Assert((contact.Flags & ContactFlags.PreInit) == 0); // Contact flag might be None here as CollideContacts() might destroy the contact after having removed the PreInit flag var fixtureA = contact.FixtureA!; var fixtureB = contact.FixtureB!; var bodyA = contact.BodyA!; var bodyB = contact.BodyB!; var aUid = contact.EntityA; var bUid = contact.EntityB; contact.Flags |= ContactFlags.Deleting; if (contact.IsTouching) { 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); } if (contact.Manifold.PointCount > 0 && contact.FixtureA?.Hard == true && contact.FixtureB?.Hard == true) { if (bodyA.CanCollide && !TerminatingOrDeleted(aUid)) SetAwake((aUid, bodyA), true); if (bodyB.CanCollide && !TerminatingOrDeleted(bUid)) SetAwake((bUid, bodyB), true); } // Fetch next node AFTER all event raising has finished. // This ensures that we actually get the next node in case the linked list was modified by the events that were // raised next = node?.Next; // Remove from the world _activeContacts.Remove(contact.MapNode); // Remove from body 1 DebugTools.Assert(fixtureA.Contacts.ContainsKey(fixtureB)); fixtureA.Contacts.Remove(fixtureB); DebugTools.Assert(bodyA.Contacts.Contains(contact.BodyANode.Value)); bodyA.Contacts.Remove(contact.BodyANode); // Remove from body 2 DebugTools.Assert(fixtureB.Contacts.ContainsKey(fixtureA)); fixtureB.Contacts.Remove(fixtureA); DebugTools.Assert(bodyB.Contacts.Contains(contact.BodyBNode.Value)); bodyB.Contacts.Remove(contact.BodyBNode); contact.Flags = ContactFlags.Deleted; _contactPool.Return(contact); } internal void CollideContacts() { // Due to the fact some contacts may be removed (and we need to update this array as we iterate). // the length may not match the actual contact count, hence we track the index. var contacts = ArrayPool.Shared.Rent(ContactCount); var index = 0; // Can be changed while enumerating // TODO: check for null instead? // Work out which contacts are still valid before we decide to update manifolds. var node = _activeContacts.First; while (node != null) { var contact = node.Value; node = node.Next; // It's possible the contact was destroyed by content in which case we just skip it. if (!contact.Enabled) continue; // No longer pre-init and can be used in the solver. contact.Flags &= ~ContactFlags.PreInit; Fixture fixtureA = contact.FixtureA!; Fixture fixtureB = contact.FixtureB!; int indexA = contact.ChildIndexA; int indexB = contact.ChildIndexB; var bodyA = contact.BodyA!; var bodyB = contact.BodyB!; var uidA = contact.EntityA; var uidB = contact.EntityB; // Do not try to collide disabled bodies if (!bodyA.CanCollide || !bodyB.CanCollide) { DestroyContact(contact); continue; } var xformA = contact.XformA!; var xformB = contact.XformB!; if (xformA.MapID == MapId.Nullspace || xformB.MapID == MapId.Nullspace) { DestroyContact(contact); continue; } // Is this contact flagged for filtering? if ((contact.Flags & ContactFlags.Filter) != 0x0) { // Check default filtering if (!ShouldCollide(fixtureA, fixtureB) || !ShouldCollideSlow(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB) || !ShouldCollideJoints(uidA, uidB)) { DestroyContact(contact); continue; } // Clear the filtering flag. contact.Flags &= ~ContactFlags.Filter; } bool activeA = bodyA.Awake && bodyA.BodyType != BodyType.Static; bool activeB = bodyB.Awake && bodyB.BodyType != BodyType.Static; // At least one body must be awake and it must be dynamic or kinematic. if (activeA == false && activeB == false) { continue; } if (xformA.MapUid == null || xformA.MapUid != xformB.MapUid) { DestroyContact(contact); continue; } // Special-case grid contacts. if ((contact.Flags & ContactFlags.Grid) != 0x0) { var gridABounds = fixtureA.Shape.ComputeAABB(GetPhysicsTransform(uidA, xformA), 0); var gridBBounds = fixtureB.Shape.ComputeAABB(GetPhysicsTransform(uidB, xformB), 0); if (!gridABounds.Intersects(gridBBounds)) { DestroyContact(contact); } else { // Grid contact is still alive. contact.Flags &= ~ContactFlags.Island; if (index >= contacts.Length) { Log.Error($"Insufficient contact length at 388! Index {index} and length is {contacts.Length}. Tell Sloth"); } contacts[index++] = contact; } continue; } if (indexA >= fixtureA.Proxies.Length) { Log.Error($"Found invalid contact index of {indexA} on {contact.FixtureAId} / {ToPrettyString(uidA)}, expected {fixtureA.Proxies.Length}"); DestroyContact(contact); continue; } if (indexB >= fixtureB.Proxies.Length) { Log.Error($"Found invalid contact index of {indexB} on {contact.FixtureBId} / {ToPrettyString(uidB)}, expected {fixtureB.Proxies.Length}"); DestroyContact(contact); continue; } var proxyA = fixtureA.Proxies[indexA]; var proxyB = fixtureB.Proxies[indexB]; var broadphaseA = xformA.Broadphase?.Uid; var broadphaseB = xformB.Broadphase?.Uid; var overlap = false; // We can have cross-broadphase proxies hence need to change them to worldspace if (broadphaseA != null && broadphaseB != null) { if (broadphaseA == broadphaseB) { overlap = proxyA.AABB.Intersects(proxyB.AABB); } else { // TODO maybe change this? Needs benchmarking. // Instead of transforming both boxes (which enlarges both aabbs), maybe just transform one box. // I.e. use (matrixA * invMatrixB).TransformBox(). Or (invMatrixB * matrixA), whichever is correct. // Alternatively, maybe just directly construct the relative transform matrix? var proxyAWorldAABB = _transform.GetWorldMatrix(XformQuery.GetComponent(broadphaseA.Value)).TransformBox(proxyA.AABB); var proxyBWorldAABB = _transform.GetWorldMatrix(XformQuery.GetComponent(broadphaseB.Value)).TransformBox(proxyB.AABB); overlap = proxyAWorldAABB.Intersects(proxyBWorldAABB); } } // Here we destroy contacts that cease to overlap in the broad-phase. if (!overlap) { DestroyContact(contact); continue; } // Contact is actually going to live for manifold generation and solving. // This can also short-circuit above for grid contacts. contact.Flags &= ~ContactFlags.Island; if (index >= contacts.Length) { Log.Error($"Insufficient contact length at 429! Index {index} and length is {contacts.Length}. Tell Sloth"); } contacts[index++] = contact; } var status = ArrayPool.Shared.Rent(index); var worldPoints = ArrayPool>.Shared.Rent(index); // Update contacts all at once. BuildManifolds(contacts, index, status, worldPoints); // Single-threaded so content doesn't need to worry about race conditions. for (var i = 0; i < index; i++) { if (i >= contacts.Length) { Log.Error($"Invalid contact length for contact events!"); continue; } var contact = contacts[i]; // It's possible the contact was disabled above if DestroyContact lead to even more being destroyed. if (!contact.Enabled) { continue; } RunContactEvents(status[i], contact, worldPoints[i]); } ArrayPool.Shared.Return(contacts); ArrayPool.Shared.Return(status); ArrayPool>.Shared.Return(worldPoints); } internal void RunContactEvents(ContactStatus status, Contact contact, FixedArray4 worldPoint) { switch (status) { case ContactStatus.StartTouching: { if (!contact.IsTouching) return; var fixtureA = contact.FixtureA!; var fixtureB = contact.FixtureB!; var bodyA = contact.BodyA!; var bodyB = contact.BodyB!; var uidA = contact.EntityA; var uidB = contact.EntityB; var points = new FixedArray2(worldPoint._00, worldPoint._01); var worldNormal = worldPoint._02; 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); break; } case ContactStatus.Touching: break; case ContactStatus.EndTouching: { var fixtureA = contact.FixtureA; var fixtureB = contact.FixtureB; // If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted) // then we'll just skip the EndCollide. if (fixtureA == null || fixtureB == null) return; var bodyA = contact.BodyA!; var bodyB = contact.BodyB!; var uidA = contact.EntityA; var uidB = contact.EntityB; 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); break; } case ContactStatus.NoContact: break; default: throw new ArgumentOutOfRangeException(); } } private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, FixedArray4[] worldPoints) { if (count == 0) return; var wake = ArrayPool.Shared.Rent(count); _parallel.ProcessNow(new ManifoldsJob() { Physics = this, Status = status, WorldPoints = worldPoints, Contacts = contacts, Wake = wake, }, count); // Can't do this during UpdateContacts due to IoC threading issues. for (var i = 0; i < count; i++) { var shouldWake = wake[i]; if (!shouldWake) continue; var contact = contacts[i]; var bodyA = contact.BodyA!; var bodyB = contact.BodyB!; var aUid = contact.EntityA; var bUid = contact.EntityB; SetAwake((aUid, bodyA), true); SetAwake((bUid, bodyB), true); } ArrayPool.Shared.Return(wake); } private record struct ManifoldsJob : IParallelRobustJob { public int BatchSize => ContactsPerThread; public SharedPhysicsSystem Physics; public Contact[] Contacts; public ContactStatus[] Status; public FixedArray4[] WorldPoints; public bool[] Wake; public void Execute(int index) { Physics.UpdateContact(Contacts, index, Status, Wake, WorldPoints); } } private void UpdateContact(Contact[] contacts, int index, ContactStatus[] status, bool[] wake, FixedArray4[] worldPoints) { var contact = contacts[index]; // TODO: Temporary measure. When Box2D 3.0 comes out expect a major refactor // of everything // It's okay past sloth it can't hurt you anymore. // This can happen if DestroyContact is called and content deletes contacts that were already processed. if (!contact.Enabled) { status[index] = ContactStatus.NoContact; wake[index] = false; return; } var uidA = contact.EntityA; var uidB = contact.EntityB; var bodyATransform = GetPhysicsTransform(uidA); var bodyBTransform = GetPhysicsTransform(uidB); var contactStatus = contact.Update(bodyATransform, bodyBTransform, out wake[index]); status[index] = contactStatus; if (contactStatus == ContactStatus.StartTouching) { var points = new FixedArray4(); contact.GetWorldManifold(bodyATransform, bodyBTransform, out var worldNormal, points.AsSpan); // Use the 3rd Vector2 as the world normal, 4th is blank. points._02 = worldNormal; worldPoints[index] = points; } } /// /// Is there a joint blocking collision between these bodies. /// internal bool ShouldCollideJoints(Entity entA, Entity entB) { // Does a joint prevent collision? // if one of them doesn't have jointcomp then they can't share a common joint. // otherwise, only need to iterate over the joints of one component as they both store the same joint. if (JointQuery.Resolve(entA.Owner, ref entA.Comp, false) && JointQuery.HasComp(entB)) { foreach (var joint in entA.Comp.Joints.Values) { // Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking. if (!joint.CollideConnected && (entB.Owner == joint.BodyAUid || entB.Owner == joint.BodyBUid)) return false; } } return true; } /// /// Used to prevent bodies from colliding; may lie depending on joints. /// internal bool ShouldCollideSlow( EntityUid uid, EntityUid other, PhysicsComponent body, PhysicsComponent otherBody, Fixture fixture, Fixture otherFixture, TransformComponent xform, TransformComponent otherXform) { if (((body.BodyType & (BodyType.Kinematic | BodyType.Static)) != 0 && (otherBody.BodyType & (BodyType.Kinematic | BodyType.Static)) != 0) || // Kinematic controllers can't collide. (fixture.Hard && body.BodyType == BodyType.KinematicController && otherFixture.Hard && otherBody.BodyType == BodyType.KinematicController)) { return false; } if (fixture.Hard && otherFixture.Hard) { // Prevent self-propelling entities. I.e., prevent a fixture on a static child entity from propelling the // parent forwards. // TODO Add recursive parent checks, without somehow killing performance. if (uid == other) return false; if (other == xform.ParentUid && body.BodyType == BodyType.Static) return false; if (uid == otherXform.ParentUid && otherBody.BodyType == BodyType.Static) return false; } // Joints already handled before the contact pair is made. var preventCollideMessage = new PreventCollideEvent(uid, other, body, otherBody, fixture, otherFixture); RaiseLocalEvent(uid, ref preventCollideMessage); if (preventCollideMessage.Cancelled) return false; preventCollideMessage = new PreventCollideEvent(other, uid, otherBody, body, otherFixture, fixture); RaiseLocalEvent(other, ref preventCollideMessage); if (preventCollideMessage.Cancelled) return false; return true; } /// /// Will destroy all contacts and queue for rebuild. /// Useful if you have one that may no longer be relevant and don't want to destroy it directly. /// public void RegenerateContacts(Entity entity) { if (!PhysicsQuery.Resolve(entity.Owner, ref entity.Comp)) return; _broadphase.RegenerateContacts(entity); } /// /// Returns the number of touching contacts this entity has. /// /// Fixture we should ignore if applicable [Pure] public int GetTouchingContacts(Entity entity, string? ignoredFixtureId = null) { if (!_fixturesQuery.Resolve(entity.Owner, ref entity.Comp)) return 0; var count = 0; foreach (var (id, fixture) in entity.Comp.Fixtures) { if (ignoredFixtureId == id) continue; foreach (var contact in fixture.Contacts.Values) { if (!contact.IsTouching) continue; count++; } } return count; } /// /// Returns all of this entity's contacts. /// [Pure] public ContactEnumerator GetContacts(Entity entity, bool includeDeleting = false) { _fixturesQuery.Resolve(entity.Owner, ref entity.Comp); return new ContactEnumerator(entity.Comp, includeDeleting); } } public record struct ContactEnumerator { public static readonly ContactEnumerator Empty = new(null); private Dictionary.ValueCollection.Enumerator _fixtureEnumerator; private Dictionary.ValueCollection.Enumerator _contactEnumerator; /// /// Also include deleting contacts. /// This typically includes the current contact if you're invoking this in the eventbus for an EndCollideEvent. /// public bool IncludeDeleting; public ContactEnumerator(FixturesComponent? fixtures, bool includeDeleting = false) { IncludeDeleting = includeDeleting; if (fixtures == null || fixtures.Fixtures.Count == 0) { this = Empty; return; } _fixtureEnumerator = fixtures.Fixtures.Values.GetEnumerator(); _fixtureEnumerator.MoveNext(); _contactEnumerator = _fixtureEnumerator.Current.Contacts.Values.GetEnumerator(); } public bool MoveNext([NotNullWhen(true)] out Contact? contact) { if (!_contactEnumerator.MoveNext()) { if (!_fixtureEnumerator.MoveNext()) { contact = null; return false; } _contactEnumerator = _fixtureEnumerator.Current.Contacts.Values.GetEnumerator(); return MoveNext(out contact); } contact = _contactEnumerator.Current; if (!IncludeDeleting && contact.Deleting) return MoveNext(out contact); return true; } } internal enum ContactStatus : byte { NoContact = 0, StartTouching = 1, Touching = 2, EndTouching = 3, }