Running without a screen: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: run (line 14), loop (line 145), report (line 152).
1/// The interface a tool that plays a game blind is written against: no
2/// window, no renderer, only a step, a save and a sentence about how things
3/// stand.
4///
5/// Quoted by `headless_run.md` and shown whole in the Source tab.
6library;
8import 'package:flutter3d/flutter3d.dart';
9import 'package:flutter3d_showcase/src/demo/demo.dart';
10import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
11import 'package:flutter3d_sim/flutter3d_sim.dart';
12import 'package:vector_math/vector_math.dart';
14/// A toy game: a walker that moves towards x=10 and stops.
15final class _WalkerRun implements HeadlessRun {
16 final Vector3 _position = Vector3.zero();
19 void step(double dt) {
20 if (outcome != RunOutcome.playing) return;
21 _position.x += 2.0 * dt;
22 if (_position.x >= 10.0) {
23 _position.x = 10.0;
24 outcome = RunOutcome.won;
25 }
26 }
29 Snapshot save() => Snapshot(<String, Object?>{'x': _position.x});
32 RunOutcome outcome = RunOutcome.playing;
35 Vector3 get position => _position;
38 void eye(Vector3 out) => out.setValues(_position.x, 1.7, _position.z);
41 void aim(Vector3 out) => out.setValues(1.0, 0.0, 0.0);
44 String get summary =>
45 'walking at x=${_position.x.toStringAsFixed(2)}, ${outcome.name}';
48 Map<String, Object?> get reading => <String, Object?>{
49 'x': _position.x,
50 'outcome': outcome.name,
51 };
52}
54final class HeadlessRunDemo extends ShowcaseDemo {
55 late final String _report;
57 bool _againAsked = false;
58 _WalkerRun _live = _WalkerRun();
59 late final MeshNode _walker;
60 late final BarGauge _progress;
61 late final MeshNode _flag;
64 void configureView(DemoContext context) {
65 context.orbit
66 ..distance = 14.0
67 ..pitch = 0.4
68 ..yaw = 0.2;
69 context.orbit.target.setValues(5.0, 0.5, 0.0);
70 }
73 Scene build(DemoContext context) {
74 _report = _drive();
75 _walker = ballNode(context, 'walker', 0.45, Vector4(0.8, 0.65, 0.3, 1.0));
76 _flag = blockNode(
77 context,
78 'goal',
79 Vector3(0.2, 2.0, 0.2),
80 Vector4(0.4, 0.85, 0.45, 1.0),
81 at: Vector3(10.0, 1.0, 0.0),
82 );
83 _progress = BarGauge(
84 context,
85 'progress',
86 Vector4(0.7, 0.45, 0.85, 1.0),
87 Vector3(0.0, 0.0, 2.0),
88 height: 10.0,
89 );
90 return sceneOf(<SceneNode>[
91 blockNode(
92 context,
93 'track',
94 Vector3(13.0, 0.1, 3.0),
95 Vector4(0.34, 0.38, 0.36, 1.0),
96 at: Vector3(5.0, -0.05, 0.0),
97 ),
98 _walker,
99 _flag,
100 ..._progress.nodes,
101 ]);
102 }
105 void update(DemoContext context, double dt) {
106 if (_againAsked) {
107 _againAsked = false;
108 _live = _WalkerRun();
109 }
110 // The same object the blind loop drives, stepped once a frame; when it
111 // has won it waits a moment and starts again.
112 _live.step(dt);
113 if (_live.outcome != RunOutcome.playing && _live.position.x >= 10.0) {
114 _pause += dt;
115 if (_pause > 1.5) {
116 _pause = 0.0;
117 _live = _WalkerRun();
118 }
119 }
120 _walker.setPosition(_live.position.x, 0.45, 0.0);
121 _progress.set(_live.position.x / 10.0);
122 _flag.material.baseColor.setValues(
123 _live.outcome == RunOutcome.won ? 0.4 : 0.85,
124 _live.outcome == RunOutcome.won ? 0.9 : 0.4,
125 0.4,
126 1.0,
127 );
128 }
130 double _pause = 0.0;
133 List<DemoControl> controls(DemoContext context) => <DemoControl>[
134 ToggleControl(
135 'Run it again',
136 value: () => false,
137 onChanged: (bool v) {
138 if (v) _againAsked = true;
139 },
140 ),
141 ];
143 /// What a tool that plays this blind does: step until the run is over.
144 static String _drive() {
145 final run = _WalkerRun();
146 var steps = 0;
147 while (run.outcome == RunOutcome.playing && steps < 1000) {
148 run.step(1 / 60);
149 steps++;
150 }
152 return '${run.summary} after $steps steps\n'
153 'reading: ${run.reading}\n'
154 'save: ${run.save().toJson()}';
155 }
158 void verify(Scene scene, FrameResult frame) {
159 if (frame.drawCalls < 1) {
160 throw StateError('the walker was not drawn');
161 }
162 if (!_report.contains('won')) {
163 throw StateError(
164 'a walker driven with no screen should still reach '
165 'the outcome its own rules describe',
166 );
167 }
168 }
169}