Skip to content

Add timeScale and public step() to RigidBodyComponentSystem - #9300

Merged
willeastcott merged 6 commits into
mainfrom
physics-paused-step
Sep 4, 2026
Merged

Add timeScale and public step() to RigidBodyComponentSystem#9300
willeastcott merged 6 commits into
mainfrom
physics-paused-step

Conversation

@willeastcott

@willeastcott willeastcott commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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 of app.timeScale. Values below 1 give slow motion, values above 1 fast forward. 0 pauses: 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, no collision* / trigger* events fire, and the physics timing stat is zeroed. Negative values are treated as 0.
  • app.systems.rigidbody.step(dt) is the former onUpdate body 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. onUpdate is now a scale check plus a step call and is @ignored.
  • Comment-only: AppBase#timeScale now says that it stops scripts, animation and physics together and points at the physics-only scale, since app.timeScale = 0 is the pause idiom most people (and coding agents) reach for first. step() states that calling it every frame while timeScale is above 0 advances the simulation twice. The RigidBodyComponentSystem class overview was tightened: it no longer says "raycasting" and "collisions" twice, it names app.systems.rigidbody as 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

  • The issue proposed a boolean paused. A numeric timeScale with 0 as pause is a strict superset (it also covers the slow-motion use case declaratively) and mirrors app.timeScale, so the PR went that way.
  • Zero is implemented as a full skip rather than 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 firing contact events from stale manifolds. The skip is keyed on the physics timeScale, not on the incoming delta, so apps that pause today via app.timeScale = 0 keep their current behaviour.
  • The property lives on the system rather than PhysicsWorld: pausing has to skip the engine-side orchestration (otherwise _updateDynamic would 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.
  • Forces applied via applyForce while paused accumulate and land on the first step after resume, because Bullet only clears forces inside stepSimulation. Documented on timeScale. Impulses and velocity changes are unaffected.
  • Slow motion below one fixed substep per frame steps the simulation intermittently. Whether that looks smooth is backend-dependent and the docs say so: the Ammo body reads the motion state, which Bullet interpolates by the residual accumulator, so transforms still advance every frame (verified below); other backends may only move bodies on frames in which a substep runs.
  • maxSubSteps and fixedTimeStep stay @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 / +paused lines reflect the first commit and no longer apply to the branch head.

Testing

  • Unit tests in test/framework/components/rigid-body/system.test.mjs: default stepping, delta scaling, full skip at 0, resume, negative scale treated as paused, manual step() while paused with an unscaled delta, and step() without a backend.
  • npm run lint, npm run build:types + npm run test:types, and typedoc are clean for the touched classes.
  • Verified against the real Ammo backend in the physics/falling-shapes example, driving frames with app.update(1 / 60): 60 frames at timeScale = 0 moved no body and the physics stat read 0; at timeScale = 0.25 the 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

  • I have read the contributing guidelines
  • My code follows the project's coding standards
  • This PR focuses on a single change

🤖 Generated with Claude Code

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>
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Public API report

This PR changes the public API surface (+2 / −0), per the docs' rules (@ignore / @Private / undocumented are excluded).

Show API diff
+RigidBodyComponentSystem.step(dt: number): void
+RigidBodyComponentSystem.timeScale: number

Informational only — this never fails the build.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2408.2 KB (+0.1 KB, +0.00%) 620.2 KB (+0.0 KB, +0.00%) 481.2 KB (+0.1 KB, +0.01%)
playcanvas.min.mjs 2405.6 KB (+0.1 KB, +0.00%) 619.0 KB (+0.0 KB, +0.01%) 480.9 KB (−0.0 KB, −0.01%)

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>
@willeastcott willeastcott changed the title Add paused flag and public step() to RigidBodyComponentSystem Add timeScale and public step() to RigidBodyComponentSystem Sep 4, 2026
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.timeScale to 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.

Comment thread src/framework/components/rigid-body/system.js Outdated
…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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to call it every step?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, maybe not. But that's unrelated to this PR.That code hasn't changed here.

@willeastcott
willeastcott merged commit 63a2c7b into main Sep 4, 2026
10 checks passed
@willeastcott
willeastcott deleted the physics-paused-step branch September 4, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: physics Physics related issue enhancement Request for a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow pausing the physics world

3 participants