/** * @file physics.h * @brief The pluggable physics backend, and the two implementations that ship. * * Same shape as the renderer: a record of function pointers plus an initializer * that fills it in. `null` accepts every call and changes nothing, which is what * a menu screen or a test harness wants; `arcade` applies gravity, drag, thrust * and a speed cap. Selecting between them is akgl_physics_factory, driven by the * `physics.engine` configuration property -- not a branch in the simulation. * * The model an actor is simulated under, and the fields on akgl_Actor that carry * it: * * | Term | Fields | Comes from | * |-----------------------|--------------|-----------------------------------------------| * | thrust | `tx, ty, tz` | the actor's own acceleration while it is moving | * | environmental | `ex, ey, ez` | gravity, less drag | * | velocity | `vx, vy, vz` | thrust + environmental | * | max speed | `sx, sy, sz` | the character; caps thrust, not velocity | * | acceleration | `ax, ay, az` | the character | * * Each step: movement logic, then gravity, then drag, then velocity, then move. * Because the cap is applied to thrust rather than to velocity, gravity can * carry an actor faster than its stated top speed -- which is the point, for a * falling one. */ #ifndef _AKGL_PHYSICS_H_ #define _AKGL_PHYSICS_H_ #include #include #include #include #include /** @brief Defines a pluggable physics backend and its environmental parameters. */ typedef struct akgl_PhysicsBackend { akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags); /**< Step the whole world. Both backends point this at akgl_physics_simulate. */ akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Apply environmental acceleration to one actor. */ akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /**< Resolve a collision between two actors. Not called by the simulation loop yet. */ akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Commit one actor's velocity to its position. */ double drag_x; /**< Fraction of environmental velocity shed per second along x. 0 disables. From `physics.drag.x`. */ double drag_y; /**< Drag along y. From `physics.drag.y`. */ double drag_z; /**< Drag along z. From `physics.drag.z`. */ double gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */ double gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */ double gravity_z; /**< Acceleration along z. Applied away from the camera. From `physics.gravity.z`. */ SDL_Time gravity_time; /**< Timestamp of the previous step, in nanoseconds. The simulation's `dt` is measured from this. */ SDL_Time timer_gravity; /**< Unused. Nothing in the library reads or writes it. */ } akgl_PhysicsBackend; /** * @brief Gravity for the null backend: accept the call and change nothing. * @param self The backend. Required -- the one thing this does check. * @param actor The actor that would have been accelerated. Ignored; `NULL` is * accepted. * @param dt Seconds since the previous step. Ignored. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** * @brief Collision for the null backend: accept the call and change nothing. * * Note that this *succeeds* where the arcade backend's collide refuses -- the * null backend's contract is "nothing collides", which is an answer, not a gap. * * @param self The backend. Required. * @param a1 First actor. Ignored; `NULL` is accepted. * @param a2 Second actor. Ignored; `NULL` is accepted. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /** * @brief Movement for the null backend: accept the call and change nothing. * * Actors under the null backend do not move on their own. Anything that sets an * actor's `x`/`y` directly still works -- this only declines to integrate * velocity. * * @param self The backend. Required. * @param actor The actor that would have moved. Ignored; `NULL` is accepted. * @param dt Seconds since the previous step. Ignored. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** * @brief Install the null backend's methods. * * Reads no configuration and touches no gravity or drag fields, so whatever was * in them stays -- harmlessly, since none of the null methods look. * * @param self The backend to initialize. Required. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self); /** * @brief Accelerate one actor's environmental velocity by gravity. * * Adds `gravity * dt` to the actor's `ex`/`ey`/`ez` on each axis whose gravity * is non-zero, with the sign chosen for screen space: x pulls left, y pulls * down, z pulls away from the camera. It touches only the environmental term, so * an actor's own thrust is unaffected and the speed cap does not apply -- which * is why a falling actor keeps accelerating past its character's top speed. * * @param self The backend supplying the gravity constants. Required. * @param actor The actor to accelerate. Required. * @param dt Seconds since the previous step. A `dt` of 0 is a no-op; a * negative one accelerates backwards. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** * @brief Collision for the arcade backend. Not implemented. * * Deliberately loud rather than silently permissive: unlike * akgl_physics_null_collide, which means "nothing collides", this means "nobody * has written this yet", and a caller that reaches it should find out. * * @param self The backend. Required, and checked before the refusal. * @param a1 First actor. Never examined. * @param a2 Second actor. Never examined. * @return Never `NULL`. * @throws AKERR_NULLPOINTER If @p self is `NULL`. * @throws AKERR_API Otherwise, always, with the message "Not implemented". */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /** * @brief Commit an actor's velocity to its position. * * The last step of a simulation tick: `position += velocity * dt` on all three * axes. It does not clamp to the map, consult the tilemap, or test for * collisions -- an actor will walk straight through a wall and off the edge of * the world. * * @param self The backend. Required, though nothing on it is read. * @param actor The actor to move. Required. Its `vx`/`vy`/`vz` must already have * been computed; akgl_physics_simulate does that immediately * before calling this. * @param dt Seconds since the previous step. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** * @brief Install the arcade backend's methods and read its constants from configuration. * * Reads `physics.gravity.x`, `.y`, `.z` and `physics.drag.x`, `.y`, `.z` from * the property registry, all defaulting to `"0.0"` -- so an arcade backend with * no configuration behaves like the null one until something is set. * * @param self The backend to initialize. Required. Its method pointers are * installed before the properties are read, so a failure part-way * leaves a usable backend with some constants still at their * previous values. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self is `NULL`. * @throws AKERR_VALUE If one of the six properties is set to something that is * not a number. * @throws ERANGE If one of them does not fit a `double`. * @throws AKGL_ERR_HEAP If the string pool is exhausted. * * @note With #AKGL_REGISTRY_PROPERTIES uninitialized every property reads back * as its default, so this silently configures zero gravity and zero drag * rather than reporting anything. See the warning in registry.h. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self); /** * @brief Select and initialize a physics backend by name. * * The dispatch point for the whole subsystem: `"null"` and `"arcade"` are the * two names, and akgl_game_init passes whatever the `physics.engine` property * holds. Adding a backend means adding a name here, not a branch in the * simulation. * * @param self The backend to initialize. Required. * @param type The backend name. Required. Matched on its leading characters, so * `"nullify"` selects `null` and `"arcadia"` selects `arcade`. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self or @p type is `NULL`. * @throws AKERR_KEY If @p type matches neither name. The message quotes what was * asked for. * @throws AKERR_* Whatever the selected initializer raises. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type); /** * @brief Step every live actor forward by the time elapsed since the previous call. * * Shared by both backends -- what differs is the `gravity` and `move` they point * at. For each live actor, in pool order: * * - a child actor is snapped to its parent's position plus its own `v` as an * offset, and skipped entirely -- children do not simulate; * - an actor with no character is skipped, since the speed and acceleration * constants live there; * - thrust accumulates along an axis the actor is moving on, then is clamped to * the character's maximum speed; * - the backend's `gravity` runs, drag is applied to the environmental term, * velocity becomes environmental plus thrust, and the backend's `move` * commits it. * * `dt` is measured from `self->gravity_time`, which is stamped at the end. The * first call after initialization therefore measures from 0 and produces an * enormous `dt` -- initialize `gravity_time` from `SDL_GetTicksNS()` before the * first step if that matters. * * @param self The backend. Required, along with its `move` pointer. * @param opflags Iterator flags. Optional -- `NULL` means "simulate everything". * Only #AKGL_ITERATOR_OP_LAYERMASK is honoured, restricting the * step to actors on `layerid`; the update, render, and release * bits are ignored here. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self or `self->move` is `NULL`. * @throws AKERR_* Whatever an actor's `movementlogicfunc`, or the backend's * `gravity` or `move`, raises. The first failure aborts the whole step, * leaving the actors already processed advanced and the rest not. * * @note An actor's `movementlogicfunc` can raise AKGL_ERR_LOGICINTERRUPT to say * "skip the rest of the simulation for me this tick". It is handled here * and does not propagate -- it is a control signal wearing an error's * clothes, not a failure. Only `movementlogicfunc` gets that treatment: * the `gravity` and `move` calls return straight out of the loop on any * status, LOGICINTERRUPT included, so a backend must not use it to opt an * actor out from there. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags); #endif // _AKGL_PHYSICS_H_