Arcade physics
Pure-C# arcade physics for large entity counts: hash-grid broadphase, exact narrowphase, contact buffers and an optional response/damage pipeline.
Fixed-step pipeline
IntegrateSystem → BroadphaseSystem → NarrowphaseSystem → ResolveSystem
→ DamageSystem → DestroySystemIntegrateSystem and ResolveSystem are optional built-ins; game code may replace either. BroadphaseSystem owns clearing CollisionEvent buffers, while NarrowphaseSystem only appends contacts.
Above 2048 dynamics, broadphase and narrowphase use deterministic partitions unless PhysicsParallel.ForceSequential or Scheduler.ForceSequential is enabled. Partition results are merged in deterministic order.
Bodies and collision filters
PhysicsVelocity, PhysicsRadius, PhysicsBox, PhysicsCapsule, PhysicsLayer, PhysicsDamping, PhysicsStaticTag and PhysicsKinematicTag are the main physics components.
PhysicsLayer uses bilateral filtering for dynamic/dynamic and dynamic/static-box-entity contacts:
(a.Mask & b.Layer) != 0 && (b.Mask & a.Layer) != 0World-static registry contacts apply both layer and mask. A missing PhysicsLayer defaults to layer 1 and an all-bits mask. StaticCollider.Mask == 0 is treated as a wildcard for compatibility.
Contact events and damage
public struct CollisionEvent : IBufferElementData
{
public Entity Other;
public Vector3 Normal;
public float Penetration;
public float Damage;
}Other == Entity.Null means a world-static collider from StaticRegistry; otherwise it identifies the other entity. Damage is per-contact damage. The current implementation assigns damage to dynamic-dynamic contacts and zero damage to world-static contacts, and DamageSystem sums Damage rather than counting events. A body resting on a floor therefore does not lose HP merely because a contact buffer contains a static contact.
Static world and cache
StaticRegistry contains Colliders, Count and Version. If colliders are changed in place, increment Version; the broadphase uses it to invalidate cached static buckets. The cache is also invalidated when the registry reference, Count or effective cell size changes.
Static colliders support sphere, box and vertical capsule shapes. Boxes use Center ± HalfExtents.
Adaptive broadphase
The grid is 64×16×64 with a nominal minimum cell size of 4 m. Every frame the implementation measures the maximum dynamic radius rmax and uses:
max(4 m, 2 × rmax)for the effective cell size. There is no longer a fixed maximum-radius assumption.
Static buckets are reused across frames when their inputs are unchanged. Each static is linked into all cells touched by its AABB, then dynamics inspect the 27 neighboring cells with stamp deduplication. This keeps static query cost tied to local density rather than O(dynamics × statics).
Built-in integration and resolution
IntegrateSystem applies gravity, gravity scale, stable linear damping and semi-implicit Euler. Kinematic entities skip gravity/damping and remain script-authoritative.
ResolveSystem is an optional arcade solver: push-out and sliding, plus deterministic relaxation for dynamic-dynamic contacts. It has a safety cap of 64 solver iterations. It does not implement rigid-body rotation, friction, restitution or sleeping.
Jolt bridge
JoltBridge.CastChunk is a physics-thread sync-point helper. It performs a BoxShape3D query matching the supplied chunk AABB; it is not the old bounding-sphere approximation. Degenerate AABBs return no hits, and copied hit positions are bounded by the supplied outHit span.
QueryRest performs a point/rest overlap query. Null space or a non-positive radius returns 0. Neither API may be called from an ECS job.
Damage/destroy example
var events = em.GetBuffer<CollisionEvent>(e);
float damage = 0f;
for (int i = 0; i < events.Length; i++)
damage += events[i].Damage;
Health h = em.GetComponentData<Health>(e);
h.Value -= (int)damage;
em.SetComponentData(e, h);
if (h.Value <= 0)
em.AddComponent<DestroyTag>(e);Structural writes and destroys are performed after the entity snapshot/iteration, never by mutating the archetype while iterating it.