Timing Tutorial

Timing tutorial : Moving from fixed to dynamic FPS

This tutorial will teach you how to keep your game-simulations running at the same speed across different hardware. The tutorial is based on Brutus2D but can easily be ported to other languages.

This tutorial was written by u9, who can be found on the Brutus2D forums.

Background

The easiest way of getting your game to run equally fast across different computers is to set the game to run at a fixed frame-rate. This can be done very simply by looking at how much time has passed since the last iteration of the game loop and then waiting until a predefined amount of time has passed. If you want a game running at 60 frames per second, you will need to keep the game loop waiting until 1/60 of a second has passed before iterating again. This is illustrated in the following code snippet:

do
    time = system.getTime()

    ' Do calculations, e.g. move player 3 pixels
    xPos = xPos + 3
    ' Update screen

    ' Wait until the entire 1/60 second has elapsed
    do while time + 1/60 > system.getTime()
    loop

loop while not gameOver

Drawbacks with Fixed FPS

The second loop inside the main loop in the code above will hold the execution looping until the current iteration has used up 1/60 of a second. There are several drawbacks with this model. Imagine if the game is run on a computer that is too slow and hence is unable to iterate the game loop within 1/60 second time limit. The game will run slower on this computer and in many games this is an advantage. This can also be very annoying for the player. Also, having the computer waiting idly by until 1/60 second has passed is a waste of processing power. It would be much better rendering more frames per second if the computer can handle it as this gives a smoother game-play.

Dynamic FPS

To make the most of the player's computer it would be best to run at high FPS (frames per second) on fast computers, and lower FPS on slower computers. Or more precisely, update the screen as often as possible. This dynamic FPS, where you let the frame-rate run free, is achieved by recording the amount of time every frame takes to process and then move your characters according to the elapsed time. Recording the amount of time since last iteration is easy to do in Brutus2D with the timers.SplitTime method, which returns the amount of time passed since it was last called.

The Math

To move the character according to the time passed you multiply the velocity with the difference in time since last iteration and add it to the current position like so:

(1)
s' = s + v t

Where s' is the new position of the character, s is the old position, v is the speed of the character and t is the elapsed time since last update. All this math is fine, but lets have a look at this in Brutus2D code.

Say you have a ball that moves at 200 pixels per second. You want to move this ball at the same speed on all computers, so the game loop looks like this:

do
    ' Get time passed in seconds since last iteration
    dt = Timers.SplitTime( myTimer )

    ' Do calculations
    xPos = xPos + 200 * dt
    ' Update screen

loop while not gameOver

The important thing to note here is that every movement has to be multiplied with the amount of time passed since last update (dt in the code above). This allows you to set player speeds in pixels per second rather then just an arbitrary number without a direct link to real time.

You should also notice that the delay loop in the code has been removed. The loop can now iterate as quickly as the computer will allow. What is especially nice is that the games frame-rate will fluctuate with the load on the computer while the simulation speed (the characters movement) will remain constant with respect to time.

A Working Demo

The concept is best shown with a working example. The following example will show a ball traveling at 200 pixels per second bouncing off the edge of the screen. The code in this section can be copied and pasted into Brutus2D and run. When run, you can simulate a slow or fast computer with the mouse buttons to see how the frame-rate drops while still keeping the ball moving at a constant speed. Press and hold down either left or right mouse button to change workload.

First, what needs to be done is initialize the needed sub-systems in Brutus2D such as graphics, mouse, keyboard and a high-precision timer. This is followed by the different variables needed.

' Forces declaration of variables
Option explicit

' Initialize needed sub-systems
Graphics.initialize 640, 480, True
Key.initialize
Mouse.initialize
Timers.initialize

' Needed variables
Dim font        ' For writing text on screen
Dim myTimer        ' Timer index
Dim isRunning        ' False when user presses escape
Dim dt            ' Change in time between frames (delta-time)

' Populate variables
font = Graphics.createfont( "system", 14, True )
myTimer = Timers.create()
Timers.start myTimer
isRunning = True

Now Brutus2D is initialized. But we also need to simulate different machines and different workloads. As such we declare a variable to tell how long we should sleep inside the game loop. This will represent different machines and workloads.

' A delay in ms to simulate workload
Dim delay
delay = 10.0

Lets follow this with the ball. The ball will contain four variables. x and y position and velocity in x and y direction (dx, dy). The angle is a random number between 0 and 2 \pi ( 0-360^\circ ) and is what is used to calculate the speed in the two directions (vertically and horizontally).

' A bouncing ball around the screen
Dim x, y, dx, dy, angle
x = 100.0 + 440 * rnd ' Random initial position
y = 100.0 + 280 * rnd

' Get a random starting angle, then calculate the speed of the ball in x and y direction
angle = 2 * pi * rnd
dx = cos(angle) * 200.0 ' Speed: 200 pixels/second
dy = -sin(angle) * 200.0

Next comes the main loop. The first thing to do in the loop is to get the time since the last frame was rendered. The first time this is called, timers.SplitTime returns the time elapsed since the timer was started. After registering the elapsed time, we check to see if the user has pressed escape or the window's close button, as this is the condition to end the program.

' The main loop
Do While isRunning

    ' Get change in time since last call
    dt = Timers.splitTime( myTimer )

    ' Exit if window closed or escape pressed
    If Key.pressed(0) Or Key.pressed(1) Then isRunning = False

In order to simulate different computers/workloads, we will have a delay in the middle of the main loop where calculations are suppose to be. We only have a single ball to move here so we will add an idle loop to simulate these differences. This delay can be changed using the left and right mouse button.

' Change the workload with the mouse
    If Mouse.LeftButton Then
        delay = delay + 10 * dt
        If delay > 60 Then delay = 60 End If
    ElseIf Mouse.RightButton Then
        delay = delay - 10 * dt
        If delay < 0 Then delay = 0 End If
    End If

    ' Simulate different workloads with a delay
    Dim rightNow
    rightNow = Timers.AppTime( myTimer )
    Do While rightNow + delay/1000 > Timers.AppTime( myTimer )
    Loop

After simulating some calculations, we need to do some real calculations on the ball. This is the essence of the tutorial. As the ball's speed is measured in pixels/second we can multiply that with the number of seconds that have passed since we last moved the ball and we end up with the number of pixels the ball should move this frame. Again, dx and dy hold the speed of the ball in the x and y direction respectively.

The calculation is followed by a quick check to see whether the ball should bounce of the edge of the screen or not. When the ball reaches an edge of the screen, its velocity is negated which has the effect of sending the ball in the opposite direction.

' Update ball's position by moving it the required amount given the time passed
    x = x + dx * dt
    y = y + dy * dt

    ' Bounce ball at edge of screen
    If x < 0 Or x >= 640 Then
        dx = -dx
    End If
    If y < 0 Or y >= 480 Then
        dy = -dy
    End If

Now, the only thing that is missing is drawing the frame and displaying some information to the user such as FPS and workload.

' Draw the circle
    Graphics.clear
    graphics.setcircle x, y, 10, argb(255,255,255,255)
    graphics.setcircle x, y, 5, argb(255,255,255,255)

    ' Write some user feedback
    Graphics.settext "FPS: " & graphics.fps, 10, 10, font, argb(255,255,255,255)
    Graphics.settext "Frame time: " & formatnumber( dt*1000 ) & " ms", 10, 30, font, argb(255,255,255,255)
    Graphics.settext "Workload: " & formatnumber( delay ) & " ms", 10, 50, font, argb(255,255,255,255)
    Graphics.settext "Use left and right mouse buttons to simulate an increase and decrease in workload!", 10, 80, font, argb(255,255,255,255)

    ' Display buffer on screen
    graphics.display
Loop

Of course, when the loop exits it is good practice to clean up any used systems, hence:

' Clean up
Mouse.terminate
Key.terminate
Graphics.terminate
page tags: fps time timing tutorial
page_revision: 2, last_edited: 1215023487|%e %b %Y, %H:%M %Z (%O ago)