Forge2D

repository·main·Indexed 41 days ago

https://github.com/flame-engine/forge2d

Idiomatic Dart bindings for the native Box2D v3 physics engine, providing high-performance physics simulations for Dart and Flutter applications across Android, iOS, macOS, Windows, Linux, and Web (via WebAssembly).

Tokens
13.3K
Snippets
41
Records
50
Agent score
52%

What's inside Forge2D

  1. How the Forge2D backend seam works

    main

    Forge2D uses a layer called RawBox2D (found in raw_box2d.dart) as the exclusive interface between the public API and the underlying Box2D library. This abstraction allows the engine to support different transport mechanisms at compile time via backend.dart:

    • Native: Uses dart:ffi to call into a library built via hook/build.dart using generated bindings in lib/src/ffi/.
    • Web (Future): Will use dart:js_interop to call into a WebAssembly build of Box2D through a pointer-based C shim.

    If you are running on an unsupported platform, the engine uses a stub that throws an UnsupportedError.

  2. Data constraints for the Forge2D backend seam

    main

    To ensure compatibility between Native (FFI) and Web (Wasm) backends, all data crossing the RawBox2D seam must follow strict contract rules. If you are extending the backend or working with low-level primitives, adhere to these constraints:

    • Primitives Only: Use only int, double, bool, String, records of these types, and typed-data lists. Do not use dart:ffi or dart:js_interop types, or any Dart classes from the API layer.
    • 32-bit Integer Limit: Do not use 64-bit integers as dart2js cannot represent them.
      • World IDs: Pack as index1 | (generation << 16) (both are 16-bit).
      • Body/Shape/Chain/Joint IDs: Use a pair (index1, world0 | (generation << 16)) passed as two separate int parameters and returned as an (int, int) record.
      • Bit Fields: 64-bit collision filter masks must be passed as int using sign-extension semantics (e.g., -1 represents all bits set).
    • No Callbacks: Avoid passing callbacks across the seam. Instead, use polling for events or ensure queries return full results to avoid the high cost of passing function pointers to Wasm.
    • Flattened Structs: Definition structs (e.g., b2BodyDef) cross the seam as flattened named parameters. The implementation overwrites native b2Default*Def() values with the received parameters.
    • Ownership: The API layer owns all Dart objects. Native user data pointers should remain untouched; user data must never cross the seam.
  3. How to handle physics events (Contact, Sensor, Body Move)

    main

    Forge2D uses a polling model for events rather than callbacks. After calling world.step(), you should poll the following properties on the World instance to process interactions:

    • world.contactEvents
    • world.sensorEvents
    • world.bodyMoveEvents

    To enable these events for specific shapes, use ShapeDef:

    • For contact events: ShapeDef(enableContactEvents: true)
    • For sensor events: ShapeDef(isSensor: true, enableSensorEvents: true)
    • For hit events: ShapeDef(enableHitEvents: true)
  4. Migrate from Forge2D 0.14 to 0.15+

    main

    Forge2D 0.15 is a rewrite based on Box2D v3. Key changes include:

    • Resource Management: World, Body, Shape, and Chain are now value-like handles. They are NOT garbage collected; you must call destroy() explicitly and check isValid for existing handles.
    • Fixtures are gone: Bodies now carry Shapes directly via body.createShape(geometry, ShapeDef(...)). Use Circle, Capsule, Segment, or Polygon as geometry.
    • Material Properties: Friction, restitution, and rolling resistance are now set via ShapeDef(material: SurfaceMaterial(...)) instead of FixtureDef.
    • Event Model: ContactListener callbacks are replaced by polling world.contactEvents, world.sensorEvents, and world.bodyMoveEvents after a step.
    • Joints: Use type-specific methods like world.createRevoluteJoint(RevoluteJointDef(...)). Note that Gear, Pulley, Rope, Friction, and Constant-Volume joints are no longer supported.
    • Rotations: Use the Rot type (e.g., Rot.fromAngle(radians)) for BodyDef(rotation: ...) and body.setTransform(position, rotation). body.angle still returns a double.
    • Forces: body.applyForce and body.applyLinearImpulse now require a named point argument. applyForceToCenter has been removed; to apply at the center of mass, omit the point argument.
    • Gravity: A default World() now has a gravity of (0, -10). For top-down games, use World(gravity: Vector2.zero()).
  5. Install and setup Forge2D

    main

    Forge2D requires Dart 3.12+ (or Flutter 3.38+) and a C toolchain on your native platform:

    • macOS/iOS: Xcode
    • Windows: Visual Studio Build Tools
    • Linux: clang or gcc
    • Android: NDK

    Supported platforms include Android, iOS, macOS, Windows, Linux, and Web. On the web, Forge2D uses a WebAssembly build. For custom hosting, you can override the WASM location using initializeForge2D(wasmUri: ...).

    import 'package:forge2d/forge2d.dart';
    
    Future<void> main() async {
      // Required for web and recommended for cross-platform consistency
      await initializeForge2D();
      // ...
    }
  6. Run the Forge2D benchmark

    main

    To run the classic bench2d benchmark—which simulates a 40-high pyramid of boxes over 256 warm-up frames and 256 measured frames—use one of the following commands depending on your environment:

    Using Dart directly: Run the benchmark script from the packages/benchmark directory.

    Using Melos (from the repository root): If you are working within the monorepo, use the Melos command to trigger the benchmark task.

    # From the benchmark package directory
    dart run bin/bench2d.dart
    
    # Or from the repository root
    melos benchmark
  7. Configure simulation scale with lengthUnitsPerMeter

    main

    Box2D is tuned for meters, kilograms, and seconds. If your world cannot be laid out in meters (e.g., you want to use pixels), you must tell the engine how many of your units make up a meter during initialization. This is a process-wide setting and cannot be changed once a World exists.

    When scaling by a factor S:

    • Lengths scale by S
    • Velocities and accelerations scale by S
    • Masses scale by S²
    • Forces and impulses scale by S³
    • Torques scale by S⁴
    • Densities, friction, restitution, and damping remain unchanged.
    // Example: setting 100 units (e.g. pixels) per meter
    await initializeForge2D(lengthUnitsPerMeter: 100);
  8. Create and manage a simulation World

    main

    The World class is the central container for all physics entities, including bodies, shapes, and joints.

    Key Lifecycle Rules:

    • Creation: Worlds are created using the World constructor. You can provide a gravity vector or a full WorldDef for advanced configuration.
    • Threading: A world must only be used from the isolate that created it.
    • Destruction: You must explicitly call destroy() to free all bodies, shapes, and joints. Once destroyed, the world is no longer valid.
    • Mutation during stepping: You cannot create bodies or joints while the world is currently in a step(). If you attempt to destroy objects during a step (e.g., inside a collision callback), Forge2D automatically defers these actions until the step completes.
    // Basic world creation
    final world = World(gravity: Vector2(0, -9.8));
    
    // Advanced creation
    final world = World(definition: WorldDef(
      gravity: Vector2(0, -9.8),
      enableSleep: true,
      enableContinuous: true,
    ));
    
    // Simulation loop
    world.step(1 / 60);
    
    // Cleanup
    world.destroy();
  9. Quickstart: Create a basic physics simulation

    main

    To run a simulation, initialize the engine, create a World, define bodies with BodyDef, and attach shapes using createShape. Use world.step(dt) to advance the simulation. Note that World, Body, Shape, and Chain are value-like handles; you must call destroy() on them explicitly to free native resources.

    import 'package:forge2d/forge2d.dart';
    
    Future<void> main() async {
      await initializeForge2D();
    
      final world = World();
    
      // Static ground: a wide box whose top surface is at y = 0.
      world
          .createBody(BodyDef(position: Vector2(0, -1)))
          .createShape(Polygon.box(50, 1));
    
      // A dynamic box dropped from above.
      final box = world.createBody(
        BodyDef(type: BodyType.dynamic, position: Vector2(0, 10)),
      )..createShape(Polygon.square(0.5));
    
      for (var i = 0; i < 90; i++) {
        world.step(1 / 60);
      }
      print(box.position); // The box has landed on the ground.
    
      world.destroy();
    }
  10. Configure a WheelJoint using WheelJointDef

    main

    To create a WheelJoint, you must first define its properties using WheelJointDef. A wheel joint provides suspension, allowing bodyB to rotate freely around an anchor while translating along a specific axis on bodyA. It supports an optional spring, translation limits, and a motor.

    Configuration Properties

    PropertyTypeDescription
    bodyABodyThe first body (the base/chassis).
    bodyBBodyThe second body (the wheel).
    localAnchorAVector2The anchor point on bodyA in local coordinates. Defaults to (0, 0).
    localAnchorBVector2The anchor point on bodyB in local coordinates. Defaults to (0, 0).
    localAxisAVector2The suspension axis in bodyA's local coordinates. Defaults to (0, 1).
    enableSpringboolWhether the suspension spring is enabled. Defaults to true.
    hertzdoubleThe spring stiffness in hertz.
    dampingRatiodoubleThe spring damping ratio (non-dimensional).
    enableLimitboolWhether the translation limit is enabled. Defaults to false.
    lowerTranslationdoubleThe lower translation limit in meters.
    upperTranslationdoubleThe upper translation limit in meters.
    enableMotorboolWhether the motor is enabled. Defaults to false.
    maxMotorTorquedoubleThe maximum motor torque in Newton-meters.
    motorSpeeddoubleThe motor speed in radians per second.
    collideConnectedboolWhether the two bodies should collide.
    userDatadynamicUser data associated with the joint.
    final wheelDef = WheelJointDef(
      bodyA: chassisBody,
      bodyB: wheelBody,
      localAnchorA: Vector2(0, -1),
      localAnchorB: Vector2.zero(),
      localAxisA: Vector2(0, 1),
      enableSpring: true,
      hertz: 5.0,
      dampingRatio: 0.7,
      enableMotor: true,
      motorSpeed: 10.0,
      maxMotorTorque: 20.0,
    );