flutter3d
Showcase Changelog 38 packages API reference

Bridging a falling rigid body and its collision: the source

since 0.7.0 The Flame bridge

Regions the guide quotes: live (line 120), world (line 198), body (line 215), bridge (line 224), fall (line 251).

1/// `RigidBodyComponent` and `CollisionBridge` carrying a falling
2/// `RigidBody`'s own physics, and its landing, onto Flame.
3///
4/// Quoted by `flame_physics_bridge.md` and shown whole in the Source tab.
5library;
6
7import 'package:flame/components.dart';
8import 'package:flame_flutter3d/flame_flutter3d.dart';
9import 'package:flutter/widgets.dart';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_physics/flutter3d_physics.dart';
12import 'package:flutter3d_showcase/src/demo/demo.dart';
13import 'package:flutter3d_showcase/src/demo/flame_layer.dart';
15/// Hears about the crate's landing the way any Flame component would, and
16/// tells whoever is watching.
17final class _TrackingRigidBodyComponent extends RigidBodyComponent {
18 _TrackingRigidBodyComponent({
19 required super.body,
20 required super.node,
21 required super.scene,
22 required super.plane,
23 this.onTouch,
24 });
26 bool collided = false;
27 PositionComponent? other;
29 /// Called with `true` when the crate starts touching the landing pad and
30 /// `false` when it stops.
31 final void Function(bool touching)? onTouch;
33 @override
34 void onCollisionStart(
35 Set<Vector2> intersectionPoints,
36 PositionComponent other,
37 ) {
38 super.onCollisionStart(intersectionPoints, other);
39 collided = true;
40 this.other = other;
41 onTouch?.call(true);
42 }
44 @override
45 void onCollisionEnd(PositionComponent other) {
46 super.onCollisionEnd(other);
47 onTouch?.call(false);
48 }
51final class FlamePhysicsBridgeDemo extends ShowcaseDemo {
52 late final String _report;
53 late final double _crateHeight;
54 late final bool _collided;
56 double dropHeight = 2.5;
58 late final DemoContext _context;
59 late final Scene _scene;
60 late final MeshNode _pad;
61 late final TransparentFlameGame _game;
62 late final Widget _body = flameOrbit(
63 _context,
64 Flutter3dFlameWidget(
65 game: _game,
66 camera: _context.camera,
67 existing: (device: _context.device, renderer: _context.renderer),
68 buildScene: (GraphicsDevice device) => _scene,
69 ),
70 );
72 static const Color _idle = Color(0xFFE6B333);
73 static const Color _touching = Color(0xFF4DD966);
74 static Vector4 get _padIdle => Vector4(0.45, 0.36, 0.14, 1.0);
75 static Vector4 get _padTouching => Vector4(0.2, 0.6, 0.28, 1.0);
77 @override
78 void configureView(DemoContext context) {
79 context.orbit
80 ..distance = 6.5
81 ..pitch = 0.3
82 ..yaw = 0.7;
83 context.orbit.target.setValues(0.0, 0.8, 0.0);
84 }
86 @override
87 Scene build(DemoContext context) {
88 _context = context;
89 final (String report, double height, bool collided) = _run(context.device);
90 _report = report;
91 _crateHeight = height;
92 _collided = collided;
94 MeshNode cuboid(String name, Vector3 size, Vector4 color) => MeshNode(
95 DeviceMesh.upload(context.device, CuboidShape(size: size).build()),
96 Material(name: name, baseColor: color),
97 name: name,
98 );
99 _pad = cuboid('landing pad', Vector3(6.0, 0.1, 6.0), _padIdle)
100 ..setPosition(0.0, 0.02, 0.0);
101 final MeshNode crate = cuboid(
102 'crate',
103 Vector3(0.6, 0.6, 0.6),
104 Vector4(0.7, 0.5, 0.3, 1.0),
105 );
106 _scene = Scene()
107 ..ambientColor = Vector3(0.5, 0.55, 0.65)
108 ..ambientIntensity = 0.3
109 ..add(
110 cuboid('floor', Vector3(6.0, 1.0, 6.0), Vector4(0.36, 0.4, 0.38, 1.0))
111 ..setPosition(0.0, -0.5, 0.0),
112 )
113 ..add(_pad)
114 ..add(crate)
115 ..add(
116 LightNode(name: 'sun', intensity: 2.5)
117 ..setLocalForward(Vector3(-0.3, -0.6, -0.4)),
118 );
120 // The same world, body and bridge as above, running: a component steps the
121 // solver and dispatches the overlaps, and the crate's own component copies
122 // where the body went. The map is a side view, Flame's y being height.
123 final CollisionWorld world = CollisionWorld()
124 ..addBox(Vector3(0.0, -0.5, 0.0), Vector3(6.0, 1.0, 6.0));
125 final Collider sensor = world.add(
126 Collider(
127 shape: CollisionBox(Vector3(3.0, 0.05, 3.0)),
128 position: Vector3(0.0, 0.02, 0.0),
129 kind: ColliderKind.trigger,
130 ),
131 );
132 final Dynamics dynamics = Dynamics(world: world);
133 final RigidBody body = dynamics.add(
134 RigidBody(
135 world: world,
136 shape: CollisionBox(Vector3(0.3, 0.3, 0.3)),
137 position: Vector3(0.0, dropHeight, 0.0),
138 mass: 1.0,
139 ),
140 );
141 final Map<String, Object?> start = body.save();
143 final RectangleComponent landing = RectangleComponent(
144 position: Vector2(-3.0, -0.03),
145 size: Vector2(6.0, 0.1),
146 paint: Paint()..color = _idle,
147 );
148 final _TrackingRigidBodyComponent crateComponent =
149 _TrackingRigidBodyComponent(
150 body: body,
151 node: crate,
152 scene: _scene,
153 plane: BridgePlane.backdrop(),
154 // The Flame side reacts to the collision it was handed.
155 onTouch: (bool touching) {
156 landing.paint.color = touching ? _touching : _idle;
157 _pad.material.baseColor.setFrom(touching ? _padTouching : _padIdle);
158 },
159 )..add(
160 RectangleComponent(
161 size: Vector2.all(0.6),
162 anchor: Anchor.center,
163 paint: Paint()..color = const Color(0xFFB2804D),
164 ),
165 );
166 CollisionBridge(
167 collider: body.collider,
168 component: crateComponent,
169 resolveOther: (Collider other) => other == sensor ? landing : null,
170 );
172 final FlameMinimap map = FlameMinimap();
173 map.world.position = Vector2(120.0, 190.0);
174 map.world.addAll(<Component>[
175 RectangleComponent(
176 position: Vector2(-4.0, 0.0),
177 size: Vector2(8.0, 0.6),
178 paint: Paint()..color = const Color(0xFF5C6660),
179 ),
180 landing,
181 crateComponent,
182 ]);
183 _game = TransparentFlameGame()
184 ..add(_Fall(world, dynamics, body, start, () => dropHeight))
185 ..add(map)
186 ..add(flameCaption('the map: a side view of the same fall'))
187 ..add(
188 flameCaption(
189 'the pad turns green when Flame hears the landing',
190 at: Vector2(16.0, 40.0),
191 ),
192 )
193 ..add(flameCaption(_report, at: Vector2(16.0, 64.0)));
194 return _scene;
195 }
197 static (String, double, bool) _run(GraphicsDevice device) {
198 final world = CollisionWorld();
199 world.addBox(Vector3(0.0, -0.5, 0.0), Vector3(6.0, 1.0, 6.0));
200 // A thin trigger embedded just above the floor, so the crate's fall
201 // genuinely overlaps something: `Dynamics` stops a falling body exactly
202 // at the surface it lands on, never inside it, so the floor itself never
203 // reports an overlap to relay. `Dynamics.step` ignores triggers entirely
204 // (`includeTriggers: false` in its own contact queries), so this sensor
205 // never affects how or where the crate actually lands.
206 final landingSensor = world.add(
207 Collider(
208 shape: CollisionBox(Vector3(3.0, 0.05, 3.0)),
209 position: Vector3(0.0, 0.02, 0.0),
210 kind: ColliderKind.trigger,
211 ),
212 );
213 final dynamics = Dynamics(world: world);
215 final body = dynamics.add(
216 RigidBody(
217 world: world,
218 shape: CollisionBox(Vector3(0.3, 0.3, 0.3)),
219 position: Vector3(0.0, 2.0, 0.0),
220 mass: 1.0,
221 ),
222 );
224 final scene = Scene();
225 final node = MeshNode(
226 DeviceMesh.upload(
227 device,
228 CuboidShape(size: Vector3(0.6, 0.6, 0.6)).build(),
229 ),
230 Material(name: 'bridged-crate', baseColor: Vector4(0.7, 0.5, 0.3, 1.0)),
231 );
232 scene.add(node);
233 final component = _TrackingRigidBodyComponent(
234 body: body,
235 node: node,
236 scene: scene,
237 plane: BridgePlane.ground(),
238 );
239 // A Flame component standing in for the landing sensor, so the bridge
240 // below has somewhere real to hand back — a collider with no Flame side
241 // of its own would make `resolveOther` answer null, and the bridge
242 // calls nothing when it does.
243 final landingMarker = PositionComponent();
244 CollisionBridge(
245 collider: body.collider,
246 component: component,
247 resolveOther: (Collider other) =>
248 other == landingSensor ? landingMarker : null,
249 );
251 // Falls, lands, and the world dispatches the overlap each step — the
252 // same two calls `physics_particles/collision_layers.dart` already makes
253 // by hand, now feeding a Flame collision callback instead of a listener
254 // this page owns directly.
255 const double step = 1 / 60;
256 for (var i = 0; i < 180; i++) {
257 dynamics.step(step);
258 world.update();
259 }
260 component.update(step);
262 return (
263 'settled at y=${body.position.y.toStringAsFixed(2)}; the bridge heard '
264 'the landing: ${component.collided}',
265 body.position.y,
266 component.collided,
267 );
268 }
270 /// The Flame game the page runs, for a test that steps it without a window.
271 @visibleForTesting
272 TransparentFlameGame get game => _game;
274 @override
275 Widget? customBody(BuildContext buildContext, DemoContext context) => _body;
277 @override
278 void update(DemoContext context, double dt) {
279 context.orbit.syncProjectionDepth(context.camera);
280 }
282 @override
283 List<DemoControl> controls(DemoContext context) => <DemoControl>[
284 SliderControl(
285 'Drop height',
286 min: 0.5,
287 max: 4.0,
288 value: () => dropHeight,
289 onChanged: (double v) => dropHeight = v,
290 format: (double v) => '${v.toStringAsFixed(1)} m',
291 ),
292 ];
294 @override
295 void verify(Scene scene, FrameResult frame) {
296 if (frame.drawCalls < 1) {
297 throw StateError('the crate marker was not drawn');
298 }
299 // Compared as numbers and booleans, not read back out of `_report`: see
300 // `sim_audio_xr/actors.dart` for why.
301 if ((_crateHeight - 0.3).abs() > 0.05) {
302 throw StateError(
303 'the crate should have settled at y = 0.3, got $_crateHeight',
304 );
305 }
306 if (!_collided) {
307 throw StateError(
308 'CollisionBridge should have relayed the landing to the Flame '
309 'component',
310 );
311 }
312 }
315/// Steps the solver and the collision world once a Flame frame, and lets the
316/// crate go again a moment after it lands. Added before the crate's own
317/// component, so the body has moved by the time the component copies it.
318final class _Fall extends Component {
319 _Fall(this._world, this._dynamics, this._body, this._start, this._height);
321 final CollisionWorld _world;
322 final Dynamics _dynamics;
323 final RigidBody _body;
324 final Map<String, Object?> _start;
325 final double Function() _height;
326 double _rest = 0.0;
328 @override
329 void update(double dt) {
330 super.update(dt);
331 _dynamics.step(1 / 60);
332 _world.update();
333 _rest = _body.isAsleep ? _rest + dt : 0.0;
334 if (_rest > 1.2) {
335 _body.restore(_start);
336 _body.position.y = _height();
337 _rest = 0.0;
338 }
339 }