flutter3d
Showcase Changelog 38 packages API reference

Systems and events: the source

since 0.5.0 Simulation, audio and XR

Regions the guide quotes: live (line 82), systems (line 159), run (line 181), remove (line 187).

1/// Work a game hangs off a fixed step without owning the step itself, and the
2/// events a step reports so a frame can react without polling.
3///
4/// Quoted by `step_systems.md` and shown whole in the Source tab.
5library;
6
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 _ScoreChanged extends GameEvent {
14 const _ScoreChanged(this.total);
15 final int total;
17 @override
18 String get name => 'ScoreChanged($total)';
21final class StepSystemsDemo extends ShowcaseDemo {
22 late final String _report;
24 double scoringOrder = 0.0;
25 bool loggingOn = true;
27 bool _dirty = true;
28 late StepSystems _systems;
29 final List<String> _ran = <String>[];
30 late final List<MeshNode> _slots;
31 late final BarGauge _tower;
32 int _score = 0;
33 double _sinceStep = 0.0;
35 static const Map<String, List<double>> _colours = <String, List<double>>{
36 'input': <double>[0.45, 0.65, 0.95],
37 'scoring': <double>[0.95, 0.8, 0.3],
38 'logging': <double>[0.75, 0.45, 0.85],
39 };
41 @override
42 void configureView(DemoContext context) {
43 context.orbit
44 ..distance = 10.0
45 ..pitch = 0.75
46 ..yaw = 0.0;
47 context.orbit.target.setValues(1.0, 0.5, 0.0);
48 }
50 @override
51 Scene build(DemoContext context) {
52 _report = _run();
53 _slots = <MeshNode>[
54 for (var i = 0; i < 3; i++)
55 blockNode(
56 context,
57 'slot $i',
58 Vector3(1.4, 0.5, 1.4),
59 Vector4(0.25, 0.26, 0.3, 1.0),
60 at: Vector3(-1.5 + i * 2.0, 0.25, 1.5),
61 ),
62 ];
63 _tower = BarGauge(
64 context,
65 'score',
66 Vector4(0.95, 0.8, 0.3, 1.0),
67 Vector3(-5.0, 0.0, 0.0),
68 height: 4.0,
69 width: 1.0,
70 vertical: true,
71 );
72 return sceneOf(<SceneNode>[
73 floorNode(context, width: 14.0, depth: 8.0),
74 ..._slots,
75 ..._tower.nodes,
76 ]);
77 }
79 /// The three systems of the page, registered in one order and asked to run
80 /// in another: `scoring` is registered before `input` and after it in order.
81 void _register() {
82 final StepSystems systems = StepSystems();
83 systems.add(
84 StepPhase.begin,
85 (StepContext step) {
86 _ran.add('scoring');
87 _score += 5;
88 },
89 order: scoringOrder.round(),
90 label: 'scoring',
91 );
92 systems.add(
93 StepPhase.begin,
94 (StepContext step) => _ran.add('input'),
95 order: -10,
96 label: 'input',
97 );
98 if (loggingOn) {
99 systems.add(
100 StepPhase.begin,
101 (StepContext step) => _ran.add('logging'),
102 order: 10,
103 label: 'logging',
104 );
105 }
106 _systems = systems;
107 }
109 @override
110 void update(DemoContext context, double dt) {
111 if (_dirty) {
112 _dirty = false;
113 _register();
114 }
115 _sinceStep += dt;
116 if (_sinceStep >= 0.6) {
117 _sinceStep = 0.0;
118 _ran.clear();
119 _systems.run(StepPhase.begin, 0.6);
120 if (_score >= 100) _score = 0;
121 }
122 // Slot i shows whichever system ran i-th this step, dimming as the next
123 // step approaches.
124 final double glow = 1.0 - _sinceStep / 0.6;
125 for (var i = 0; i < _slots.length; i++) {
126 final List<double> c = i < _ran.length
127 ? _colours[_ran[i]]!
128 : const <double>[0.25, 0.26, 0.3];
129 final double k = i < _ran.length ? 0.35 + 0.65 * glow : 1.0;
130 _slots[i].material.baseColor.setValues(c[0] * k, c[1] * k, c[2] * k, 1.0);
131 }
132 _tower.set(_score / 100.0);
133 }
135 @override
136 List<DemoControl> controls(DemoContext context) => <DemoControl>[
137 SliderControl(
138 'Scoring order',
139 min: -20,
140 max: 20,
141 value: () => scoringOrder,
142 onChanged: (double v) {
143 scoringOrder = v.roundToDouble();
144 _dirty = true;
145 },
146 format: (double v) => v.round().toString(),
147 ),
148 ToggleControl(
149 'Logging registered',
150 value: () => loggingOn,
151 onChanged: (bool v) {
152 loggingOn = v;
153 _dirty = true;
154 },
155 ),
156 ];
158 static String _run() {
159 final systems = StepSystems();
160 final events = GameEvents();
161 var score = 0;
162 final order = <String>[];
164 final logging = systems.add(
165 StepPhase.begin,
166 (StepContext step) => order.add('logging'),
167 order: 10,
168 label: 'logging',
169 );
170 systems.add(
171 StepPhase.begin,
172 (StepContext step) {
173 order.add('scoring');
174 score += 5;
175 events.add(_ScoreChanged(score));
176 },
177 order: 0,
178 label: 'scoring',
179 );
181 // Registered "logging" first but ordered after "scoring": order wins,
182 // and only ties fall back on registration.
183 systems.run(StepPhase.begin, 1 / 60);
184 final drained = events.drain();
185 final firstOrder = List<String>.of(order);
187 // The handle `add` returned removes the system again; a step run after
188 // that no longer calls it.
189 systems.remove(logging);
190 order.clear();
191 systems.run(StepPhase.begin, 1 / 60);
192 final afterRemoval = List<String>.of(order);
194 return 'ran in order: ${firstOrder.join(', ')}; '
195 'score is now $score; '
196 'events this step: ${drained.map((GameEvent e) => e.name).join(', ')}; '
197 'after removing logging: $afterRemoval';
198 }
200 @override
201 void verify(Scene scene, FrameResult frame) {
202 if (frame.drawCalls < 1) {
203 throw StateError('the coin was not drawn');
204 }
205 if (!_report.contains('ran in order: scoring, logging')) {
206 throw StateError(
207 'a lower order number should run first, whatever order '
208 'the systems were registered in',
209 );
210 }
211 if (!_report.contains('after removing logging: [scoring]')) {
212 throw StateError('a removed system should not run on the next step');
213 }
214 }