Author : Filipe Medeiros
Page : << Previous 13 Next >>
will have to wait for the next one. This actually happens quite a lot under some semi-multitasking operating systems when the OS takes control of the computer just before the VBI -- `vsync' then misses that VBI and, again, waits for the next one. The effect is pretty severe (the game seems to jerk a lot, and worst of all (IMHO) it loses time).
The better system is to use a timer to keep the game running at a constant speed. The video updates must still be controlled by `vsync', to reduce flicker, but our timer can effectively see exactly how long `vsync' had to wait, and we can make up for the delay by running through the game logic an appropriate number of times later on.
We'll look at exactly how this is accomplished after seeing how timers themselves work. Or you can skip straight to that section now -- it's up to you. See Subsec 9.4.3: Regulating game speed.
* Getting input at regular intervals
If we set up a timer running frequently we can get input at regular intervals. So what? Well, some game systems (such as the one described above) tend to run through the game loop several times in a row, then go into a long graphics updating routine. They spend a great deal of their time updating graphics, and rush through a number of game updates very quickly when that's finished. If we're reading the input state exactly when the game update is occuring, we'll get just about the same input state for all of the updates.
As an extreme example, imagine that the graphics routine took a whole second to complete, and the game update routine was then executed say 10 times, to make up for it. If the user tapped a key during the graphics routine's execution, or even held it down for most of the period, the game update routine wouldn't notice, because it would only be checking for input at the end of the graphics routine. But if the user just tapped the key at the end of the graphics routine, all 10 of the game's updates would see the key as being pressed, which is bad too. What we want is for each game update to see a different input state, taken from samples occuring at regular intervals.
9.2 Setting up timer callbacks
9.2.1 Initialising the timer system
Before you can use any of the timer functions, you must initialise Allegro's timer system. As noted earlier on, many other parts of Allegro need this anyway, so perhaps you have already initialised it. These other components include the mouse pointer updating code and the MIDI player.
To initialise the timer system, just call:
install_timer();
After making this call, the libc `delay' function will no longer work; you should use Allegro's `rest' function, which operates in almost the same way.
9.2.2 Locking and volatility
Allegro's timer routines are driven by interrupts. These are generated on certain events, and "interrupt" the execution of your program. At this point, the CPU starts running the code of an ISR (interrupt service routine). Allegro hooks itself in here in various ways so that it is informed whenever certain interrupts occur -- in this case, the timer interrupt.
Allegro's timer interrupt handler calls various functions you specify, called "callbacks", at certain intervals (which you also specify). The key point is that these callbacks are called "inside" an interrupt.
For various technical reasons, it's a very bad idea to try to page things to and from disk during an interrupt. This means that you must ensure that your callbacks are never paged out to disk -- if they were, they'd need paging back in during the interrupt. You must also ensure that any data they touch -- global variables, etc -- is never paged out to disk. You do this by "locking" the memory in which they are located. Some functions are provided by djgpp to do this, and Allegro provides some nicer wrappers for them in the form of the following macros:
END_OF_FUNCTION (function_name);
LOCK_FUNCTION (function_name);
LOCK_VARIABLE (variable_name);
`LOCK_VARIABLE' locks a static or global variable and `LOCK_FUNCTION' locks a function. `LOCK_VARIABLE' can work on its own, but `LOCK_FUNCTION' needs you to mark the end of the function in question using `END_OF_FUNCTION'. If you forget the `END_OF_FUNCTION' you'll get some odd-looking (but fairly self-explanatory) compiler warnings and linker errors.
The other issue that comes up when dealing with interrupts is volatility. Any decent compiler will try to optimise the code it generates. In doing so, it will normally assume that if you don't write to a variable then its value won't change. Normally this is true, but if there's an interrupt-called function that modifies the variable then the variable's value might change without the compiler noticing. To solve this problem you must mark all such variables as being "volatile". `volatile' is a keyword in the C language that directs compilers to make no assumptions about the contents of the variable -- in particular, it will never cache the variable's value in a register.
You don't want to make all of your variables volatile, of course. If you did this you'd be restricting the compiler's ability to optimise your code, and would probably end up with slower code. Try to only make variables volatile when they're written to or read from in an interrupt context (that is, inside a timer routine or other interrupt callback).
9.2.3 Installing timer callbacks
When you've initialised the timer system, locked your callback and any data it touches, and marked any such data as `volatile', you can at last tell Allegro to call it.
install_int (callback, msecs);
Replace `callback' with the name of a function returning void that takes no parameters (in C) or a variable number (in C++). `msecs' is the time between calls to your function.
Here's an example function for C:
volatile int counter = 0; /* We'll increase this in the callback, so it must be volatile. */
void my_callback_func()
{
counter++; /* Nice and simple */
} END_OF_FUNCTION (my_callback_func); /* Note the syntax here */
Note that the callback function must be very simple. It must not take a long time, it must not call any C library functions, it must not do anything fancy like calling DOS routines. If it calls Allegro routines, they must be reentrant ones that obey these same rules.
If we were using C++, we'd have to change the function definition to:
void my_callback_func(...)
otherwise we'd get an error.
Next, here's the code we'd use to lock the things mentioned above:
LOCK_FUNCTION (my_callback_func); /* Lock the function */
LOCK_VARIABLE (counter); /* It touches this variable, so we lock it too. */
Finally, the following line asks Allegro to call our function ten times per second (once per hundred milliseconds):
install_int (my_callback_func, 100);
Note that even though `my_callback_func' is a function, we don't write parentheses (`(' and `)') after its name -- if we did, the function would be called and its (non-existant) return value would be passed to `install_int'. We're passing the entry point of the function to `install_int', so we don't write the parentheses.
Incidentally, you can call `install_int' again later on to change the rate of calls to the function.
For more control over your callback functions, you can use the `install_int_ex' function. The parameter to this function is given in hardware ticks, but you can use some macros to convert to this format from more useful units -- seconds or milliseconds per call, or calls per second or minute. See the Allegro documentation for this function for full details, and more information on what timer callbacks should and should not try to do.
9.2.4 Removing timer callbacks
Removing timer callbacks is simple -- just call this function:
remove_int (my_callback_proc);
In theory, you could now unlock the memory of the function, but in practice there's little point.
9.3 Limitations of timers
Listed here are some things you should and should not do when writing and using timer callbacks (and interrupt callbacks in general).
Do:
+ Do lock all memory your callbacks touch -- lock the callbacks themselves and also any variables they use. If they call other functions, you must lock those too, as well as anything they touch; if those call other functions, you must lock them too, etc.
+ Do make used variables volatile. If you don't do this, strange things will happen. There won't be any compile-time warnings or errors, and there won't be any specific ones at run-time either, but you'll find that your program doesn't do what it should do. A typical symptom of this is having a loop that waits for a variable to be changed by a timer, and seeing this loop never terminate.
Don't:
- Don't call
Page : << Previous 13 Next >>