1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
use std::io::{self, Write};
use std::time;
/// Help animate [`TextCanvas`](crate::TextCanvas).
///
/// The main functions of interest are [`GameLoop::loop_fixed()`], and
/// [`GameLoop::loop_variable()`].
///
/// # Note
///
/// The other functions constitute the lower-level machinery. They are
/// made available in case one wanted to be more hands on. But for
/// normal use, use the `loop_*` functions.
pub struct GameLoop<'a> {
stdout: io::StdoutLock<'a>,
}
impl GameLoop<'_> {
/// Run a game loop with fixed time step.
///
/// In this mode, the time step is fixed to a given duration. This
/// means the frame rate is constant. This is simple to implement,
/// and better for physics.
///
/// <div class="warning">
///
/// The given time step must always be greater than the time it
/// takes to render a frame. If rendering a frame takes longer than
/// the time step, the frame rate will obviously be affected, and
/// the "fixed" nature of it will not hold.
///
/// </div>
///
/// # Return values
///
/// The `render_frame` closure must return an `Option<String>`. If
/// it returns `Some<String>`, then `String` will be rendered, and
/// on to the next iteration. If it returns `None`, the game loop
/// stops right there.
///
/// <div class="warning">
///
/// The screen is never cleared between frames; the new frame only
/// overwrites the old one in-place. This is to reduce the risk of
/// flickering. Thus, the string should never get smaller from one
/// iteration to the next, else it would not completely erase the
/// old frame.
///
/// </div>
pub fn loop_fixed(
time_step: time::Duration,
render_frame: &mut impl FnMut() -> Option<String>,
) {
let mut game_loop = Self::new();
game_loop.set_up();
loop {
let start = time::Instant::now();
let Some(frame) = render_frame() else {
break;
};
#[cfg(not(tarpaulin_include))] // Wrongly marked uncovered.
game_loop.update(&frame);
#[cfg(not(tarpaulin_include))] // Wrongly marked uncovered.
let delta_time = start.elapsed();
#[cfg(not(tarpaulin_include))] // Wrongly marked uncovered.
let time_to_next_frame = time_step.checked_sub(delta_time);
// If not, rendering took longer than time step. In which
// case, we want to render the next frame immediately.
if let Some(time_to_next_frame) = time_to_next_frame {
game_loop.sleep(time_to_next_frame);
}
}
game_loop.tear_down();
}
/// Run a game loop with variable time step.
///
/// In this mode, the time step is whatever time it takes to render
/// a frame. There is no artificial delay between frames, it's just
/// one after the other as fast as possible (although it _is_
/// possible for the user to `sleep()` manually inside the loop, to
/// save on some CPU cycles).
///
/// The render duration of the last frame is passed to the loop as
/// `delta_time`. `delta_time` should be used to modulate values to
/// make the speed of the animations independent of the frame rate.
///
/// # Return values
///
/// The `render_frame` closure must return an `Option<String>`. If
/// it returns `Some<String>`, then `String` will be rendered, and
/// on to the next iteration. If it returns `None`, the game loop
/// stops right there.
///
/// <div class="warning">
///
/// The screen is never cleared between frames; the new frame only
/// overwrites the old one in-place. This is to reduce the risk of
/// flickering. Thus, the string should never get smaller from one
/// iteration to the next, else it would not completely erase the
/// old frame.
///
/// </div>
pub fn loop_variable(render_frame: &mut impl FnMut(f64) -> Option<String>) {
let mut game_loop = Self::new();
game_loop.set_up();
// The first one is a bit... arbitrary. Close to 60fps.
let mut delta_time = time::Duration::from_millis(17);
loop {
let start = time::Instant::now();
let Some(frame) = render_frame(delta_time.as_secs_f64()) else {
break;
};
game_loop.update(&frame);
delta_time = start.elapsed();
}
#[cfg(not(tarpaulin_include))] // Wrongly marked uncovered.
game_loop.tear_down();
}
// Lower-level machinery, made available if one wants to be more
// hands on. For normal use, use the loop functions.
/// Create new `GameLoop` instance, acquiring a lock to stdout.
///
/// The lock will be valid for the entire lifetime of the loop.
#[must_use]
pub fn new() -> Self {
// Acquire the lock once (instead of on every call to `print!`).
let stdout = io::stdout().lock();
Self { stdout }
}
/// Set the stage for the render loop.
///
/// This function should be called once before the loop starts.
///
/// This effectively hides the blinking text cursor and clears the
/// screen.
pub fn set_up(&mut self) {
self.hide_text_cursor();
self.clear_screen();
}
/// Update the screen buffer with a new render.
///
/// This function should be called on every iteration of the loop.
///
/// This overwrites the current frame in-place, without clearing the
/// screen (to prevent flickering).
///
/// Any terminating newline character is stripped to ensure the
/// output exactly matches the canvas' height.
pub fn update(&mut self, frame: &str) {
// Always do extra operations _before_ drawing, to help keep
// drawing time to a minimum and help reduce flickering.
let frame = frame.strip_suffix('\n').unwrap_or(frame);
self.move_cursor_top_left();
self.print(frame);
// Flush because output may not contain newline.
self.flush();
}
/// Clean up after the render loop.
///
/// This function should be called once after the loop ends.
///
/// This restores the blinking text cursor, prints an end-of-line
/// (`\n`) character not to mess with the system prompt, and ensures
/// the output buffer is flushed.
pub fn tear_down(&mut self) {
self.show_text_cursor();
// The newline we've been omitting in the rendered frames.
self.print("\n");
self.flush();
}
// Even-lower-level machinery.
/// Clear the screen (soft).
///
/// This is a "soft" clear. Since it does not flush, it may not be
/// applied immediately (because of output buffering). It also won't
/// reset the cursor to the top-left.
///
/// For something more analogous to the system "clear" command, see
/// [`clear_screen()`](GameLoop::clear_screen).
pub fn clear(&mut self) {
self.print("\x1b[2J");
}
/// Clear the screen (hard).
///
/// This is a "hard" clear. Since it flushes, it will force the
/// screen to be empty.
///
/// <div class="warning">
///
/// This is meant to be used during setup, before the render loop.
/// To clear between frames, you're better off _overwriting_ instead
/// of clearing. See [`GameLoop::move_cursor_top_left()`].
///
/// </div>
pub fn clear_screen(&mut self) {
self.clear();
self.move_cursor_top_left();
self.flush();
}
/// Hide the blinking text cursor.
pub fn hide_text_cursor(&mut self) {
self.print("\x1b[?25l");
}
/// Restore the blinking text cursor.
pub fn show_text_cursor(&mut self) {
self.print("\x1b[?25h");
}
/// Move the cursor to the top-left corner of the screen.
///
/// That is, first row, first column.
///
/// This function is meant to be used as a screen-wide carriage
/// return (`\r`). With a regular `\r`, you move the cursor to the
/// start of the line, and anything you write will overwrite the
/// existing line. Here it is the same, but at the level of the
/// entire screen. Since the render always has the same dimension,
/// instead of clearing the screen, you can simply move the caret to
/// the start and overwrite the exising characters.
///
/// The benefit over a full-fledged `clear()` is that you never have
/// an intermediate state where the screen is empty. Not only does
/// this reduces the number of screen updates, it also prevents
/// flickering (no blank screen between two frames).
pub fn move_cursor_top_left(&mut self) {
self.print("\x1b[1;1H");
}
/// Print to the locked stdout.
#[inline]
pub fn print(&mut self, string: &str) {
#![allow(unreachable_code)]
#[cfg(test)]
{
// `print!` is silenced in tests, while stdout is not.
print!("{string}");
return;
}
_ = write!(self.stdout, "{string}");
}
/// Flush stdout.
///
/// Most often stdout is line-buffered. This means, what you write
/// to stdout will be kept in memory and not be displayed until it
/// sees and end-of-line `\n` character.
///
/// Flushing stdout forces the immediate display of what's in the
/// buffer, even if there is no `\n`.
pub fn flush(&mut self) {
_ = self.stdout.flush();
}
/// Sleep for some duration.
pub fn sleep(&self, duration: time::Duration) {
std::thread::sleep(duration);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TextCanvas;
#[test]
fn loop_fixed() {
let mut canvas = TextCanvas::new(3, 2);
let mut i = 0;
GameLoop::loop_fixed(time::Duration::from_millis(1), &mut || {
i += 1;
if i == 4 {
return None;
}
canvas.draw_text(&format!("{i}"), i - 1, 0);
Some(canvas.to_string())
});
assert_eq!(canvas.to_string(), "123\n⠀⠀⠀\n");
assert_eq!(i, 4);
}
#[test]
fn loop_fixed_exits_on_none() {
let mut canvas = TextCanvas::new(3, 2);
let mut i = 0;
GameLoop::loop_fixed(time::Duration::from_millis(1), &mut || {
i += 1;
if i == 2 {
return None;
}
canvas.draw_text(&format!("{i}"), 0, 0);
Some(canvas.to_string())
});
// First iteration passed.
assert_eq!(canvas.to_string(), "1⠀⠀\n⠀⠀⠀\n");
// Second iteration stopped.
assert_eq!(i, 2);
}
#[test]
fn loop_fixed_takes_longer_than_time_step() {
let mut i = 0;
GameLoop::loop_fixed(time::Duration::from_millis(1), &mut || {
i += 1;
if i == 2 {
return None;
}
// 10ms > 1ms
std::thread::sleep(time::Duration::from_millis(10));
Some(String::new())
});
}
#[test]
fn loop_variable() {
let mut canvas = TextCanvas::new(3, 2);
let mut i = 0;
GameLoop::loop_variable(&mut |_| {
i += 1;
if i == 4 {
return None;
}
canvas.draw_text(&format!("{i}"), i - 1, 0);
Some(canvas.to_string())
});
assert_eq!(canvas.to_string(), "123\n⠀⠀⠀\n");
assert_eq!(i, 4);
}
#[test]
fn loop_variable_exits_on_none() {
let mut canvas = TextCanvas::new(3, 2);
let mut i = 0;
GameLoop::loop_variable(&mut |_| {
i += 1;
if i == 2 {
return None;
}
canvas.draw_text(&format!("{i}"), 0, 0);
Some(canvas.to_string())
});
// First iteration passed.
assert_eq!(canvas.to_string(), "1⠀⠀\n⠀⠀⠀\n");
// Second iteration stopped.
assert_eq!(i, 2);
}
}