Skip to content

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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
public enum StaticShapeKind : byte
{
    Sphere = 0,
    Box = 1,
    Capsule = 2
}

Shape discriminator for StaticCollider.

StaticCollider

csharp
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

csharp
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

csharp
public struct PlayerBodyRef : IComponentData
{
    public Entity Value;
}

Plain component referencing the player body entity (not a singleton, despite the older spec sketch).

PhysicsConfig

csharp
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

csharp
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) — throws ArgumentNullException for null args. Snapshots dynamics into grow-only arrays; clears every dynamic's CollisionEvent buffer (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, exact EcsMath.SphereVsBox / SphereVsSphere (capsule = clamped-Y segment + sphere). Hits append { Other = Entity.Null, Normal, Penetration }. Missing registry or Count == 0 → no static tests. Never O(D×S): cost follows local density. Assumption: max radius ≤ 2 (cell ≥ 2× max radius).

NarrowphaseSystem

csharp
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).

  1. 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).
  2. Dyn-vs-static-box-entity (LocalTransform + PhysicsBox + PhysicsStaticTag): brute force over the few statics, bilateral filter, world-space SphereVsBox, symmetric events with real counterpart entities.
  3. Entities without PhysicsLayer default to Layer = 1, Mask = all.

DamageSystem

csharp
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

csharp
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

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);
}

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). SphereShape3D overlap (CollideWithBodies, no areas), up to 32 results, copies positions into outHit up 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 or radius ≤ 0 → 0.

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