Skip to content

feat!: Migrate flame_forge2d to the Box2D v3 based forge2d - #3952

Open
spydon wants to merge 22 commits into
mainfrom
forge2d-box2d-v3
Open

feat!: Migrate flame_forge2d to the Box2D v3 based forge2d#3952
spydon wants to merge 22 commits into
mainfrom
forge2d-box2d-v3

Conversation

@spydon

@spydon spydon commented Jul 19, 2026

Copy link
Copy Markdown
Member

Description

Migrates flame_forge2d (and everything in the monorepo that uses it) to the new forge2d, the
native Box2D v3.1.1 bindings that are currently in review in the stacked chain
flame-engine/forge2d#115 (the native rewrite) and flame-engine/forge2d#116 (web support through a
WebAssembly build). This both prepares the migration and serves as real-world validation of the new
forge2d API while that chain is in review.

flame_forge2d now depends on the published forge2d: ^0.15.0. flame_forge2d stays in the
customer_testing.dart exclusions, because forge2d compiles Box2D from source through the Dart
build hooks and so needs a C toolchain on the flutter/flutter presubmit runner; the reasoning is
documented next to the exclusion.

Core package

  • Forge2DWorld steps the world with physicsWorld.step(dt, subStepCount: subStepCount) and then
    dispatches the polled contact and sensor events through the new overridable
    ContactEventsDispatcher (replacing WorldContactListener, since listener interfaces no longer
    exist). It keeps a Dart-side bodies set (upstream no longer exposes one) which the gravity
    setter uses to wake bodies, and exposes the new query API (castRayClosest, castRay,
    castRayAll, overlapAabb) plus forwarding setters for preSolveCallback and
    customFilterCallback.
  • ContactCallbacks keeps its familiar beginContact(Object other, Contact contact) shape through
    a new lightweight flame-side Contact class that wraps both contact and sensor events.
  • BodyComponent renders from the new Shape.geometry read-back (Circle, Capsule, Segment,
    Polygon; chain segments arrive as Segments), with renderShape/renderSegment and a new
    renderCapsule. fixtureDefs is replaced by shapeSpecs (a list of ShapeSpec, pairing a
    ShapeGeometry with an optional ShapeDef). The default createBody() auto-enables
    contact/sensor events on shapes whose body or shape userData is a ContactCallbacks, since the
    new engine only generates events for shapes that opted in.
  • SDK floors raised to Dart >=3.12.0 / Flutter >=3.44.0 (root workspace, melos bootstrap,
    package, and FLUTTER_MIN_VERSION in CI).
  • Forge2DGame awaits initializeForge2D() in its onLoad, and Forge2DWorld creates its
    physics world lazily so that this can happen first. Without it every Forge2DGame throws on the
    web, since that call is what loads the Box2D WebAssembly module. Code that creates a world
    outside of a Forge2DGame has to await it itself.
  • Tests rewritten against real physics worlds (mocked Fixture/Contact/Manifold are gone) and
    all goldens regenerated. flame's MultiTapDispatcher.handleTapDown annotation changed from
    @internal to @visibleForTesting so the tests can use it without ignores.

Examples and docs

  • The examples stories, padracing, and the package example are migrated. The examples for joints
    that no longer exist in Box2D v3 (gear, pulley, rope, friction, constant-volume) and the blob
    example are removed.
  • doc/bridge_packages/flame_forge2d/forge2d.md and joints.md are rewritten for the new API
    (including the new filter and wheel joints).

Verification

  • flutter test in packages/flame_forge2d (45 tests, compiles native Box2D through build hooks),
    goldens visually inspected.
  • dart analyze clean across the whole workspace.
  • flutter build web of the examples app confirms the Box2D wasm module is bundled automatically
    at the package asset path.

Notes for the forge2d review (found during this migration)

  • Shape.geometry read-back was added upstream during this work and is what makes
    BodyComponent rendering possible without a Dart-side geometry registry.
  • There is no upstream way to enumerate a world's bodies, hence the Dart-side set in
    Forge2DWorld.
  • Behavior change to be aware of: destroying a body clears its userData registries, so removed
    BodyComponents no longer receive a final endContact for contacts that end due to the
    destruction (the old engine fired those synchronously inside destroy).

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Migration instructions

  • Fixture and FixtureDef are gone: create shapes with
    body.createShape(geometry, ShapeDef(...)), where the geometry is a Circle, Capsule,
    Segment, or Polygon. Friction and restitution now live in ShapeDef.material
    (a SurfaceMaterial). body.fixtures becomes body.shapes.
  • BodyComponent.fixtureDefs becomes shapeSpecs, a list of ShapeSpec(geometry, [shapeDef]).
    renderFixture becomes renderShape, renderEdge becomes renderSegment, and renderChain
    is gone (chain segments render as segments).
  • Shape construction: CircleShape()..radius = r becomes Circle(radius: r, center: c),
    EdgeShape()..set(a, b) becomes Segment(point1: a, point2: b),
    PolygonShape()..setAsBoxXY(w, h) becomes Polygon.box(w, h), and
    ChainShape()..createChain/createLoop becomes body.createChain(ChainDef(points: ..., isLoop: ...)) (chains now require at least four points; the first and last points of an open chain are
    ghost anchors).
  • BodyDef.angle becomes BodyDef(rotation: Rot.fromAngle(angle)).
  • Contact events are now opt-in per shape: set ShapeDef.enableContactEvents (and
    enableSensorEvents for sensors and their visitors). The default BodyComponent.createBody()
    enables them automatically when a ContactCallbacks is present in the body's or shape's
    userData.
  • ContactCallbacks.beginContact/endContact keep their signatures but receive the new flame-side
    Contact (with shapeA, shapeB, bodyA, bodyB, begin-only normal/points, and
    isSensorEvent). preSolve/postSolve are removed: use Forge2DWorld.preSolveCallback with
    ShapeDef.enablePreSolveEvents, and hit events (ShapeDef.enableHitEvents +
    world.physicsWorld.contactEvents.hit) respectively.
  • WorldContactListener is replaced by ContactEventsDispatcher, and the contactListener
    parameter of Forge2DGame by contactEventsDispatcher.
  • Joints are now created with typed methods and destroyed on the joint:
    world.physicsWorld.createRevoluteJoint(RevoluteJointDef(bodyA: ..., bodyB: ...)) and
    joint.destroy(). The available joints are distance, filter, motor, mouse, prismatic, revolute,
    weld, and wheel; gear, pulley, rope, friction, and constant-volume joints no longer exist. The
    def initialize helpers are gone; anchors are passed as local points
    (body.localPoint(worldAnchor)).
  • Queries: world.raycast(callback, p1, p2) becomes castRayClosest/castRay/castRayAll
    (taking an origin and a translation), queryAABB becomes overlapAabb, and clearForces and
    the particle system are removed.
  • Body API renames: worldCenter becomes worldCenterOfMass, setAwake(x) becomes isAwake = x,
    getInertia() becomes rotationalInertia, and worldVector(v) becomes rotation.rotate(v).
  • Platform requirements: Dart 3.12+ / Flutter 3.44+, a C toolchain when building for native
    platforms, and nothing extra on the web (the Box2D wasm module is bundled automatically).

Related Issues

Depends on flame-engine/forge2d#115 and flame-engine/forge2d#116.

@spydon
spydon force-pushed the forge2d-box2d-v3 branch from 3778fcb to af1b435 Compare July 19, 2026 22:49
spydon and others added 16 commits July 20, 2026 21:24
Every widget of the default flutter create counter app rests on a
Forge2D body and drops to the floor, keeping its normal function.
Pressing the button counts and launches it, and pressing any widget
sends it flying. The bodies follow the shape Material draws: capsules
for the text, a rounded box for the button. Every widget is given the
same mass so the wide app bar does not crush the small counter, and the
walls are solid boxes so nothing squeezes out through a corner.
Add a heroTag to the counter button, drop a redundant trailing zero in
hue_decorator, teach cspell the word subclassing and use the en_US
spelling of neighboring.
@spydon
spydon marked this pull request as ready for review July 21, 2026 12:03

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.

Pull request overview

This PR migrates flame_forge2d (and the repo’s usages/examples/docs) from the legacy pure-Dart Box2D 2.x API to the new Box2D v3-based forge2d ^0.15.0, introducing the new polled contact/sensor event model, updated shape/joint/query APIs, and the required Forge2D initialization flow (especially for web/WASM).

Changes:

  • Reworks flame_forge2d core APIs: lazy physics-world creation + stepping/substepping, new contact event dispatching, new Contact wrapper, and BodyComponent shape rendering via Shape.geometry.
  • Introduces meters-to-pixels scaling via Forge2DViewfinder / Forge2DGame.metersToPixels (decoupled from camera zoom).
  • Migrates/updates tests, examples, and documentation (including new migration guides) to the Forge2D 0.15 / Box2D v3 API surface.

Reviewed changes

Copilot reviewed 71 out of 77 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
scripts/customer_testing.dart Excludes flame_forge2d from customer testing with rationale (native toolchain requirement).
packages/flame/lib/src/rendering/hue_decorator.dart Minor constant formatting change.
packages/flame/lib/src/events/dispatchers/multi_tap_dispatcher.dart Marks handleTapDown as @visibleForTesting for tests.
packages/flame_forge2d/test/world_contact_listener_test.dart Removes tests for deleted legacy listener API.
packages/flame_forge2d/test/helpers/mocks.dart Removes mocks tied to removed legacy Forge2D types.
packages/flame_forge2d/test/helpers/helpers.dart Removes obsolete export.
packages/flame_forge2d/test/forge2d_world_test.dart Updates/expands world behavior tests for new stepping/bodies/query/callback APIs.
packages/flame_forge2d/test/forge2d_viewfinder_test.dart Adds tests for new meters-to-pixels viewfinder behavior.
packages/flame_forge2d/test/forge2d_game_test.dart Updates screen/world conversion tests for meters-to-pixels scaling.
packages/flame_forge2d/test/contact_test.dart Adds tests for new flame-side Contact wrapper.
packages/flame_forge2d/test/contact_events_dispatcher_test.dart Adds tests for new contact/sensor event dispatcher + integration with Forge2DGame.
packages/flame_forge2d/test/contact_callbacks_test.dart Updates tests for new contact model and auto-enabling event flags.
packages/flame_forge2d/test/body_component_test.dart Migrates rendering/shape tests and adds coverage for new behaviors.
packages/flame_forge2d/README.md Updates Forge2D description + adds 0.19→0.20 migration section.
packages/flame_forge2d/pubspec.yaml Bumps dependency to forge2d: ^0.15.0.
packages/flame_forge2d/lib/world_contact_listener.dart Removes legacy listener-based API.
packages/flame_forge2d/lib/forge2d_world.dart Implements lazy world creation, stepping/substeps, bodies tracking, queries, callbacks, and event dispatching.
packages/flame_forge2d/lib/forge2d_viewfinder.dart Adds Forge2DViewfinder to separate meters-to-pixels scaling from camera zoom.
packages/flame_forge2d/lib/forge2d_game.dart Awaits Forge2D initialization on load; wires Forge2DViewfinder and metersToPixels.
packages/flame_forge2d/lib/flame_forge2d.dart Updates exports (adds new types, removes old listener).
packages/flame_forge2d/lib/contact.dart Adds flame-side Contact wrapper spanning contact + sensor events.
packages/flame_forge2d/lib/contact_events_dispatcher.dart Adds polled-events dispatcher routing to ContactCallbacks via userData.
packages/flame_forge2d/lib/contact_callbacks.dart Updates docs/API to shape-based contacts and new preSolve/hit guidance.
packages/flame_forge2d/lib/body_component.dart Replaces fixtures with ShapeSpec/shapes; updates rendering and hit-testing for new geometry model.
packages/flame_forge2d/example/lib/main.dart Migrates package example to new shape/material APIs.
examples/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart Rebuilds widget overlay example on new API; bodies fitted to measured widget sizes.
examples/lib/stories/bridge_libraries/flame_forge2d/utils/style.dart Adds shared example palette + Forge2DExampleGame + glowing rendering mixin.
examples/lib/stories/bridge_libraries/flame_forge2d/utils/joint_renderer.dart Adds joint rendering helpers compatible with new joint API.
examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart Migrates box components + adds mouse-joint rendering.
examples/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart Migrates boundary walls; updates defaults and styling.
examples/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart Migrates balls to new shapes/materials; integrates shared styling.
examples/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart Migrates example to Forge2DExampleGame + async onLoad.
examples/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart Migrates sprite-body example to new API and async onLoad.
examples/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart Migrates revolute/motor example; updates shapes/joints/materials.
examples/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart Migrates raycast example to castRayClosest/castRayAll.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/wheel_joint.dart Adds new wheel joint example for Box2D v3 API.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart Migrates weld joint example and adds joint rendering.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart Removes rope joint example (not in Box2D v3).
examples/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart Migrates revolute joint example and adds joint rendering.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart Removes pulley joint example (not in Box2D v3).
examples/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart Migrates prismatic joint example; replaces custom renderer with shared renderer.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart Migrates mouse joint example; adds joint rendering.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart Migrates motor joint example; uses new joint API + renderer.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart Removes gear joint example (not in Box2D v3).
examples/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart Removes friction joint example (not in Box2D v3).
examples/lib/stories/bridge_libraries/flame_forge2d/joints/filter_joint.dart Adds filter joint example for Box2D v3 API.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart Migrates distance joint example; uses new spring parameters + renderer.
examples/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart Removes constant-volume joint example (not in Box2D v3).
examples/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart Updates dashbook story set to match new/removed joint examples.
examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart Migrates drag callbacks example and async onLoad.
examples/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart Reworks domino example for new API and shared styling.
examples/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart Migrates contact callbacks example and async onLoad.
examples/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart Migrates composition example to new base game + async onLoad.
examples/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart Migrates camera example to Forge2DExampleGame and new scaling model.
examples/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart Removes blob example (depended on removed joint/system).
examples/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart Migrates animated body example and async onLoad.
examples/lib/main.dart Updates direct-route game registry to match available joint examples.
examples/games/padracing/lib/wall.dart Migrates PadRacing wall shape/material usage.
examples/games/padracing/lib/tire.dart Migrates tire body + revolute joint creation; updates renamed APIs.
examples/games/padracing/lib/padracing_game.dart Updates base game scaling init (meters-to-pixels vs zoom).
examples/games/padracing/lib/lap_line.dart Migrates lap sensor to shape-based sensor events.
examples/games/padracing/lib/car.dart Migrates car body shapes/materials and enables sensor events for lap detection.
examples/games/padracing/lib/ball.dart Migrates ball component to shape/material/contact-event flags.
doc/other_modules/other_modules.md Adds Forge2D module + navigation entries.
doc/other_modules/forge2d/migration.md Adds Forge2D 0.14→0.15 migration guide.
doc/other_modules/forge2d/forge2d.md Adds Forge2D module overview and getting-started docs.
doc/bridge_packages/flame_forge2d/migration.md Adds flame_forge2d 0.19→0.20 migration guide.
doc/bridge_packages/flame_forge2d/forge2d.md Updates Forge2D bridge docs for new initialization and scaling model.
doc/bridge_packages/flame_forge2d/flame_forge2d.md Adds migration page to docs toctree.
.github/.cspell/words_dictionary.txt Adds “subclassing” to spelling dictionary.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/flame_forge2d/lib/forge2d_world.dart
Asserts are stripped in release builds, so a Box2D world that fails to
allocate (for example when the world limit is hit) would be cached and
used by every later step, crashing far from the cause. Check it at
runtime and throw a StateError.
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