Physics & Collision
SafiEngine's physics is powered by Jolt Physics — a AAA-proven C++ rigid-body engine (Horizon Forbidden West). The engine wraps Jolt behind a C API so user code stays pure C. A single .cpp bridge file in the engine handles all Jolt access.
Physics runs on the SafiFixedUpdate phase at a stable timestep (default 1/60s). After each step, dynamic body transforms are written back to SafiTransform. The transform propagation system (on EcsPreStore) then updates SafiGlobalTransform before the render stage sees them.
Components
SafiRigidBody
Fields prefixed with _ are managed by the physics system — don't write to them.
SafiCollider
Adding a physics entity
Attach SafiTransform, SafiRigidBody, and SafiCollider to an entity. The physics system automatically registers it with Jolt on the next fixed step.
Per-tick flow
The physics system runs on SafiFixedUpdate with four phases per tick:
- Register — entities with
_registered == falseget a Jolt body created from their collider shape + transform. - Push kinematic — kinematic bodies push their
SafiTransforminto Jolt (user gameplay may have moved them inOnUpdate). - Step —
safi_jolt_step(fixed_dt)advances the simulation by one fixed timestep. - Pull dynamic — dynamic bodies pull Jolt's computed position/rotation back into
SafiTransform.
Frame ordering
Propagation runs on EcsPreStore (render pipeline) so it captures both user-gameplay changes and physics changes before the renderer sees them.
Lifecycle
Physics is initialized and shut down automatically by safi_app_init / safi_app_shutdown. Gravity defaults to (0, -9.81, 0). The simulation is single-threaded (JobSystemSingleThreaded), adequate for hundreds of bodies.
Collision queries
Two query families are exposed: a closest-hit raycast and box / sphere overlap checks. Both return the owning ecs_entity_t of each hit body directly — no lookup table needed (the engine stores the entity as Jolt BodyUserData at creation time).
Raycast
direction must be normalized. Pass ignore = 0 to hit everything, or the entity id of a body you want to skip (useful when casting from a character to avoid self-hits). Returns true and fills out_hit when something was hit.
Overlap
Returns the total number of overlapping bodies found; fills up to cap entries into out_entities. If the return value exceeds cap, grow the buffer and call again.
:::warning WIP — Not yet implemented
- Capsule collider — only box and sphere are supported
- Layer masks / filtering — raycasts support a single
ignoreentity; no group masks - All-hits raycast — only closest hit
- Constraints / joints — not exposed
- Contact callbacks — no collision event system
- Debug wireframe — no physics debug rendering
- Parented physics bodies — all physics bodies must be root entities (no
EcsChildOfhierarchy for physics) :::
See also
- Transform System —
SafiGlobalTransformpropagation - Scheduler & Stages —
SafiFixedUpdatephase - Time —
fixed_deltaandfixed_overshoot - Stock Components —
SafiTransform,SafiGlobalTransform