Game Loop

In the previous tutorial we saw the need for timing the loop of our application. We need a timing mechanism to enable game objects to move at specific speeds and for animations to run at the framerate we want. There are two fundamental approaches to achieving this. One is to pause the loop so that it repeats at a specified rate, lets say 30 times per second. The other approach is to let the loop run as fast as possible but keep a timer of how long it took to run each loop iteration and use that timer in animation and movement calculations. Lets look at the pausing approach first which gives us a fixed framerate.

Fixed Framerate

To start, we need a time measuring mechanism.

import time
timer := time.Now()
// do stuff...
dt := time.Since(timer)

Go’s timer resolves down to a nanosecond so its accurate enough for our needs. We use this mechanism to measure how long it took to run an iteration of our loop. We will use the code from the previous tutorial as our base. To check how long it took to run a loop iteration we take a time stamp at the start of the loop and another at the end and then check the time between them.

for {
    timer := time.Now()
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    
    // triangle updates
    //...
    
    // bufferData calls to send stuff to the GPU
    //...
    
    gl.DrawArrays(gl.TRIANGLES, 0, 12)
    window.GLSwap()
    dt := time.Since(timer)
    fmt.Println(dt)
}

If we want our loop to run at 30 frames per second we want dt to be 1000/30=33.31000/30=33.3ms. If our loop runs fast, dt will be smaller than that so we will pause for the difference. This ensures that our loop will run at a constant framerate (as long as the whole loop doesn’t take more than frametime).

frameTime := time.Millisecond * 33
for {
    dt := time.Since(timer)
    
    // game stuff...

    if dt < frameTime {
        sleep := frameTime - dt
        time.Sleep(sleep)
    }
}

Object Updates with Fixed Timing

In games we want our players, enemies, projectiles and the like to move at specific speeds that make sense for our game. We typically express these speeds as a function of some known value. For example, in a tile-based game we might want our character to move 3 tiles per second. In another game we might express the speed as 2*[playerHeight]2*[playerHeight] per second1. We could also use global values and say our character moves 10 pixels per second or 5% of the window’s width per second.

In our example code, lets make it so our sprite moves the width of the screen in 5 seconds. The OpenGL viewport is mapped to the range [1,1][-1,1]. That means x=1,y=1x=-1, y=-1 is the bottom left corner of the window and x=1,y=1x=1, y=1 is the top right and the width and height of the window are 2.

We want this distance of 2 units to be traversed in 5 seconds. We defined a fixed framerate of 30FPS so that means the movement must take 30*5=15030*5=150 frames. So we must travel 2 units in 150 frames which means each frame we will be traveling 2/150=0.01332/150=0.0133 units (phew!).

frameTime := time.Millisecond * 33
for {
    timer := time.Now()
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    x += 0.0133
    y += 0.000
    // triangle 1
    triangles[0], triangles[1], triangles[2] = x, y, 0
    triangles[3], triangles[4], triangles[5] = x+1, y, 0
    triangles[6], triangles[7], triangles[8] = x, y+1, 0
    triangles[9], triangles[10], triangles[11] = x, y+1, 0
    triangles[12], triangles[13], triangles[14] = x+1, y, 0
    triangles[15], triangles[16], triangles[17] = x+1, y+1, 0
    //...

In fixed framerate games, frametime is a constant and that means we can use it to calculate time sensitive game values, like our sprite’s movement per frame, as constants. This a nice optimization which is especially useful for computationally intensive tasks like game physics.

Check the code up to this point here. Play around with the timing and change the code so that the sprite’s movement is constant (takes 5 seconds to cross the screen) even if you change the framerate.

Animation Speed

With a fixed framerate controlling animation speed is straightforward. If we wanted to run the animation at 3 frames per second we would do an update every 10th frame:

uvIndex := 0
frameTime := time.Millisecond * 33
frame := 0
for {
    timer := time.Now()
    // Triangle movement
    // ...
    
    // update to uvs - causes sprites to animate
    gl.BindBuffer(gl.ARRAY_BUFFER, uvVbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(uvs), gl.Ptr(uvs[uvIndex*12:(uvIndex+2)*12]), gl.DYNAMIC_DRAW)
    if frame%10 == 0 {
        uvIndex = (uvIndex + 1) % 2
    }
    frame++
    
    //...
}

Slowdowns

Note: This section covers more advanced concepts that probably don’t apply to hobby 2D game engines. Feel free to skip it.

In our loop we have seen what to do when our code runs fast (dt < frametime). The opposite case is when dt > frametime which means that we are running slower than our target framerate. This happens becuse our game updates are taking too long or because rendering is slow. The optimal fix for that is to make our code run faster :). Barring that, there are a few approaches we can take. For many games we can just ignore slowdowns especially if they happen rarely. If we absolutely must have a fixed framerate we must first identify the cause of the slowdown.

One cause of slowdowns is slow render times. If our loop is slow to render then we might look to speed it up by reducing the resolution, drawing low res sprites or using simpler shaders. In most games, bad performance is primarily due to slow renders which is why games expose many graphics settings that the user can tweak in order to get the performance they want. Even if we do setup our graphics options appropriately it might be the case that occasionally the game slows down because some of some unexpected game condition (the player decided to stack 100 firework crates and light them up all at once). When that happens we don’t want the whole game to slow down. We want the game logic to keep updating but reduce the rate at which we are rendering.

If the slowdown is not due to rendering then it is due to CPU-side computations taking too long. One cause could be that it is taking too long to create the arrays (vertices, uvs etc) used for rendering or that copying the arrays over to the GPU is slow. We touched on this issue on the previous tutorial and we mentioned that making a few big transfers is better than making a lot of small ones. In later tutorials we will see also see how we can reduce the number of data that needs to be transferred using scene organization techniques.

Another issue is that it sometimes takes long to update the game logic itself. This can happen in simulation games where there are thousands of game entities and we need to do calculations for each of them on every game update 2. It can also be the case that we are doing computationally intensive calculations for each game object, like simulating physics. In both cases we can reduce the game update rate to something that our machine can handle, lets say 10FPS but keep updating the graphics at a visually pleasing framerate, like 30FPS. The challenge with this approach is that if the game state is updating slower than the graphics then the graphics have nothing new to render!

The solution is to update the graphics speculatively. Imagine we have a player character moving from left to right. The last game update tells us that the character is still moving to the right and now we have a few frames where we must render before the next game update. We can keep rendering the character to the right in between game updates. If the next update tells us the player is still moving to the right we are golden, we just hid the game’s update slowness. If the next update tells us that the player did something else, lets say the player jumped, then we continue rendering the new action from the updated position. This creates a small visual glitch but the occasional glitch is preferable to always rendering at 10FPS.

Predictive updates require that the game is designed around them. For example here is pseudocode for a hypothetical game without prediction:

struct Player {
    x, y int      //position
    speed int 
    sprite Sprite 
}

func GameUpdate(player *Player) {
    // read gamepad input
    // ...
    player.x += gamepad.xAxis() * player.speed
    player.y += gamepad.yAxis() * player.speed
}

func Render(player Player) {
    // render player's sprite at position x,y
}

If we want to use prediction the code must be redesigned for it3:

struct Player {
    x, y int      // position
    vx, vy int    // display position
    sprite Sprite // player's sprite
}

func GameUpdate(player *Player) {
    // read gamepad input
    // ...
    player.x += gamepad.xAxis() * player.speed
    player.y += gamepad.yAxis() * player.speed
}

func Render(player Player) {
    // calc movement direction
    dx := normalize(player.vx-player.x)
    player.vx := dx * player.speeds
    // same for y
    // render player's sprite at position vx,vy
}

Predictive updating is very important in networked games where it is used to smooth out (i.e hide) network lag. Unlike our example however, in networked games we often predictively update the game state and not just the visuals.

Variable Framerate

In variable framerate we let the loop run as fast as possible and modify our calculations according to the time it took to complete each iteration. Lets see our previous example of the sprite crossing the screen in 5 seconds but done with variable timing (this code can be found here).

dt := time.Duration(0)
for {
    timer := time.Now()
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    x += 0.4 * float32(dt.Seconds())
    y += 0.0000
    // triangle 1
    triangles[0], triangles[1], triangles[2] = x, y, 0
    triangles[3], triangles[4], triangles[5] = x+1, y, 0
    triangles[6], triangles[7], triangles[8] = x, y+1, 0
    triangles[9], triangles[10], triangles[11] = x, y+1, 0
    triangles[12], triangles[13], triangles[14] = x+1, y, 0
    triangles[15], triangles[16], triangles[17] = x+1, y+1, 0
    gl.BindBuffer(gl.ARRAY_BUFFER, vertexVbo)   
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(triangles), gl.Ptr(&triangles[0]), gl.DYNAMIC_DRAW)
    gl.BindBuffer(gl.ARRAY_BUFFER, uvVbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(uvs), gl.Ptr(uvs[uvIndex*12:(uvIndex+2)*12]), gl.DYNAMIC_DRAW)
    gl.DrawArrays(gl.TRIANGLES, 0, 12)
    window.GLSwap()
    dt = time.Since(timer)
}

The key part of the code is x += 0.4 * float32(dt.Seconds()).

Remember that the window width is 2 because of the way our OpenGL viewport is setup. Since we want the sprite to move 2 units in 5 seconds we must move 2/5=0.42/5=0.4 units per second. If our frametime is one second, this is exactly what happens as that line resolves to x += 0.4 * 1 . If our loop goes faster, lets say 4 times per second, the code will resolve to x += 0.4 * 0.25 but will be called 4 times in one second so the end result will be the same. If our frametime is slow for some reason and takes 2 seconds to complete, the calculated x will be 0.4*20.4*2 so it will catch up.

Variable timing is nice because it works without modification both when the loop is fast and when it is slow. A downside is that the code needs to be written so that all time sensitive calculations have access to the current frametime (dt) whereas in fixed timing that can be a constant.

Animation with Variable Framerate

In a variable time loop, animating (or doing a task every x amount of time) requires that we use a timer. In the example below we change the animation frame every 2 seconds.

dt := time.Duration(0)
waitTime := time.Duration(0)
for {
    timer := time.Now()
    
    waitTime += dt
    if waitTime > time.Second*2 {
        uvIndex = (uvIndex + 1) % 3
        waitTime = (waitTime - time.Second*2)
    }
    
    dt = time.Since(timer)
}

In this code we count the time until 2 seconds have passed and then we switch to the next frame of the animation. Notice that we do not reset the wait time to zero because we might have a slow frame and we want the lag to be added to the next waitTime. For example, if waitTime was 1 second and the last frame took 1.5 seconds to complete we want the 0.5 excess wait time to carry over otherwise the next animation update would be in 2.5 seconds instead of 2. This also works for very high delays. If waitTime was zero and dt was 4, the code would change the animation frame and set waitTime to 2 which would cause the animation to change again on the next frame. This is important, otherwise big delays could cause our animations to desynchronize with the rest of the game.

Hybrid

We can combine both approaches to accomplish various goals. With variable timing we get the benefit that our game updates work when the loop runs fast and when the loop is slow. In variable timing we can still add a pause at the end of our loop which essentially puts an upper limit to the game’s framerate. This is desirable to save power, for example in mobile devices, where running at 200FPS might cause the battery to drain really fast. In PC/console gaming it might not be worth runnning faster than 60FPS since many TVs and monitors refresh at 60Hz.

There are other more complicated approaches. Unity for example uses variable timing for game object updates and fixed update for its physics subsystem. This is because physics simulations are more stable at a fixed framerate. In multithreaded environments we can have multiple loops running on different threads with each using its own timing approach.