Skip to content

feat(physics): add continuous collision detection for fast circles - #717

Merged
stormmuller merged 5 commits into
devfrom
claude/serene-goldberg-kd6be9
Oct 6, 2026
Merged

stormmuller merged 5 commits into
devfrom
claude/serene-goldberg-kd6be9

Conversation

@stormmuller

@stormmuller stormmuller commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements Phases 1 and 2 of design/continuous-collision-detection.md.

  • Sweep primitives: sweepCircleCircle, sweepCirclePolygon and sweepCircleTerrain, plus a SweepHit type (point, normal, t). They live in src/physics/ccd/ and are exported from @forge-game-engine/forge/physics. Each one sweeps a ray from the circle's center against the target's Minkowski sum with the circle. Only hits that enter the shape on its real boundary count: front faces within their span, and corner rounds within their normal cone. A sweep that starts already touching a feature ignores that feature, since narrow phase already handles that contact. Terrain is swept against its one-sided surface chain, the same one narrow phase uses, so a fast wheel doesn't catch on the tops of neighbouring columns.
  • createContinuousCollisionEcsSystem(): registered right after createEulerIntegrationEcsSystem. It sweeps each fast dynamic circle from position.world (where this tick's collision detection saw it) to position.local (where integration moved it), against static colliders whose collider.aabb overlaps the path. It follows the same rules as discrete collision: sensors are never swept and never stop anything, and pairs whose category/mask exclude each other are skipped. It only acts when the step would leave the circle more than 0.1·r inside a surface. In that case it moves position.local back to the first contact, leaving the circle 0.01·r inside so the next tick's narrow phase reports the contact. Velocity is left alone, and the next tick's collision resolution handles the contact.
  • Registered in every pipeline that runs Euler integration: all physics demos (including the new sensors demo) and /demo. The physics guides list it, and there's a new Continuous Collision Detection guide.

Deviations from the design doc

The solution-reviewer agent approved these. They are also recorded in the doc's new §11.

  • DL-3: the design had the CCD system write a one-tick clamp field on RigidBodyEcsComponent that createEulerIntegrationEcsSystem then reads and clears, which gives that field two writers. Instead, the system runs after integration and rewinds position.local. Box2D's b2SolveContinuous and Avian's swept CCD are structured the same way. Euler integration is unchanged.
  • DL-4: no per-body continuousDetection flag and no configurable threshold. Sensors and category/mask filtering already decide which pairs collide, and CCD honours them; any other opt-out could only let a body tunnel.
  • Threshold is 0.1·r, not 0.5·r. The diagnosed wheel moves 0.2-0.25·r per tick, so 0.5 would never fire for the reported bug. The same value is used to skip grazes, so fast rolling over bumpy ground isn't slowed (Box2D's "prevent pausing" rule).
  • Kinematic bodies are not swept targets. They move during the tick, so a sweep against their start pose gives the wrong time of impact.

Car demo finding

I probed the live Car demo under sustained throttle. CCD removes every landing that goes from no contact to deep in one tick: without it, 11-19 units at 750-1500 units/s downward, which matches the doc's diagnosis.

The demo's deepest penetrations (50-80 units) are a different problem: wheels that are already touching the ground sink further over several ticks. The joint systems run after createCollisionResolutionEcsSystem and get the last word on velocity, so they drive the wheel back into the ground. As an experiment, running contact resolution after the joints dropped the worst depth to about 12 units. The fix is a coupled contact and joint solver, as in Box2D, which needs its own design. It is not part of this PR.

Related issue(s)

Implements design/continuous-collision-detection.md.

Verification checklist

  • npm run check-types passes with 0 errors
  • npm test passes (1940 tests). New tests cover each sweep primitive and the system (including sensors and category/mask filtering), plus a full-pipeline regression test: the deepest wheel landing on the Car demo's terrain is 28 units without CCD and 9.5 with it. Another test checks that CCD doesn't slow a fast wheel rolling over bumpy terrain.
  • npm run lint passes with 0 errors
  • npm run cspell passes with 0 errors
  • npm run check-exports passes
  • Any new/changed public API is exported from the module's index.ts
  • Documentation under /documentation-site/docs/docs is updated
  • Demos updated and verified: root npm run build, docs npm run typecheck and npm run build, then the physics demos (car, sensors, physics, brick-breaker, rolling-ball, newtons-cradle and the rest) loaded in Chromium with no page errors.

Changelog

  • A bullet has been added under ## [Unreleased] in CHANGELOG.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01GPKzFGs2aYn7pN8vMzcQ6o

claude added 2 commits October 6, 2026 16:26
Adds createContinuousCollisionEcsSystem, registered right after
createEulerIntegrationEcsSystem. It sweeps each fast dynamic circle from
where this tick's collision detection saw it (position.world) to where
integration moved it (position.local) against static colliders, and moves
position.local back to the first contact when the step would sink the
circle more than a tenth of its radius into a surface. Adds the public
sweepCircleCircle/sweepCirclePolygon/sweepCircleTerrain primitives and
SweepHit, registers the system in every demo pipeline and the physics
guides, and records the implementation's deviations from the design doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GPKzFGs2aYn7pN8vMzcQ6o
Resolves the CHANGELOG conflict (keeps both the CCD Added entry and dev's
Changed entries) and updates the terrain sweep and CCD system tests for
TerrainCollider's slab now extending toward -y.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GPKzFGs2aYn7pN8vMzcQ6o
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.24812% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/physics/ccd/circle-sweep-hits.ts 98.85% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@stormmuller
stormmuller enabled auto-merge (squash) October 6, 2026 20:52
Resolves the CHANGELOG and physics index conflicts with collision
filtering, sensors and per-entity contacts, keeping both sides. Adapts
createContinuousCollisionEcsSystem to colliders owning their aabb: it now
queries [position, collider], never sweeps sensors or sweeps against them,
and skips target pairs whose category/mask exclude each other. Registers
the system in the new sensors demo and updates the CCD guide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GPKzFGs2aYn7pN8vMzcQ6o
@stormmuller
stormmuller merged commit 2b3fc6a into dev Oct 6, 2026
13 checks passed
@stormmuller
stormmuller deleted the claude/serene-goldberg-kd6be9 branch October 6, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants