Skip to content

Physics API ​

Namespace GodotECS.Physics. Source: addons/godotecs/src/physics/.

Documentation target: GodotECS commit 699338bcefd3edc31ff6b82a054932402f4d2970.

Components ​

csharp
public struct PhysicsVelocity : IComponentData { public Vector3 Value; }
public struct PhysicsDamping : IComponentData
{
    public float Linear;
    public float GravityScale;
    public static PhysicsDamping Default { get; }
}
public struct PhysicsRadius : IComponentData { public float Value; }
public struct PhysicsBox : IComponentData { public Vector3 HalfExtents; }
public struct PhysicsCapsule : IComponentData { public float Radius; public float Height; }
public struct PhysicsLayer : IComponentData
{
    public uint Layer;
    public uint Mask;
    public static PhysicsLayer Default { get; }
}
public struct PhysicsStaticTag : IComponentData { }
public struct PhysicsKinematicTag : IComponentData { }
public struct CollisionEvent : IBufferElementData
{
    public Entity Other;
    public Vector3 Normal;
    public float Penetration;
    public float Damage;
}
public enum StaticShapeKind : byte { Sphere = 0, Box = 1, Capsule = 2 }
public struct StaticCollider
{
    public StaticShapeKind Kind;
    public Vector3 Center;
    public Vector3 HalfExtents;
    public float Radius;
    public float Height;
    public uint Layer;
    public uint Mask;
}
public struct StaticRegistry : ISingleton
{
    public StaticCollider[] Colliders;
    public int Count;
    public uint Version;
}
public struct PlayerBodyRef : IComponentData { public Entity Value; }
public struct PhysicsConfig : ISingleton
{
    public Vector3 Gravity;
    public int SolverIterations;
    public float ContactSlop;
    public static PhysicsConfig Default { get; }
}

Collision filters ​

PhysicsLayer.Layer is the membership bit set and Mask is the accepted-layer mask. Dynamic/dynamic and dynamic/static-box-entity checks are bilateral:

text
(a.Mask & b.Layer) != 0 && (b.Mask & a.Layer) != 0

World-static tests require the dynamic mask to accept the static layer and the static mask to accept the dynamic layer. For compatibility, StaticCollider.Mask == 0 is treated as a wildcard. Entities without PhysicsLayer use Layer = 1, Mask = 0xFFFFFFFF.

CollisionEvent and damage ​

CollisionEvent is rebuilt for the current fixed tick. BroadphaseSystem clears dynamic contact buffers; NarrowphaseSystem only appends.

Other == Entity.Null identifies a world-static contact from StaticRegistry; otherwise it is the actual counterpart entity. Normal points from the other shape toward the receiving entity. Penetration is the push-out depth.

Damage is the damage caused by that contact. Current physics writes Damage = 1 for dynamic-dynamic contacts and Damage = 0 for world-static contacts. DamageSystem sums CollisionEvent.Damage; it does not count contacts. Therefore resting on a floor does not consume HP by itself.

StaticRegistry cache invalidation ​

StaticRegistry.Version is part of the cache contract. The broadphase rebuilds static buckets when the registry reference, Count, or Version changes, or when the frame's effective cell size changes. If Colliders are mutated in place, bump Version; otherwise the cached buckets can remain stale.

StaticShapeKind is Sphere, Box, or vertical Capsule. Boxes are represented as Center ± HalfExtents; capsules use total height and are tested as a Y-segment plus sphere.

PhysicsParallel ​

csharp
public static class PhysicsParallel
{
    public static bool ForceSequential;
    public const int MinParallelN = 2048;
    public const int TargetBatch = 512;
    public const int MaxPartitions = 64;
}

Broadphase and narrowphase become deterministically parallel at 2048 dynamics unless either PhysicsParallel.ForceSequential or Scheduler.ForceSequential is set. Work is partitioned over contiguous dynamic ranges and merged in partition order so sequential and parallel event order remain bit-identical.

Built-in systems ​

csharp
public struct IntegrateSystem : ISystem
{
    public void OnCreate(ref SystemState state);
    public void OnUpdate(ref SystemState state);
    public void OnDestroy(ref SystemState state);
    public void Update(EntityManager em, World world, float dt);
}

public struct ResolveSystem : ISystem
{
    public const int MaxSolverIterations = 64;
    public void OnCreate(ref SystemState state);
    public void OnUpdate(ref SystemState state);
    public void OnDestroy(ref SystemState state);
    public void Update(EntityManager em, World world);
}

public struct BroadphaseSystem : ISystem
{
    public const int GridX = 64, GridY = 16, GridZ = 64;
    public const float CellSize = 4f;
    public void OnCreate(ref SystemState state);
    public void OnUpdate(ref SystemState state);
    public void OnDestroy(ref SystemState state);
    public static int CellOf(in Vector3 p);
    public void Update(EntityManager em, World world);
}

public struct NarrowphaseSystem : ISystem
{
    public const int GridX = 64, GridY = 16, GridZ = 64;
    public const float CellSize = 4f;
    public void OnCreate(ref SystemState state);
    public void OnUpdate(ref SystemState state);
    public void OnDestroy(ref SystemState state);
    public void Update(EntityManager em, World world);
}

public struct DamageSystem : ISystem
{
    public void OnCreate(ref SystemState state);
    public void OnUpdate(ref SystemState state);
    public void OnDestroy(ref SystemState state);
    public void Update(EntityManager em, World world);
}

public struct DestroySystem : ISystem
{
    public void OnCreate(ref SystemState state);
    public void OnUpdate(ref SystemState state);
    public void OnDestroy(ref SystemState state);
    public void Update(EntityManager em, World world);
}

IntegrateSystem applies gravity scaled by PhysicsDamping.GravityScale, stable damping v *= 1/(1 + Linear*dt), then semi-implicit Euler Pos += v*dt. Kinematic bodies receive velocity integration but not gravity/damping. Non-positive/NaN dt is a no-op.

ResolveSystem is optional arcade response: push-out + sliding, with configurable dynamic-dynamic relaxation. Kinematics are immovable. It does not implement rigid-body impulses, rotation, friction or sleeping.

BroadphaseSystem dynamically computes the frame cell size as max(4 m, 2*rmax), caches world-static buckets, scans 27 neighboring cells and performs exact sphere/box/sphere tests. It never assumes a fixed maximum dynamic radius.

NarrowphaseSystem handles dynamic-dynamic and dynamic-static-box-entity contacts using the same effective cell size and symmetric events.

JoltBridge ​

csharp
public sealed class JoltBridge
{
    public int CastChunk(PhysicsDirectSpaceState3D space, in GodotECS.Math.Aabb chunkBox,
        uint mask, Span<Vector3> outHit);
    public static int CastChunk(PhysicsDirectSpaceState3D space, in GodotECS.Math.Aabb chunkBox,
        uint mask, Span<Vector3> outHit, bool _static = true);
    public int QueryRest(PhysicsDirectSpaceState3D space, in Vector3 pos, float radius, uint mask);
    public static int QueryRest(PhysicsDirectSpaceState3D space, in Vector3 pos, float radius,
        uint mask, bool _static = true);
}

CastChunk performs one BoxShape3D query matching the chunk AABB, not a sphere approximation. A degenerate AABB performs no query. It copies hit positions into outHit and caps the returned number of copied results by outHit.Length; the implementation does not promise a fixed public capacity such as 32 beyond the supplied span.

QueryRest performs a point/rest overlap query. Null space or a non-positive radius returns 0. Both APIs are physics-thread/sync-point only; never call them from ECS jobs.

Determinism and allocation ​

The phase systems use reused grow-only scratch arrays and fixed chunk/index traversal. Parallel broad/narrow results are merged deterministically. Keep ECS structural changes out of parallel jobs and use the ECB when needed.

GodotECS docs — version 0.2.0. All rights reserved until a license is added.