Author : Filipe Medeiros
Page : << Previous 14 Next >>
DOS functions inside your callbacks. DOS is not in general reentrant, and this causes big problems under most circumstances.
- Don't call C library functions -- unless you really know what you're doing. C library functions might call DOS, and as mentioned above that's normally a very bad idea in an interrupt context.
- Don't do anything too fancy. Your callbacks should execute very quickly. Just because they're executing out of the main flow of the program, it doesn't mean they aren't burdenning the CPU. The longer your callback takes to run, the less CPU time is being spent on your mainstream code. It's no coincidence that all the examples below just modify a few variables then quit -- that's the sort of level you should think on. Set some variables, then let your mainstream code do the hard work.
- Don't ask to have your callbacks called more frequently than you need. Note that under Windows the time between calls must be a multiple of 5 milliseconds; consequently if you want your game to work reasonably well in a variety of operating systems you should definitely keep your times multiples of 5 milliseconds.
- Don't install too many timer callbacks. There are a limited number (16) and keep it in mind that Allegro needs some of these for itself.
9.4 Examples of timers
9.4.1 Timing a game
This one's fairly simple. We set up a global variable to contain the number of seconds elapsed. We make our timer callback, which increases that number. We lock the counter and the callback routine. Then, when we want to start timing we install the callback routine to be called once per second. After that we can read from the (volatile!) counter variable to find out how much time has elapsed.
When the period we're timing has ended, we can either take a copy of the counter, or just remove the callback. If we do remove it, the counter won't be changed any more.
As a variation, we could implement a time limit. In this case we'd set the counter to the number of seconds allowed (for example, to complete a level). The callback would now be made to decrement that value. In the main game loop we'd check on each cycle whether or not the counter is positive. If it's not, the player has run out of time. At this stage we ought to remove the callback routine, to stop the number decreasing further.
9.4.2 Measuring the frame rate
This, too, is pretty simple.
First of all, we make two volatile global variables -- one called `last_fps' and the other `frame_counter'. Then we make a timer callback that simply copies `frame_counter' to `last_fps' and then sets `frame_counter' to zero. We now lock both variables and the timer callback, then install the callback to be executed once per second.
Now we can display the variable `last_fps' on the screen somewhere, or log it, or do what we like with it. At the moment it will be zero. All we need to do to make it count the frames in each second is to make our graphics routines increment the `frame_counter' variable once per frame.
This works because the callback routine is called every second, and it copies the value into `last_fps', zeroing `frame_counter'. Next we draw some frames, and as we do so the `frame_counter' variable increases. One second after its previous call, the callback is called again, and it copies the number of frames drawn in that second from `frame_counter' to `last_fps'.
9.4.3 Regulating game speed
This topic is my nomination for FAQ of the year on the Allegro mailing list. It's very important. Games that do not run at the same speed on different computers or under different circumstances annoy me. I don't mean that the frame rate should be the same on all computers -- that's not possible, of course. I'm referring to the actual game speed -- the rate of movement of the game characters, for instance.
To make this happen, you want ideally to move the characters at regular intervals. Inside a timer callback? No way. That's far too complicated; remember, timer callbacks must be simple. Besides, think how much of a problem it would be to try to lock everything touched by such a callback!
The best thing we can do realistically is to make a timer increment a variable, which holds the number of game cycles which *should* have elapsed by now. Then we can make the game loop compare this to the number of game cycles which really have elapsed. If we're behind target, we need to call the game logic function one or more times, until we're back on target. If not, we don't need to do any more game cycles at the moment.
If we are up-to-date then we can go off and do other things, like display a frame of graphics. Displaying graphics is often a slow process, partly because it's just a slow thing to do and partly because it often involves waiting around for the VBI, using `vsync'. While we're drawing graphics, and waiting for the VBI, our timer callback will still be called at the right intervals. We won't be dealing with that immediately, of course; it will have to wait until after we've done the graphics. But when we have done the graphics, we know exactly how many game updates we need to do to get back on target.
So, in summary, we set up two counters -- one being increased at a constant rate (the target game cycle rate) by a timer callback, and the other being increased once per actual game cycle, by the function that performs a game cycle. Initially these counters start at zero. On every pass through our main game loop, we first draw a frame of graphics. This will take a relatively long time, in most games. Then we test whether the target game cycle is greater than the actual game cycle (it probably is), and if so we keep doing game cycles until it is no longer the case. Then we loop again.
Outline example (to serve until a real example is written):
volatile int target_cycle;
void target_incrementor()
{
target_cycle++;
} END_OF_FUNCTION (target_incrementor);
int actual_cycle; int end_game;
void game_loop()
{
LOCK_VARIABLE (target_cycle);
LOCK_FUNCTION (target_incrementor);
install_int_ex (target_incrementor, BPS_TO_TIMER (cycles_per_sec));
end_game = 0;
actual_cycle = target_cycle = 0; /* Start up-to-date */
do { draw_one_frame(); /* First do some graphics, */
while (target_cycle > actual_cycle) /* then until up-to-date... */
do_one_game_cycle(); /* ... do game cycles. */ }
while (!end_game);
}
`draw_one_frame' should draw one frame of graphics, vsyncing if it needs to. `do_one_game_cycle' should, as it says, perform one game cycle, and increment `actual_cycle'. The `cycles_per_sec' variable should hold the target number of game cycles per second. Everything else should be familiar. This example assumes that the relevant Allegro components are already initialised, of course.
The file `grabber.txt' in the `tools' subdirectory of Allegro gives a good description of almost every aspect of datafiles. If any point mentioned here is unclear, or if you want more information on something not covered fully here, that is the place to look.
10.1 Concept
By now you can see that a game can require many different files; there is the executable file the users run, then there are the graphics, which could be in several large files with many graphics on each, or could be each in its own file (I tend to use the former approach). We also have the sound effects (one file for each) and the music (one file for each piece).
That's just the generic files that nearly all games need. On top of that, each game may have other files describing for example level designs, enemy AI, or other data needed. In total there can be a large number of files.
Due to clustering arrangements under DOS, each file must be stored in a number of blocks of a certain size. On partitions of just under 1Gb these "clusters" are about 16K each. Most files won't fully fill their last cluster, and so the remaining space in it is wasted -- on average, assuming evenly distributed file lengths, this would be 8K per file, but it's probably more in practice. This wasted space can mount up, especially if you store many files which are much smaller than even one cluster. If you have Windows 95, try typing `dir /s/v' in your `djgpp\zoneinfo' directory, and comparing `bytes' to `bytes allocated'. After doing this you might consider deleting the contents of that directory, or at least ZIPping it up -- you probably don't need it.
Having many files with `.PCX', `.WAV' or `.MID' extensions also encourages
Page : << Previous 14 Next >>