Add timeScale and public step() to RigidBodyComponentSystem - #9300
Conversation
Allow the physics simulation to be frozen while the rest of the application keeps running, and expose manual stepping. - `app.systems.rigidbody.paused` skips the entire per-frame physics update: trigger/compound/kinematic sync, the backend step, dynamic transform sync and contact reporting. Bodies freeze, no collision or trigger events fire, and the physics timing stat is zeroed while paused. - `app.systems.rigidbody.step(dt)` is the former onUpdate body made public so callers can advance the simulation manually (slow motion, fast forward, stepping while paused). It is a no-op without a physics backend. - `maxSubSteps` and `fixedTimeStep` are now documented rather than @ignore'd since step() semantics depend on them. The flag lives on the system rather than PhysicsWorld because pausing must skip the engine-side orchestration, not just the backend step, and because the system exists before a backend is installed. No backend changes needed. Closes #9272 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Build size reportThis PR changes the size of the minified bundles.
|
A numeric time scale is a strict superset of the boolean: 0 pauses the simulation, values below 1 give slow motion and values above 1 fast forward, mirroring AppBase#timeScale (the two multiply). step(dt) stays as the unscaled manual-stepping primitive. Zero is implemented as a full skip of the per-frame update rather than a zero-delta backend step, which is backend-dependent and would keep reporting stale contacts on Ammo builds without an internal tick callback. The skip is keyed on the physics time scale only, so apps that pause via app.timeScale = 0 keep their current behaviour. Negative values are treated as 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Exposing them is a separate API decision from pausing and scaling the simulation, so restore their @ignore tags exactly as on main. The timeScale and step() docs now describe fixed substeps generically instead of linking to or naming the hidden fields, and the step() example uses a literal 1/60 delta. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new public timeScale documentation currently reads like it guarantees Ammo-specific interpolation behavior for all backends, which should be clarified to avoid misleading API docs.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds physics-local time control to RigidBodyComponentSystem, enabling apps to pause/slow/fast-forward physics independently of app.timeScale, and to manually advance physics via a public step(dt) API (requested for UI/pause-menu scenarios in #9272).
Changes:
- Added
app.systems.rigidbody.timeScaleto scale (or pause) automatic per-frame physics stepping. - Exposed a public
app.systems.rigidbody.step(dt)method for manual, unscaled simulation advancement. - Added unit tests covering default stepping, scaling, pausing/skip semantics, resume, negative scale, and backend-missing behavior.
File summaries
| File | Description |
|---|---|
src/framework/components/rigid-body/system.js |
Adds timeScale, exposes step(dt), and updates per-frame onUpdate logic + JSDoc. |
test/framework/components/rigid-body/system.test.mjs |
Adds tests verifying scaled stepping, pausing behavior, resume, and manual stepping. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…uble stepping Comment-only. app.timeScale = 0 is the pause idiom most readers reach for first, so its docs now say it stops scripts, animation and physics together and redirect to RigidBodyComponentSystem#timeScale for pausing or slowing physics alone. step() now states that automatic stepping continues while timeScale is above 0, so calling it every frame as well advances the simulation twice, and that timeScale should be 0 when taking over stepping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…erpolation note Comment-only. The class overview said "raycasting" and "collisions" twice across two paragraphs and described the system as creating the physics world, which predates backend injection. It now says the system owns the world, names app.systems.rigidbody as the access path and keeps the backend and stepping paragraphs. The timeScale slow-motion note is now explicitly backend-dependent: the Ammo backend interpolates body transforms between substeps, other backends may only move bodies on frames in which a substep runs. This addresses the Copilot review comment that it read like a guarantee for every PhysicsWorld implementation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
|
||
| // Check to see whether we need to update gravity on the physics world | ||
| this._world.setGravity(this.gravity); | ||
| world.setGravity(this.gravity); |
There was a problem hiding this comment.
Do we need to call it every step?
There was a problem hiding this comment.
Maybe, maybe not. But that's unrelated to this PR.That code hasn't changed here.
Description
Adds a way to slow down, speed up or freeze the physics simulation independently of the rest of the application, plus manual stepping. Requested in #9272 for pause menus and inventory screens that must stay interactive while the game world stands still.
app.systems.rigidbody.timeScale(default 1) scales the delta the simulation is advanced by each frame, on top ofapp.timeScale. Values below 1 give slow motion, values above 1 fast forward.0pauses: the system skips its whole per-frame update (trigger, compound and kinematic sync, the backend step, dynamic transform sync and contact reporting), so bodies freeze, nocollision*/trigger*events fire, and the physics timing stat is zeroed. Negative values are treated as 0.app.systems.rigidbody.step(dt)is the formeronUpdatebody made public, so callers can advance the simulation manually with an explicit, unscaled delta: frame-by-frame debugging, fast forward by stepping several times in one frame, or a custom time source. No-op without a backend.onUpdateis now a scale check plus astepcall and is@ignored.AppBase#timeScalenow says that it stops scripts, animation and physics together and points at the physics-only scale, sinceapp.timeScale = 0is the pause idiom most people (and coding agents) reach for first.step()states that calling it every frame whiletimeScaleis above 0 advances the simulation twice. TheRigidBodyComponentSystemclass overview was tightened: it no longer says "raycasting" and "collisions" twice, it namesapp.systems.rigidbodyas the access path, and it describes the system as owning (rather than creating) the physics world now that backends can be injected.Notes for reviewers
paused. A numerictimeScalewith 0 as pause is a strict superset (it also covers the slow-motion use case declaratively) and mirrorsapp.timeScale, so the PR went that way.world.step(0). A zero-delta step is backend-dependent (Bullet takes no substeps, Jolt still runs collision detection), and on Ammo builds without an internal tick callback the post-step contact walk would keep firingcontactevents from stale manifolds. The skip is keyed on the physicstimeScale, not on the incoming delta, so apps that pause today viaapp.timeScale = 0keep their current behaviour.PhysicsWorld: pausing has to skip the engine-side orchestration (otherwise_updateDynamicwould keep snapping entity transforms every frame) and the system exists before a backend is installed. No backend changes; Ammo, Null and third-party worlds get this for free.applyForcewhile paused accumulate and land on the first step after resume, because Bullet only clears forces insidestepSimulation. Documented ontimeScale. Impulses and velocity changes are unaffected.maxSubStepsandfixedTimeStepstay@ignored as before. The new docs describe fixed substeps generically rather than linking to them, so whether to expose those two is left as a separate decision. The API report bot's+fixedTimeStep / +maxSubSteps / +pausedlines reflect the first commit and no longer apply to the branch head.Testing
test/framework/components/rigid-body/system.test.mjs: default stepping, delta scaling, full skip at 0, resume, negative scale treated as paused, manualstep()while paused with an unscaled delta, andstep()without a backend.npm run lint,npm run build:types+npm run test:types, andtypedocare clean for the touched classes.physics/falling-shapesexample, driving frames withapp.update(1 / 60): 60 frames attimeScale = 0moved no body and the physics stat read 0; attimeScale = 0.25the sampled body moved on all 12 sampled frames with steady per-frame deltas of about 0.023 m (versus about 0.11 m per frame at full speed), apart from a catch-up jump on the first frames after unpausing as Bullet's fixed-step accumulator realigns.Fixes #9272
Checklist
🤖 Generated with Claude Code