< Summary

Line coverage
100%
Covered lines: 262
Uncovered lines: 0
Coverable lines: 262
Total lines: 465
Line coverage: 100%
Branch coverage
100%
Covered branches: 62
Total branches: 62
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Pairs/2D/CollisionPair2D.cs

#LineLine coverage
 1//=======================================================================
 2// CollisionPair2D.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2026–present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using FixedMathSharp;
 9using Gravitas.Colliders;
 10using Gravitas.CollisionHandling;
 11using SwiftCollections;
 12using System;
 13
 14namespace Gravitas;
 15
 16internal sealed partial class CollisionPair2D
 17{
 18    private bool _isColliding;
 19    private ContactWarmStartCache2D _warmStart;
 20    private bool _notificationInProgress;
 21    private bool _separationPending;
 22    private bool _colliderANotified;
 23    private bool _colliderBNotified;
 24    private SolidBody2D? _pendingBodyA;
 25    private SolidBody2D? _pendingBodyB;
 26    private long _lifetimeVersion;
 27
 34228    public CollisionPair2D(LSCollider2D colliderA, LSCollider2D colliderB)
 29    {
 34230        ColliderA = colliderA;
 34231        ColliderB = colliderB;
 34232        Initialize(colliderA, colliderB);
 34233    }
 34
 35    public LSCollider2D ColliderA { get; private set; }
 36
 37    public LSCollider2D ColliderB { get; private set; }
 38
 39    public int Id1 { get; private set; }
 40
 41    public int Id2 { get; private set; }
 42
 43    public CollisionType2D CollisionType { get; private set; }
 44
 45    public int LastFrame { get; private set; } = -1;
 46
 3047    public bool IsColliding => _isColliding;
 48
 8549    internal bool IsNotificationInProgress => _notificationInProgress;
 50
 98851    internal long LifetimeVersion => _lifetimeVersion;
 52
 53    public ContactManifold2D Manifold { get; } = new();
 54
 55    public void Initialize(LSCollider2D colliderA, LSCollider2D colliderB)
 56    {
 35157        _lifetimeVersion++;
 35158        AssignPriority(colliderA, colliderB);
 35159        Id1 = ColliderA.Id;
 35160        Id2 = ColliderB.Id;
 35161        CollisionType = ColliderSettings2D.GetCollisionType(ColliderA.Shape, ColliderB.Shape);
 35162        ResetPairState();
 35163    }
 64
 65    private void AssignPriority(LSCollider2D colliderA, LSCollider2D colliderB)
 66    {
 35167        if (ShouldFirstColliderLead(colliderA, colliderB))
 68        {
 30069            ColliderA = colliderA;
 30070            ColliderB = colliderB;
 30071            return;
 72        }
 73
 5174        ColliderA = colliderB;
 5175        ColliderB = colliderA;
 5176    }
 77
 78    internal static bool ShouldFirstColliderLead(LSCollider2D colliderA, LSCollider2D colliderB)
 79    {
 35180        if (colliderA.Priority != colliderB.Priority)
 4081            return colliderA.Priority > colliderB.Priority;
 82
 31183        SolidBody2D? bodyA = colliderA.Body;
 31184        SolidBody2D? bodyB = colliderB.Body;
 31185        if (bodyA == null || bodyB == null)
 2686            return colliderA.Id <= colliderB.Id;
 87
 28588        if (bodyA.LinearSpeed != bodyB.LinearSpeed)
 3089            return bodyA.LinearSpeed > bodyB.LinearSpeed;
 90
 25591        return colliderA.Id <= colliderB.Id;
 92    }
 93
 94    public void MarkColliding(int frame)
 95    {
 5396        if (!Manifold.HasContact)
 197            return;
 98
 5299        bool changed = MarkCollidingState(frame);
 100
 52101        if (!ColliderA.IsTrigger && !ColliderB.IsTrigger)
 102        {
 29103            CollisionResponse2D.Resolve(this);
 29104            WakeSleepingBodiesForCollision();
 105        }
 106
 52107        NotifyColliders(isColliding: true, changed);
 52108    }
 109
 110    internal void MarkCollidingDeferred(int frame)
 111    {
 504112        if (!Manifold.HasContact)
 1113            return;
 114
 503115        bool changed = MarkCollidingState(frame);
 503116        NotifyColliders(isColliding: true, changed);
 501117    }
 118
 119    public void MarkResting(int frame)
 120    {
 8121        LastFrame = frame;
 8122    }
 123
 124    public void MarkSeparated()
 125    {
 80126        bool wasColliding = _isColliding;
 80127        ResetCollisionState();
 80128        if (!wasColliding)
 2129            return;
 130
 78131        if (_notificationInProgress)
 132        {
 13133            _separationPending = true;
 13134            _pendingBodyA = ColliderA.Body;
 13135            _pendingBodyB = ColliderB.Body;
 13136            return;
 137        }
 138
 65139        NotifyColliders(isColliding: false, isChanged: true);
 65140    }
 141
 142    internal void StoreWarmStartImpulse(ulong contactId, Fixed64 normalImpulse, Fixed64 tangentImpulse) =>
 1035143        _warmStart.Set(contactId, normalImpulse, tangentImpulse);
 144
 145    internal bool TryGetWarmStartImpulse(ulong contactId, out ContactWarmStartImpulse impulse) =>
 1040146        _warmStart.TryGet(contactId, out impulse);
 147
 148    internal void RemoveWarmStartImpulse(ulong contactId) =>
 7149        _warmStart.Remove(contactId);
 150
 2151    internal void ClearWarmStart() => _warmStart.Clear();
 152
 153    private void ResetPairState()
 154    {
 351155        ResetCollisionState();
 351156        _notificationInProgress = false;
 351157        _colliderANotified = false;
 351158        _colliderBNotified = false;
 351159        ClearPendingNotificationState();
 351160    }
 161
 162    private void ResetCollisionState()
 163    {
 431164        _isColliding = false;
 431165        LastFrame = -1;
 431166        Manifold.Reset();
 431167        _warmStart.Clear();
 431168    }
 169
 170    private void NotifyColliders(bool isColliding, bool isChanged)
 171    {
 620172        var registrationA = new ColliderLifetimeToken2D(ColliderA);
 620173        var registrationB = new ColliderLifetimeToken2D(ColliderB);
 620174        LSCollider2D colliderA = registrationA.Collider;
 620175        LSCollider2D colliderB = registrationB.Collider;
 620176        SolidBody2D? bodyA = colliderA.Body;
 620177        SolidBody2D? bodyB = colliderB.Body;
 620178        bool isTriggerPair = colliderA.IsTrigger || colliderB.IsTrigger;
 620179        bool shouldRaiseTriggerA = isTriggerPair && ColliderTriggerEventPolicy.ShouldRaise(colliderA, colliderB);
 620180        bool shouldRaiseTriggerB = isTriggerPair && ColliderTriggerEventPolicy.ShouldRaise(colliderB, colliderA);
 620181        _notificationInProgress = true;
 620182        SwiftList<Exception>? notificationExceptions = null;
 183        try
 184        {
 620185            if (isColliding)
 186            {
 555187                bool notifyEnterA = isChanged || !_colliderANotified;
 555188                _colliderANotified = true;
 555189                colliderA.NotifyContact(
 555190                    colliderB,
 555191                    bodyB,
 555192                    isColliding: true,
 555193                    notifyEnterA,
 555194                    allowInactive: false,
 555195                    registrationA,
 555196                    registrationB,
 555197                    isTriggerPair,
 555198                    shouldRaiseTriggerA);
 554199                if (registrationA.IsActive && registrationB.IsActive && !_separationPending)
 200                {
 544201                    bool notifyEnterB = isChanged || !_colliderBNotified;
 544202                    _colliderBNotified = true;
 544203                    colliderB.NotifyContact(
 544204                        colliderA,
 544205                        bodyA,
 544206                        isColliding: true,
 544207                        notifyEnterB,
 544208                        allowInactive: false,
 544209                        registrationB,
 544210                        registrationA,
 544211                        isTriggerPair,
 544212                        shouldRaiseTriggerB);
 213                }
 214            }
 215            else
 216            {
 65217                NotifySeparation(
 65218                    registrationA,
 65219                    registrationB,
 65220                    bodyA,
 65221                    bodyB,
 65222                    isChanged,
 65223                    allowInactive: false,
 65224                    isTriggerPair,
 65225                    shouldRaiseTriggerA,
 65226                    shouldRaiseTriggerB);
 227            }
 619228        }
 1229        catch (Exception exception)
 230        {
 1231            CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 1232        }
 233
 234        try
 235        {
 620236            EndNotification(
 620237                registrationA,
 620238                registrationB,
 620239                isTriggerPair,
 620240                shouldRaiseTriggerA,
 620241                shouldRaiseTriggerB);
 619242        }
 1243        catch (Exception exception)
 244        {
 1245            CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 1246        }
 247
 620248        CollisionNotificationExceptions.ThrowIfAny(notificationExceptions);
 618249    }
 250
 251    private void EndNotification(
 252        in ColliderLifetimeToken2D registrationA,
 253        in ColliderLifetimeToken2D registrationB,
 254        bool isTriggerPair,
 255        bool shouldRaiseTriggerA,
 256        bool shouldRaiseTriggerB)
 257    {
 258        try
 259        {
 620260            if (!_separationPending)
 607261                return;
 262
 13263            SolidBody2D? bodyA = _pendingBodyA;
 13264            SolidBody2D? bodyB = _pendingBodyB;
 13265            ClearPendingNotificationState();
 13266            NotifySeparation(
 13267                registrationA,
 13268                registrationB,
 13269                bodyA,
 13270                bodyB,
 13271                isChanged: true,
 13272                allowInactive: true,
 13273                isTriggerPair,
 13274                shouldRaiseTriggerA,
 13275                shouldRaiseTriggerB);
 12276        }
 277        finally
 278        {
 620279            ClearPendingNotificationState();
 620280            _notificationInProgress = false;
 620281        }
 619282    }
 283
 284    private void NotifySeparation(
 285        in ColliderLifetimeToken2D registrationA,
 286        in ColliderLifetimeToken2D registrationB,
 287        SolidBody2D? bodyA,
 288        SolidBody2D? bodyB,
 289        bool isChanged,
 290        bool allowInactive,
 291        bool isTriggerPair,
 292        bool shouldRaiseTriggerA,
 293        bool shouldRaiseTriggerB)
 294    {
 78295        bool notifyA = _colliderANotified;
 78296        bool notifyB = _colliderBNotified;
 78297        _colliderANotified = false;
 78298        _colliderBNotified = false;
 78299        SwiftList<Exception>? notificationExceptions = null;
 78300        if (notifyA)
 301        {
 302            try
 303            {
 78304                registrationA.Collider.NotifyContact(
 78305                    registrationB.Collider,
 78306                    bodyB,
 78307                    isColliding: false,
 78308                    isChanged,
 78309                    allowInactive,
 78310                    registrationA,
 78311                    registrationB,
 78312                    isTriggerPair,
 78313                    shouldRaiseTriggerA);
 77314            }
 1315            catch (Exception exception)
 316            {
 1317                CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 1318            }
 319        }
 320
 78321        if (notifyB && registrationB.IsCurrentLifetime)
 322        {
 323            try
 324            {
 68325                registrationB.Collider.NotifyContact(
 68326                    registrationA.Collider,
 68327                    bodyA,
 68328                    isColliding: false,
 68329                    isChanged,
 68330                    allowInactive,
 68331                    registrationB,
 68332                    registrationA,
 68333                    isTriggerPair,
 68334                    shouldRaiseTriggerB);
 67335            }
 1336            catch (Exception exception)
 337            {
 1338                CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 1339            }
 340        }
 341
 78342        CollisionNotificationExceptions.ThrowIfAny(notificationExceptions);
 77343    }
 344
 345    private void ClearPendingNotificationState()
 346    {
 984347        _separationPending = false;
 984348        _pendingBodyA = null;
 984349        _pendingBodyB = null;
 984350    }
 351
 352    private bool MarkCollidingState(int frame)
 353    {
 555354        bool changed = !_isColliding;
 555355        _isColliding = true;
 555356        LastFrame = frame;
 555357        return changed;
 358    }
 359
 360    internal void WakeSleepingBodiesForCollision()
 361    {
 107362        SolidBody2D? bodyA = ColliderA.Body;
 107363        SolidBody2D? bodyB = ColliderB.Body;
 107364        if (bodyA == null || bodyB == null)
 15365            return;
 366
 92367        bool bodyAAwake = !bodyA.IsSleeping;
 92368        bool bodyBAwake = !bodyB.IsSleeping;
 92369        if (bodyA.IsSleeping && bodyBAwake)
 2370            bodyA.Wake();
 92371        if (bodyB.IsSleeping && bodyAAwake)
 1372            bodyB.Wake();
 92373    }
 374}

/home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Pairs/2D/CollisionPair2D.ReplayHash.cs

#LineLine coverage
 1//=======================================================================
 2// CollisionPair2D.ReplayHash.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2026-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using Chronicler;
 9using FixedMathSharp.Chronicler;
 10using Gravitas.CollisionHandling;
 11using Gravitas.Materials;
 12
 13namespace Gravitas;
 14
 15internal sealed partial class CollisionPair2D
 16{
 17    internal void ContributeReplayHash(
 18        ref ChronicleHashWriter writer,
 19        GravitasReplayHashMode mode)
 20    {
 1021        writer.WriteSection("pair.2d", 2);
 1022        writer.WriteInt32(ColliderA.ReplayOrdinal);
 1023        writer.WriteInt32(ColliderB.ReplayOrdinal);
 1024        writer.WriteEnum(CollisionType);
 1025        writer.WriteInt32(LastFrame);
 1026        writer.WriteBool(_isColliding);
 1027        ContributeManifoldReplayHash(ref writer, Manifold);
 1028        ContributeWarmStartReplayHash(ref writer, _warmStart);
 1029    }
 30
 31    private static void ContributeManifoldReplayHash(
 32        ref ChronicleHashWriter writer,
 33        ContactManifold2D manifold)
 34    {
 1035        writer.WriteSection("manifold.2d", 4);
 1036        writer.WriteInt32(manifold.LastUpdatedFrame);
 1037        writer.WriteInt32(manifold.Count);
 4038        for (int i = 0; i < manifold.Count; i++)
 39        {
 1040            ManifoldContact2D contact = manifold[i];
 1041            writer.WriteUInt64(contact.ContactId);
 1042            writer.WriteInt32(contact.FeatureNamespaceA);
 1043            writer.WriteVector2d(contact.AnchorA.Origin);
 1044            writer.WriteVector2d(contact.AnchorA.LocalPoint);
 1045            writer.WriteVector2d(contact.AnchorA.LocalDisplacement);
 1046            writer.WriteUInt64(contact.AnchorA.GetLocalFeatureHash64());
 1047            writer.WriteFixed64(contact.AnchorA.Rotation);
 1048            writer.WriteInt32(contact.FeatureNamespaceB);
 1049            writer.WriteVector2d(contact.AnchorB.Origin);
 1050            writer.WriteVector2d(contact.AnchorB.LocalPoint);
 1051            writer.WriteVector2d(contact.AnchorB.LocalDisplacement);
 1052            writer.WriteUInt64(contact.AnchorB.GetLocalFeatureHash64());
 1053            writer.WriteFixed64(contact.AnchorB.Rotation);
 1054            writer.WriteFixed64(contact.Depth);
 1055            writer.WriteBool(contact.DepthIsClamped);
 1056            writer.WriteVector2d(contact.Normal);
 1057            writer.WriteBool(contact.HasMaterialOverride);
 1058            if (contact.HasMaterialOverride)
 59            {
 460                WriteMaterial(ref writer, contact.MaterialA);
 461                WriteMaterial(ref writer, contact.MaterialB);
 62            }
 63        }
 1064    }
 65
 66    private static void WriteMaterial(ref ChronicleHashWriter writer, PhysicsMaterial material)
 67    {
 868        writer.WriteFixed64(material.StaticFriction);
 869        writer.WriteFixed64(material.DynamicFriction);
 870        writer.WriteFixed64(material.Restitution);
 871        writer.WriteEnum(material.FrictionCombine);
 872        writer.WriteEnum(material.RestitutionCombine);
 873    }
 74
 75    private static void ContributeWarmStartReplayHash(
 76        ref ChronicleHashWriter writer,
 77        ContactWarmStartCache2D warmStart)
 78    {
 1079        writer.WriteSection("warm-start.2d", 1);
 1080        writer.WriteInt32(warmStart.Count);
 2481        for (int i = 0; i < warmStart.Count; i++)
 82        {
 283            writer.WriteUInt64(warmStart.GetContactIdForReplayHash(i));
 284            ContactWarmStartImpulse impulse = warmStart.GetImpulseForReplayHash(i);
 285            writer.WriteVector3d(impulse.Normal);
 286            writer.WriteFixed64(impulse.NormalImpulse);
 287            writer.WriteFixed64(impulse.TangentImpulse);
 288            writer.WriteFixed64(impulse.SecondaryTangentImpulse);
 89        }
 1090    }
 91}

Methods/Properties

.ctor(Gravitas.Colliders.LSCollider2D,Gravitas.Colliders.LSCollider2D)
get_IsColliding()
get_IsNotificationInProgress()
get_LifetimeVersion()
Initialize(Gravitas.Colliders.LSCollider2D,Gravitas.Colliders.LSCollider2D)
AssignPriority(Gravitas.Colliders.LSCollider2D,Gravitas.Colliders.LSCollider2D)
ShouldFirstColliderLead(Gravitas.Colliders.LSCollider2D,Gravitas.Colliders.LSCollider2D)
MarkColliding(System.Int32)
MarkCollidingDeferred(System.Int32)
MarkResting(System.Int32)
MarkSeparated()
StoreWarmStartImpulse(System.UInt64,FixedMathSharp.Fixed64,FixedMathSharp.Fixed64)
TryGetWarmStartImpulse(System.UInt64,Gravitas.CollisionHandling.ContactWarmStartImpulse&)
RemoveWarmStartImpulse(System.UInt64)
ClearWarmStart()
ResetPairState()
ResetCollisionState()
NotifyColliders(System.Boolean,System.Boolean)
EndNotification(Gravitas.Colliders.ColliderLifetimeToken2D&,Gravitas.Colliders.ColliderLifetimeToken2D&,System.Boolean,System.Boolean,System.Boolean)
NotifySeparation(Gravitas.Colliders.ColliderLifetimeToken2D&,Gravitas.Colliders.ColliderLifetimeToken2D&,Gravitas.SolidBody2D,Gravitas.SolidBody2D,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean)
ClearPendingNotificationState()
MarkCollidingState(System.Int32)
WakeSleepingBodiesForCollision()
ContributeReplayHash(Chronicler.ChronicleHashWriter&,Gravitas.GravitasReplayHashMode)
ContributeManifoldReplayHash(Chronicler.ChronicleHashWriter&,Gravitas.CollisionHandling.ContactManifold2D)
WriteMaterial(Chronicler.ChronicleHashWriter&,Gravitas.Materials.PhysicsMaterial)
ContributeWarmStartReplayHash(Chronicler.ChronicleHashWriter&,Gravitas.CollisionHandling.ContactWarmStartCache2D)