< Summary

Information
Class: Gravitas.GravitasCollisionService
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Core/3D/GravitasCollisionService.cs
Line coverage
100%
Covered lines: 242
Uncovered lines: 0
Coverable lines: 242
Total lines: 460
Line coverage: 100%
Branch coverage
100%
Covered branches: 70
Total branches: 70
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/Core/3D/GravitasCollisionService.cs

#LineLine coverage
 1//=======================================================================
 2// GravitasCollisionService.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 GridForge.Grids;
 12using GridForge.Spatial;
 13using GridForge.Utility;
 14using SwiftCollections;
 15using System;
 16using System.Collections.Generic;
 17using System.Runtime.CompilerServices;
 18
 19namespace Gravitas;
 20
 21/// <summary>
 22/// Owns collision partitioning state for one <see cref="GravitasWorldContext"/>.
 23/// </summary>
 24public sealed class GravitasCollisionService
 25{
 26    private const int DefaultPartitionPoolCapacity = 1024;
 127    private static readonly PhysicsPartitionOrderComparer PartitionOrderComparer = new();
 28
 29    private readonly GravitasWorldContext _context;
 508330    private readonly SwiftBucket<PhysicsPartition> _activePartitions = new();
 508331    private readonly SwiftStack<PhysicsPartition> _inactivePartitionPool = new(DefaultPartitionPoolCapacity);
 508332    private readonly SwiftList<Voxel> _coveredVoxels = new();
 508333    private readonly GridTraceScratch _traceScratch = new();
 508334    private readonly SwiftList<PhysicsPartition> _retainedPartitions = new();
 508335    private readonly SwiftList<PhysicsPartition> _distributionPartitions = new();
 508336    private readonly SwiftList<int> _distributionDynamicIds = new();
 508337    private readonly SwiftList<int> _distributionStaticIds = new();
 508338    private readonly SwiftList<int> _planarQueryPartitionColliderIds = new();
 508339    private readonly SwiftHashSet<int> _planarQueryUniqueColliderIds = new();
 508340    private readonly DynamicCcdCandidateIndex2D _planarQueryCandidates =
 508341        new(capacity: 0, supportsUpdates: true);
 42    private readonly Action<PhysicsPartition> _releaseRetainedPartition;
 508343    private readonly object _cullDistributorLock = new();
 44
 45    private int _cullDistributor;
 46    private int _retainedPartitionRetirementCursor;
 47    private uint _planarQueryWorldVersion;
 48
 49    /// <summary>
 50    /// Initializes a new collision service for the supplied context.
 51    /// </summary>
 52    /// <param name="context">The owning world context.</param>
 508353    public GravitasCollisionService(GravitasWorldContext context)
 54    {
 508355        SwiftThrowHelper.ThrowIfNull(context, nameof(context));
 508356        _context = context;
 508357        _releaseRetainedPartition = ReleasePartition;
 508358    }
 59
 60    /// <summary>
 61    /// Gets the owning world context.
 62    /// </summary>
 75588363    public GravitasWorldContext Context => _context;
 64
 65    /// <summary>
 66    /// Gets the collision distribution version for this context.
 67    /// </summary>
 68    public uint Version { get; private set; } = 1;
 69
 70    /// <summary>
 71    /// Gets the number of active partitions in this context.
 72    /// </summary>
 2073    public int ActivePartitionCount => _activePartitions.Count;
 74
 75    /// <summary>
 76    /// Gets the number of inactive partitions currently available for reuse.
 77    /// </summary>
 1578    public int InactivePartitionCount => _inactivePartitionPool.Count;
 79
 1880    internal int RetainedPartitionCount => _retainedPartitions.Count;
 81
 82    internal int CullDistributor
 83    {
 84        get
 85        {
 168286            lock (_cullDistributorLock)
 87            {
 168288                if (_cullDistributor > 1)
 53989                    _cullDistributor = -1;
 90
 168291                return _cullDistributor++;
 92            }
 168293        }
 94    }
 95
 96    /// <summary>
 97    /// Resets transient collision state owned by this context.
 98    /// </summary>
 99    public void Reset()
 100    {
 31101        DetachRetainedPartitions();
 31102        _activePartitions.Clear();
 31103        _coveredVoxels.FastClear();
 31104        _traceScratch.Clear();
 31105        _distributionPartitions.FastClear();
 31106        _distributionDynamicIds.FastClear();
 31107        _distributionStaticIds.FastClear();
 31108        _planarQueryPartitionColliderIds.FastClear();
 31109        _planarQueryUniqueColliderIds.Clear();
 31110        _planarQueryCandidates.Clear();
 31111        _inactivePartitionPool.Clear();
 31112        Version = 1;
 31113        _cullDistributor = 0;
 31114        _retainedPartitionRetirementCursor = 0;
 31115        _planarQueryWorldVersion = 0;
 31116    }
 117
 31118    private void DetachRetainedPartitions() => RetainedPartitionLifecycle.DetachAll(
 31119        _retainedPartitions,
 31120        _context.World,
 31121        this,
 31122        _releaseRetainedPartition,
 31123        nameof(PhysicsPartition),
 31124        "Unable to detach retained physics partition from its voxel during reset.");
 125
 405247126    private void TrackRetainedPartition(PhysicsPartition partition) => RetainedPartitionLifecycle.Track(
 405247127        _retainedPartitions,
 405247128        this,
 405247129        partition,
 405247130        nameof(PhysicsPartition));
 131
 405252132    private void UntrackRetainedPartition(PhysicsPartition partition) => RetainedPartitionLifecycle.Untrack(
 405252133            _retainedPartitions,
 405252134            this,
 405252135            partition,
 405252136            ref _retainedPartitionRetirementCursor);
 137
 138    internal bool IsPartitionRefreshRequired(LSCollider collider) =>
 19685139        !collider.MatchesPartitionGridBounds(collider.BoundsMin, collider.BoundsMax, ResolvePartitionKind(collider));
 140
 43107141    internal int ResolvePartitionKind(LSCollider collider) => (int)GetMobilityKind(collider);
 142
 143    internal bool PartitionObject(
 144        LSCollider collider,
 145        ref SwiftList<WorldVoxelIndex> partitionedCoordinates)
 146    {
 23907147        SwiftThrowHelper.ThrowIfNull(collider, nameof(collider));
 23907148        SwiftThrowHelper.ThrowIfArgument(
 23907149            !ReferenceEquals(collider.Context, _context),
 23907150            nameof(collider),
 23907151            "Collider must belong to this collision service context.");
 152
 23907153        partitionedCoordinates.FastClear();
 154
 23907155        PartitionCoveredVoxels(collider, partitionedCoordinates, GetMobilityKind(collider));
 23907156        if (partitionedCoordinates.Count == 0)
 157        {
 1201158            _planarQueryCandidates.Remove(collider.Id);
 1201159            return false;
 160        }
 161
 22706162        _planarQueryCandidates.AddOrUpdate(
 22706163            collider.Id,
 22706164            new DynamicCcdPlanarBounds(
 22706165                collider.BoundsMin.X,
 22706166                collider.BoundsMin.Z,
 22706167                collider.BoundsMax.X,
 22706168                collider.BoundsMax.Z));
 22706169        return true;
 170    }
 171
 172    private void PartitionCoveredVoxels(
 173        LSCollider collider,
 174        SwiftList<WorldVoxelIndex> partitionedCoordinates,
 175        PhysicsPartitionMobilityKind kind)
 176    {
 23907177        GridWorld world = _context.World;
 23907178        GridTracer.GetCoveredVoxelsInto(
 23907179            world,
 23907180            collider.BoundsMin,
 23907181            collider.BoundsMax,
 23907182            _coveredVoxels,
 23907183            _traceScratch,
 23907184            Fixed64.Half);
 185
 23907186        var traversal = new GridTraversalState(world, GridTraversalPaddingMode.MaxCellEdge);
 2451866187        for (int i = 0; i < _coveredVoxels.Count; i++)
 1202026188            TryPartitionVoxel(collider, partitionedCoordinates, _coveredVoxels[i], ref traversal, kind);
 23907189    }
 190
 191    private void TryPartitionVoxel(
 192        LSCollider collider,
 193        SwiftList<WorldVoxelIndex> partitionedCoordinates,
 194        Voxel voxel,
 195        ref GridTraversalState traversal,
 196        PhysicsPartitionMobilityKind kind)
 197    {
 1202026198        Fixed64 cellEdge = traversal.GetCellEdge(voxel);
 1202026199        if (!collider.IsPositionInBounds(cellEdge, voxel.WorldPosition))
 227763200            return;
 201
 974263202        if (!voxel.TryGetPartition(out PhysicsPartition? partition))
 203        {
 405247204            partition = RentPartition();
 405247205            SwiftThrowHelper.ThrowIfTrue(
 405247206                !voxel.TryAddPartition(partition),
 405247207                nameof(GravitasCollisionService),
 405247208                "Unable to attach 3D physics partition to voxel.");
 209
 405247210            TrackRetainedPartition(partition);
 211        }
 212
 974263213        partitionedCoordinates.Add(voxel.WorldIndex);
 974263214        AddObject(partition!, collider.Id, kind);
 974263215    }
 216
 217    internal bool ClearPartitionedObject(LSCollider collider, bool force = false)
 218    {
 18056219        SwiftThrowHelper.ThrowIfNull(collider, nameof(collider));
 18056220        SwiftThrowHelper.ThrowIfArgument(
 18056221            !ReferenceEquals(collider.Context, _context),
 18056222            nameof(collider),
 18056223            "Collider must belong to this collision service context.");
 224
 18056225        GridWorld world = _context.World;
 18056226        if (!collider.IsPartitioned)
 1227            return false;
 228
 18055229        PhysicsPartitionMobilityKind currentKind = GetMobilityKind(collider);
 18055230        if (!force && collider.MatchesPartitionGridBounds(collider.BoundsMin, collider.BoundsMax, (int)currentKind))
 29231            return false;
 232
 18026233        PhysicsPartitionMobilityKind partitionKind = GetStoredMobilityKind(collider.PartitionKind);
 18026234        _planarQueryCandidates.Remove(collider.Id);
 235
 1050798236        for (int i = 0; i < collider.PartitionCoordinates!.Count; i++)
 237        {
 507373238            WorldVoxelIndex coordinate = collider.PartitionCoordinates[i];
 507373239            if (!world.ActiveGrids.IsAllocated(coordinate.GridIndex)
 507373240                || !world.TryGetVoxel(coordinate, out Voxel? voxel)
 507373241                || !voxel!.TryGetPartition(out PhysicsPartition? partition))
 242            {
 243                continue;
 244            }
 245
 507264246            RemoveObject(partition!, collider.Id, partitionKind);
 247
 248            // Keep the voxel partition attached after it becomes empty. Re-adding
 249            // the same partition type through GridForge carries metadata overhead,
 250            // while an empty PhysicsPartition is inactive and query-invisible.
 251        }
 252
 18026253        collider.MarkUnpartitioned();
 18026254        collider.ClearPartitionCoordinates();
 255
 18026256        return true;
 257    }
 258
 259    internal void QueryPlanarColliderCandidates(
 260        DynamicCcdPlanarBounds bounds,
 261        SwiftList<int> results)
 262    {
 40263        if (_planarQueryWorldVersion != _context.World.Version)
 30264            RebuildPlanarQueryCandidates();
 265
 40266        _planarQueryCandidates.Query(bounds, results);
 40267    }
 268
 269    private void RebuildPlanarQueryCandidates()
 270    {
 30271        _planarQueryCandidates.Clear();
 30272        _planarQueryUniqueColliderIds.Clear();
 4098273        for (int partitionIndex = 0; partitionIndex < _retainedPartitions.Count; partitionIndex++)
 274        {
 2019275            _retainedPartitions[partitionIndex].CopyAllColliderIds(_planarQueryPartitionColliderIds);
 9824276            for (int colliderIndex = 0; colliderIndex < _planarQueryPartitionColliderIds.Count; colliderIndex++)
 277            {
 2893278                int colliderId = _planarQueryPartitionColliderIds[colliderIndex];
 2893279                if (!_planarQueryUniqueColliderIds.Add(colliderId)
 2893280                    || !_context.Physics.TryGetColliderById(colliderId, out LSCollider? collider))
 281                {
 282                    continue;
 283                }
 284
 42285                _planarQueryCandidates.AddOrUpdate(
 42286                    colliderId,
 42287                    new DynamicCcdPlanarBounds(
 42288                        collider!.BoundsMin.X,
 42289                        collider.BoundsMin.Z,
 42290                        collider.BoundsMax.X,
 42291                        collider.BoundsMax.Z));
 292            }
 293        }
 294
 30295        _planarQueryWorldVersion = _context.World.Version;
 30296    }
 297
 298    internal void RefreshPartitionAwakeState(LSCollider collider)
 299    {
 392300        SwiftThrowHelper.ThrowIfNull(collider, nameof(collider));
 392301        SwiftThrowHelper.ThrowIfArgument(
 392302            !ReferenceEquals(collider.Context, _context),
 392303            nameof(collider),
 392304            "Collider must belong to this collision service context.");
 305
 392306        SwiftList<WorldVoxelIndex> coordinates = collider.PartitionCoordinates!;
 392307        SolidBody? body = collider.Body;
 392308        if (collider.IsStatic || body!.IsKinematic)
 2309            return;
 310
 390311        bool awake = body.IsAwakeForCollision;
 390312        GridWorld world = _context.World;
 313
 18190314        for (int i = 0; i < coordinates.Count; i++)
 315        {
 8705316            WorldVoxelIndex coordinate = coordinates[i];
 8705317            if (!world.ActiveGrids.IsAllocated(coordinate.GridIndex)
 8705318                || !world.TryGetVoxel(coordinate, out Voxel? voxel)
 8705319                || !voxel!.TryGetPartition(out PhysicsPartition? partition))
 320            {
 321                continue;
 322            }
 323
 8650324            partition!.SetDynamicObjectAwake(collider.Id, awake);
 325        }
 390326    }
 327
 328    internal void CheckAndDistributeCollisions()
 329    {
 2188330        Version++;
 331
 2188332        _distributionPartitions.FastClear();
 256814333        foreach (PhysicsPartition partition in _activePartitions)
 126219334            _distributionPartitions.Add(partition);
 335
 2188336        _distributionPartitions.SortInPlace(PartitionOrderComparer);
 337
 256814338        for (int i = 0; i < _distributionPartitions.Count; i++)
 339        {
 126219340            _distributionPartitions[i].Distribute(
 126219341                _distributionDynamicIds,
 126219342                _distributionStaticIds);
 343        }
 2188344    }
 345
 2182346    internal void RetireExpiredRetainedPartitions() => RetainedPartitionLifecycle.RetireExpired(
 2182347        _retainedPartitions,
 2182348        _context.World,
 2182349        this,
 2182350        _context.Settings.RetainedPartitionRetirementSweepBudget,
 2182351        _context.FrameCount,
 2182352        _context.Settings.RetainedPartitionTimeToKillFrames,
 2182353        _releaseRetainedPartition,
 2182354        ref _retainedPartitionRetirementCursor);
 355
 356    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 357    private static PhysicsPartitionMobilityKind GetMobilityKind(LSCollider collider)
 358    {
 85069359        if (collider.IsStatic)
 9908360            return PhysicsPartitionMobilityKind.Static;
 361
 75161362        SolidBody? body = collider.Body;
 75161363        return body!.IsKinematic ? PhysicsPartitionMobilityKind.Kinematic : PhysicsPartitionMobilityKind.Dynamic;
 364    }
 365
 366    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 18026367    private static PhysicsPartitionMobilityKind GetStoredMobilityKind(int partitionKind) => partitionKind == (int)Physic
 18026368        ? PhysicsPartitionMobilityKind.Kinematic
 18026369        : partitionKind == (int)PhysicsPartitionMobilityKind.Static
 18026370            ? PhysicsPartitionMobilityKind.Static
 18026371            : PhysicsPartitionMobilityKind.Dynamic;
 372
 373    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 374    private static void AddObject(PhysicsPartition partition, int id, PhysicsPartitionMobilityKind kind)
 375    {
 974263376        if (kind == PhysicsPartitionMobilityKind.Static)
 377        {
 308354378            partition.AddStaticObject(id);
 308354379            return;
 380        }
 381
 665909382        if (kind == PhysicsPartitionMobilityKind.Kinematic)
 383        {
 358934384            partition.AddKinematicObject(id);
 358934385            return;
 386        }
 387
 306975388        partition.AddDynamicObject(id);
 306975389    }
 390
 391    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 392    private static void RemoveObject(PhysicsPartition partition, int id, PhysicsPartitionMobilityKind kind)
 393    {
 507264394        if (kind == PhysicsPartitionMobilityKind.Static)
 395        {
 6151396            partition.RemoveStaticObject(id);
 6151397            return;
 398        }
 399
 501113400        if (kind == PhysicsPartitionMobilityKind.Kinematic)
 401        {
 342278402            partition.RemoveKinematicObject(id);
 342278403            return;
 404        }
 405
 158835406        partition.RemoveDynamicObject(id);
 158835407    }
 408
 409    private sealed class PhysicsPartitionOrderComparer : IComparer<PhysicsPartition>
 410    {
 411        public int Compare(PhysicsPartition? left, PhysicsPartition? right) =>
 653330412            WorldVoxelIndexOrdering.Compare3D(left!.WorldIndex, right!.WorldIndex);
 413    }
 414
 415    internal int ActivatePartition(PhysicsPartition partition)
 416    {
 220726417        SwiftThrowHelper.ThrowIfNull(partition, nameof(partition));
 220726418        SwiftThrowHelper.ThrowIfArgument(
 220726419            !ReferenceEquals(partition.Owner, this),
 220726420            nameof(partition),
 220726421            "Partition must belong to this collision service.");
 422
 220726423        return _activePartitions.Add(partition);
 424    }
 425
 220725426    internal void DeactivatePartition(int activationId) => _activePartitions.TryRemoveAt(activationId);
 427
 428    internal PhysicsPartition RentPartition()
 429    {
 405255430        if (_inactivePartitionPool.Count == 0)
 405200431            TryRetireEmptyRetainedPartitionForReuse();
 432
 405255433        PhysicsPartition partition = _inactivePartitionPool.Count > 0
 405255434            ? _inactivePartitionPool.Pop()
 405255435            : new PhysicsPartition();
 405255436        partition.SetOwner(this);
 405255437        return partition;
 438    }
 439
 405200440    private bool TryRetireEmptyRetainedPartitionForReuse() => RetainedPartitionLifecycle.TryRetireEmptyForReuse(
 405200441            _retainedPartitions,
 405200442            _inactivePartitionPool,
 405200443            _context.World,
 405200444            this,
 405200445            _releaseRetainedPartition,
 405200446            ref _retainedPartitionRetirementCursor);
 447
 448    internal void ReleasePartition(PhysicsPartition partition)
 449    {
 405252450        SwiftThrowHelper.ThrowIfNull(partition, nameof(partition));
 405252451        SwiftThrowHelper.ThrowIfArgument(
 405252452            !ReferenceEquals(partition.Owner, this),
 405252453            nameof(partition),
 405252454            "Partition must be released through its owning collision service.");
 455
 405252456        UntrackRetainedPartition(partition);
 405252457        partition.ResetForPool();
 405252458        _inactivePartitionPool.Push(partition);
 405252459    }
 460}

Methods/Properties

.cctor()
.ctor(Gravitas.GravitasWorldContext)
get_Context()
get_ActivePartitionCount()
get_InactivePartitionCount()
get_RetainedPartitionCount()
get_CullDistributor()
Reset()
DetachRetainedPartitions()
TrackRetainedPartition(Gravitas.PhysicsPartition)
UntrackRetainedPartition(Gravitas.PhysicsPartition)
IsPartitionRefreshRequired(Gravitas.Colliders.LSCollider)
ResolvePartitionKind(Gravitas.Colliders.LSCollider)
PartitionObject(Gravitas.Colliders.LSCollider,SwiftCollections.SwiftList`1<GridForge.Spatial.WorldVoxelIndex>&)
PartitionCoveredVoxels(Gravitas.Colliders.LSCollider,SwiftCollections.SwiftList`1<GridForge.Spatial.WorldVoxelIndex>,Gravitas.PhysicsPartitionMobilityKind)
TryPartitionVoxel(Gravitas.Colliders.LSCollider,SwiftCollections.SwiftList`1<GridForge.Spatial.WorldVoxelIndex>,GridForge.Grids.Voxel,GridForge.Utility.GridTraversalState&,Gravitas.PhysicsPartitionMobilityKind)
ClearPartitionedObject(Gravitas.Colliders.LSCollider,System.Boolean)
QueryPlanarColliderCandidates(Gravitas.CollisionHandling.DynamicCcdPlanarBounds,SwiftCollections.SwiftList`1<System.Int32>)
RebuildPlanarQueryCandidates()
RefreshPartitionAwakeState(Gravitas.Colliders.LSCollider)
CheckAndDistributeCollisions()
RetireExpiredRetainedPartitions()
GetMobilityKind(Gravitas.Colliders.LSCollider)
GetStoredMobilityKind(System.Int32)
AddObject(Gravitas.PhysicsPartition,System.Int32,Gravitas.PhysicsPartitionMobilityKind)
RemoveObject(Gravitas.PhysicsPartition,System.Int32,Gravitas.PhysicsPartitionMobilityKind)
Compare(Gravitas.PhysicsPartition,Gravitas.PhysicsPartition)
ActivatePartition(Gravitas.PhysicsPartition)
DeactivatePartition(System.Int32)
RentPartition()
TryRetireEmptyRetainedPartitionForReuse()
ReleasePartition(Gravitas.PhysicsPartition)