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:
63
TODO.md
63
TODO.md
@@ -1884,6 +1884,69 @@ currently promises the opposite. `aksl_strncpy` already reports exactly that
|
||||
status when the bytes do not fit, so the change is to stop capping `n` at
|
||||
`size - 1` and let it raise.
|
||||
|
||||
## Arcade physics feel
|
||||
|
||||
Found by building `tests/physics_sim.c`, which runs the arcade backend as a game
|
||||
would -- a Mario-esque jump and fall, Zelda-style top-down movement, and a
|
||||
fast run reversed at speed -- and prints what the actor actually did. Three
|
||||
defects it found are fixed in 0.6.0 (an unbounded first step, release handlers
|
||||
zeroing the gravity accumulator, and per-axis thrust caps composing to a 141%
|
||||
diagonal). These are what it found and did **not** fix.
|
||||
|
||||
### No terminal velocity
|
||||
|
||||
`akgl_physics_arcade_gravity` adds `gravity_y * dt` to `ey` every step and
|
||||
nothing bounds it. `sy` caps *thrust*, deliberately -- it is the character's own
|
||||
top speed, not a speed limit on the world -- so a fall accelerates without limit
|
||||
for as long as it lasts. The jump simulation reaches 560 px/s in 0.7 s and would
|
||||
keep going.
|
||||
|
||||
Drag is the mechanism that exists for this: `physics.drag.y` makes `ey`
|
||||
approach `gravity_y / drag_y` instead of diverging. So the behaviour is
|
||||
reachable, it just is not the default and is not documented as the way to get a
|
||||
terminal velocity. Either document it that way or add an explicit
|
||||
`physics.terminal_velocity`. Touches `src/physics.c` and
|
||||
`include/akgl/physics.h`; no ABI change if it is a property rather than a field.
|
||||
|
||||
### Releasing a direction stops the actor dead
|
||||
|
||||
`akgl_actor_cmhf_*_off` sets `tx` (or `ty`) to zero, so a character running at
|
||||
full speed stops within one frame of the key coming up -- the top-down
|
||||
simulation measures 0.0 px of drift in the second after release. That is correct
|
||||
for Zelda and wrong for Mario, who slides. There is no deceleration or ground
|
||||
friction anywhere in the arcade backend; `ax` is the only rate, and it applies
|
||||
only while a direction is held.
|
||||
|
||||
The shape that fits the existing model is a per-character deceleration that
|
||||
decays `tx` toward zero over several frames instead of assigning zero, with the
|
||||
current behaviour as the value that means "instant". That is a new
|
||||
`akgl_Character` field and an ABI break, so it wants to land with other
|
||||
character changes rather than alone.
|
||||
|
||||
### Integration is explicit Euler, so trajectories are frame-rate dependent
|
||||
|
||||
Velocity is applied to position using the velocity computed *this* step, and
|
||||
thrust is accumulated before the cap, so the same jump traces a slightly
|
||||
different arc at 30 Hz than at 144 Hz. The reversal simulation measures 0.500 s
|
||||
to turn around where the closed form predicts 0.489 s -- about 2% at 60 Hz, and
|
||||
it grows with the step.
|
||||
|
||||
`max_timestep` bounds how bad it gets (that is half of why it exists), and 2% is
|
||||
not a bug a player can see. It is recorded because the fix -- a fixed-step
|
||||
accumulator, integrating in whole `max_timestep` slices and carrying the
|
||||
remainder -- would also remove the slow-motion trade that bounding the step
|
||||
currently makes, and the two are the same piece of work.
|
||||
|
||||
### A large drag coefficient can invert velocity
|
||||
|
||||
`actor->ex -= actor->ex * self->drag_x * dt` is a first-order decay, so it only
|
||||
decays for `drag * dt < 1`. Past that it overshoots zero, and past `2` it
|
||||
diverges. With `dt` bounded to the default 0.05 s that needs a drag coefficient
|
||||
above 20, which is not a plausible setting, so this is a sharp edge rather than
|
||||
a live defect -- but nothing rejects it, and `max_timestep` is caller-settable.
|
||||
`src/physics.c`, in the three drag blocks. The exact fix is
|
||||
`expf(-drag * dt)` instead of `1 - drag * dt`, which is unconditionally stable.
|
||||
|
||||
## Carried over
|
||||
|
||||
1. **Make character-to-sprite state bindings release their references symmetrically.**
|
||||
|
||||
Reference in New Issue
Block a user