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:
44
AGENTS.md
44
AGENTS.md
@@ -559,6 +559,50 @@ CTest recorded a pass. That is not hypothetical: `tests/character.c` aborted at
|
||||
its second of four tests on a bad renderer and was green until 0.5.0. The trap
|
||||
in `tests/testutil.h` collapses any status a byte cannot carry onto 1. Add focused tests as `tests/<feature>.c`, create a matching `test_<feature>` target, and register it with `add_test`. Put reusable fixtures in `tests/assets/` and keep paths compatible with tests launched from the build tree. Coverage mode wraps the suite in a CTest fixture so counters are reset before tests and reports are generated afterward.
|
||||
|
||||
### Physics simulations
|
||||
|
||||
`tests/physics_sim.c` is not a unit suite. `tests/physics.c` checks that
|
||||
`akgl_physics_simulate` does the arithmetic it says it does; this one runs the
|
||||
arcade backend for a second or two the way a game does and asks whether the
|
||||
result is what a player would expect. It carries a Mario-esque jump and fall,
|
||||
Zelda-style top-down walking, and a run reversed at full speed, because those
|
||||
are the three shapes most 2D games are made of.
|
||||
|
||||
It found three defects that every existing unit test passed straight over, so
|
||||
the distinction is worth keeping:
|
||||
|
||||
- **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 is 0.44 px. Both
|
||||
initializers seed the clock now, and `akgl_physics_simulate` bounds `dt` to
|
||||
`max_timestep` (`physics.max_timestep`, default 0.05 s).
|
||||
- **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, so tapping down mid-jump stopped the character in the
|
||||
air. The handlers clear `ax`/`tx` (or `ay`/`ty`) and nothing else. 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 now.
|
||||
|
||||
Two rules for working on this suite:
|
||||
|
||||
- **Drive the clock, do not sleep on it.** `sim_step()` sets `gravity_time` to
|
||||
`now - dt` and calls `simulate` once, so a simulated second is 60 steps and
|
||||
takes no wall-clock time. Only `test_sim_first_step_is_not_a_leap` uses a real
|
||||
`SDL_Delay`, because a real stall is the thing it is measuring.
|
||||
- **`main` must `SDL_Init`.** Without it, SDL sets its clock epoch on first use,
|
||||
`SDL_GetTicksNS()` returns something around 130 ns, and every dt-dependent
|
||||
assertion passes for a reason that has nothing to do with the code. The
|
||||
first-step test passed exactly that way before the `SDL_Init` call was added,
|
||||
and only started failing -- correctly -- once it was there.
|
||||
|
||||
Each simulation prints what it measured whether it passes or fails, and `main`
|
||||
exits with the number that failed. Read the numbers when changing the backend;
|
||||
a change that keeps every assertion green while moving the apex of a jump by
|
||||
30 px is a change worth noticing.
|
||||
|
||||
### Performance suites
|
||||
|
||||
`tests/perf.c` (nothing that draws) and `tests/perf_render.c` (everything that
|
||||
|
||||
Reference in New Issue
Block a user