The ECS world: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: live (line 117), world (line 175), remap (line 188), read (line 208).
1/// The entity store every simulation in the engine is built on, and the tool
2/// that carries a save across a level that has since been edited.
3///
4/// Quoted by `ecs_world.md` and shown whole in the Source tab.
5library;
7import 'package:flutter3d/flutter3d.dart';
8import 'package:flutter3d_showcase/src/demo/demo.dart';
9import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
10import 'package:flutter3d_sim/flutter3d_sim.dart';
11import 'package:vector_math/vector_math.dart';
13final class _Position {
14 const _Position(this.x);
15 final double x;
16}
18final class EcsWorldDemo extends ShowcaseDemo {
19 late final double? _goblinX;
20 late final double? _trollX;
22 int reload = 0;
23 int _shown = -1;
25 late final Map<String, Object?> _saved;
26 late final List<MeshNode> _balls;
27 late final List<BarGauge> _bars;
28 late final List<MeshNode> _pedestals;
30 /// The names the reloaded level offers, in slot order, for each choice.
31 static const List<List<String?>> _levels = <List<String?>>[
32 <String?>['goblin', 'ogre', 'troll'],
33 <String?>['troll', 'goblin'],
34 <String?>['goblin'],
35 <String?>['ogre', 'goblin', 'ogre', 'troll'],
36 ];
39 void configureView(DemoContext context) {
40 context.orbit
41 ..distance = 12.0
42 ..pitch = 0.55
43 ..yaw = 0.2;
44 context.orbit.target.setValues(0.0, 0.8, 0.0);
45 }
48 Scene build(DemoContext context) {
49 final (String _, double? goblinX, double? trollX) = _run();
50 _goblinX = goblinX;
51 _trollX = trollX;
53 // The saved world, the same two entities `_run` saves.
54 final EcsWorld world = _newWorld();
55 final Entity goblin = world.spawn();
56 final Entity troll = world.spawn();
57 world
58 ..set(goblin, const _Position(3.0))
59 ..set(troll, const _Position(9.0));
60 _saved = world.save();
62 // Four slots is the most any choice below has: a pedestal, a ball that
63 // sits on it and a bar that stands for the number stored there.
64 const int slots = 4;
65 _pedestals = <MeshNode>[];
66 _balls = <MeshNode>[];
67 _bars = <BarGauge>[];
68 final List<SceneNode> nodes = <SceneNode>[
69 floorNode(context, width: 16.0, depth: 8.0),
70 ];
71 for (var i = 0; i < slots; i++) {
72 final double x = (i - (slots - 1) / 2) * 2.6;
73 final MeshNode pedestal = blockNode(
74 context,
75 'slot $i',
76 Vector3(1.8, 0.3, 1.8),
77 Vector4(0.5, 0.42, 0.34, 1.0),
78 at: Vector3(x, 0.15, 0.0),
79 );
80 final MeshNode ball = ballNode(
81 context,
82 'entity $i',
83 0.5,
84 Vector4(0.9, 0.9, 0.9, 1.0),
85 at: Vector3(x, 0.8, 0.0),
86 );
87 final BarGauge bar = BarGauge(
88 context,
89 'value $i',
90 Vector4(0.9, 0.75, 0.3, 1.0),
91 Vector3(x, 0.3, 1.8),
92 height: 2.5,
93 width: 0.4,
94 vertical: true,
95 );
96 _pedestals.add(pedestal);
97 _balls.add(ball);
98 _bars.add(bar);
99 nodes
100 ..add(pedestal)
101 ..add(ball)
102 ..addAll(bar.nodes);
103 }
104 return sceneOf(nodes);
105 }
107 EcsWorld _newWorld() => EcsWorld()
108 ..register<_Position>(
109 'position',
110 encode: (_Position p) => p.x,
111 decode: (Object? data) => _Position((data! as num).toDouble()),
112 );
114 /// The position stored in each slot of a level laid out as [names], or null
115 /// for a slot nothing was saved into.
116 List<double?> _reloadInto(List<String?> names) {
117 // The same remap as above, for whichever level is chosen: each saved
118 // entity finds its own name in the new layout, wherever that now is.
119 final remap = remapEntitySave(
120 _saved,
121 oldNames: <String?>['goblin', 'troll'],
122 newNames: names,
123 newGenerations: List<int>.filled(names.length, 0),
124 newFree: <int>[],
125 );
126 final EcsWorld reloaded = _newWorld();
127 final List<Entity> ids = <Entity>[
128 for (var i = 0; i < names.length; i++) reloaded.spawn(),
129 ];
130 reloaded.restore(remap.save);
131 return <double?>[
132 for (final Entity id in ids) reloaded.get<_Position>(id)?.x,
133 ];
134 }
137 void update(DemoContext context, double dt) {
138 if (reload == _shown) return;
139 _shown = reload;
140 final List<String?> names = _levels[reload];
141 final List<double?> found = _reloadInto(names);
142 for (var i = 0; i < _balls.length; i++) {
143 final bool has = i < names.length;
144 final double? x = has ? found[i] : null;
145 _pedestals[i].visible = has;
146 _balls[i].visible = x != null;
147 for (final SceneNode n in _bars[i].nodes) {
148 n.visible = x != null;
149 }
150 if (x == null) continue;
151 // Whoever held 3 is the goblin and whoever held 9 the troll.
152 _balls[i].material.baseColor.setFrom(
153 x < 5.0 ? Vector4(0.5, 0.8, 0.35, 1.0) : Vector4(0.85, 0.35, 0.3, 1.0),
154 );
155 _bars[i].set(x / 9.0);
156 }
157 }
160 List<DemoControl> controls(DemoContext context) => <DemoControl>[
161 ChoiceControl(
162 'The level reloads as',
163 options: const <String>[
164 'goblin, ogre, troll',
165 'troll, goblin',
166 'goblin only',
167 'ogre, goblin, ogre, troll',
168 ],
169 index: () => reload,
170 onChanged: (int i) => reload = i,
171 ),
172 ];
174 static (String, double?, double?) _run() {
175 final world = EcsWorld()
176 ..register<_Position>(
177 'position',
178 encode: (_Position p) => p.x,
179 decode: (Object? data) => _Position((data! as num).toDouble()),
180 );
181 final goblin = world.spawn();
182 final troll = world.spawn();
183 world
184 ..set(goblin, const _Position(3.0))
185 ..set(troll, const _Position(9.0));
186 final saved = world.save();
188 // The level is reloaded with the goblin's old slot now empty and a third
189 // monster ahead of the troll, so the indices EcsWorld would otherwise
190 // reuse no longer mean what they meant.
191 final remap = remapEntitySave(
192 saved,
193 oldNames: <String?>['goblin', 'troll'],
194 newNames: <String?>['goblin', 'ogre', 'troll'],
195 newGenerations: <int>[0, 0, 0],
196 newFree: <int>[],
197 );
198 final reloaded = EcsWorld()
199 ..register<_Position>(
200 'position',
201 encode: (_Position p) => p.x,
202 decode: (Object? data) => _Position((data! as num).toDouble()),
203 );
204 // Three spawns to give the world the same three slots `newNames` describes.
205 final ids = <Entity>[for (var i = 0; i < 3; i++) reloaded.spawn()];
206 reloaded.restore(remap.save);
208 final goblinX = reloaded.get<_Position>(ids[0])?.x;
209 final trollX = reloaded.get<_Position>(ids[2])?.x;
210 return (
211 'goblin at x=$goblinX, troll at x=$trollX, dropped: ${remap.dropped}',
212 goblinX,
213 trollX,
214 );
215 }
218 void verify(Scene scene, FrameResult frame) {
219 if (frame.drawCalls < 1) {
220 throw StateError('the entity marker was not drawn');
221 }
222 // Compared as numbers, not read back out of `_report`: a double that
223 // survives a JSON-shaped map on the way here is not guaranteed the type
224 // inference a web backend needs to print it with its trailing `.0`, and
225 // a compiled `3` failing a substring match against `'x=3.0'` would be
226 // this check catching its own string, not the remap.
227 if (_goblinX != 3.0 || _trollX != 9.0) {
228 throw StateError('the remap should carry each position to its own name');
229 }
230 }
231}