flutter3d
Showcase Changelog 38 packages API reference

Keeping a Flame position and a flutter3d node in step: the source

since 0.7.0 The Flame bridge

Regions the guide quotes: live (line 92), shared-plane (line 138), scene-authoritative (line 143), flame-authoritative (line 165).

1/// `Object3dComponent` keeping a Flame `PositionComponent` and a flutter3d
2/// `SceneNode` at the same place, in both `SyncDirection`s.
3///
4/// Quoted by `flame_transform_bridge.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8
9import 'package:flame/components.dart';
10import 'package:flame_flutter3d/flame_flutter3d.dart';
11import 'package:flutter/widgets.dart';
12import 'package:flutter3d/flutter3d.dart';
13import 'package:flutter3d_showcase/src/demo/demo.dart';
14import 'package:flutter3d_showcase/src/demo/flame_layer.dart';
16final class FlameTransformBridgeDemo extends ShowcaseDemo {
17 late final double _flamePositionX;
18 late final double _flamePositionY;
19 late final double _scenePositionX;
20 late final double _scenePositionZ;
22 late final DemoContext _context;
23 late final Scene _scene;
24 late final MeshNode _leader;
25 late final TransparentFlameGame _game;
26 late final Widget _body = flameOrbit(
27 _context,
28 Flutter3dFlameWidget(
29 game: _game,
30 camera: _context.camera,
31 existing: (device: _context.device, renderer: _context.renderer),
32 buildScene: (GraphicsDevice device) => _scene,
33 ),
34 );
35 double _clock = 0.0;
37 @override
38 void configureView(DemoContext context) {
39 context.orbit
40 ..distance = 8.0
41 ..pitch = 0.7
42 ..yaw = 0.5;
43 context.orbit.target.setValues(0.0, 0.0, 0.0);
44 }
46 MeshNode _cube(DemoContext context, String name, Vector4 color) => MeshNode(
47 DeviceMesh.upload(
48 context.device,
49 CuboidShape(size: Vector3(0.5, 0.5, 0.5)).build(),
50 ),
51 Material(name: name, baseColor: color),
52 name: name,
53 )..setPosition(0.0, 0.25, 0.0);
55 @override
56 Scene build(DemoContext context) {
57 _context = context;
58 final (double flameX, double flameY, double sceneX, double sceneZ) = _run(
59 context.device,
60 );
61 _flamePositionX = flameX;
62 _flamePositionY = flameY;
63 _scenePositionX = sceneX;
64 _scenePositionZ = sceneZ;
66 _leader = _cube(context, 'scene leads', Vector4(0.5, 0.7, 0.9, 1.0));
67 final MeshNode follower = _cube(
68 context,
69 'flame leads',
70 Vector4(0.9, 0.6, 0.3, 1.0),
71 );
72 _scene = Scene()
73 ..ambientColor = Vector3(0.5, 0.55, 0.65)
74 ..ambientIntensity = 0.3
75 ..add(
76 MeshNode(
77 DeviceMesh.upload(
78 context.device,
79 CuboidShape(size: Vector3(8.0, 0.1, 8.0)).build(),
80 ),
81 Material(name: 'floor', baseColor: Vector4(0.36, 0.4, 0.38, 1.0)),
82 name: 'floor',
83 )..setPosition(0.0, -0.05, 0.0),
84 )
85 ..add(_leader)
86 ..add(follower)
87 ..add(
88 LightNode(name: 'sun', intensity: 2.5)
89 ..setLocalForward(Vector3(-0.3, -0.6, -0.4)),
90 );
92 // The same two components as above, now in a running game: each frame
93 // Flame updates them and each copies one way. The map in the corner is
94 // Flame's own view of the ground plane, a metre to 24 pixels.
95 final BridgePlane plane = BridgePlane.ground();
96 final Object3dComponent reads = Object3dComponent(
97 node: _leader,
98 scene: _scene,
99 plane: plane,
100 )..add(flameDot(const Color(0xFF80B3E6)));
101 final Object3dComponent writes = Object3dComponent(
102 node: follower,
103 scene: _scene,
104 plane: plane,
105 direction: SyncDirection.flameToScene,
106 )..add(flameDot(const Color(0xFFE6994D)));
107 final FlameMinimap map = FlameMinimap()
108 ..world.addAll(<Component>[reads, writes]);
109 _game = TransparentFlameGame();
110 _game
111 // Before `writes`, so the position is set before it is copied.
112 ..add(_Drift(writes))
113 ..add(map)
114 ..add(flameCaption('blue: the node moves, Flame reads it'))
115 ..add(
116 flameCaption(
117 'orange: Flame moves, the node reads it',
118 at: Vector2(16.0, 40.0),
119 ),
120 );
121 return _scene;
122 }
124 @override
125 void update(DemoContext context, double dt) {
126 _clock += dt;
127 context.orbit.syncProjectionDepth(context.camera);
128 // The flutter3d side leads: it moves its own node round in a circle.
129 _leader.setPosition(
130 2.6 * math.cos(_clock * 0.8),
131 0.25,
132 2.6 * math.sin(_clock * 0.8),
133 );
134 }
136 static (double, double, double, double) _run(GraphicsDevice device) {
137 final scene = Scene();
138 // One ground plane, shared by both components below, so a Flame (x, y)
139 // and a flutter3d (x, height, y) mean the same point everywhere in this
140 // scene.
141 final plane = BridgePlane.ground();
143 // The flutter3d side moves; `SyncDirection.sceneToFlame` is the default,
144 // so `Object3dComponent` reads the node and writes the Flame component.
145 final node = MeshNode(
146 DeviceMesh.upload(
147 device,
148 CuboidShape(size: Vector3(0.4, 0.4, 0.4)).build(),
149 ),
150 Material(
151 name: 'scene-authoritative',
152 baseColor: Vector4(0.5, 0.7, 0.9, 1.0),
153 ),
154 );
155 scene.add(node);
156 final sceneToFlame = Object3dComponent(
157 node: node,
158 scene: scene,
159 plane: plane,
160 );
161 node.setPosition(3.0, 0.0, 2.0);
162 sceneToFlame.update(1 / 60);
163 final Vector2 flamePosition = sceneToFlame.position;
165 // The Flame side moves instead; `SyncDirection.flameToScene` makes
166 // `Object3dComponent` read the Flame component and write the node.
167 final otherNode = MeshNode(
168 DeviceMesh.upload(
169 device,
170 CuboidShape(size: Vector3(0.4, 0.4, 0.4)).build(),
171 ),
172 Material(
173 name: 'flame-authoritative',
174 baseColor: Vector4(0.9, 0.6, 0.3, 1.0),
175 ),
176 );
177 scene.add(otherNode);
178 final flameToScene = Object3dComponent(
179 node: otherNode,
180 scene: scene,
181 plane: plane,
182 direction: SyncDirection.flameToScene,
183 );
184 flameToScene.position = Vector2(4.0, -1.0);
185 flameToScene.update(1 / 60);
186 final Vector3 scenePosition = otherNode.readPosition();
188 return (flamePosition.x, flamePosition.y, scenePosition.x, scenePosition.z);
189 }
191 /// The Flame game the page runs, for a test that steps it without a window.
192 @visibleForTesting
193 TransparentFlameGame get game => _game;
195 @override
196 Widget? customBody(BuildContext buildContext, DemoContext context) => _body;
198 @override
199 void verify(Scene scene, FrameResult frame) {
200 if (frame.drawCalls < 1) {
201 throw StateError('the marker was not drawn');
202 }
203 // Compared as numbers, not read back out of a formatted report: a whole
204 // number double loses its trailing `.0` when a web backend formats it,
205 // and this repository's own showcase pages were bitten by exactly that
206 // once already.
207 if (_flamePositionX != 3.0 || _flamePositionY != 2.0) {
208 throw StateError(
209 'moving the node to (3, _, 2) on a ground plane should read back as '
210 'Flame position (3, 2), got ($_flamePositionX, $_flamePositionY)',
211 );
212 }
213 if (_scenePositionX != 4.0 || _scenePositionZ != -1.0) {
214 throw StateError(
215 'moving the Flame position to (4, -1) on a ground plane should read '
216 'back as scene x=4, z=-1, got x=$_scenePositionX, z=$_scenePositionZ',
217 );
218 }
219 }
222/// Moves a bridged component along a figure of eight, the way a Flame game
223/// moves anything of its own: by setting its position.
224final class _Drift extends Component {
225 _Drift(this._target);
227 final Object3dComponent _target;
228 double _t = 0.0;
230 @override
231 void update(double dt) {
232 super.update(dt);
233 _t += dt;
234 _target.position = Vector2(
235 2.6 * math.sin(_t * 0.8),
236 1.6 * math.sin(_t * 1.6),
237 );
238 }