This article appeared in Pixelate magazine (issue #11, June 03).
In the Allegro FAQ is the question:
How can I make my game run at the same speed on any computer?
There follows the answer:
You need to make sure the game logic gets updated at a regular rate, but skip the screen refresh every now and then if the computer is too slow to keep up. This can be done by installing a timer handler… and there follows an explanation of how to keep the game speed constant. Keep a timer which increments a counter at the rate you want the game to run, typically 50 times per second. When you’re behind the counter, run the game logic. When you’re ahead of the counter, run the drawing routines. In this way, a fast computer will give you a good frame rate and keep the game at the right speed, while a slow computer like mine will run the game at the same speed, but have a lower frame rate. This method is well known.
Digression: This works in games where the drawing is what takes most of the CPU time. This is usually the case, but if your game logic routines are what takes the time, like if your game world has thousands of objects which require collision testing with each other or if you have complicated AI, this method will be of no help.
When you are playing a game on a fast copmuter and want to move or rotate slightly, a tap on an arrow key will register for a few 50ths of a second. On a slow computer running with at 5 fps, the drawing takes a fifth of a second, and about 10 logic updates will happen quickly before the next frame is drawn. Thus your quick tap on the arrow key will happen during the drawing and not register at all, or it will register for all ten logic updates, and you will move more than you want to, making the game more difficult to play.
Thus, I too easily get shot in a FPS game because my FPS is shot. Well, I’m not really a FPS fan but that is a double pun! (But if anyone is wondering how to make an enemy in a shooting game less skilful, try to create the conditions of a player with a low frame rate by not letting it set its move variables as often – it will have less accuracy shooting).
In the recent SpeedHack, the rules required the inclusion of ‘3D elements’, so some people, including myself, made 3D games. 3D graphics are generally slower to draw than 2D, and in a lot of the games I had a lower frame rate, and suffered the control problems I’ve mentioned. PodFight and Wormhole Skipper were two games regarded highly by many, but on my computer they were too hard to play for this reason. Even my game Cubed suffered a bit from this problem, so I implemented the feature I’m about to describe. It made it much easier to aim at enemies quickly rather than rotate back and forth trying to line it up, but which time it has had another shot at me.
What is needed is for the input to occur at regular intervals despite the drawing routines running for a long time. Make a struct which includes all the information you want to collect each time. Have an array of this struct. At every tick of the timer, the state of the keys is written into the array. When the game logic catches up, it moves through the array and uses each entry once.
How do you jump out of the drawing routine to check the keys? I can think of two alternatives:
- Normally you will have a timer function which gets called via an interrupt. Put the code which stores the input in there. I’m not sure how sensible this is, since you’re normally supposed to keep interrupt routines as short as possible and lock everything you use, so in my example program I’ve used the other alternative.
- Find a place in your drawing code which you go through regularly, preferably not too often. Put a call there to a function which checks whether the timer has incremented again and if so, store the input.
I’ve made an example program to demonstrate this. Try increasing the level of detail (LoD) until your frame rate is down to about 5 and move the circle around. Try to move one way just a few pixels. CapsLock turns the feature I’ve described on and off. Notice how much better the control is when it’s turned on. I was going to create a demo with graphics which would work with varying LoD, but all the LoD represents now is how many times the screen is coloured black before drawing everything else. If you have a super-clever optimising compiler it will know it doesn’t need to draw it all again, and my example will be pointless.
Part of the Allegro game programming paradigm has been to write programs which will run on different platforms and the Allegro community has been good about considering a wider audience in this way. But many developers with fast computers don’t realise that their games don’t play as well on slow computers. My plea is for those developers to use methods like I’ve described here so that more users can play their games – without the superior frame rate, but still with adequate accuracy.
While I’m on the topic of making games more portable, the Windows version of Cubed didn’t work for most of the people who tried it, and I can’t think what it would have been other than a graphics mode problem. I also found problems in other entries due to choosing the wrong graphics mode. Perhaps we ought to have the option of choosing which graphics mode to use, so the user can try to find something that works. I’ve used some code from one of the Allegro example programs to do this in my example program.
The input in the example program is all in the form of keys (or joystick buttons) held down. If you’re using the mouse you can save the mouse variables in the same way. If you want to use keys being hit (rather than held down) as input, this method won’t work the same way, and if you still want to preserve accuracy you’ll need to put in a keyboard interrupt which saves each key hit with the time. On the other hand, because you have an array set up with the keys at each time, you can always check the array from the previous time to see whether a key has just been pressed, eg if you have moves array like in my example program with a “space” member, you could say
if (moves[t3%MAX_MOVES].space && !moves[(t3-1)%MAX_MOVES].space)
which returns true if space has just been hit.
That’s about all. Any questions or comments are welcome.