Inside the Game Loop

The current design of the Piston game loop:

/// Returns the next game event.
fn next(&mut self) -> Option<GameEvent> {
    loop {
        match self.state {
            RenderState => {
                if self.game_window.should_close() { return None; }

                let start_render = time::precise_time_ns();
                self.last_frame = start_render;

                let (w, h) = self.game_window.get_size();
                if w != 0 && h != 0 {
                    // Swap buffers next time.
                    self.state = SwapBuffersState;
                    return Some(Render(RenderArgs {
                            // Extrapolate time forward to allow smooth motion.
                            ext_dt: (start_render - self.last_update) as f64 / billion as f64,
                            width: w,
                            height: h,
                        }
                    ));
                }

                self.state = UpdateLoopState;
            },
            SwapBuffersState => {
                self.game_window.swap_buffers();
                self.state = UpdateLoopState;
            },
            UpdateLoopState => {
                let current_time = time::precise_time_ns();
                let next_frame = self.last_frame + self.dt_frame_in_ns;
                let next_update = self.last_update + self.dt_update_in_ns;
                let next_event = cmp::min(next_frame, next_update);
                if next_event > current_time {
                    sleep( Duration::nanoseconds((next_event - current_time) as i32) );
                } else if next_event == next_frame {
                    self.state = RenderState;
                } else {
                    self.state = HandleEventsState;
                }
            },
            HandleEventsState => {
                // Handle all events before updating.
                return match self.game_window.poll_event() {
                    None => {
                        self.state = UpdateState;
                        // Explicitly continue because otherwise the result
                        // of this match is immediately returned.
                        continue;
                    },
                    Some(x) => Some(Input(x)),
                }
            },
            UpdateState => {
                self.state = UpdateLoopState;
                self.last_update += self.dt_update_in_ns;
                return Some(Update(UpdateArgs{
                    dt: self.dt,
                }));
            },
        };
    }
}

These 63 lines of code represent many hours of work from 3 people: bfops, gmorenz and bvssvni.

  • It is written as a state machine to be used as an Iterator. This makes it possible to use the game loop as an object in the code, that can be paused or continued at will, and be passed from one function to another. For example, you can have one function for each scene and load the assets you need as local variables.
  • It uses a loop to avoid recursive calls.
  • Updates are deterministic. This means if you are not using random numbers, the application will produce the same results for the same user input.
  • Updates are always progressed by a fixed time step. If updates or rendering takes too much time, it will try to “catch up” without sleeping.
  • Rendering is slipping. The loop schedules the next frame from the last time it was rendered. If rendering gets too slow, then it leads to a lower frame rate. This is because it makes no sense to render if the application state is not updated. For objects that move in a straight line, you can use the extrapolated time to generate smoother motion on rendering. This can also be used for non-linear motion if the frame rate is high enough.

Breaking Changes

Recently we have started reexporting libraries like rust-image and rust-graphics under the piston crate. This means less work to configure the Cargo.toml when starting a new project.

This allows us to make the libraries smaller without sacrificing ergonomics. When you type cargo doc it will generate the documentation for all libraries.

The window back-ends glfw_game_window and sdl2_game_window are not reexported, so you need to include one of these when writing a game.

We will attempt to reexport gfx-rs as well. The motivation is to push Rust game development to use a safer interface against graphics drivers. gfx-rs is not pure Rust yet, because the OpenGL back-end is included in the crate, but it will be moved out when Rust gets associated types.

Libraries should not depend on Piston if they are intended to be reexported. Stuff will move out of the piston crate and into smaller libraries, so new libraries can depend directly on those.

We are developing a common structure for input events to make widget and controller programming easier. The API in Piston will be unstable as we iterate on the design. For more information see input.

If you find your code breaking and don’t know how to fix it, ask in the #rust-gamedev IRC channel or take a look at piston-examples.

The Piston Blog

The Piston project is pleased to announce that we now have a blog!

Here you’ll find the latest Piston and rust-gamedev news, as well as updates on the progress of the project.

You’ll also be informed on any changes relevant to your Piston development work, along with interesting and useful Rust development tidbits.

If you use ATOM, you can find the feed for the blog here.

Older Newer