gemini_mainloop/macros.rs
1/// You can use the `fps_gameloop!` macro to avoid writing a lot of boilerplate code. Take this block of code from a program written using the `gemini-engine` crate, for example:
2/// ```no_run
3/// # use gemini_engine::{core::{ColChar, Vec2D}, view::View, mesh3d::{Mesh3D, Transform3D}, view3d::Viewport, gameloop};
4/// # use std::time::Instant;
5/// # let mut view = View::new(0, 0, ColChar::BACKGROUND);
6/// # let mut viewport = Viewport::new(Transform3D::default(), 0.0, Vec2D::ZERO);
7/// viewport.objects.push(Mesh3D::default_cube());
8/// let FPS = 30.0;
9///
10/// let mut frame_skip = false;
11/// loop {
12/// let now = Instant::now();
13///
14/// // Logic
15/// let cube_transform = &mut viewport.objects[0].transform;
16/// *cube_transform = cube_transform
17/// .mul_mat4(&Transform3D::from_rotation_y(-0.05));
18///
19/// // Rendering
20/// if !frame_skip {
21/// view.clear();
22/// view.draw(&viewport);
23/// view.display_render().unwrap();
24/// }
25///
26/// let elapsed = now.elapsed();
27/// frame_skip = gameloop::sleep_fps(FPS, Some(elapsed));
28/// }
29/// ```
30/// There's a lot of boilerplate code here. That's where this macro comes in. Here is the same block of code, rewritten with `fps_gameloop!`:
31/// ```no_run
32/// # use gemini_engine::{core::{ColChar, Vec2D}, view::View, view3d::{Viewport, DisplayMode}, mesh3d::{Mesh3D, Transform3D}};
33/// # use main_loop_root::fps_gameloop;
34/// # let mut view = View::new(0, 0, ColChar::BACKGROUND);
35/// # let mut viewport = Viewport::new(Transform3D::default(), 0.0, Vec2D::ZERO);
36/// viewport.objects.push(Mesh3D::default_cube());
37/// let FPS = 30.0;
38///
39/// fps_gameloop!(
40/// {
41/// let cube_transform = &mut viewport.objects[0].transform;
42/// *cube_transform = cube_transform
43/// .mul_mat4(&Transform3D::from_rotation_y(-0.05));
44/// },
45/// {
46/// view.clear();
47/// view.draw(&viewport);
48/// view.display_render().unwrap();
49/// },
50/// FPS
51/// );
52/// ```
53/// The code is now a lot less cluttered. This macro accepts three fragments (and an optional fourth fragment):
54/// - A logic block fragment for code that should run every single frame
55/// - A render block fragment for code related to displaying to the terminal (all plots, draws and renders). This will not run if the previous frame took too long
56/// - An `f32` fragment representing the desired frames per second.
57/// - Optionally, a function of type `Fn(`[`Duration`](std::time::Duration)`, bool)`. The passed duration will be the time taken to render everything, and the passed `bool` indicates whether the last frame was skipped or not. It can be used to, say, print debug info. Here's an example:
58/// ```no_run
59/// # use main_loop_utils::fps_gameloop;
60/// # use std::time::Duration;
61/// fps_gameloop!(
62/// // -- other fields --
63/// # {}, {}, 0.0,
64/// |elapsed: Duration, frame_skip: bool| {
65/// println!(
66/// "Elapsed: {:.2?}µs | Frame skip: {}",
67/// elapsed.as_micros(),
68/// frame_skip
69/// );
70/// }
71/// );
72#[macro_export]
73macro_rules! fps_gameloop {
74 ($logic:block, $render:block, $fps:expr) => {
75 fps_gameloop!($logic, $render, $fps, |_, _| ());
76 };
77 ($logic:block, $render:block, $fps:expr, $handle_elapsed:expr) => {
78 use std::time::Instant;
79 let mut frame_skip = false;
80 loop {
81 let now = Instant::now();
82
83 $logic; // Logic
84
85 if !frame_skip {
86 $render; // Rendering
87 }
88
89 // Allow user to handle elapsed time
90 ($handle_elapsed)(now.elapsed(), frame_skip);
91
92 let elapsed = now.elapsed();
93 frame_skip = gemini_engine::gameloop::sleep_fps($fps, Some(elapsed));
94 }
95 };
96}