Physics API
Namespace GodotECS.Physics. Source: addons/godotecs/src/physics/. All components are unmanaged structs; systems are ISystem structs with OnCreate/OnUpdate/OnDestroy plus a testable Update(EntityManager, World). Health/DestroyTag (consumed by DamageSystem/DestroySystem) and LocalTransform live in GodotECS.Core.
Contract drift
The frozen spec block in docs/API.md §10 sketches older names (R, Extents, Depth, PhysicsStatic, Min/Max colliders). The file implementations below are authoritative; §10's own note says so.
PhysicsVelocity
public struct PhysicsVelocity : IComponentData
{
public Vector3 Value;
}Linear velocity, units/s, world space. Integrated by game code (demos advance Pos += Value · dt over chunk spans, applying gravity/damping first).
PhysicsDamping
public struct PhysicsDamping : IComponentData
{
public float Linear;
public float GravityScale;
public static PhysicsDamping Default => new PhysicsDamping { Linear = 0f, GravityScale = 1f };
}Linear damping (1/s) and gravity scale (1 = full gravity, 0 = none).
PhysicsRadius
public struct PhysicsRadius : IComponentData
{
public float Value;
}Sphere shape, radius > 0. Snapshots clamp non-positive values to 0. Dynamics are spheres; this is the shape both phases test.
PhysicsBox
public struct PhysicsBox : IComponentData
{
public Vector3 HalfExtents;
}Box shape, half extents > 0 per axis. Used on static entities (+ PhysicsStaticTag, tested by NarrowphaseSystem); world boxes use StaticCollider/StaticRegistry instead.
PhysicsCapsule
public struct PhysicsCapsule : IComponentData
{
public float Radius;
public float Height;
}Vertical capsule: Radius > 0, total Height >= 2 · Radius. Shape definition for StaticCollider capsule tests (segment + sphere internally).
PhysicsLayer
public struct PhysicsLayer : IComponentData
{
public uint Layer;
public uint Mask;
public static PhysicsLayer Default => new PhysicsLayer { Layer = 1u, Mask = 0xFFFFFFFFu };
}Membership (Layer) + collision mask (Mask). Pair tests are bilateral: dyn-dyn and dyn-box-entity require (a.Mask & b.Layer) != 0 && (b.Mask & a.Layer) != 0; broadphase static tests require (dynMask & static.Layer) != 0. Entities without the component default to Layer = 1, Mask = all.
PhysicsStaticTag / PhysicsKinematicTag
public struct PhysicsStaticTag : IComponentData { }
public struct PhysicsKinematicTag : IComponentData { }Markers. Static: never integrated, collision source only (excluded from the dynamic query, included as box entities or via StaticRegistry). Kinematic: script-moved, not gravity-driven.
CollisionEvent
public struct CollisionEvent : IBufferElementData
{
public Entity Other;
public Vector3 Normal;
public float Penetration;
}One contact in the entity's buffer for the current tick. Other == Entity.Null marks a world static from StaticRegistry; otherwise the real counterpart entity. Normal points from other toward self (negated on the symmetric copy); Penetration is the push-out depth. Buffers are cleared by BroadphaseSystem each tick; both phases append.
StaticShapeKind
public enum StaticShapeKind : byte
{
Sphere = 0,
Box = 1,
Capsule = 2
}Shape discriminator for StaticCollider.
StaticCollider
public struct StaticCollider
{
public StaticShapeKind Kind;
public Vector3 Center;
public Vector3 HalfExtents; // Box only
public float Radius; // Sphere and Capsule
public float Height; // Capsule: total height
public uint Layer;
}World-space static collider (plain struct, not a component — stored in the StaticRegistry array, never moved by the solver). Box spans Center ± HalfExtents (never Min/Max directly). Capsules are vertical (Height total, tested as Y-segment + sphere).
StaticRegistry
public struct StaticRegistry : ISingleton
{
public StaticCollider[] Colliders;
public int Count;
}World static set, populated at scene start. Count is clamped to the array length (and floored at 0); a missing singleton means no static tests. Consumed by BroadphaseSystem.
PlayerBodyRef
public struct PlayerBodyRef : IComponentData
{
public Entity Value;
}Plain component referencing the player body entity (not a singleton, despite the older spec sketch).
PhysicsConfig
public struct PhysicsConfig : ISingleton
{
public Vector3 Gravity;
public int SolverIterations;
public float ContactSlop;
public static PhysicsConfig Default => new PhysicsConfig
{
Gravity = new Vector3(0f, -9.81f, 0f),
SolverIterations = 4,
ContactSlop = 0.001f
};
}Gravity, solver iterations, contact tolerance. No CellSize — both grids use a local const float CellSize = 4f.
BroadphaseSystem
public struct BroadphaseSystem : ISystem
{
public const int GridX = 64;
public const int GridY = 16;
public const int GridZ = 64;
public const float CellSize = 4f;
public void OnCreate(ref SystemState state);
public void OnUpdate(ref SystemState state) => Update(state.Entities, state.World);
public void OnDestroy(ref SystemState state) { }
public static int CellOf(in Vector3 p);
public void Update(EntityManager em, World world);
}Hash-grid broadphase over dynamics (LocalTransform + PhysicsRadius, without PhysicsStaticTag), tested against the StaticRegistry.
CellOf(p)— grid cell for a position:floor(p / 4)per axis, dynamics wrap (Mod), statics clamp. Pure, testable.Update(em, world)— throwsArgumentNullExceptionfor null args. Snapshots dynamics into grow-only arrays; clears every dynamic'sCollisionEventbuffer (owns the cleanup); buckets each static into every cell its AABB touches; per dynamic scans the 27 neighboring cells with stamp dedup (each static once), layer filter(mask & static.Layer) != 0, exactEcsMath.SphereVsBox/SphereVsSphere(capsule = clamped-Y segment + sphere). Hits append{ Other = Entity.Null, Normal, Penetration }. Missing registry orCount == 0→ no static tests. NeverO(D×S): cost follows local density. Assumption: max radius ≤ 2 (cell ≥ 2× max radius).
NarrowphaseSystem
public struct NarrowphaseSystem : ISystem
{
public const int GridX = 64;
public const int GridY = 16;
public const int GridZ = 64;
public const float CellSize = 4f;
public void OnCreate(ref SystemState state);
public void OnUpdate(ref SystemState state) => Update(state.Entities, state.World);
public void OnDestroy(ref SystemState state) { }
public void Update(EntityManager em, World world);
}Exact narrowphase. Does not clear buffers (broadphase owns that).
- Dyn-vs-dyn via the grid: bucket by cell, per dynamic scan 27 neighbors with
j > a+ stamp dedup — each pair tested exactly once in deterministic order — bilateral layer filter,SphereVsSphere. Symmetric events on both buffers (normal negated). - Dyn-vs-static-box-entity (
LocalTransform + PhysicsBox + PhysicsStaticTag): brute force over the few statics, bilateral filter, world-spaceSphereVsBox, symmetric events with real counterpart entities. - Entities without
PhysicsLayerdefault toLayer = 1, Mask = all.
DamageSystem
public struct DamageSystem : ISystem
{
public void OnCreate(ref SystemState state);
public void OnUpdate(ref SystemState state) => Update(state.Entities, state.World);
public void OnDestroy(ref SystemState state) { }
public void Update(EntityManager em, World world);
}Health.Value -= CollisionEvent count per entity with Health (core Health { int Value, Max }); at ≤ 0 adds DestroyTag (idempotent). Snapshots first, structural writes after (SetComponentData / AddComponent are archetype moves, IsAlive-checked, never inside iteration).
DestroySystem
public struct DestroySystem : ISystem
{
public void OnCreate(ref SystemState state);
public void OnUpdate(ref SystemState state) => Update(state.Entities, state.World);
public void OnDestroy(ref SystemState state) { }
public void Update(EntityManager em, World world);
}Snapshots entities with DestroyTag (core marker), then Destroys each (IsAlive-checked), never during iteration.
JoltBridge
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);
}Chunk-level queries into Godot/Jolt at a sync point — never in a job, only on the physics thread. Instance and static overloads behave identically (the static variants carry an ignored bool _static = true selector).
CastChunk(space, chunkBox, mask, outHit)— one swept shape query per chunk AABB (approximated as a bounding sphere: center + half-diagonal; radius ≤ 0 → 0).SphereShape3Doverlap (CollideWithBodies, no areas), up to 32 results, copiespositions intooutHitup to its length. Returns hit count. Null space or empty span → 0.QueryRest(space, pos, radius, mask)— one rest overlap at a point (single-result query). Returns result count. Null space orradius ≤ 0→ 0.