Fixed timestep
Description
When we switched to continuous movement, we updated the camera position every time the event queue was clear. However, we didn't add any checks to see how often this update occurred.
On some systems, OpenGL will wait when swapping buffers to match up with the monitor refresh rate. In this case, we're probably updating the camera 60, 75, 120, or 144 times per second.
If OpenGL does not wait for the refresh, it will run the loop as fast as possible, which could be many thousands of times per second for such a simple scene.
The number of times we update our position affects how fast we move. If my velocity is set to 1.0 and I'm adding that to my position 120 times a second, I will move twice as fast as if my refresh rate was 60 times per second. And if my frame rate isn't locked to my refresh rate and the position is updated thousands of times per second, I'll zoom right past the blocks.
One solution is to lock down how often we update the world (and the camera position). Regardless of how many times MainEventsCleared is called, we only update the world if enough time has elapsed. We can specify our desired frames per second, and from there determine the amount of time per frame.
With this change, regardless of the refresh rate or vsync, the camera will move at the same speed.
See the Fix Your Timestep post for more options.
Commands
git clone git@github.com:atsheehan/iridium
cd iridium
git checkout 14437eb49ec667839c5fc2320f42197dac045925
cargo run --release
Code Changes
Modified src/main.rsGitHub
@@ -2,6 +2,8 @@22 mod render;
33 mod world;
44
5+ use std::time::{Duration, Instant};
6+
57 use render::Renderer;
68 use winit::{
79 event::{ElementState, Event, KeyEvent, WindowEvent},
@@ -10,6 +12,10 @@1012 };
1113 use world::World;
1214
15+ const FRAMES_PER_SECOND: u64 = 60;
16+ const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000;
17+ const FRAME_DURATION: Duration = Duration::from_nanos(NANOSECONDS_PER_SECOND / FRAMES_PER_SECOND);
18+
1319 fn main() {
1420 let options = get_options();
1521
@@ -18,6 +24,8 @@1824
1925 let mut world = World::new(20, 20);
2026
27+ let mut last_instant = Instant::now();
28+
2129 event_loop.set_control_flow(ControlFlow::Poll);
2230 event_loop
2331 .run(move |event, window_target| match event {
@@ -90,7 +98,10 @@9098 renderer.set_viewport();
9199 }
92100 Event::AboutToWait => {
93- world.update();
101+ while Instant::now() - last_instant > FRAME_DURATION {
102+ world.update();
103+ last_instant += FRAME_DURATION;
104+ }
94105
95106 renderer.set_camera(world.camera());
96107 renderer.clear();
@@ -2,6 +2,8 @@2 mod render;
3 mod world;
4
5 use render::Renderer;
6 use winit::{
7 event::{ElementState, Event, KeyEvent, WindowEvent},
@@ -10,6 +12,10 @@10 };
11 use world::World;
12
13 fn main() {
14 let options = get_options();
15
@@ -18,6 +24,8 @@18
19 let mut world = World::new(20, 20);
20
21 event_loop.set_control_flow(ControlFlow::Poll);
22 event_loop
23 .run(move |event, window_target| match event {
@@ -90,7 +98,10 @@90 renderer.set_viewport();
91 }
92 Event::AboutToWait => {
93- world.update();
94
95 renderer.set_camera(world.camera());
96 renderer.clear();
@@ -2,6 +2,8 @@2 mod render;
3 mod world;
4
5+ use std::time::{Duration, Instant};
6+
7 use render::Renderer;
8 use winit::{
9 event::{ElementState, Event, KeyEvent, WindowEvent},
@@ -10,6 +12,10 @@12 };
13 use world::World;
14
15+ const FRAMES_PER_SECOND: u64 = 60;
16+ const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000;
17+ const FRAME_DURATION: Duration = Duration::from_nanos(NANOSECONDS_PER_SECOND / FRAMES_PER_SECOND);
18+
19 fn main() {
20 let options = get_options();
21
@@ -18,6 +24,8 @@24
25 let mut world = World::new(20, 20);
26
27+ let mut last_instant = Instant::now();
28+
29 event_loop.set_control_flow(ControlFlow::Poll);
30 event_loop
31 .run(move |event, window_target| match event {
@@ -90,7 +98,10 @@98 renderer.set_viewport();
99 }
100 Event::AboutToWait => {
101+ while Instant::now() - last_instant > FRAME_DURATION {
102+ world.update();
103+ last_instant += FRAME_DURATION;
104+ }
105
106 renderer.set_camera(world.camera());
107 renderer.clear();