Rewinding: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: live (line 78), back (line 91), buffer (line 132), step (line 140), rewind (line 148).
1/// The last few seconds of a run, kept as one snapshot a second and the raw
2/// inputs between them, so a game can live them again for a kill camera or a
3/// rewind mechanic; and what each step of that run actually cost.
4///
5/// Quoted by `rewind.md` and shown whole in the Source tab.
6library;
8import 'dart:math' as math;
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
13import 'package:flutter3d_sim/flutter3d_sim.dart';
14import 'package:vector_math/vector_math.dart';
16final class RewindDemo extends ShowcaseDemo {
17 late final String _report;
19 bool automatic = true;
20 bool _rewindAsked = false;
22 late RewindBuffer _buffer;
23 late final MeshNode _runner;
24 late final MeshNode _ghost;
25 late final BarGauge _held;
26 double _x = 0.0;
27 double _owed = 0.0;
28 double _clock = 0.0;
29 double _sinceRewind = 0.0;
31 /// Metres to a unit of `x`.
32 static const double _scale = 0.25;
35 void configureView(DemoContext context) {
36 context.orbit
37 ..distance = 12.0
38 ..pitch = 0.5
39 ..yaw = 0.0;
40 context.orbit.target.setValues(8.0, 0.0, 0.0);
41 }
44 Scene build(DemoContext context) {
45 _report = _run();
46 _buffer = RewindBuffer(stepsPerSecond: 10, keyframeEvery: 5, history: 5);
47 _runner = ballNode(context, 'runner', 0.4, Vector4(0.5, 0.8, 0.6, 1.0));
48 _ghost = ballNode(context, 'ghost', 0.4, Vector4(0.45, 0.55, 0.95, 1.0));
49 _held = BarGauge(
50 context,
51 'history',
52 Vector4(0.7, 0.4, 0.8, 1.0),
53 Vector3(0.0, 0.0, 2.5),
54 height: 16.0,
55 );
56 return sceneOf(<SceneNode>[
57 blockNode(
58 context,
59 'track',
60 Vector3(18.0, 0.1, 3.0),
61 Vector4(0.34, 0.38, 0.36, 1.0),
62 at: Vector3(8.0, -0.05, 0.0),
63 ),
64 _runner,
65 _ghost,
66 ..._held.nodes,
67 ]);
68 }
71 void update(DemoContext context, double dt) {
72 _clock += dt;
73 _sinceRewind += dt;
74 if (automatic && _sinceRewind > 4.0) _rewindAsked = true;
75 _owed += dt * 10.0;
76 while (_owed >= 1.0) {
77 _owed -= 1.0;
78 // One step: record the input, take a keyframe when one is due, run the
79 // step.
80 _buffer.recorder.tape.frames.add(InputFrame(stickX: 1.0));
81 if (_buffer.keyframeDue) {
82 _buffer.keyframe(Snapshot(<String, Object?>{'x': _x}));
83 }
84 _x += 1.0;
85 if (_x * _scale > 15.5) {
86 // Off the end of the track: start the run again.
87 _buffer.reset();
88 _x = 0.0;
89 }
90 }
91 // Where the run stood a second ago, and the way to get there: the state of
92 // the nearest keyframe, and the steps to play forward from it.
93 final RewindPoint? back = _buffer.rewindBy(1.0);
94 final double? then = back == null
95 ? null
96 : (back.snapshot.data['x']! as num).toDouble() + back.replayed;
97 if (_rewindAsked && back != null && then != null) {
98 _rewindAsked = false;
99 _sinceRewind = 0.0;
100 // Make that the present: what came after it is forgotten.
101 _buffer.cut(back);
102 _x = then;
103 }
104 _rewindAsked = false;
105 _runner.setPosition(
106 _x * _scale,
107 0.4 + 0.15 * (0.5 + 0.5 * math.sin(_clock * 12.0)),
108 0.0,
109 );
110 _ghost.visible = then != null;
111 if (then != null) _ghost.setPosition(then * _scale, 0.4, 1.2);
112 _held.set(_buffer.step / 160.0);
113 }
116 List<DemoControl> controls(DemoContext context) => <DemoControl>[
117 ToggleControl(
118 'Rewind one second',
119 value: () => false,
120 onChanged: (bool v) {
121 if (v) _rewindAsked = true;
122 },
123 ),
124 ToggleControl(
125 'Rewind by itself',
126 value: () => automatic,
127 onChanged: (bool v) => automatic = v,
128 ),
129 ];
131 static String _run() {
132 final buffer = RewindBuffer(
133 stepsPerSecond: 10,
134 keyframeEvery: 5,
135 history: 5,
136 );
137 final timing = StepTimeTrace();
138 var x = 0.0;
140 for (var i = 0; i < 30; i++) {
141 buffer.recorder.tape.frames.add(InputFrame(stickX: 1.0));
142 if (buffer.keyframeDue) {
143 buffer.keyframe(Snapshot(<String, Object?>{'x': x}));
144 }
145 timing.record(buffer.step, () => x += 1.0);
146 }
148 // One second back from the present, thirty steps in at ten steps a
149 // second, lands on step 20.
150 final point = buffer.rewindBy(1.0)!;
152 return 'now at step ${buffer.step}, x=$x\n'
153 'one second back: step ${point.step}, '
154 'x=${point.snapshot.data['x']}, replaying ${point.replayed} inputs\n'
155 'mean step cost: ${timing.meanMillis?.toStringAsFixed(3)} ms';
156 }
159 void verify(Scene scene, FrameResult frame) {
160 if (frame.drawCalls < 1) {
161 throw StateError('the runner was not drawn');
162 }
163 if (!_report.contains('now at step 30') || !_report.contains('step 20')) {
164 throw StateError(
165 'a rewind of one second at ten steps a second should land on step 20 '
166 'of a run now at step 30',
167 );
168 }
169 }
170}