The simulation steps at a fixed rate
RP412PHYSICSHZ names a rate and the simulation advances in whole steps of exactly that size on every machine, whatever the display does. 0 - the default, and the shipped behaviour until the play testers have spoken - is the game as it has always run: the step is however long the last frame took, which makes the frame rate part of the physics. Measured over two seconds of free fall, a 30 fps machine's pod fell three times further than a 144 fps machine's. Two players on the same track were not in the same gravity. With a rate set, the same race is bit-identical across frame rates: 30, 60 and 144 fps produce the same trajectory to the last printed digit, and identical runs reproduce exactly - which was never true of this engine before, at any frame rate. It took three pieces, and every one was found by measuring, not by reading: - Simulation::PerformTo turns lastPerformance into the accumulator it always secretly was: whole steps while time remains, the remainder carried to the next frame. Watchers and update records stay once per frame - stepping is physics, watching is I/O. - Entity::PerformAndWatch interleaves subsystems and entity per STEP. The frame loop ran all subsystems to the frame boundary and then the entity, indistinguishable from correct at one step per frame - which is why thirty years of code never noticed - and wrong at two: the thrusters raycast twice from a vehicle that had not moved, and the hover spring fired twice on one stale height sample. The subsystems are also snapped onto their entity's step grid; each Simulation anchors its grid at its own creation time, a per-run phase no seed could pin. - Mover::BeginStep clears the force accumulator per step. It was cleared once per frame while the thrusters ADD per step, so step two of a frame integrated step one's thrust again - and how many steps a frame holds rides on wall-clock jitter, which is why identical configs measured a quarter-metre apart. The quaternion renormalise counts steps now too, for the same reason. The catch-up clamp is a quarter second of simulation whatever the rate, so a machine that cannot keep up slows down rather than seizing, and does so identically everywhere. The engine's clock counts milliseconds, so rates that do not divide 1000 - 60 among them - quietly run at the neighbouring millisecond step; the log now says so and names the exact ones. 25, 50 and 100 are exact, and all three are verified bit-identical across frame rates and across runs. Verified for a single vehicle settling under gravity and hover. Driving, collisions and the network are the next frontiers, in that order: the collision path writes the victim's state with wall-clock stamps and a hard-coded 0.1 s bounce, which single-player survives and lockstep will not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+40
-1
@@ -658,9 +658,48 @@ Bye_Bye:
|
||||
//
|
||||
//-----------------------------------------------
|
||||
// Make sure the position quaternion stays stable
|
||||
//
|
||||
// Frame-counting, so it only runs on the frame-coupled path - fixed
|
||||
// steps do the same thing in BeginStep, counted in STEPS, because
|
||||
// "every 20 frames" lands at a different point of the step sequence
|
||||
// on every machine and rounding at different points is drift.
|
||||
//-----------------------------------------------
|
||||
//
|
||||
if (++normalizeCount == 20)
|
||||
if (Simulation::FixedStep() <= (Scalar) 0 && ++normalizeCount >= 20)
|
||||
{
|
||||
localOrigin.angularPosition.Normalize();
|
||||
normalizeCount = 0;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// The per-STEP set-up. This is the same work Mover::PerformAndWatch does
|
||||
// once per frame above - and once per frame is exactly wrong under fixed
|
||||
// stepping: the thrusters ADD their forces into localAcceleration every
|
||||
// step, so an accumulator cleared per frame carries step one's thrust
|
||||
// into step two whenever a frame holds two steps. How many steps a frame
|
||||
// holds depends on wall-clock jitter, which made identical runs diverge
|
||||
// by a quarter of a metre while sitting still on the pad.
|
||||
//
|
||||
// Idempotent on purpose: the frame-level copy still runs first on every
|
||||
// path, and repeating this at each step start is a recompute from
|
||||
// current state, not an accumulation.
|
||||
//
|
||||
void
|
||||
Mover::BeginStep()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
localVelocity.linearMotion.MultiplyByInverse(
|
||||
worldLinearVelocity,
|
||||
localToWorld
|
||||
);
|
||||
localAcceleration = Motion::Identity;
|
||||
previousOrigin = localOrigin;
|
||||
|
||||
if (++normalizeCount >= 20)
|
||||
{
|
||||
localOrigin.angularPosition.Normalize();
|
||||
normalizeCount = 0;
|
||||
|
||||
Reference in New Issue
Block a user