Bridging a falling rigid body and its collision
Open the live demo · Read the source · View on GitHub
RigidBodyComponent bridges one flutter3d_physics RigidBody the same way
Object3dComponent bridges any other transform — the solver decides where
the body is, and the Flame side only ever reads it. CollisionBridge
re-fires flutter3d's own collision events as Flame's CollisionCallbacks.
Step 1: A world and a body to fall through it #
A static floor, a Dynamics solver, and a RigidBody dropped above it. A
thin trigger sits just above the floor too — Dynamics stops a falling body
exactly at the surface it lands on and never inside it, so the floor itself
never has an overlap to report; the trigger genuinely overlaps the crate the
moment it arrives, without affecting how or where Dynamics actually settles
it, since Dynamics.step ignores triggers entirely.
final world = CollisionWorld();
world.addBox(Vector3(0.0, -0.5, 0.0), Vector3(6.0, 1.0, 6.0));
// A thin trigger embedded just above the floor, so the crate's fall
// genuinely overlaps something: `Dynamics` stops a falling body exactly
// at the surface it lands on, never inside it, so the floor itself never
// reports an overlap to relay. `Dynamics.step` ignores triggers entirely
// (`includeTriggers: false` in its own contact queries), so this sensor
// never affects how or where the crate actually lands.
final landingSensor = world.add(
Collider(
shape: CollisionBox(Vector3(3.0, 0.05, 3.0)),
position: Vector3(0.0, 0.02, 0.0),
kind: ColliderKind.trigger,
),
);
final dynamics = Dynamics(world: world);
final body = dynamics.add(
RigidBody(
world: world,
shape: CollisionBox(Vector3(0.3, 0.3, 0.3)),
position: Vector3(0.0, 2.0, 0.0),
mass: 1.0,
),
);
Step 2: Bridge the body and its collisions #
RigidBodyComponent wraps the body; CollisionBridge attaches itself to the
body's own collider and relays what it touches. The trigger has no Flame
component of its own, so a stand-in answers resolveOther for it — without
one, the bridge would have nothing to hand back and would call nothing at
all.
final scene = Scene();
final node = MeshNode(
DeviceMesh.upload(
device,
CuboidShape(size: Vector3(0.6, 0.6, 0.6)).build(),
),
Material(name: 'bridged-crate', baseColor: Vector4(0.7, 0.5, 0.3, 1.0)),
);
scene.add(node);
final component = _TrackingRigidBodyComponent(
body: body,
node: node,
scene: scene,
plane: BridgePlane.ground(),
);
// A Flame component standing in for the landing sensor, so the bridge
// below has somewhere real to hand back — a collider with no Flame side
// of its own would make `resolveOther` answer null, and the bridge
// calls nothing when it does.
final landingMarker = PositionComponent();
CollisionBridge(
collider: body.collider,
component: component,
resolveOther: (Collider other) =>
other == landingSensor ? landingMarker : null,
);
Step 3: Fall, land, and hear about it #
Each Dynamics step moves the body; each CollisionWorld.update dispatches
whatever now overlaps.
// Falls, lands, and the world dispatches the overlap each step — the
// same two calls `physics_particles/collision_layers.dart` already makes
// by hand, now feeding a Flame collision callback instead of a listener
// this page owns directly.
const double step = 1 / 60;
for (var i = 0; i < 180; i++) {
dynamics.step(step);
world.update();
}
component.update(step);
The crate settled where the solver's own resting height says it should, and
on the way down CollisionBridge had already turned its overlap with the
landing trigger into a call on the Flame-side component — the same event a
native Flame body colliding with another would fire.
Step 4: Watch it fall, over and over #
The same three pieces, now running. A component steps Dynamics and the
collision world once a Flame frame; the crate's own component copies where the
body went. The map in the corner is a side view of the same fall, Flame's y
being the height, drawn with BridgePlane.backdrop. When the crate touches the
landing pad, CollisionBridge hands the overlap to a Flame callback, and that
callback turns the pad green on both layers; a moment after the crate comes to
rest it is put back and dropped again from the height on the slider.
// The same world, body and bridge as above, running: a component steps the
// solver and dispatches the overlaps, and the crate's own component copies
// where the body went. The map is a side view, Flame's y being height.
final CollisionWorld world = CollisionWorld()
..addBox(Vector3(0.0, -0.5, 0.0), Vector3(6.0, 1.0, 6.0));
final Collider sensor = world.add(
Collider(
shape: CollisionBox(Vector3(3.0, 0.05, 3.0)),
position: Vector3(0.0, 0.02, 0.0),
kind: ColliderKind.trigger,
),
);
final Dynamics dynamics = Dynamics(world: world);
final RigidBody body = dynamics.add(
RigidBody(
world: world,
shape: CollisionBox(Vector3(0.3, 0.3, 0.3)),
position: Vector3(0.0, dropHeight, 0.0),
mass: 1.0,
),
);
final Map<String, Object?> start = body.save();
final RectangleComponent landing = RectangleComponent(
position: Vector2(-3.0, -0.03),
size: Vector2(6.0, 0.1),
paint: Paint()..color = _idle,
);
final _TrackingRigidBodyComponent crateComponent =
_TrackingRigidBodyComponent(
body: body,
node: crate,
scene: _scene,
plane: BridgePlane.backdrop(),
// The Flame side reacts to the collision it was handed.
onTouch: (bool touching) {
landing.paint.color = touching ? _touching : _idle;
_pad.material.baseColor.setFrom(touching ? _padTouching : _padIdle);
},
)..add(
RectangleComponent(
size: Vector2.all(0.6),
anchor: Anchor.center,
paint: Paint()..color = const Color(0xFFB2804D),
),
);
CollisionBridge(
collider: body.collider,
component: crateComponent,
resolveOther: (Collider other) => other == sensor ? landing : null,
);