Timers
From Allegro Wiki
Contents |
[edit] Introduction
Welcome to the tutorial on Allegro timer usage. You should read this if you have problems making your game run at the right speed on your computer, or if you made a game and put it on another computer just to find out that it suddenly runs at a different speed, or if your game for some reason takes up the entirety of the CPU. This tutorial is supposed to teach you how to use Allegro timers to make your game run at the right speed on all computers. If this tutorial doesn't help, go to allegro.cc and search the forums, this subject has been discussed _alot_.
To make use of this tutorial: You should be familiar with Allegro, if not visit http://www.allegro.cc/ and http://alleg.sourceforge.net/. You should also have tried to make a game run at the right speed and felt that there must be a better way.
[edit] Timer
[edit] What?
Timers are a part of the Allegro library, so include allegro.h and don't forget to link to the lib when you compile.
#include <allegro.h>
The way the timer works in Allegro is as follows: You use the function install_int or install_int_ex to start a timer. These functions take a function parameter void *(proc)(), and a speed parameter. The function you pass to the timer will be called as often as the speed parameter dictates. In this article, each call to the function is called a 'tick'. If you use install_int, speed is the time in milliseconds between ticks.
But timer itself doesn't use milliseconds, they are converted to hardware clock ticks. The other function is trickier to use, but is much more flexible. The speed parameter of install_int_ex takes hardware ticks instead of milliseconds. Using this function as is would of course be trickier than the easy to use millisecond scale. However, Allegro has a bunch of useful macros defined that converts from a useful scale to hardware ticks.
Here is an excerpt from the allegro docs describing all of those macros.
SECS_TO_TIMER(secs) - give the number of seconds between
each tick
MSEC_TO_TIMER(msec) - give the number of milliseconds
between ticks
BPS_TO_TIMER(bps) - give the number of ticks each second
BPM_TO_TIMER(bpm) - give the number of ticks per minute
You can see that the first two take normal time values, seconds and milliseconds. Using the second would have exactly the same effect as using install_int. These would be useful for timing things, session length, reaction time and such. But if you want a light that flashes 10 times per minute or an object that moves 30 pixels per second you should use the other two.
[edit] How?
First, you need a function that the timer will call every tick, the ticker. This ticker function must execute very fast, so all we're going to do is to increase a tick counter. Since timers are using interrupts to call your ticker there are some special rules for the safety of your program. You should read the documentation for specific information about this.
//One of these rules state that the variables handled in your ticker need be volatile
//So let's declare it globally.
volatile int ticks=0;
//Then comes the ticker, in my example it simply increments ticks.
//After the function another rule bothers us.
//You need the macro END_OF_FUNCTION after the ticker.
void ticker()
{
ticks++;
}
END_OF_FUNCTION(ticker)
//Time to install our timer.
//Before doing that, we simply must follow the rules.
//Lock variable and function using those nice macros.
void main()
{
LOCK_VARIABLE(ticks);
LOCK_FUNCTION(ticker);
//Finally we can install the timer, using either method. In your final program it is only necessary to use one of these calls
install_int(ticker,10);
install_int_ex(ticker, BPS_TO_TIMER(100));
}
END_OF_MAIN()
And now we have a timer that ticks 100 times per second.
[edit] Making use of the ticks
Before the timer your main game loop would in general look like this.
while(!quit)
{
DoLogic();
DrawEverything();
Delay();
}
Without the delay, it might be a pause, for loop or even vsync, your program runs as fast as it can. Of the methods listed, your best result when trying to run your game at the same speed on any computer would be when using vsync. vsync isn't very reliable, however, it works differently on different systems and causes all kinds of problems.
Now we shall use the ticks instead. Let's look at the whole picture.
#include <allegro.h>
volatile int ticks = 0;
void ticker()
{
ticks++;
}
END_OF_FUNCTION(ticker)
const int updates_per_second = 60;
int main()
{
allegro_init();
install_timer();
LOCK_VARIABLE(ticks);
LOCK_FUNCTION(ticker);
install_int_ex(ticker, BPS_TO_TIMER(updates_per_second));
bool quit = false;
while(!quit)
{
while(ticks > 0)
{
int old_ticks = ticks;
DoLogic();
ticks--;
if(old_ticks <= ticks)//the logic is taking too long, break from the logic loop
break;
}
DrawEverything();
}
return 0;
}
END_OF_MAIN()
As you can see we first create a timer, and install it at 60 BPS. Then we start the game loop, that is terminated by the 'quit' variable. In this we update the stuff as many times as it's supposed to be updated the current loop. Each time the timer ticks, the logic is supposed to be updated. We simply update as many times as there has been ticks since the last loop. The method shown takes care of any eventual situations. If the drawing takes more than one tick, it updates for every tick before drawing again. If there has been no ticks since last time we just draw again (this is far from optimal, so we fix it in the next version of this code below). If the logic starts taking too long, we break out of the logic loop and draw, this way the game won't freeze during the occasional heavy computation.
[edit] Yielding the CPU
You many notice in the previous example that your program uses a lot of CPU. Even while just waiting for ticks to increment above 0 your cpu is busy continously executing those simple loop and check instructions. Besides taking time away from other programs, this method uses a lot of power and can cause fans to turn on whereas they would normally be off. There is no need to worry as there is a perfectly simple solution!
Our goal is to put the process to sleep while we are busy waiting and to wake it back up when we are ready again.
One way is to modify the code from above, and add a loop that explicitly yields the cpu while there is nothing new to draw or do.
#include <allegro.h>
volatile int ticks = 0;
void ticker()
{
ticks++;
}
END_OF_FUNCTION(ticker)
const int updates_per_second = 60;
int main()
{
allegro_init();
install_timer();
LOCK_VARIABLE(ticks);
LOCK_FUNCTION(ticker);
install_int_ex(ticker, BPS_TO_TIMER(updates_per_second));
bool quit = false;
while(!quit)
{
while(ticks == 0)
{
rest(100 / updates_per_second);//rest until the next tick
}
while(ticks > 0)
{
int old_ticks = ticks;
DoLogic();
ticks--;
if(old_ticks <= ticks)
break;
}
DrawEverything();
}
return 0;
}
END_OF_MAIN()
Alternatively, you could use a semaphore, since using rest in general can be inefficient on some computers. Below is some code which will run natively on Linux and Mac OS X. Windows has its own semaphore mechanism but the pthreads-win32 [1] library provides the means to run this code under Windows.
#include <allegro.h>
#include <semaphore.h>
sem_t semaphore;//declare the global semaphore
volatile int ticks = 0;
void ticker()
{
ticks++;
sem_post(&semaphore);//wake up the main thread
}
END_OF_FUNCTION(ticker)
const int updates_per_second = 60;
int main()
{
allegro_init();
install_timer();
LOCK_VARIABLE(ticks);
LOCK_FUNCTION(ticker);
install_int_ex(ticker, BPS_TO_TIMER(updates_per_second));
sem_init(&semaphore, 0, 1);//initialize the semaphore
bool quit = false;
while(!quit)
{
if(ticks == 0)
sem_wait(&semaphore);//use the semaphore to sleep until the next tick
while(ticks > 0)
{
int old_ticks = ticks;
DoLogic();
ticks--;
if(old_ticks <= ticks)
break;
}
DrawEverything();
}
return 0;
}
END_OF_MAIN()
All we did is replace the loop with the rest function by a more efficient semaphore. The complete documentation of semaphores available at opengroup [2].
[edit] FPS
Sometimes it's good to know how many frames your game produces. The standard value to measure this is fps, frames per second. Using the method described, logic updates with the same interval on all computers, but the drawing does not. And that is what is usually more interesting, since you can check the fps on your game on different computers to decide required and recommended systems. Some people also like to see their fps so they can brag about their computers.
Since the 'ticks' variable is modified by the logic loop it is somewhat unsuitable to measure absolute time necessary for the fps computation. It is easier to create a new timer variable specifically for timekeeping. An additional benefit to doing this is that you will now have a variable that tells you how long your program has been running, which is useful for logging purposes.
#include <allegro.h>
volatile int ticks = 0;
void ticker()
{
ticks++;
}
END_OF_FUNCTION(ticker)
volatile int game_time = 0;//the new time keeping variable
void game_time_ticker()
{
game_time++;
}
END_OF_FUNCTION(game_time_ticker)
const int updates_per_second = 60;
int main()
{
allegro_init();
install_timer();
LOCK_VARIABLE(ticks);
LOCK_FUNCTION(ticker);
install_int_ex(ticker, BPS_TO_TIMER(updates_per_second));
LOCK_VARIABLE(game_time);
LOCK_FUNCTION(game_time_ticker);
install_int_ex(game_time_ticker, BPS_TO_TIMER(10));//i.e. game_time is in tenths of seconds
bool quit = false;
int fps = 0;//how many total frames we have done last second
int frames_done = 0;//how many frames we have done so far this second
int old_time = 0;//the last time we updated our fps variable
while(!quit)
{
while(ticks == 0)
{
rest(100 / updates_per_second);
}
while(ticks > 0)
{
int old_ticks = ticks;
DoLogic();
ticks--;
if(old_ticks <= ticks)
break;
}
if(game_time >= old_time + 10)//i.e. a second has passed since we last counted the frames
{
fps = frames_done;//update the fps
frames_done = 0;//reset the frames done
old_time += 10;
}
DrawEverything();
frames_done++;//we drew a frame!
}
return 0;
}
END_OF_MAIN()
This method, however, has some problems - fps is only updated every second. What if we want to update at a faster speed? Well, the next example will show one way to do that.
#include <allegro.h>
volatile int ticks = 0;
void ticker()
{
ticks++;
}
END_OF_FUNCTION(ticker)
volatile int game_time = 0;
void game_time_ticker()
{
game_time++;
}
END_OF_FUNCTION(game_time_ticker)
const int updates_per_second = 60;
int main()
{
allegro_init();
install_timer();
LOCK_VARIABLE(ticks);
LOCK_FUNCTION(ticker);
install_int_ex(ticker, BPS_TO_TIMER(updates_per_second));
LOCK_VARIABLE(game_time);
LOCK_FUNCTION(game_time_ticker);
install_int_ex(game_time_ticker, BPS_TO_TIMER(10));//i.e. game time is in tens of seconds
bool quit = false;
int fps = 0;
int frames_done = 0;
int old_time = 0;
int frames_array[10];//an array to store the number of frames we did during the last 10 tenths of a second
int frame_index = 0;//used to store the index of the last updated value in the array
for(int ii = 0; ii < 10; ii++)
frames_array[ii] = 0;//initialize the array to 0
while(!quit)
{
while(ticks == 0)
{
rest(100 / updates_per_second);
}
while(ticks > 0)
{
int old_ticks = ticks;
DoLogic();
ticks--;
if(old_ticks <= ticks)
break;
}
if(game_time >= old_time + 1)//i.e. a 0.1 second has passed since we last counted the frames
{
fps -= frames_array[frame_index];//decrement the fps by the frames done a second ago
frames_array[frame_index] = frames_done;//store the number of frames done this 0.1 second
fps += frames_done;//increment the fps by the newly done frames
frame_index = (frame_index + 1) % 10;//increment the frame index and snap it to 10
frames_done = 0;
old_time += 1;
}
DrawEverything();
frames_done++;
}
return 0;
}
END_OF_MAIN()
The method is simple. We store the history of frame rates per tenths of a second in a ring buffer. Ever tenth of a second we subtract the value from 10 tenths of a second ago(conveniently stored at the current frame_index) from the fps variable and write in the current frames_done. This makes the fps be accurate to the tenth of a second. You can modify the code to accommodate a faster update rate, although in principle this interval is usually enough.
