Game Development

Table of Contents

BytePath - A Full Game in Lua

Part #1: Game Loop

Part #2: Libraries

Part #3: Rooms and Areas

Part #4: Exercises

State Management

Managing Game States in C++

SO: “How can I implement a main menu?”

Rendering

ECS & Rendering with OpenGL

SO: Entity/Component based engine rendering separation from logic SO: Should actors in a game be responsible for drawing themselves?

SDL2: Resolution-independent rendering

With the Rust SDL2 bindings, instances of Renderer have the set_logical_size(w, h) method. If window size is different, SDL will scale everything appropriately.

However, this approach will letter box the viewport in certain window sizes. If writing a tiling-based engine, this could cause artifacts between tiles. Use set_integer_scale to fix that.

Sources: 1

2D Pixel Art Rendering & Asset Resolution

Catlike Coding Unity3D Tutorials

General Tips

1.1 - Clock

1.1 - Fractal

1.1 - FPS

1.1 - Object Pools

1.1 - Curves and Splines

1.2 - Procedural Grid

1.2 - Rounded Cube

Interesting Articles

Fix Your Timestep!

Simple: Fixed Delta Time

let mut t: 0.0;
let dt = 1.0 / 60.0;

loop {
    update(t, dt);
    render();

    t += dt;
}

If physics delta time matches refresh rate and one can guarantee the updae loop will take less than a frame, this is ideal. However, it’s nearly impossible to make this guarantees across all systems.

Variable Delta Time

let mut t: 0.0;
let mut current_time = std::time::Instant::now();

loop {
    let new_time = std::time::Instant::now();
    let frame_time = new_time.from(current_time).as_nanos / 1000000000; // from ns to s
    current_time = new_time;

    update(t, frame_time);
    render();

    t += frame_time;
}

Allows delta time to be variable but this will affect physics with inconsistent delta times.

Semi-fixed Timestep

let mut t: 0.0;
let dt = 1.0 / 60.0;
let mut current_time = std::time::Instant::now();

loop {
    let new_time = std::time::Instant::now();
    let mut frame_time = new_time.from(current_time).as_nanos / 1000000000; // from ns to s
    current_time = new_time;

    while frame_time > 0.0 {
        let delta_time = std::cmp::min(frame_time, dt);
        update(t, delta_time);
        frame_time -= delta_time;
        t += delta_time;
    }

    render();
}

Delta time has a max. value here; it is “semi-fixed.

However, this has a problem: multiple update steps are taken every render frame. If an update is more expensive than a render, this will lead to issues such as the “spiral of death”. This occurs when simulating N seconds of physics takes M real-world seconds, and M > N. The update loop will always be behind and struggling to catch up but it will always be too expensive to do so.

“Free the Physics” / Back to Fixed Step

const dt = 0.01;

let mut t: 0.0;
let mut current_time = std::time::Instant::now();
let mut accumulator = 0.0;

loop {
    let new_time = std::time::Instant::now();
    let mut frame_time = new_time.from(current_time).as_nanos / 1000000000; // from ns to s
    current_time = new_time;

    accumulator += frame__time;

    while accumulator >= dt {
        update(t, dt);
        accumulator -= dt;
        t += dt;
    }

    render();
}

We want a fixed delta time for simulation & the ability to render at different frame rates.

Instead of thinking about it like “updates need to take less than renders”, frame it (HA!) like this: Render frames produce time and the simulation consumes it in discreet delta time-sized chunks. This allows render FPS to vary but simulations are always moved forward in delta-time sized chunks.

The remaining time in the accumulator will build up over time. This will sometimes cause two sim. frames per render. This can cause a slight difference in sim. FPS and render FPS, causing what is known as “temporal aliasing”.

The Final Timestep

const dt = 0.0;

let mut t = 0.0;
let mut current_time = std::time::Instant::now();
let mut accumulator = 0.0;

let mut prev: State = State::new();
let mut curr: State = State::new();

loop {
    let new_time = std::time::Instant::now();
    let mut frame_time = new_time.from(current_time).as_nanos / 1000000000; // from ns to s
    if frame_time > 0.25 { // where did this constant come from?
        frame_time = 0.25;
    }
    current_time = new_time;

    accumulator += frame__time;

    while accumulator >= dt {
        prev = curr;
        update(curr, t, dt);
        accumulator -= dt;
        t += dt;
    }

    let alpha = accumulator / dt;
    let state: State = curr * alpha + prev * (1.0 - alpha);

    render(state); // render state should match physics state
}

The ‘remainder’ in the accumulator can be used to see how much time is required before the next sim. step. We can use that time value (the ‘alpha’) to belond (via linear interlopation) between the current & previous physics states to keep sim. and render steps in porportional sync.

Breaking the NES for Shovel Knight


  1. http://catlikecoding.com/unity/tutorials/procedural-grid/ [return]