Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a Mario-esque jump and fall, Zelda-style top-down walking, and a run reversed at full speed -- and prints what the actor actually did. tests/physics.c already checked that akgl_physics_simulate does the arithmetic it claims. Every one of these got past it. The first step was however long the level took to load. gravity_time was never initialized and dt was unbounded, so a 250 ms load produced a 250 ms step: 100 px of fall where a 60 Hz frame under the same gravity is 0.44 px, straight through whatever was underneath. Both initializers seed the clock, and simulate bounds dt to max_timestep -- a new physics.max_timestep property, default 0.05 s, read like gravity and drag. Zero disables the bound. The field replaces the dead timer_gravity, so the struct is the same size. Releasing a vertical key cancelled gravity. The _off handlers zeroed ay, ey, ty and vy together, and ey is where the arcade backend accumulates gravity -- tapping down mid-jump stopped the character in the air. They clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs to clear either: simulate recomputes v as e + t every step. Diagonal movement was 41% too fast. Thrust was capped per axis, so an actor holding two directions got both caps at once and travelled their diagonal. It is capped as a vector against the sx/sy/sz ellipse, which also keeps a character whose horizontal and vertical top speeds differ moving at the ratio it asked for. An axis with a zero max speed stays out of the magnitude and is forced to zero, as the old clamp did. Each fix was checked by reverting it and confirming the simulation goes red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively. tests/actor.c and tests/physics.c pinned the old behaviour in both places and now assert the new contract. Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four things the simulations found and this does not fix -- no terminal velocity, no deceleration on release, Euler's frame-rate dependence, and a drag coefficient large enough to invert velocity. 26/26 ctest, memcheck clean, warning-clean at -Wall -Werror. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
This commit is contained in:
@@ -16,15 +16,29 @@
|
||||
* | 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 |
|
||||
* | max speed | `sx, sy, sz` | the character; caps the thrust vector, 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.
|
||||
*
|
||||
* The cap is on the thrust **vector**, not on each axis separately. Capping the
|
||||
* axes independently lets the corner of the box through: an actor holding two
|
||||
* directions at once got both caps at once and travelled their diagonal, 41%
|
||||
* faster than either alone. Scaling to the ellipse instead keeps a character
|
||||
* whose horizontal and vertical speeds differ moving at the ratio it asked for.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Default bound on one simulation step, in seconds.
|
||||
*
|
||||
* A twentieth of a second: three frames at 60 Hz, so an ordinary dropped frame
|
||||
* or two passes through untouched, and a load or a breakpoint does not.
|
||||
*/
|
||||
#define AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP 0.05
|
||||
|
||||
#ifndef _AKGL_PHYSICS_H_
|
||||
#define _AKGL_PHYSICS_H_
|
||||
|
||||
@@ -47,8 +61,8 @@ typedef struct akgl_PhysicsBackend {
|
||||
float64_t gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */
|
||||
float64_t gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */
|
||||
float64_t 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. */
|
||||
SDL_Time gravity_time; /**< Timestamp of the previous step, in nanoseconds. Stamped by akgl_physics_simulate and seeded by the initializers; the simulation's `dt` is measured from this. */
|
||||
float64_t max_timestep; /**< Longest step the simulation will take, in seconds. A hitch longer than this advances the world by this much instead. 0 disables the bound. From `physics.max_timestep`, default #AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP. */
|
||||
} akgl_PhysicsBackend;
|
||||
|
||||
/**
|
||||
@@ -203,16 +217,26 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *sel
|
||||
* 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;
|
||||
* - thrust accumulates along an axis the actor is moving on, then the thrust
|
||||
* *vector* is scaled to the ellipse the character's per-axis maximum speeds
|
||||
* describe;
|
||||
* - 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.
|
||||
* `dt` is measured from `self->gravity_time`, which is stamped at the end and
|
||||
* seeded by akgl_physics_init_arcade and akgl_physics_init_null. It is then
|
||||
* bounded by `self->max_timestep`: a step longer than that advances the world
|
||||
* by `max_timestep` instead of by the whole elapsed time.
|
||||
*
|
||||
* That bound is not a nicety. Anything that stalls a frame -- a level load
|
||||
* between initialization and the first step, a breakpoint, a dragged window, an
|
||||
* alt-tab -- otherwise arrives as one enormous `dt`, and one enormous `dt`
|
||||
* moves every actor by however far its velocity carries it in that whole
|
||||
* interval. A quarter-second load under ordinary platformer gravity is a
|
||||
* hundred pixels of fall between the first frame and the second, straight
|
||||
* through whatever was underneath. Advancing the world in slow motion during a
|
||||
* hitch is the trade every game engine makes here.
|
||||
*
|
||||
* @param self The backend. Required, along with its `move` pointer.
|
||||
* @param opflags Iterator flags. Optional -- `NULL` means "simulate everything".
|
||||
|
||||
Reference in New Issue
Block a user