BeautifulTimer


Now let me tell you about BeautifulTimer; this is (close to) the best way I've found for doing game loop timing.

First, you (of course) split your game logic into update (T) and render () sections. update (T) controls all game logic and animation state, and T is an input variable representing elapsed time since the last frame. render () just draws things to the screen, based on the current game logic and animation state.

Timer Type 0: Type 0 game loop timers are those that ignore or don't have the T-parameter in update (), and don't have any special waiting cycles. This can be effective I'd imagine on very specific hardware, but it's problematic for two reasons. i) if the hardware is too fast, the game will run too fast, and ii) if the hardware is too slow, the game will run too slow.

Timer Type 1: Type 1 game loop timers are where you have an additional wait () method in between frames. The idea is simply to figure out how long it's been since the last frame, and wait long enough. Think of this as a fixed frame rate timer. Given that ii) above isn't a problem, then using this the game will run at a constant framerate. In this situation the update (T) does not need the T parameter; each time it's called, you can call update with the same value for T since the same amount of time (ignoring ii) has elapsed.

Timer Type 2: Type 2 game loop timers are where you now call update (T) with a T value containing how long has elapsed since the last frame, and where you have a wait () method as in type 1. This takes care (ostensibly) of i) because you wait () to make sure that the game doesn't run too quickly, and ii) because you're updating game logic with larger timesteps: the player's brain will fill in the details.

Most game developers will be familiar with all of these types of timers.

There are some problems with Timer Type 2, that cause many developers to revert to using Timer Type 1 and trying to optimize their code enough to avoid ii).

Problem 1: Physics and other game code becomes unstable/unreliable with large timesteps. As the game takes longer to process each frame, the gameplay can completely go off the rails: stuff can fly through walls, oscillating relationships can explode, and so forth. You don't want a game where on a slow enough machine, the player can just walk through walls or past the final boss, or where the player can't swing his sword because it causes a floating point error. Eew!

Problem 2: Coding everything to relate to parameter T can be a chore. This might be true, but I actually don't think so. When you code logic updates without T, you are thinking in terms of frames. This is an additional calculation for your brain to make to create a very good intuitive guess as to what game behaviour should be. For example, do you want the NPC to stand and look at the wall for 1.5 seconds, or 90 frames?

Parameter T is always present, it's just that if absent then it's implied to be the time for one frame. And, well, you can teach yourself to think in frames, but I think seconds might be a bit more intuitive. And coding relative to a floating-point time value might make some code easier to tweak.

This bring us to BeautifulTimer.

First, we observe that if we just clamp T to a certain range, say to a maximum of 1/10th of a second, we can avoid many "exploding" or gameplay logic errors. How big MAX_T can be is a bit of an exercise, but you can always implement a debug mode with the largest-possible T and do your testing that way.

Second, we observe that often the render () code is why things run slowly. When the camera pans out, there usually isn't more game logic to calculate, there is just more to draw to the screen. Likewise, situations that cause the game logic to suddenly grind to a halt are probably things that have to be avoided or optimized away (e.g., a flock of enemies replicating exponentially will cause your game logic to stutter, but probably indicates a bug somewhere).

Further, the update (T) game logic code can often be optimized more easily than render (). You've always got to show a complete screenful of plants, buildings, people, monsters and items. You just can't normally leave any of them out (assuming they are supposed to be visible). But, you could tweak your spaceship AI code to only run half as often, when you discover using a profiler that it's a bottleneck. Easy.

What we do then, is allow to run update (T) more than once per call to render (), if T exceeds MAX_T. We still probably want to clamp this, i.e., only allow at maximum 3 or 4 calls to update (T) per one call to render (), and there might be more sophisticated ways to figure out where the bottleneck is, but that's the core idea.

So the final game loop using BeautifulTimer looks like this:

MinT = (1/60)

MaxT = (1/10)

MaxCycles = 5

LastFrameTime = TimeNow ()

loop:

-- handle the waiting; make sure the game never runs faster thna 1/MinT FPS

CurFrameTime = TimeNow ()

T = CurFrameTime - LastFrameTime

if (Elapsed MaxT

NumCycles = 1 + floor (T / MaxT)

CycleT = T / (NumCycles)

if (NumCycles > MaxCycles) then:

NumCycles = MaxCycles

-- call update T the appropriate number of times

for NumCycles times do:

Update (CycleT)

-- now, render to screen

Render ()

That, my friends, is BeautifulTimer

2009-05-18


◀ Back