Bridging a flutter3d_sim actor: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: brain (line 16), live (line 84), system (line 144), actor (line 148), bridge (line 158), step (line 175).
1/// `ActorComponent` and `ActorSystemComponent` bridging a `flutter3d_sim`
2/// `Actor`'s own body onto a Flame position and a flutter3d mesh.
3///
4/// Quoted by `flame_ecs_bridge.md` and shown whole in the Source tab.
5library;
7import 'package:flame/components.dart' show Component;
8import 'package:flame_flutter3d/flame_flutter3d.dart';
9import 'package:flutter/widgets.dart';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_showcase/src/demo/flame_layer.dart';
13import 'package:flutter3d_sim/flutter3d_sim.dart';
14import 'package:vector_math/vector_math.dart';
16/// Walks straight towards +x, every step, for as long as it lives.
17final class _WalkBrain extends Brain {
19 void act(Mind it) => it.steer(Vector3(1.0, 0.0, 0.0));
20}
22final class FlameEcsBridgeDemo extends ShowcaseDemo {
23 late final String _report;
24 late final double _actorX;
25 late final double _flameX;
27 late final DemoContext _context;
28 late final Scene _scene;
29 late final TransparentFlameGame _game;
30 late final Widget _body = flameOrbit(
31 _context,
32 Flutter3dFlameWidget(
33 game: _game,
34 camera: _context.camera,
35 existing: (device: _context.device, renderer: _context.renderer),
36 buildScene: (GraphicsDevice device) => _scene,
37 ),
38 );
40 static const List<Color> _colors = <Color>[
41 Color(0xFF80CC4D),
42 Color(0xFF80B3E6),
43 Color(0xFFE6994D),
44 ];
46 /// Where the walkers wrap round, on x.
47 static const double _edge = 4.5;
50 void configureView(DemoContext context) {
51 context.orbit
52 ..distance = 10.0
53 ..pitch = 0.8
54 ..yaw = 0.4;
55 context.orbit.target.setValues(0.0, 0.0, 0.0);
56 }
59 Scene build(DemoContext context) {
60 _context = context;
61 final (String report, double actorX, double flameX) = _run(context.device);
62 _report = report;
63 _actorX = actorX;
64 _flameX = flameX;
66 _scene = Scene()
67 ..ambientColor = Vector3(0.5, 0.55, 0.65)
68 ..ambientIntensity = 0.3
69 ..add(
70 MeshNode(
71 DeviceMesh.upload(
72 context.device,
73 CuboidShape(size: Vector3(12.0, 0.1, 12.0)).build(),
74 ),
75 Material(name: 'floor', baseColor: Vector4(0.36, 0.4, 0.38, 1.0)),
76 name: 'floor',
77 )..setPosition(0.0, -0.05, 0.0),
78 )
79 ..add(
80 LightNode(name: 'sun', intensity: 2.5)
81 ..setLocalForward(Vector3(-0.3, -0.6, -0.4)),
82 );
84 // Three goblins in one shared system, each with a bridge component that
85 // copies its body onto its node and, through the plane, onto a Flame
86 // position. One `ActorSystemComponent` steps the system, once a frame; the
87 // map's dots are those Flame positions.
88 final CollisionWorld world = CollisionWorld()
89 ..addBox(Vector3(0.0, -0.5, 0.0), Vector3(20.0, 1.0, 20.0));
90 final ActorSystem system = ActorSystem(world: world, random: GameRandom(1));
91 final FlameMinimap map = FlameMinimap();
92 final List<Component> bridges = <Component>[];
93 final List<CharacterController> bodies = <CharacterController>[];
94 for (var i = 0; i < 3; i++) {
95 final CharacterController body = CharacterController(
96 world: world,
97 position: Vector3(-_edge + i * 1.5, 1.0, (i - 1) * 2.0),
98 );
99 bodies.add(body);
100 final Actor goblin = system.spawn(
101 body: body,
102 brain: _WalkBrain(),
103 name: 'goblin $i',
104 );
105 final MeshNode node = MeshNode(
106 DeviceMesh.upload(
107 context.device,
108 SphereShape(segments: 24, radius: 0.4).build(),
109 ),
110 Material(
111 name: 'goblin $i',
112 baseColor: Vector4(_colors[i].r, _colors[i].g, _colors[i].b, 1.0),
113 ),
114 name: 'goblin $i',
115 );
116 _scene.add(node);
117 bridges.add(
118 ActorComponent(
119 actor: goblin,
120 node: node,
121 scene: _scene,
122 plane: BridgePlane.ground(),
123 )..add(flameDot(_colors[i])),
124 );
125 }
126 map.world.addAll(bridges);
127 _game = TransparentFlameGame()
128 ..add(ActorSystemComponent(system: system, focus: Vector3.zero))
129 // After the system, so the bodies have moved by the time they are read.
130 ..add(_Wrap(bodies))
131 ..add(map);
132 _game
133 ..add(flameCaption('the dots: Flame positions read off the actors'))
134 ..add(flameCaption(_report, at: Vector2(16.0, 40.0)));
135 return _scene;
136 }
139 void update(DemoContext context, double dt) {
140 context.orbit.syncProjectionDepth(context.camera);
141 }
143 static (String, double, double) _run(GraphicsDevice device) {
144 final world = CollisionWorld();
145 world.addBox(Vector3(0.0, -0.5, 0.0), Vector3(20.0, 1.0, 20.0));
146 final system = ActorSystem(world: world, random: GameRandom(1));
148 final controller = CharacterController(
149 world: world,
150 position: Vector3(0.0, 3.0, 0.0),
151 );
152 final goblin = system.spawn(
153 body: controller,
154 brain: _WalkBrain(),
155 name: 'goblin',
156 );
158 final scene = Scene();
159 final node = MeshNode(
160 DeviceMesh.upload(device, SphereShape(segments: 16).build()),
161 Material(name: 'bridged-goblin', baseColor: Vector4(0.5, 0.7, 0.3, 1.0)),
162 );
163 scene.add(node);
164 final actorComponent = ActorComponent(
165 actor: goblin,
166 node: node,
167 scene: scene,
168 plane: BridgePlane.ground(),
169 );
170 final systemComponent = ActorSystemComponent(
171 system: system,
172 focus: Vector3.zero,
173 );
175 // The system decides where the goblin's body goes; the actor component
176 // copies that body's position onto the node, then the plane it shares
177 // carries it onto the Flame side — the same seam `Object3dComponent`
178 // gives every other bridged transform.
179 for (var i = 0; i < 90; i++) {
180 systemComponent.update(1 / 60);
181 actorComponent.update(1 / 60);
182 }
184 final double actorX = goblin.body!.position.x;
185 final double flameX = actorComponent.position.x;
186 return (
187 'the goblin walked to x=${actorX.toStringAsFixed(2)}; the Flame '
188 'position reads x=${flameX.toStringAsFixed(2)}',
189 actorX,
190 flameX,
191 );
192 }
194 /// The Flame game the page runs, for a test that steps it without a window.
196 TransparentFlameGame get game => _game;
199 Widget? customBody(BuildContext buildContext, DemoContext context) => _body;
202 void verify(Scene scene, FrameResult frame) {
203 if (frame.drawCalls < 1) {
204 throw StateError('the goblin marker was not drawn');
205 }
206 // Compared as numbers, not read back out of `_report`: see
207 // `sim_audio_xr/actors.dart` for why a formatted string is the wrong
208 // thing to assert a double against.
209 if (_actorX < 0.5) {
210 throw StateError(
211 'the goblin should have walked forward under the system, got '
212 'x=$_actorX',
213 );
214 }
215 if (_flameX != _actorX) {
216 throw StateError(
217 'the bridged Flame position should track the actor body exactly on '
218 'a ground plane, got flame x=$_flameX vs body x=$_actorX',
219 );
220 }
221 }
222}
224/// Sends a goblin that has walked off the far edge back to the near one.
225final class _Wrap extends Component {
226 _Wrap(this._bodies);
228 final List<CharacterController> _bodies;
231 void update(double dt) {
232 super.update(dt);
233 for (final CharacterController body in _bodies) {
234 if (body.position.x > FlameEcsBridgeDemo._edge) {
235 body.position.x = -FlameEcsBridgeDemo._edge;
236 }
237 }
238 }
239}