Pausing and stepping a running game: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: timeline (line 19), play (line 115), pause (line 126), rewind (line 130).
1/// Pausing, stepping and rewinding a running game from outside it, built on
2/// a live rewind buffer.
3///
4/// **`flutter3d_game`'s real `RunTimeline` is not a dependency of this
5/// app.** Every field it holds and every line it runs is a type this app
6/// already depends on through `flutter3d_sim` — `RewindBuffer`,
7/// `InputState`, `InputTapePlayback`, `Snapshot` — so this page carries the
8/// class over in full rather than approximating it.
9///
10/// Quoted by `run_timeline.md` and shown whole in the Source tab.
11library;
13import 'package:flutter/widgets.dart';
14import 'package:flutter3d/flutter3d.dart';
15import 'package:flutter3d_showcase/src/demo/demo.dart';
16import 'package:flutter3d_sim/flutter3d_sim.dart';
17import 'package:vector_math/vector_math.dart';
19/// Pause, step and rewind, over a live [RewindBuffer] — carried over from
20/// `flutter3d_game`'s real class, whose whole body is `flutter3d_sim` types.
21final class _RunTimeline {
22 _RunTimeline({
23 required this.rewind,
24 required this.input,
25 required this.stepSim,
26 required this.restore,
27 this.stepSeconds = 1.0 / 60.0,
28 });
30 final RewindBuffer rewind;
31 final InputState input;
32 final void Function(double dt) stepSim;
33 final void Function(Snapshot snapshot) restore;
34 final double stepSeconds;
36 bool _paused = false;
37 bool get isPaused => _paused;
39 void pause() => _paused = true;
40 void resume() => _paused = false;
42 void stepOnce() {
43 if (!_paused) {
44 throw StateError('stepOnce is only valid while the timeline is paused');
45 }
46 stepSim(stepSeconds);
47 }
49 RewindPoint? preview(double secondsAgo) => rewind.rewindBy(secondsAgo);
51 void releaseAt(RewindPoint point) {
52 restore(point.snapshot);
53 final toPoint = InputTapePlayback(point.tapeToPoint);
54 while (!toPoint.isFinished) {
55 toPoint.applyTo(input);
56 stepSim(stepSeconds);
57 }
58 rewind.cut(point);
59 _paused = false;
60 }
61}
63final class RunTimelineDemo extends ShowcaseDemo {
64 late final String _report;
65 late final double _beforePause;
66 late final double _afterOneStep;
67 late final double _afterRelease;
68 late final bool _timelinePausedAfterRelease;
71 Scene build(DemoContext context) {
72 final (
73 String report,
74 double beforePause,
75 double afterOneStep,
76 double afterRelease,
77 bool timelinePausedAfterRelease,
78 ) = _run();
79 _report = report;
80 _beforePause = beforePause;
81 _afterOneStep = afterOneStep;
82 _afterRelease = afterRelease;
83 _timelinePausedAfterRelease = timelinePausedAfterRelease;
84 final material = Material(
85 name: 'runner',
86 baseColor: Vector4(0.6, 0.8, 0.5, 1.0),
87 );
88 final node = MeshNode(
89 DeviceMesh.upload(context.device, SphereShape(segments: 16).build()),
90 material,
91 );
92 return Scene()
93 ..add(node)
94 ..add(
95 LightNode(name: 'sun', intensity: 3.0)
96 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
97 );
98 }
100 static (String, double, double, double, bool) _run() {
101 var x = 0.0;
102 final buffer = RewindBuffer(
103 stepsPerSecond: 10,
104 keyframeEvery: 5,
105 history: 5,
106 );
107 final timeline = _RunTimeline(
108 rewind: buffer,
109 input: InputState(),
110 stepSim: (double dt) => x += 1.0,
111 restore: (Snapshot s) => x = (s.data['x']! as num).toDouble(),
112 stepSeconds: 1 / 10,
113 );
115 // Twenty steps of ordinary play, recorded the way a game loop already
116 // records every step for the rewind buffer.
117 for (var i = 0; i < 20; i++) {
118 buffer.recorder.tape.frames.add(InputFrame(stickX: 1.0));
119 if (buffer.keyframeDue) {
120 buffer.keyframe(Snapshot(<String, Object?>{'x': x}));
121 }
122 timeline.stepSim(1 / 10);
123 }
124 final beforePause = x;
126 timeline.pause();
127 timeline.stepOnce();
128 final afterOneStep = x;
130 final point = timeline.preview(1.0)!;
131 timeline.releaseAt(point);
132 final afterRelease = x;
134 final report =
135 'after twenty steps: x=$beforePause\n'
136 'paused, then stepped once by hand: x=$afterOneStep\n'
137 'rewound one second and released: x=$afterRelease, timeline paused: '
138 '${timeline.isPaused}';
139 return (report, beforePause, afterOneStep, afterRelease, timeline.isPaused);
140 }
143 Widget? customBody(BuildContext buildContext, DemoContext context) =>
144 Container(
145 color: const Color(0xFF14161A),
146 padding: const EdgeInsets.all(24),
147 alignment: Alignment.topLeft,
148 child: DefaultTextStyle(
149 style: const TextStyle(color: Color(0xFFE8E8EC), fontSize: 16),
150 child: Text(_report),
151 ),
152 );
155 void verify(Scene scene, FrameResult frame) {
156 if (frame.drawCalls < 1) {
157 throw StateError('the runner marker was not drawn');
158 }
159 // Compared as numbers, not read back out of `_report`: each of these is
160 // a whole-number double, and a web backend prints one of those without
161 // its trailing `.0` — a compiled `20` failing a substring match against
162 // `'x=20.0'` would be this check catching its own string, not the
163 // timeline.
164 if (_beforePause != 20.0) {
165 throw StateError('twenty recorded steps of one each should reach 20');
166 }
167 if (_afterOneStep != 21.0) {
168 throw StateError('a manual step while paused should still advance x');
169 }
170 if (_afterRelease != 10.0) {
171 throw StateError(
172 'rewinding one second on a ten-steps-a-second buffer should land '
173 'on the recorded state ten steps back, discarding the manual step '
174 'that was never recorded',
175 );
176 }
177 if (_timelinePausedAfterRelease) {
178 throw StateError(
179 'releasing a rewind point should leave the timeline '
180 'running, not paused',
181 );
182 }
183 }
184}