Copyright © 1997-1999 George Foot
Permission is granted to distribute this documentation verbatim in any form without restriction and to distribute modified copies provided it is stated that modifications have been made.
This is the seventh draft. The Texinfo source was updated on 26 August 1999 and built on 25 August 2000.
You may freely distribute this document in any form without modifying the text; I don't expect anything in return, but an email would be nice.
You may also distribute modified copies in any form, provided it is made clear that such copies have been modified.
I accept no liability for any damage use or abuse of this tutorial may cause to anything at all. The examples presented as part of this package are not guarranteed to work, and are not guarranteed to be safe. In general you shouldn't compile programs you don't understand anyway.
This tutorial is available in several different formats - plain text, Info format (which also works with RHIDE) and as Texinfo source. The source distribution includes utility files to create the other formats, including DVI and Postscript, which can be printed on paper. Support for HTML format is planned.
The tutorial is accompanied by a suite of example programs, mostly written by Grzegorz Adam Hanciewicz. They demonstrate the functions and techniques discussed in the tutorial text, and are a valuable resource if you get stuck trying to understand something.
All parts of Allegro Vivace - the tutorial in the main formats, the tutorial source distribution, and the example programs - can be downloaded, individually or in groups, from the web site:
http://www.canvaslink.com/gfoot/vivace/
If you want to contact me, my email address is:
gfoot@users.sourceforge.net
Comments, suggestions and complaints are, as always, much appreciated.
The aim of this tutorial is to guide newcomers to game programming and Allegro through the process of writing a simple game.
As stated above, this tutorial is aimed at newcomers to game programming and Allegro. It is not aimed at complete newcomers to the C language. By this, I mean some knowledge of and experience using the C language will be assumed, and I expect you to already have a working compiler that Allegro supports.
By working compiler I mean one that is capable of compiling C programs and linking them to executables for your platform. It also needs to be able to work with the Allegro library, of course, but we'll go through that below.
If your compiler does not work properly, you need to
troubleshoot the problem, and this is not the place for me to
explain this in detail. For djgpp, read readme.1st
and
the djgpp FAQ; if this doesn't help follow the instructions
given in the FAQ for posting a help request to the djgpp
newsgroup/mailing list. If your version of djgpp is v2.00, you
are strongly advised to upgrade it.
This tutorial was last updated during the WIP phase of Allegro 4.0, but does not yet fully cover the new features. Nevertheless, Allegro 4.0 is the primary version this tutorial should be consistent with, and I am updating it.
Most things should still work for older versions, but some
things may not -- such is life. I'll do my best to note
changes needed for Allegro 3.x. Versions of Allegro older than
3.0 all require C++ support to build. You don't have to program
in C++ but you must have the support. If you don't, either
upgrade Allegro to the latest WIP (recommended, they're very
stable indeed) or install C++ support (for djgpp, see
readme.1st
). If you don't upgrade your Allegro version,
you will need to edit parts of the sample code in this
tutorial.
Hopefully more recent versions of the above packages will retain backward-compatibility with these versions; if not, you will again have to figure the changes out for yourself.
You will need the GNU make utility, despite the fact that
djgpp's readme.1st
marks it as optional. It is a highly
useful package, and both Allegro and the examples accompanying
this tutorial are designed to be built (painlessly) using it.
The latest version for djgpp (as of writing) is
mak3761b.zip
, which is compiled with version 2.01 of
djgpp and will not work correctly with version 2.00.
Most people seem to program using one of three systems: some people use IDEs like RHIDE, some prefer Emacs, and others use simple text editors and the Make utility. This tutorial will not attempt to force any of these systems onto you; you should program in whatever environment you like best. The examples should be built using the Make utility, but you could easily create RHIDE projects for them.
This tutorial is aimed at people of a range of programming abilities, from those who have only just learned to those who have much experience already. If you feel a section is at all patronising, don't read all of it -- most likely that section is aimed at people below your standard and all you need to do is skim it to familiarise yourself with the commands it introduces.
I have enclosed some sections' titles with [ and ], indicating that the topic is advanced, complicated, tedious to implement or just a bit of a tangent. Don't read it if you don't want to.
A big issue in writing games is inspiration -- if you can't think of something to write, then you can't write it. At first you will probably either not think of anything, or think of too many ideas which seem far too complicated to implement.
If you can't think of anything, relax and read this tutorial. Use the example programs as a base, add new things to them and personalise them. As you read through you will get a better idea of what can and cannot be done. Look at commercial games, too -- see if you can firstly see how they do what they do, and secondly improve upon them. Don't assume it's completely out of reach -- as you gain experience you will be able to see different ways of doing things. Remember, the authors wrote it somehow; it must be possible.
If your ideas are too complicated, the above point applies again -- the more games you write, the better you are at writing them and the more games you are able to write. In almost all situations for beginners, the key point is to start simple and get a working game early on, then build up from there. This technique does have its disadvantages but for now it should be fine.
Last of all, games are supposed to be fun; that's their purpose. You cannot write a fun game if you're not enjoying writing it. If you find this tutorial boring then I've written it badly and it should be changed, so please tell me. This tutorial is for you and others like you; enjoy it!
Allegro is a game programming library, mainly written and coordinated by Shawn Hargreaves. It is a very good library; it is fast, and has a large number of features, and with version 4 it makes a great cross-platform compatibility layer, letting you write code that works on platforms you've never touched. For full information, see its web pages:
http://www.talula.demon.co.uk/allegro/
Allegro is available primarily from the links on its web site
(see What is Allegro), and also as part of the djgpp
distribution at ftp.simtel.net
. The versions included
with djgpp are usually the most recent stable ones; if you want
the latest work-in-progress versions you must go to the Allegro
web site.
I do recommend that you get a WIP version, because this tutorial is written with that in mind. They are very stable. As of this writing, 3.9.25 has been out for a few weeks and I don't recall any major problems with it.
These instructions are for guidance only - the procedure does vary from platform to platform, and is explained more fully in the corresponding readme files that come with the version you download.
Create a directory for it, anywhere you like, and unzip it
preserving its directory structure (pass -d
to
pkunzip
under DOS; Unix unzippers tend to handle this
automatically so no switches are necessary). Make sure it has,
for instance, created a directory called docs
.
When that's done, go to Allegro's base directory. If you are using a WIP, you now need to fix the distribution to suit your platform. For DOS with djgpp, type:
fixdjgpp
For Unix systems, type:
chmod 700 fixunix.sh ./fixunix.sh
Next you can configure the build. See the readme files for your version and platform for details here.
To build the library and utilities, run `make':
make
This should then go all by itself, and finish with a short
message telling you it worked. If this does not happen, read
the Allegro documentation if you have not already done so
(in the allegro directory -- allegro.txt
,
faq.txt
and particularly readme.txt
).
Finally, you need to install the library file and header files, so that your programs can easily use them:
make install
In Unix you probably need to be the root user to do this.
If you're still stuck, try asking on the Allegro mailing list
-- allegro@canvaslink.com
. See the Contact Info
section at the end of Allegro's readme.txt
for
subscription information. Most people just reply to the mailing
list, though, so if you're not subscribed you should point this
out and ask people to Cc: their replies to you as well.
The installation is pretty much self-testing, because it
compiles all the Allegro example programs and utilities. However,
for completeness I have included a short program to check
the header file and library can be found. To perform this
test, cd to this tutorial's examples
directory and type:
make test
If there are no errors, great - you can now build the rest of
the Vivace example programs by rerunning make
without the
test
argument.
If, however, it couldn't find allegro.h
,
liballeg.a
or -lalleg
, the installation didn't
work correctly and you should reread the documentation in more
detail.
This duplicates briefly what the Allegro documentation says.
Firstly, any source file which used Allegro functions or
variables must #include <allegro.h>
. Put this after
any standard header files -- in particular, it must go after
#include <stdio.h>
-- to prevent clashes between the
header files.
Secondly, you should call allegro_init()
near the start of
your program. This initialises some important general
things in the library and is essential. You also need to
initialise any individual subsystems that you will be
using, but we'll come to that later. I suggest that you
make allegro_init()
the first thing in your main
function.
Thirdly, you must [in Allegro 4 and WIPs] write
END_OF_MAIN()
after the closing brace of your main
function. If you don't do this, your program will not link
properly with the Allegro library.
Finally, at link stage you must link in the Allegro
library. This is accomplished by adding -lalleg
to the
end of your linking command. The linking command is the
one which produces the executable file; it may be the only
command you use. During this tutorial, though, I will
encourage you to split your projects between multiple
files.
For an example of all of this, see test.c
in the
examples/test/ex_1
subdirectory of the main tutorial
directory.
Anyway, enough of that -- let's get on to the interesting bits.
Most games are built around the same basic concepts. There are, of course, exceptions, and I am not a professional game coder, but in my experience the following system generally works very well.
First of all we need to put the game into a known state, so that it always starts in the same way. For example, you might want to start the player in the middle of the screen, make a maze, or create a random map. All that would go in here.
The game will need to keep doing things, over and over again, until the player dies, wins, gives up, or whatever. So we have a main game loop. It has three main components:
When the game has finished, we may need to do some other things, like tell the player why it finished, update a high score table, or something like that.
In reality the above sequence is usually also contained in a larger loop, so that the game can be played over and over without dropping to the OS in between, and there's some initialisation before the outer loop and tidy-up code after it, e.g. loading the high score table from disk on startup and saving it again on exit. At first, though, we'll just do a single run of the game per execution.
Here, then, is the above structure in C code:
#include <allegro.h> #include "game.h" int end_game; /* flag we'll set to end the game */ int main (void) { allegro_init(); /* initialise the Allegro library */ init(); /* initialise the game */ end_game = 0; /* reset flag */ do { /* loop */ input(); /* get input */ process(); /* process it */ output(); /* give output */ } while (end_game == 0); /* until the flag is set */ shutdown(); /* shut down anything that needs it */ allegro_exit(); /* just for luck */ return 0; /* tell the OS that all went well */ } END_OF_MAIN()
In the above example, game.h
would be a header file
prototyping the functions init
, input
, process
,
output
and shutdown
which must be defined somewhere
else in the project.
I mentioned earlier that I was going to encourage you to split your source between files. A lot of people find this hard to do at first, and don't see why it is worthwhile. In this section of the tutorial I intend to justify it and present what I consider to be a good system for organising a project.
Firstly, then, why are multi-file projects a good thing? They appear to complicate things no end, requiring header files, extern declarations, and meaning you need to search through more files to find the function you're looking for.
In fact, though, there are strong pros to this argument. When you modify a line of your code, gcc has to recompile everything to create a new executable. However, if your project is in several files and you modify one of them, gcc already has the object files it generated from your source last time you compiled it, and so it only needs to recompile the file that was changed. In a large project this can mean the difference between a lengthy (5 minutes or more, depending on computer speed) recompile and a twenty second adjustment.
With a little organisation, splitting a project between files can make it much easier to find the piece of code you are looking for. It's simple -- you split the code between the files based on what it does. Then if you're looking for the routine to draw the player, you know it will be in the graphics source file.
Also, since your program is very modular with the minimum amount of sharing between files, bugs are easier to track down; if not many files have access to a certain variable, an erroneous value must be a bug in one of those files.
This is strictly IMHO; other people may prefer to lay things out differently. But since I find these guidelines useful, I suggest you follow them.
#include
one in the other,
but don't write out the same header information
twice. This way, if you change the information in the
future you will only need to change it once, rather than
hunting for duplicates which would also need modifying.
I hope you can see the advantages of splitting up your projects, but if you don't, by all means keep your project in a single file. The examples I will use later on will normally be multi-file, based around the guidelines above and the game structure discussed earlier (see Proposed structure), so there will be plenty more opportunities to see the benefits of this.
If you are still confused about this, please have a look at the article I wrote for the electronic magazine C-Scene:
http://www.cscene.org/
The article is in issue 2, entitled Multi-file projects and the GNU Make utility. It duplicates the information here, in more detail, and explains some common problems newcomers have to this technique.
This section assumes you are familiar with the idea of graphics programming, i.e. coordinate systems, resolution, etc. If you are not, try reading it anyway; if you don't understand you should get a book on computer graphics.
To put the computer into a graphics mode, you use this function:
int set_gfx_mode (int card, int w, int h, int v_w, int v_h);
For card, you put one of the GFX_*
constants, normally
GFX_AUTODETECT
. w and h are the minimum width and
height of screen space you want; the actual mode selected
might have a slightly larger display area. For now just
pass 0 for v_w and v_h.
Allegro's graphics routines aren't restricted to writing to the
screen; they write to bitmaps. A bitmap could be the
screen, it could be a block of memory, or it could be a
subbitmap (part of another bitmap). When you call
Allegro's graphics routines you pass them a pointer to a
BITMAP
struct, which contains information about the
bitmap you want to write to.
It's best to think of your pointer simply as a handle to the bitmap -- it's just like a reference number. Allegro will assign it initially, and you then pass it back to the graphics routines to tell them where to draw.
The only interesting parts of the BITMAP
struct as far as
we're concerned are the w
and h
fields; these hold
the width and height of the bitmap respectively, in pixels.
Allegro defines the variable screen
to be a pointer to a
BITMAP
struct representing the screen, so for example:
clear_to_color (screen, 5);
will clear the screen to colour 5 (often magenta).
To plot a pixel you use the Allegro function:
void putpixel (BITMAP *bmp, int x, int y, int color);
This sets the pixel in the bitmap referenced by bmp at coordinates x,y to the colour color. Simple enough.
See the example program in examples/chap_04/ex_2_2
for an example
of the use of set_gfx_mode
, clear_to_color
and
putpixel
.
Functions like putpixel are called graphics primitives, because they are the basic graphics output functions.
Some other simple graphics routines are:
void vline(BITMAP *bmp, int x, int y1, int y2, int color); void hline(BITMAP *bmp, int x1, int y, int x2, int color); void line(BITMAP *bmp, int x1, int y1, int x2, int y2, int color); void triangle(BITMAP *bmp, int x1, y1, x2, y2, x3, y3, int color); void rect(BITMAP *bmp, int x1, int y1, int x2, int y2, int color); void rectfill(BITMAP *bmp, int x1, int y1, int x2, int y2, int color); void circle(BITMAP *bmp, int x, int y, int radius, int color); void circlefill(BITMAP *bmp, int x, int y, int radius, int color);
Try modifying the example program from the last section
(examples/chap_04/ex_2_2
) to use
some of these. For information about them, read the
file allegro.txt
in the docs
subdirectory of your
Allegro directory.
If you have trouble with this, look at the example
examples/chap_04/ex_2_3
.
To write text onto the screen, you use the following functions:
void textout(BITMAP *bmp, FONT *f, unsigned char *s, int x, y, int c); void textout_center(BITMAP *bmp, FONT *f, unsigned char *s, int x, y, int c); void textprintf
bmp
is, of course, the bitmap onto which you want
to write.
The FONT
struct is defined in allegro.h
and is used
to refer to different fonts in memory. You use it in much
the same way that you use the BITMAP
struct -- most of the
time you just pass pointers to them to various functions,
and don't need to know anything about what is actually in
the struct.
You can create your own fonts, use the ones from the
distribution of GRX (another graphics library), which
originally come from XFree86 (a windowing system for Unix),
and convert fonts from Windows's TTF format. For all of
these you need to know how to use Allegro's Grabber utility
and datafiles. You can see grabber.txt
for more
information, but datafiles aren't discussed until much later
in this tutorial. For now, though, you can just pass
font
to these routines; it's a FONT *
declared in
allegro.h
and refers to the 8x8 BIOS font.
s
is the string you want to print, x
and
y
are the coordinates, and c
is the colour.
If c
is -1 and the font is proportional, Allegro will
use the colour information stored in the font, allowing
multicolour fonts.
textout_center
is almost identical; the only
difference is that it centres text about the x-coordinate.
textprintf
is a wrapper around textout
which takes
a format string in the same way as printf
.
textout
is faster, but textprintf
is more
versatile.
The background colour for text output can be set with:
void text_mode(int mode);
where mode
is the new background colour. If mode is
negative the background will not be drawn, and what was
there before will show through.
Play around with these for yourself, or look at
examples/chap_04/ex_2_4
which demonstrates them.
For most purposes, palettes only have meaning in 8-bit (256 colour) modes. You can also set modes with higher colour depths; if you do then the rest of this section won't have much effect. See Shawn Hargreaves's Pot of Gold, linked from the Documentation part of the Allegro web pages, for information on higher colour depths.
When you draw to the screen in an 8-bit mode, you specify a colour number from 0 to 255. Conventionally the lower 16 colours are black, blue, green, cyan, red, magenta, brown, light grey, dark grey, bright blue, bright green, ..., bright magenta, yellow, white (but that may not be the case).
The remaining 240 colours are not always the same. Before using them you should tell the computer what colour you want to appear on the screen when you set pixels to the given colour number. It's safest to explicitly set all colours you use, including the lower 16 colours; that way you can be sure that they'll appear how you want them to appear.
I will call the colour numbers you send to the graphics functions logical colours and the actual colour which appears on the screen for each will be the associated physical colour. So we need functions to assign physical colours to logical colours.
Logical colours are expressed as numbers from 0 to 255. Each
possible physical colour is expressed as three numbers, each of
which ranges from 0 to 63. These numbers represent the
strengths of red, green and blue in the colour. 63 is full
strength of course. To store physical colours, use the RGB
struct which is defined in allegro.h
:
typedef struct RGB { unsigned char r, g, b; } RGB;
Needless to say, r
, g
and b
are the red,
green and blue intensities of the colour, as described above.
To change the appearance of a logical colour, use the function:
void set_color (int index, RGB *p);
index
is the logical colour number and p
points to an RGB record as above.
The example program examples/chap_04/ex_3_2
demonstrates
the use of this function by setting colour 0 (the
background colour) to be different colours as you
press keys. Press <ESC> to quit.
Note that there is a slight complication here, in that
if you intend to change colours repeatedly over a
period of time, you should call the Allegro function
vsync
between changes (which will also add a
certain delay). The main reason for this is that some
video cards don't like you fiddling with the palette
when they are trying to display things, and they display
the wrong colours from time to time, resulting in
snow appearing. vsync
waits for the
monitor to reach the end of the frame, which removes
the problem. We'll see more of vsync
later on.
To change the whole palette at once we could call
set_color
repeatedly, but this would be slow and
awkward. Instead, allegro.h
defines the type PALETTE
to be an array of 256
RGB records, one for each logical colour. You can
create your entire palette in this array and then set
it with a single function call:
void set_palette (PALETTE p);
The example examples/chap_04/ex_3_3
demonstrates this
function by drawing a rainbow of colours and then
switching the palette to a greyscale one.
In low colour-depth modes (e.g. 256 colours) this is simple: to fade out you keep making the palette darker and darker until it's all black, and to fade in you do it the other way around.
Allegro has several functions to do this; the most basic are:
void fade_in (PALETTE p, int speed); void fade_out (int speed);
Note that fade_in
requires you to say what
palette you are fading to; fade_out
doesn't
need a palette because it assumes you mean to fade
from the current palette.
speed
is the speed of the fade, ranging from
1 to 64 with 64 being an instantaneous change.
examples/chap_04/ex_3_4
demonstrates these functions.
Animation is making things appear to move. This could mean making a pixel move around the screen, but it is usually taken to mean repeatedly changing something's appearance so that it seems to propel itself.
For now we will just look at how to make simple things move around the screen; the second interpretation will be dealt with later on when we look at sprites.
The simplest way to make something appear to move is to erase it from where it is and put it on the screen again somewhere else. It's a philosophical matter whether it has in fact moved; since it doesn't exist anyway there's not a lot of point worrying about it. So we'll just say that it has moved.
To make a solid circle move across the screen you could do something like this:
int x; install_timer(); /* required for `rest' */ circlefill (screen, 0, 100, 5, 15); /* draw first time */ for (x = 1; x < 320; x++) { /* loop across the screen */ rest (10); /* slow it down */ circlefill (screen, x - 1, 100, 5, 0); /* erase from last place */ circlefill (screen, x, 100, 5, 15); /* redraw at new place */ }
Try this out to see that it works. Now make the circle bigger (radius 50, say) and see what happens.
Okay, so that started flickering quite a bit. If it didn't, make the circle bigger. If it still doesn't, well, it would if you had used a high-resolution screen mode (try it). If you can't make it flicker at all then congratulations, you have a supercomputer.
Us mere mortals, though, have to do something about the flickering. There are many approaches; some are more complicated than others, some are more effective, and some are simply good because they don't take much CPU time (and so the game runs at a higher frame rate).
This is the most important thing to do. The vertical retrace is the time period during which your monitor is preparing to draw a new frame. During this time nothing is drawn on the screen, so if we can erase the object and redraw it before the monitor starts the next frame our problem is solved.
The vsync
function waits until the start of
the next vertical retrace period. When this happens,
we want to update the screen display as quickly as
possible. So, replace the rest(10)
with
vsync()
and try it again. Much better.
Note that the vertical retrace is like a timer in a way; it occurs at a fixed rate (for each video mode; it varies between modes). Consequently, since we have to sync with it anyway, it can be a useful way to regulate the game speed. We'll find better ways in chapter 9, though, when we deal with Allegro's timer system.
If we are drawing so much that we can't get it all done during the retrace, we need to reduce flicker in some other way as well. Consider what the situation would be if we had a lot of circles, and we erased all of them before redrawing them all. They would all be undrawn for a relatively long time; there's a high chance that the monitor will try to display their area of screen when they aren't drawn.
We could solve this problem by erasing and redrawing them one by one. This would help reduce flicker, but it would introduce other problems. The point, though, is that maximising the amount of time things are drawn for is a good aim.
For example, if you erase all your objects and then work out where you should redraw them, they're off the screen while you're calculating. If you can do the calculations before erasing them, they'll spend more time on the screen and less time off it.
As we noted above, sometimes we might not get all our drawing done during the retrace period. So what happens next? The monitor begins displaying the screen, from the top down. So, if we're only going to get some of the drawing done in time, it would be better to get the stuff at the top ready so that the monitor can draw it.
In fact, the important thing is not to erase something while the monitor is trying to display it. We can't know for sure which part of the screen is being updated at any time though, unless we use a very high-resolution timer, which has other drawbacks.
Several drawing orders are sensible. You can draw things strictly from the top down. This also improves the frame redraw speed in higher resolution/colour depth modes by reducing video bank switches. You could consider how complex each object is, and perhaps draw the simplest ones first.
Another technique is to draw from the bottom up. This may sound odd, but if you do this you'll only run into the retrace once, and only for a short period. Using this system there will be some flicker, but it's likely to be less severe.
Think about travelling quickly along a motorway in a car, looking sideways out of the window. Better make sure somebody else is driving though. You're trying to get a good view of the hedge. Cars travelling in the same direction as you have a low relative velocity, and so they block your view of the hedge for long periods. Cars travelling in the opposite direction will only block your view for very short periods, though.
This is a very popular, simple system, which works well in fast (i.e. low resolution) video modes. Newer graphics cards (AGP and to a lesser extent PCI) can also handle it in higher resolutions.
The technique of double buffering involves writing all the graphics to a temporary bitmap, until a whole frame has been updated, and then copying the final image to the real screen memory in one big chunk.
This can be effective for several reasons. Firstly, it means that we are no longer erasing anything from the screen -- we are just replacing one image (with everything drawn) with another (still with everything drawn).
Secondly, video memory is rather slow. If we tend to overwrite areas of the screen a lot when drawing the frame, it will be faster to do all these writes to the (fast) system memory and then copy that to the (slow) screen memory than it would be to perform the writes directly to the screen memory. In addition, reading from screen memory is often even slower; so if we will want to read from the image it is far better to read from a copy in system memory. This is very much like disk caching, where copies of bits of the disk are kept in system memory to avoid having to access the disk directly.
So, instead of drawing to the screen, we draw to
another bitmap in memory. Then, when we're ready,
we vsync()
and then copy the entire buffer
onto the screen. In the 320x200x256 mode this fits
well inside the retrace interval, so there should
be no flicker at all. In higher resolutions or
colour depths there is of course more image data to
copy, so it takes longer. The effects of this range
from shearing (where the top part of the screen shows
one frame and the bottom part shows another) to drops
in frame rate (where it takes so long that we miss
the start of the next retrace period, so vsync
waits for the one after that).
See the example program examples/chap_04/ex_4
. We
won't look too deeply at memory bitmaps and the blit
function yet; they'll come up in more detail in chapter 7.
This is a variant of double buffering that doesn't require such a fast video mode, but it is trickier to implement. The theory is that you do exactly as you would when double-buffering, i.e. do all your screen updates to a memory bitmap, but you only copy to the screen those regions which have changed.
This involves keeping track of which areas have changed; the simplest way is to mark rectangles as dirty when you write to them. Then you can easily blit all of these rectangles to the screen.
This is better than double buffering if not much has changed, but if a lot has changed it can be worse, partly because a single blit of a large area is more efficient than a lot of blits of smaller areas, and partly because you may have two dirty rectangles which overlap; unless you take care to avoid this, that region of the image will be copied twice. However, if you can detect situations where double buffering would be more efficient, you can simply blit the whole image in one chunk, as you would when double buffering. The two techniques have a lot in common.
Dirty rectangles are more awkward to implement, and if you make small mistakes you can get strange results. We'll return to these in chapter 7 too.
Again, this is a variant of double buffering. The idea here is to copy to the screen only every other line. You can copy just the even numbered lines, and leave the odd ones blank; then you get a slightly darker picture with effectively half the vertical resolution. You could also copy the odd lines in one frame and the even lines in the next; this way you don't lose any brightness, but some lines are `older' than others, resulting in some blur. These techniques work best for things like video.
This sounds pretty simple, and it would be, but we're also going to alter it in a few more ways. Our goal here is to fit it into the game structure presented earlier.
This time we're going to use a struct to hold together all the elements which describe each circle -- its location, its colour, its radius, and the speed with which it is moving in each direction. It will also be convenient to remember what location it was drawn at, to simplify the erasing function. So let's start by defining the struct:
struct circle_t { int x,y,r; /* x, y, radius */ int col; /* colour */ int xs,ys; /* x speed, y speed */ int dx,dy; /* drawn x, drawn y */ };
Now, a good way to write modular code like this is to first think about the interface a litte; decide what you want the layer that's calling the module to see. Then you can write the functions themselves, having decided roughly what they should do -- or you can write the calling routines, even if you haven't yet written the functions that they're calling. An advantage of this is that if you have more than one person working on the game, one of you could write the object's routines and the other could write the caller.
Another advantage, which we'll see later, is that it's easy to add new objects to this sort of system -- all the objects will use the same interface functions. So the person who's writing the coordinator, which calls all the objects' routines, could start by using dummy objects that don't do much, to check their routines work, then plug in the real objects later.
Let's decide what functions each object should have. Ideally, we want to have the following routines for a circle: a function to draw it, a function to erase it, and a function to update its internal variables. These will be called frequently, from the main game loop. In addition we require a function to create it, initialising its internal variables, along with a function to destroy it, which won't do much in this case but would, for example, deallocate any memory which may have been allocated for this circle.
We'll prototype our functions like this:
void circle_draw (struct circle_t *circle, BITMAP *bmp); void circle_erase (struct circle_t *circle, BITMAP *bmp); void circle_update (struct circle_t *circle); struct circle_t *circle_init (int new_x, int new_y, int new_radius, int new_col, int new_xs, int new_ys); void circle_destroy (struct circle_t *circle);
Note that I've prefixed these functions' names with
circle_
. This is a useful thing to do, because these
functions will be global, and we don't want clashes with functions
for updating other types of object. Also, the prefix reminds us
that they're in the circle module. If we weren't sure of this, we
could of course look in the header files to find out which module
they were from, but it's faster if you know just by looking at the
functions' names.
We'll have an array of pointers to circle_t
structs, and
our main init
function will initialise them all by
calling circle_init
with various parameters.
Having initialised them, we'll draw them all, before we
return from the init
function.
For now the input
function will not do anything other
than setting the end-of-game flag if <ESC> is pressed. The
use of the keyboard will be explained in the next section.
In process
, we will calculate the new positions of all
the circles. To do this, we need to set up a for loop
stepping through the array of circles, and call
circle_update
for each.
In output
we need to set up a similar loop, calling
circle_draw
for each circle. In most cases (depending
upon what animation technique we're using) we need to call
the circle_erase
functions first.
Finally, when we exit it is tidy to deallocate the circles
created by circle_init
, using circle_destroy
.
I suggest now that you try to implement this yourself, for
a single circle, then expand the program to move more of
them. The example programs examples/chap_05/1_a
and
examples/chap_05/1_b
demonstrate these. If you have
trouble making your own program to move just one circle,
I suggest you look at the first example, and try to modify
that to move more than one.
Note though that when moving more than one circle, if you erase and draw them one by one, the erasing of the later circles can rub out parts of the earlier ones, which have already been drawn. To solve this, erase all the circles at once, and then redraw them all. However, this could increase the amount of flicker seen (remember maximising the drawn time?), unless you use a double buffer.
The above technique works fine for many similar objects. In a real game, though, there are usually many different types of object. So let's look at some ways of dealing with squares as well as circles, for example.
One solution is to add more information to the struct, enabling it to describe a circle or a square, with a field to say which. Then we could modify the functions to differentiate between the two and draw them differently. Some functions might not need changing at all.
Another solution is to create a new struct for the squares, and a new set of functions to deal with them.
These two techniques both have their advantages. The first one works well when the two types are very similar; in these cases many functions wouldn't need changing. All the objects could be stored in one array, and only one loop would be needed to draw them, move them, etc. In addition, the similarities can be exploited so that we can make generic routines for things like collision detection.
The second technique works well when the objects are quite different. In this case it gets awkward to make a struct that can hold the data for any of the several objects, and the functions no longer share much code. With this technique you need a separate array for each object type, and you need to scan the arrays one by one when drawing/erasing/updating them.
In practise, a set of many different object types can be partitioned into classes of object type. For example, the criterion could be behaviour, appeareance, or which player owns the object. Each class can then be treated using the first mentioned system, with a general struct for each class capable of describing any object in that class, and these separate classes are treated like separate objects in the second system.
If you have understood this, try to adapt the multiple circle
program above to deal also with squares, using each of the first
two methods. In case you get stuck, there are some examples for
you to look at: examples/chap_05/ex_2_a
and
examples/chap_05/ex_2_b
. If you do not manage it on your
own, look at these and try to add another object type to each.
As an exercise based on the mixed technique, try making a
program to move circles, squares and... er... triangles
(running out of shapes!) around, with the circles and squares
moving as before, but with the triangles not bouncing; make them
come back on from the opposite side to where they went off. An
example of this is examples/chap_05/ex_2_c
; you might
want to run it to see what it does, and then try to mimic its
behaviour with your own program.
Now suppose we want to be change the number of shapes at run-time. In all the examples above we have had a set number of each shape moving around. We could make it animate less shapes by reducing the loop count, but we'd always be destroying the last shape in the list, and we could never add extra shapes (unless we'd destroyed some first). In short, what the game could do was limited at compile-time by the sizes of the arrays.
There are two techniques I want to introduce here: dynamic allocation and linked lists. Linked lists depend on dynamic allocation, so you should probably read that section first.
During this section we'll see what dynamic allocation is, and look at how we can use it to extend the linear array system we've been using so far.
Dynamic allocation allows us to ask for memory at run-time (i.e. when the program is running, as opposed to compile-time). This means, for example, that we could ask the user at the start of the program how many of each shape to animate, and then create the arrays however large they need to be. Better still, we can resize the arrays while the game is running, so if we ever run out of room we can just make the arrays a bit bigger.
To do this we use the function malloc
. This
standard C function takes a parameter of type
size_t
(basically, a number) and allocates
that many of bytes of memory. The return value is a
void *
, pointing to the block of memory allocated.
So, we want to create an array of num_circles
circles. Each circle will be represented by a
struct circle_t
. We can use the sizeof
keyword to find out how many bytes we need for each
struct, and allocate the memory like this:
struct circle_t *circles; ... circles = (struct circle_t *) malloc (num_circles * sizeof (struct circle_t));
The cast to struct circle_t *
is optional in C
programs; a C++ compiler will complain if you don't use
it, though. Its use is largely a matter of taste.
If the allocation failed (e.g. out of memory),
malloc
will return NULL
. Under
some circumstances you might like to detect this
and inform the user. It's good practice to do
this, but often people don't bother because there
may be no useful way to recover. If you're using
djgpp with a robust DPMI host (e.g. CWSDPMI) it
will terminate your program if you ever try to use
the NULL pointer. Some other DPMI hosts just let
your program carry on, which hides the problem from
you; it's best to avoid these when developping, or
at least to use CWSDPMI sometimes (i.e. from plain
DOS, no Windows) to check that things still seem
OK.
After the allocation, we can refer to the circles as circles[0], circles[1], etc, as if we'd made a real array. What we have is not an array in the strict C sense; it's just a pointer to a block of memory. In many ways this behaves much like an array, though.
Finally, when we've finished with the memory we should free it so that malloc can reuse it later if necessary:
free (circles);
Now try modifying your program from section 5.1 (just
circles, no squares or triangles) to allow the user to
specify the number of circles at the start of the
program. If you get stuck, have a look at
examples/chap_05/ex_3_1
.
Modifying examples/chap_05/ex_2_a
in the same way should be
fairly simple. Modifying examples/chap_05/ex_2_b
will need
more mallocs.
Now, what about letting the game expand the array as it runs? This isn't possible with a real array, but as noted earlier our `array' is not really an array. It was dynamically allocated, so we can use the realloc() function to reallocate it with a different size. The data that was in it before will still be there when we expand the allocation. If we're shrinking it, the data that's still in the new block stays there of course but the data that is cut off the end of the block is lost. Even if you reexpand the block, you shouldn't rely on regaining the data.
The problem with reallocating the block like this often is that if the memory area just after our block is already taken then realloc needs to copy the whole block to somewhere else. This isn't particularly slow (memory copy operations are pretty fast actually) but it's not the sort of thing you want to happen too often.
Most implementations of malloc
put some empty
space at the end of each block, but we shouldn't rely
too heavily on this. It's better to reduce the number
of reallocations some other way. The simplest way to
do this is to always request plenty of extra space -
then we don't need to call realloc
so often.
So, we need to keep track of a few things:
int num_objs; /* how many are active */ int alloced_objs; /* how many have been allocated */ int block_size; /* how many we should allocate at a time */
Then when we want to add a new object, we first check
whether we already have enough space (is alloced_objs >
num_objs
?). If so, we can just increase num_objs
and
use that space. Otherwise, we need to increase
alloced_objs
by block_size and realloc the block like
this:
objects = (struct obj_t *) realloc (objects, alloced_objs * sizeof (stuct obj_t));
Note that realloc's syntax is like malloc's, but you
must pass the pointer's old value. After calling
realloc, you shouldn't use the pointer's old value
any more -- if realloc has moved the block, the old
value is now invalid. If you'd assigned another
pointer the value of objects
and realloc had to
move the block somewhere else, the other pointer
would be pointing at a bad block.
After reallocating the new block size, we can of
course increase num_objs
and use the newly
allocated objects.
Linked lists are an extremely useful data structure. Unfortunately, for some reason many people find them hard to understand. If you can get a good visualisation of them in your mind, though, they're really very simple, and when you've used them a few times you get an intuitive feel for how to maintain them.
In this section we'll look at three types of linked list, and try using them in the circles/squares/triangles programs from earlier on.
Firstly you'll need to be comfortable with pointers. Pointers are just objects that can point to other objects. Try not to think of them exactly as memory addresses; this is a common analogy, but it isn't necessarily correct, and pointer arithmetic becomes confusing if you think of them in this way.
So pointers can point at other data objects. Most pointers are designed to point at a certain type of data. In particular, they can point at user-defined structs:
struct little_struct { int number; } *ptr;
Here ptr
can point at any object of type struct
little_struct
. To access the field number
in the object
that ptr
is pointing at we can write (*ptr).number
or use the shorthand ptr->number
, which means the same
thing. Note that the above definition doesn't actually create
an object for ptr
to point at; it just creates the object
ptr
, which is capable of pointing at objects of type
struct little_struct
but initially points nowhere (of
course it points somewhere, but where exactly depends upon other
circumstances; you certainly can't rely on it being a valid
pointer though). Now how about:
struct linked_list_node { int number; struct linked_list_node *next; } *ptr;
Here the linked_list_node
struct contains
not only an integer number
, but also a
pointer next
which can point at an object
of type struct linked_list_node
. So if we
create an object of this type, and point ptr
at it, ptr->next
can point at another object
of this type. And ptr->next->next
can point
at another... so you see we can have a whole
list of these structures, allocated one at a time
(rather than in one large block), and we can keep
track of them all using just one pointer to the
start of the list. To mark the end of the list we
can set the last node's next
pointer to NULL.
ptr --> +------------+ +------------+ +---------------+
| number : 7 | | number : 3 | | number : 285 |
| next : ----> | next : ----> | next : NULL |
+------------+ +------------+ +---------------+
The diagram above shows ptr
pointing to the
first of a list of three records. The first record, with
number
7, has its next
pointer pointing at the
second, with number
3. That record's next
pointer points at record 3, which has number
285,
and record 3's next
pointer is NULL because it's
the last record in the list. The whole list is
storing the numbers 7, 3, and 285, in that order.
You're probably wondering why we bother with this system. It has several advantages and several disadvantages when compared to the linear dynamic block storage mentioned above.
Firstly, it's very easy to manipulate things in the linked list.
New records can be added anywhere in the list, old records can
be removed, existing records can be reordered, or moved from
list to list, and lists can be split or joined with little
hassle; you just need to change the values of some of the
next
pointers. Any of these operations would be awkward
and time consuming to perform on a linear block -- we might
have to move large amounts of data.
For example, imagine removing the second data item in a large
sequential list -- we'd have to move the rest of the list
forward one place. In a linked list, we just have to set the
first item's next
pointer to whatever the second item's
next
pointer is set to (i.e. point it to the third item,
or NULL if there is no third item), and then we can free
the second item's struct.
Similarly, adding a new item to the list is simple. Assuming we
have a pointer to the item in the list before the place where we
want to put our item, we just set our item's next
pointer
to that item's next
pointer, and then set that item's
next
pointer to point at the new item.
The disadvantages of linked list are memory usage and
inefficiency in finding an item by number. The memory usage is
greater because of all the pointers in the list, and because
it's generally a large number of small chunks (in particular, as
of writing this, djgpp's malloc
function has a minimum
block size of 4096 bytes). Finding an item by number is
inefficient because we need to step through the whole list from
the beginning to get to it, whereas in a sequential list we can
jump straight to it with a single addition.
As always, which system you'll want to use will depend upon what you're using it for.
It often simplifies code if the first item in a linked list is a dummy item (sometimes referred to as the list head), whose data has no meaning. The reason for this is that it makes storing empty lists simpler -- they become lists with only one item, the dummy.
I suggest at this point that you have a look at
examples/chap_05/ex_3_2a
for some examples of adding,
removing and finding items in linked lists.
I think you'll probably see now why linked lists can be so useful -- we can create as many shapes as we like, whenever we like, and just add them to the list in an efficient way. We don't really care what their number is in the list; if we need to refer to a specific item in the list for some reason we can use a pointer to that item, rather than its ordinal number.
Assuming you understood the preceeding section, this should be fairly easy to grasp. In the singly linked list above, each node had one pointer to the next node in the list. In a doubly linked list, each node has two pointers -- one to the next item in the list, and one to the previous item in the list.
struct dllnode { struct dllnode *next; struct dllnode *prev; int number; } *ptr;
ptr --> +--------+ +--------+ +--------+
| next -----> | next -----> | next ---> NULL
NULL <--- prev | <----- prev | <----- prev |
| number | | number | | number |
+--------+ +--------+ +--------+
Now it is possible to find the whole list given a pointer to any
node in it. It is still often useful to make the first node in
the list (the one with prev == NULL
) a dummy node whose
data is not used, for the same reason as above. It can act as a
handle to the list -- something to hold on to, that will
always be part of the list. The other nodes will be added and
removed at times, remember; the head element is the only one
that's guarranteed to be there all the time.
One advantage of using a doubly linked list is that you can remove a node from a list without needing to know which list it is in. Another advantage is that if an entity in the list needs to scan the list it can do so without needing to be given a pointer to the head of the list. Also, given any item in the list you can add an item on either side without needing to scan the list. Doubly linked lists are generally easier (and more efficient) to maintain, but they do of course have a little more overhead.
examples/chap_05/ex_3_2b
is an example of a doubly linked list
and routines to manipulate it.
Again, if you've understood linked lists so far this shouldn't
be too hard. In our singly linked list we had the next
pointer of the last node set to NULL, marking the end of the
list. In a circularly linked list, the last node's next
pointer points back to the start of the list -- so there isn't
really a last node. Think of it as a ring of nodes, each
pointing to the next.
If the circular list is doubly linked, the first node's
prev
pointer points to the last node.
Empty circular lists are also a problem; using a dummy node is useful here too, but you must remember to skip over it when you're stepping around the loop. Alternatively you can make your pointer to the list NULL when it's completely empty (as you can with any of these linked lists), but you still have to make special cases.
Circular lists are useful for different things to linear linked
lists; in game programming you generally want to do something to
everything in the list once per game cycle, and for this purpose
a linear linked list is often better. I mentioned circular
lists here for completeness, and because they're not very
different to linear lists. For an example, see
examples/chap_05/ex_3_2c
.
Try modifying your shape moving program, making it
put the shapes into a linked list and animate them
from there. You could also make it add a new shape
every 100 game cycles, or make it delete one. See
examples/chap_05/ex_3_2d
for an example of this.
This method of putting all game entities into a linked list and animating them is very useful. You can put the player's ship/man/being in there too; if the player's character is similar in action to the enemies then this can work well.
This section has not yet been written, sorry.
An input device is anything you use to give information to the computer. The standard PC only has a keyboard, although most have mice and many have joysticks. In a way I suppose a disk drive is also a sort of input device, as are microphones, scanners, and modems, but they're not generally good things to use to control a game.
We'll look at each of the main three in detail, then consider abstracting our input system. Finally we'll look at different ways to use the input information.
The standard PC keyboard interface is almost, but not quite, entirely useless as a game control device. There are several reasons for this; firstly, whether you hold a key down or just tap it the game would only see one keypress for a certain period (usually a quarter of a second). Then if you held the key down it would fill the buffer with many keypresses, which means the game might still be reading them after you let go of the key. Also, if you hold one key down and tap another, the first key will seem to have been released.
These combine to make the standard PC keyboard routines useless for controlling action games. Thankfully Allegro provides a set of much better routines.
In your Allegro programs, you should call
install_keyboard()
during initialisation. Then you can
use Allegro's keyboard routines.
Most importantly, there is an array of chars called key
which has one element for each key on the keyboard. It is
indexed by scancode (not ASCII code), and you can get the
scancode for a key using the KEY_*
constants defined in
allegro.h
. For example, to see whether the space bar is
currently down, test whether key[KEY_SPACE]
is non-zero.
You probably remember seeing the input routine earlier on
checking whether or not the <ESC> key was pressed.
After installing Allegro's keyboard handler, the BIOS routines
won't work anymore. This includes conio's getch
, stdio's
getchar
, gets
, scanf
and anything else
using stdin. Allegro provides a replacement for getch
called readkey
, which waits for a key to be pressed and
returns an integer composed of its keyboard scancode and the
ASCII value; to split it up, you must do something like this:
int value, scancode, ascii; value = readkey(); scancode = value >> 8; ascii = value & 0xff;
Similarly, keypressed
is a replacement for conio's
kbhit
, returning nonzero if there's a keypress in the
input buffer.
See the Allegro documentation for more information.
There are several ways in which the game objects can respond to the input. For now we will make the objects move at a constant speed in the direction the user indicates. We'll discuss other models later on.
So, what we require is something like this:
if (key[KEY_LEFT]) x--; /* if the user is pressing left arrow, go left */ if (key[KEY_RIGHT]) x++; /* same for right */ if (key[KEY_UP]) y--; /* same for up; note that y _decreases_ to go up */ if (key[KEY_DOWN]) y++; /* and for down y _increases_ */
where x
and y
are the object's X and Y coordinates.
Try modifying the moving (single) circle program to
allow the user to move it around. Try to fit your
program into the suggested structure, too. Example
program examples/chap_06/ex_1_3
demonstrates this.
There are a few problems with using explicit KEY_*
constants in your games. Generally the best use of the
key
array is when you don't care what the key normally
means. If you ask somebody to press the <Q> key then you
should avoid using the key
array to test this, because
the mapping of KEY_*
constants to physical keys is
independent of the software keyboard mapping. KEY_Q
refers to the key next to <TAB>, but foreign and alternative
keyboard mappings may not have the <Q> key in that position.
This means that you can use the KEY_*
constants to choose
control keys in comfortable layouts, but you shouldn't use them
if the symbol on the key is important (e.g. Q for Quit).
Since the PC keyboard wasn't really designed as a game control
device, it cannot track all the keys independently. It doesn't
even come close. There are certain key combinations which, when
pressed, mask out other keys -- even when you have good
keyboard routines like Allegro's. It's a hardware limitation.
Example examples/chap_06/ex_1_4
is a simple program which
repeatedly prints a list of all keys currently flagged by
Allegro as pressed; run this and try holding down keys, until
you find that a keypress is not detected. On my keyboard,
holding down <Z>, <X> and <C> together means
pressing <F> goes unnoticed.
Even more unfortunately, not all keyboards clash in the same places. So how are you supposed to choose keys for your game if they might not work on all systems?
The answer is, of course, to allow the users to define their own keys. Then if there is a keyclash they can choose some other keys.
To do this, you will want to replace the KEY_*
constants
in your input routines with variables, which you can initialise
to some default values, and allow the user to change them if
they want to. Naturally you shouldn't expect the user to know
what all the scancodes are; you should write a routine to check
this for them.
Using the key
array this is fairly trivial; you simply
cycle from 1 to KEY_MAX
, the highest scancode, and note
which key is pressed. Put this into your userkey_left
variable, wait for the user to let go of it, and then ask for a
right key, and so on. It's safe to assume the first pressed key
you find is the only one; what stupid luser would press two keys
together when asked for a single key? :)
We'll see an example program that lets the user redefine the control keys a bit later on.
Reading the joystick digitally is simple. Make sure the
user has the joystick centred (i.e. tell them to centre it
and wait for them to acknowledge that they've done this),
then call initialise_joystick()
. Now whenever you
want to read from the joystick, call poll_joystick()
.
This reads from the joystick and, among other things, updates
the global variables joy_left
, joy_right
,
joy_up
and joy_down
, each indicating whether or
not the joystick is pushed in its direction.
The input code example given in the keyboard section earlier now becomes:
poll_joystick(); if (joy_left) x--; /* if the joystick is pushed to the left, go left */ if (joy_right) x++; /* same for right */ if (joy_up) y--; /* same for up; note that y _decreases_ to go up */ if (joy_down) y++; /* and for down y _increases_ */
Make sure, though, that the joystick is centred when
you initialise it, though. The initialise_joystick
function returns zero only if there is a joystick
present, so you could check this, then ask the user to
centre the joystick, and finally call the
initialise_joystick
function again to update the
centre position.
Try modifying the previous example to read the joystick
instead of the keyboard. If you get stuck, look at the
example program examples/chap_06/ex_2_1
.
These are easy to read; the average joystick has two,
and their status is stored in joy_b1
and joy_b2
,
which go TRUE (non-zero) when the buttons are pressed. As
with all of the joy_*
variables, they are only
updated when you call the poll_joystick
function. If
you never call that function, they'll always stay the same;
you don't need to call it too often, though. Once per game
cycle should be plenty, except when you're checking the
following...
Apparently, most joysticks do not have their fire buttons debounced, so you should be aware that pressing a fire button can cause several changes from `off' to `on' and back; this has never caused me any problems but maybe my joystick just doesn't suffer from this. In most games where this is a problem, setting a maximum fire rate will get around this anyway (i.e. not allowing the user to fire a second time until, say, 20 game cycles have passed). One thing bouncing can really mess up is double-clicks on the joystick button; single presses may look like two (or more) presses. Note though that this is only an issue if the time between joystick polls is very short.
Try modifying the digital joystick example to make the circle change colour when the fire button is pressed. Try to make it only change once on each press (by waiting for the button to release each time -- remember to poll the joystick inside your waiting loop, otherwise the variables won't get updated). If the colours don't change in the right order, the button may be bouncing; try to debounce it. Now (harder) make it still let you move the circle around while you're holding down fire.
Examples of all the above are in examples/chap_06/ex_2_2
;
look particularly at the technique used in the last case.
[Note: this information is out of date. It'll still work, but needs updating for Allegro 4.]
The PC joystick is supposed to be an analogue device; in other
words, you can find out not only which way the stick is pointing
but also how far it is pointing in that direction. However, as
Shawn points out in allegro.txt
:
...the exact results of this can vary depending on the type of joystick, the speed of your computer, the temperature of the room, and the phases of the moon. If you want to get meaningful input you have to calibrate the joystick before using it, which is a royal pain in the arse.
Calibration isn't really that hard, but it is a little irritating to the user. Most PC gamers are pretty used to calibrating their joysticks by now, though.
The point about calibration is that the joystick routines need
to find out what range of values the joystick gives, in both the
X and Y axes. Allegro does this using three functions:
initialise_joystick
, as mentioned before, marks the
centre values; calibrate_joystick_tl
notes the values
given when the stick is in the extreme top-left position, and
calibrate_joystick_br
does the same for bottom-right.
Again, normally you'll want to call initialise_joystick
once on startup, to see whether or not a joystick is present.
If one is, and you later want to calibrate it, follow this
sequence:
initialise_joystick
function to note the centre position
calibrate_joystick_tl
function
calibrate_joystick_br
function
Now Allegro has everything it needs to know to be able to give you analogue positions. For aesthetic reasons it's a good idea to now have the joystick returned to its centre position; otherwise you might dive into something important with them still holding it in the bottom right hand corner. Either ask them to centre it and acknowledge once again, or keep reading it until they do (see below).
For an example of this in code, have a look at
examples/chap_06/ex_2_3a
.
Having calibrated the joystick we can at last read its position
in more detail. The variables joy_x
and joy_y
give its X and Y positions, from -128 to 128, with (-128,-128)
being the top left corner, (0,0) being the centre, and (128,128)
the bottom right corner.
Have a go at modifying the circle-moving program to move the
circle at a speed relative to how far the stick is moved.
examples/chap_06/ex_2_3b
demostrates this in case you
can't do it. Note the #define
near the start; it
controls the maximum speed for the circle, which corresponds to
full deflection of the joystick. Using a #define
or a
constant variable rather than just a number makes it simpler to
change the speed in the future.
You can view the mouse in one of two ways -- either it is a point-and-click device, or it is somewhat like an analogue joystick, giving direct control over a character.
The point-and-click approach involves putting a pointer of some sort on the screen, and making the mouse move it around. When a button is pressed you do something, depending on what the mouse is pointing at. Examples of this are WIMP GUIs (like Windows), Dune 1 and 2, Warcraft 1 and 2, numerous front-ends (menus), etc. It is a very popular technique.
To initialise the mouse, call install_mouse()
. As
noted in allegro.txt, this returns -1 on failure, or the
number of buttons on the mouse if it succeeds.
Now you can get the mouse pointer's X and Y coordinates
using the variables mouse_x
and mouse_y
(which should be screen coordinates) and the button status
is in mouse_b
, which is a bit field like this:
Bits: 2 1 0 . . X = left button flag . X . = right button flag X . . = middle button flag
The remaining bits are unused at present. The three button flag bits are set if the corresponding button is pressed, clear if not. If there is no middle button, its flag bit is always clear.
So, to read each button you bitwise-AND (&) mouse_b
with 1, 2 or 4 like so:
if (mouse_b & 1) printf ("Left "); if (mouse_b & 2) printf ("Right "); if (mouse_b & 4) printf ("Middle "); printf ("\n");
Note though that the value of mouse_b
can (and does)
change `behind your back'; the following sort of test might
give strange results:
if (mouse_b) { printf ("Buttons pressed: "); if (mouse_b & 1) printf ("Left "); if (mouse_b & 2) printf ("Right "); if (mouse_b & 4) printf ("Middle "); printf ("\n"); }
Imagine what happens if initially one of the buttons is pressed,
so that the if
condition passes, but then the button is
released; the string "Buttons pressed:" will be printed, but
since now none of the buttons are pressed neither "Left" nor
"Right" nor "Middle" will be printed.
If this sort of oddity could be a problem, you can take a copy
of the mouse_b
variable and work from that; this is a bit
like polling the joystick, in a way.
my_mouse_b = mouse_b; if (my_mouse_b) { printf ("Buttons pressed: "); if (my_mouse_b & 1) printf ("Left "); /* etc */ }
To show the pointer on the screen, you simply call the function
show_mouse
, telling it which bitmap to draw onto, for
example:
show_mouse (screen);
Whenever the mouse is moved, Allegro will update the mouse
pointer (unless you've told it not to -- see the Allegro
documentation). Because of this, you have to hide the mouse
before writing anything to the bitmap it is shown on, otherwise
Bad Things happen to the display. To do this, call
show_mouse(NULL)
.
Note, though, that the functions which actually draw the mouse pointer require Allegro's timer routines to be installed (as do several other Allegro components) -- that's (partly) how they manage to do things in the background. See Timers, and in particular Initialising the timer system, for more information.
If you need to adjust the range of the mouse, you can call:
set_mouse_range (min_x, min_y, max_x, max_y);
where min_x
and min_y
are the minimum X
and Y values, and max_x
and max_y
are the
maximum values.
If for any reason you want to move the mouse pointer to
a certain location on the screen, call the function
position_mouse
, passing the new X and Y coordinates
of the mouse.
The example program examples/chap_06/ex_3_1
demonstrates
the point-and-click mouse.
This technique involves, at each game cycle, seeing how far the mouse has moved in each direction and treating this in the same way as analogue joystick data. Examples of games that do this are:
I don't think it's a coincidence that this list contains only first person perspective games (i.e. games where the display is from the player's point of view). That's not to say that this method can't work well for other games; I think it's more obvious when this type of game benefits from it.
When using this system, you initialise the mouse in the same way as for the point-and-click system (see Initialising the mouse).
To make this sort of control system you ask the mouse driver for
a mickey update. A mickey is a very fine measure of mouse
movement, and is given relative to last time you asked. Allegro
provides the get_mouse_mickeys
function to do this:
void get_mouse_mickeys (int *mickeyx, int *mickeyy);
Pass this function two pointers to int
variables,
and it will fill the variables pointed to with the change
in the mouse's X and Y positions since the last call to
this function. For example, if dx
and dy
are int
variables,
get_mouse_mickeys (&dx, &dy);
will put the mickey differences into dx
and
dy
. Note that the units are not the same as
the units of mouse_x
and mouse_y
--
the mickey is a much smaller unit.
The example for this section, examples/chap_06/ex_3_2
,
uses this technique to move our wonderful circle around by tying
the mouse movement to acceleration. As [ed: will be] discussed
below, this is a common model for more realistic games,
effectively linking mouse movement and force applied to the
object. You ought to take a look at this example, just to see
how it works.
Given all the above input types, we have written several programs allowing us to control things with each. However, how can we write a game which allows the user to use whichever device they want to?
We could write versions of the reaction code to interpret each
type of information, and only use the ones corresponding to the
user's choice of device, but it would be better to minimise the
number of links between our input routine and the reaction
routine. The example in examples/chap_06/ex_4
shows a
way of doing this; I suggest you refer to it while reading this.
Look in particular at input.c
and input.h
.
We need a common format for the data which is sent to the reaction routines. We need information about how far the input device is in each direction, and information about the status of any fire buttons. I created this struct to hold the information:
struct input_t { int dx, dy; int fire1, fire2; };
Let's define dx
and dy
to be from -128 to 128,
as in the joystick routines, indicating which direction the
device is pointing and how much. fire1
and fire2
will be non-zero when the fire button in question is
pressed. We're limited to two, because most joysticks only
have two; it's often sensible to aim for the lowest common
denominator when writing code to work over a variety of systems
or configurations.
The input routine will need to fill in the information
required every time it is called. For joystick input,
this is simple; just copy the values across, since the
magnitudes match up. If the joystick is digital, though,
we should check joy_left
, joy_right
, etc,
using the same system as for the keyboard, which follows.
For the keyboard, we will need to work out where it is
pointing'. Given a left_key
and a right_key
,
we can work out the rightness as follows:
rightness = (key[left_key] ? -1 : 0) + (key[right_key] ? 1 : 0);
Think about what this means in each situation: If neither key is pressed, both brackets are zero and it returns 0. If both are pressed, the first bracket given -1 and the second gives 1; so it returns 0 again. If only left is pressed, the first bracket gives -1 and the second gives 0, so it returns -1, and if the right key is pressed it returns 0+1, which is 1. Now we can adapt this to fit our structure by saying:
dx = (key[left_key] ? -128 : 0) + (key[right_key] ? 128 : 0); dy = (key[up_key] ? -128 : 0) + (key[down_key] ? 128 : 0); fire1 = key[fire1_key]; fire2 = key[fire2_key];
Hence any keyboard input is always like full deflection of the joystick.
As for the mouse, we'll use the direct control method; if we wanted a point-and-click interface we would be pretty much dependent on the user using a mouse. You could use these input routines to simulate that system with any input device, but we won't go into that here.
To map the dx
and dy
from the mouse code onto the range
[-128,128] that we are using here, we need to divide by
max_mousespeed
and multiply by 128, where
max_mousespeed
is a variable controlling the sensitivity
of the mouse. We will do this the other way around, though,
because then we can leave it as an integer calculation. Where
possible, you should avoid casting between floating point types
and integer types -- it's slow to convert between them.
The complete code for what we have discussed so far is in
function input_getinput
.
Before we can use that, though, we need a way of telling
this input module what sort of input we're looking for. This
will be by a call to input_create_*
, which may
take some parameters (depending on what *
actually is),
make an input structure, and return a pointer to it. The
joystick input routine doesn't need any extra information,
but the mouse input routine needs to know what value to
use for max_mousespeed
, and the keyboard input routine
needs to know which keys to use. We could have used a
single function with a variable number of parameters, but
that's a bit complicated for this tutorial.
So we define these functions:
input_t *input_create_joystick (); input_t *input_create_mouse (int max_mousespeed); input_t *input_create_keybd (int left_key, int right_key, int up_key, int down_key, int fire1_key, int fire2_key);
We also need a function to initialise the input module, a shutdown function for it, and a function to destroy previously-registered input devices (e.g. if you change the keys); we'll prototype these:
void input_init (); void input_shutdown (); void input_destroy (input_t *what);
In summary, programs will call input_init
initially,
to set up the module. They will then call some of the
input_create_*
functions to register input devices for
the players; the input_create_keybd
may be called more
than once, with different keys, to allow for more than one
player on the keyboard. The input_create_*
functions
return pointers to input_t
structs, whose fields will
be updated on each call to input_getinput
. Registered
devices can be unregistered using input_destroy
by
passing the struct input_t
pointer; this will also
deallocate the structure pointed to. The input_shutdown
function will unregister all devices and deallocate any memory
still allocated.
The input_t struct contains fields dx
, dy
,
fire1
and fire2
. dx
and dy
range
from -128 to 128, indicating the amount of movement in each
direction, and fire1
and fire2
are non-zero if
the corresponding fire button is pressed.
Once again, see the program in examples/chap_06/ex_4
for
an example of how to use this system.
This section isn't written yet. When it is written, it will contain information about physical models (no, it's not as boring as it sounds!) and how you can use the input data.
This section isn't written yet. When it is, it will talk about:
To install the sound drivers, you call the function:
install_sound (digi_driver, midi_driver, NULL);
putting a DIGI_*
constant in place of digi_driver
and
a MIDI_*
constant in place of midi_driver
. The third
parameter is obselete and you should just pass NULL.
The function returns zero if it was successful. A non-zero
return probably means that the driver you requested is not
available. If you used a *_AUTODETECT
setting for either
driver, this may mean that a config file specified a precise
driver to use, and that driver was unusable.
I think there's rarely any good reason not to use
DIGI_AUTODETECT
and MIDI_AUTODETECT
. My reasoning
here is that if you specify a device explicitly the game won't
work if that device is not available, and won't be able to make
use of a better device if one is available. Even if you have
some hardware conflict which makes these options fail normally,
that's no reason to stop your game working properly on other
people's machines -- you should create a configuration file
(see below). Config files always override the autodetection
routines, so the conflict should disappear -- you're relying on
this to be true so that the end-users can fix such problems if
they occur. If you hardcode values here people won't be able to
override the settings without rebuilding -- bad news!
So unless there's a very good reason (I can't think of any) just
use the *_AUTODETECT
constants here. Even the
play.exe
example program in Allegro's tests directory is
borderline in my mind -- it uses a hardcoded table of driver
numbers and names. That's pretty bad; the justification (I
think) is that its purpose is to test drivers quickly. Not much
justification really...
One further thing to note is that the MIDI player uses Allegro's timer routines to operate, so you must initialise those before you'll be able to play MIDI files. See Initialising the timer system, for more details.
Allegro supports a wide range of features in config files; sound
configuration is just part of the standard data, and you can add
data of your own for your game's internal use. For full details
on using config files, see Allegro's help system (type
info allegro config
at a DOS prompt, if you use Info).
Here I'll only mention what applies to the sound drivers.
Unless you say otherwise, the files allegro.cfg
and
sound.cfg
will be checked for in the program's home
directory. To specify a different filename, pass it to the
set_config_file
function.
The sound configuration information is in the [sound]
section of the file, and holds information about which drivers
to use if you ask for autodetection and the settings for those
drivers, among other things. The way the autodetection works in
Allegro is such that if you specify a driver explicitly in the
config file then that driver will be used. If you don't specify
one then Allegro will start poking around trying to see what's
there; this may have adverse effects, and in addition
some things (like an external MIDI device) are never
autodetected for technical reasons.
The safest and easiest way to write config files is through the setup utility. This is designed to be packageable with your games, and lets the user choose some settings (or autodetect them) and try them out. It then saves the settings to a config file ready for your program to read.
The simplest way to distribute this, then, is to put the
setup.exe
file in the same directory as your program's
executable, so that the output from the setup program is right
where your program expects to see it. There are many things
about the setup program that you can customise; or you can just
leave it alone. See setup.txt
for full information (it's
in the same directory as the rest of the setup program).
Allegro can read digital samples from WAV and VOC files. Both types of file must be mono. WAV files can be 8 or 16 bit; VOC files must be 8 bit.
Loading a sample couldn't be easier -- you pass the filename of
the WAV or VOC file (it must have the right extension) to the
load_sample
function, and it returns a SAMPLE *
which you can use to refer to the sample, or NULL if it could
not load the sample (e.g. it didn't recognise the extension, the
file was not found, or the format of the file was incorrect or
not supported).
You can also call load_voc
or load_wav
directly,
in the same way. The benefit of doing this is that if the file
does not have a .WAV or .VOC extension it will still be
processed.
Allegro's digital sound player can manipulate samples in several ways whilst playing them. The parameters you can set at the basic level are the volume, pan and frequency. You can also tell the sound player whether or not to loop back to the beginning of the sample when it reaches the end.
To start a sample playing, call the play_sample function. You pass the following parameters:
SAMPLE *
)
Note that offsetting a sample's frequency can introduce distortion of the sample -- Allegro is a game programming library, not a sophisticated audio suite. Also, playing sounds at generally low volumes but with your stereo system's volume knob turned right up will also introduce distortion, not to mention noise ;). Try to use the full range of volume values, and also use samples which internally use the full range of values, if possible.
A typical call to play_sample, then, would be:
play_sample (gun_sound, 255, 128, 1000, 0);
which plays gun_sound
(previously loaded as above) at
full volume, centre pan, and its original frequency, without
looping it.
play_sample (some_sound, 192, 96, 1200, 1);
would play some_sound
at 3/4 volume, panned slightly
left, a little above its normal pitch, and set it to loop back
to the start each time it reached the end.
To stop a sample playing, use the stop_sample
function:
stop_sample (gun_sound);
This is especially useful on looped samples.
You can also adjust these parameters while a sound is playing.
To do this, you call adjust_sample
. It takes the same
parameters as play_sample
, and searches through the list
of playing samples for the one you pass. So if
some_sound
is still playing from above, we could make it
stop looping like this:
adjust_sample (some_sound, 192, 96, 1200, 0);
We could also have changed some of the other parameters, of
course. If you want to adjust samples while they're playing,
avoid playing more than one copy of the sample at a time --
adjust_sample
will only change the first sample it finds,
so the others will carry on the way they were.
For a more sophisticated way to play and control samples, which does not suffer from the above problem, see Voice functions.
When you have finished with a sample you can unload it from
memory using the destroy_sample
function like this:
destroy_sample (some_sound);
If the sample is playing at the time, it will automatically be stopped.
This section has not yet been written. It will contain information on using the voice functions directly to gain more control over sounds.
There is an example program though:
examples/chap_08/ex_2_5
At present Allegro supports only MIDI music, which it can load from type 0 or type 1 (i.e. most) .MID files.
To load MIDI files you pass the filename to the load_midi
function, which returns a MIDI *
, or NULL if it's not
successful.
To start a MIDI object playing you simply call
play_midi
:
play_midi (title_theme, 0);
The second parameter is a loop flag; 0 means no looping, anything else causes the file to loop back to the start when it ends. If you want your file to loop at other positions (for example, if it has an introduction you might want it to skip that when it loops) you can call:
play_looped_midi (background_music, loop_start, loop_end);
When position loop_end
is reached the player will jump
back to loop_start
. If loop_end
is -1 the end loop
point is the very end of the music. The units for
loop_start
and loop_end
are beats, as measured by the
midi_pos
variable mentioned below.
In either case you can stop the music by calling
stop_midi
:
stop_midi();
Since Allegro can only play one piece of music at a time this
function needs no parameters. Starting a piece of music while
other music is already playing will stop the first piece
immediately. Calling the stop_midi
function has the same
effect as calling play_midi
with a NULL first parameter.
While a MIDI file is playing, Allegro increases the global
variable midi_pos
once per beat. If there is no MIDI
file playing (or a non-looped file has finished) it will be set
to -1.
Note that it won't necessarily increase on every beat -- if the music doesn't play a note on the beat, it won't be updated until the next note is played. If you want to synchronise events with this you need to make sure there's a note on the beat; you could put in a zero volume note, for instance, just to trigger the event.
You can pause and resume playback using the midi_pause
and midi_resume
functions. You can jump to a certain
position in the file by passing the target midi_pos
value
to the midi_seek
function. Be aware that seeking
backwards involves rewinding the file to the beginning, and then
seeking forwards; consequently doing a lot of small reverse
seeks isn't a good idea.
Lastly, if the file is looping you can change its loop points on
the fly. The two variables are midi_loop_start
and
midi_loop_end
, and are measured in midi_pos
units.
A value of -1 for midi_loop_start
or midi_loop_end
indicates the start or end of the file, respectively.
I mentioned earlier that you might have some music with an
introduction that you don't want to loop every time, and the
play_looped_midi
function can solve that problem;
adjusting the loop points can allow you to do a similar thing if
the piece has an ending that you don't want to play while it's
looping -- i.e. you want to play the introduction once, then
loop the main body of the music a few times, and finally play
the ending.
To do this you'd start the music using the
play_looped_midi
function, passing the position of the
start of the main part of the music as the loop start parameter
and the position of the end of the main part as the loop end
parameter. Then you'd let the music play; it will loop each
time it reaches the loop end. Later on you'd then set the loop
end parameter to the end of the music.
There are a few caveats though:
midi_loop_start
must always be earlier in the file
than midi_loop_end
; if your new start value is
greater than the old end value then you must alter the
end value first, and if your new end value is less
than the old start value then you should alter the
start value first.
midi_loop_end
before midi_loop_start
if the end
point is moving later in the file, and to change
them the other way around if the end point is moving
earlier in the file. This reduces the possibility of
extra skipping during the change.
The chances of either of the above being a serious problem are pretty small, but it's better to be safe than sorry. The second one is purely cosmetic.
To unload a MIDI file use the destroy_midi
function:
destroy_midi (background_music);
If it's playing at the time, the music will be stopped first.
This is a list of some uses of timers. Of course it's not complete -- it's just meant to give you some idea of their most common uses. Some or all of these will appear as examples later on.
This is probably the most obvious use. You can use a timer to count how long a player has been on a level, perhaps to give them a bonus later for completing it quickly, or to kill them for taking too long.
If you increase a global variable each time you draw a frame to the screen, you can set up a timer to copy this value elsewhere once per second. The place to which it is copied will then show the number of frames drawn in the previous second -- in short, it's the FPS (frames per second) number, or frame rate, of the game. This is very useful when you're finding out how well your game runs on other people's systems; if the number is high, they are getting smooth graphics; if not, the graphics are more jerky.
Earlier you may remember that we briefly tried using a constant
delay when moving a circle across the screen, then switched to
using vsync
to control the speed.
The former system is fatally flawed in that the speed of the circle still depends on the speed of the computer -- if we delay 10 ms between draws, and the draws take 5 ms, we'll have one draw every 15 ms; but if the draws take 10 ms, we'll only have one every 20 ms. So much for speed regulation.
Using vsync
is better; the vertical blanking intervals occur at
regular intervals, so provided our drawing and movement always
takes less time than a full retrace (or more accurately, always
takes the same number of retraces to complete) we'll get a
constant speed. If we take a bit too long on the occasional
frame, the game will seem to stop for a fraction of a second,
because vsync
will have missed one VBI and 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 Regulating game speed.
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.
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.
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).
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.
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.
Listed here are some things you should and should not do when writing and using timer callbacks (and interrupt callbacks in general).
Do:
Don't:
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.
See examples/chap_09/ex_4_1
.
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
.
See examples/chap_09/ex_4_2
.
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.
See examples/chap_09/ex_4_3
.
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.
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 people to `borrow' them for their
own purposes, or modify them, changing the game. If you don't
want them to do this, it's wise not to leave those files lying
around.
Quite apart from the above two reasons, it's just messy to have all those files stored `loose', and your game will need to load them all when it starts. Datafiles provide a way of packaging all or some of your files into one big datafile, which can be loaded all at once, optionally compressing and encrypting the data at the same time.
Datafiles are created from files on disk. There are two
main utilities provided with Allegro to handle them: the
Grabber and the DAT utility. They are both well documented
in the file grabber.txt
, in the tools
directory of
Allegro, but I'll give brief instructions here too.
(not yet written)
(not yet written)
There are a number of ways you can read back the data from a datafile. Firstly, you can read in the whole datafile, all at once. Secondly you might want to read one component of the datafile on its own. Lastly, you can open a component of the datafile as if it were a normal file.
This is the obvious way to do things. With this
system, you issue just one function call and Allegro
loads in all the objects you put in the datafile. The
function to call for this is load_datafile
:
DATAFILE *load_datafile (char *filename);
You pass it the filename of your datafile, and it
returns a pointer an array of DATAFILE structs,
one for each object. So if data
is a DATAFILE *
and you write:
data = load_datafile ("datafile.dat");
then data[0]
will be the first object in the datafile,
data[1]
the second, etc.
The most important field in the DATAFILE struct is the
dat
field. It is a pointer which points to the
actual data for the object. If the object was a
bitmap, then this field will point to a BITMAP struct
-- so you could write:
blit (data[0].dat, screen, ...);
If the object is a sound sample then this is a SAMPLE struct:
play_sample (data[1].dat, 255, 128, 1000, 0);
The same is true for MIDI files, palettes, animations
and a few other things. In the grabber or dat
utility you can see what type of data each object is
just to the left of the object name in the list (type
dat -l <filename>
, or look at the left side of the
grabber screen).
You can also tell what type of data each object holds
by its type
field. This will be set to a constant
like DAT_BITMAP
, DAT_SAMPLE
or DAT_MIDI
. If
the data isn't in a special format, this will be DAT_DATA
-- this indicates that dat
just points to a block of
data. You can get the size of this block from the
size
field of the DATAFILE struct:
load_map_data (data[2].dat, data[2].size);
would pass the pointer to the binary block and its size to the
load_map_data
function (which would be one you'd
write yourself).
Now, how do you know which object number in the datafile corresponds to which object you put into the file? There are three ways you can do this, and as always which way you choose is entirely up to you -- depending on the circumstances you might want to mix together all three methods.
The first way is to use the header file optionally created by the grabber or dat utility. This file will contain one macro for each object, #defining the name of the object to its index in the datafile. So instead of using what I wrote above you can say:
blit (data[MY_BITMAP].dat, screen, ...); play_sample (data[MY_SAMPLE].dat, 255, 128, 1000, 0); load_map_data (data[MY_MAP].dat, data[MY_MAP].size);
This way, if you add other objects to the datafile and they get put before the old objects you won't have to change all the indices in your code.
The text of each #define is exactly what you entered in
the grabber as the object's name. If you used the dat
utility to create the object its name will probably be
its original filename in upper case, with the .
replaced by _
. This replacement is done so that the
resulting object name is a valid thing to #define in C
-- dots aren't allowed.
Along the same lines, when you're making your object
names you should bear in mind that they'll be used as
#defines in your program -- don't pick anything you
might want to use for something else (e.g. main
,
BITMAP
, etc). You can use the Prefix
option of the
grabber or dat utility to prefix all the #defines in
the header file with something, e.g. DATAFILE
-- then you'd have:
blit (data[DATAFILE_BITMAP].dat, screen, ...);
if the object's name was BITMAP
.
The second way of knowing where an object is in the
datafile is by relying on the fact that they are stored
in alphabetical order. According to the author, this
is true now and always will be. I don't recommend
overusing this feature, but it can be useful if, for
example, you have a number of bitmaps (all frames of
the same sprite, perhaps) called ENEMY_000
,
ENEMY_001
, ENEMY_002
, etc. You can use
the #defines to refer to these, of course, but it's more
convenient to be able to pick one of these bitmaps by
number based on a variable.
Because of the way preprocessing works (pre
= before,
i.e. before compilation) it isn't possible to change
ENEMY_000
to ENEMY_xxx
at run-time. But, since the
objects are in alphabetical order you know that
ENEMY_000
will be stored first in the file, and
ENEMY_001
will be immediately after it -- i.e. the nth
enemy bitmap will be object number ENEMY_000 + n
if,
as all sane programmers do, you start counting from
0 (so ENEMY_000
is the 0th object). Then the following
will work:
void draw_nth_enemy (int n, BITMAP *where, int x, int y) { draw_sprite (where, data[ENEMY_000 + n], x, y); }
The third way of finding an object in the datafile
involves searching through the array for an object with
the right name. The object names are stored as
properties of the objects. For full details on
properties, see the grabber.txt
file. Briefly, though,
to get the name of an object you write:
name = get_datafile_property (&dat[obj_number], DAT_ID ('N','A','M','E'));
Note the &
-- you pass a pointer to the object in the
array. If the string you are returned is empty (i.e.
name[0] == '\0', not name == NULL) then the object
doesn't have a NAME
property. This probably means
you stripped the properties from the datafile; in this
case you'll have to use one of the first two methods to
find your objects.
A couple more things are worth mentioning here. Firstly,
if you're using C++ then the compiler won't like you
passing the dat
field (a void pointer) to functions
which expect some other type of pointer, so you have to
explicitly cast it to whatever type the function wants:
play_midi ( (MIDI *) data[MUSIC].dat, 0);
Secondly (and somewhat linked to the above) it is often convenient to alias all the objects in the datafile, by creating global variables or arrays, something like this:
BITMAP *enemy_bitmap; MIDI *background_music; SAMPLE *crash_sound;
then setting them to point to bits of the datafile:
data = load_datafile ("DATAFILE.DAT"); if (!data) barf(); /* loading failed */ enemy_bitmap = (BITMAP *) data[ENEMY_BITMAP]; background_music = (MIDI *) data[MUSIC]; crash_sound = (SAMPLE *) data[CRASH_SOUND];
and finally using them in the game:
draw_sprite (screen, enemy_bitmap, x, y); play_midi (background_music, 1); play_sample (crash_sound, 255, 128, 1000, 0);
The advantages of this are that it gets rid of the annoying casts needed for C++, it makes the later code neater and slightly faster, and it also makes it easier to change the code from being datafile-based to being file-based and vice versa.
Thirdly, the datafile array is terminated by an object
of type DAT_END
. You may or may not find this very
useful; it's semi-redundant information, a bit like the
fact that argv[argc]
is NULL. If you want to search
the whole datafile for something (perhaps an object
with a certain property) you could use this fact to
know where to stop.
Finally, when you've finished with the datafile you can
unload it using the unload_datafile
function:
unload_datafile (data);
Needless to say, after doing this you must not use the data array any more, and if you have any aliases to objects in the array (as above) you must not use them either.
See the example: examples/chap_10/ex_3_1
You can load an individual datafile object on its own provided
the name information is still in the datafile (i.e. you didn't
strip all the properties in the grabber, or apply -s2
in
the dat utility). The function to do this is
load_datafile_object
and it is used like this:
data_object = load_datafile_object ("datafile.dat", "object_name");
data_object
should be a DATAFILE *
, just as if you were
loading a whole datafile. To access the object you
just dereference this pointer, e.g.:
music_object = load_datafile_object ("datafile.dat", "MUSIC"); play_midi (music_object->dat); /* or music_object[0].dat */
Since this loads only one object, it returns a pointer to a
single DATAFILE struct, not an array of them. So you don't use
the object's index from the header file, and also there is no
DAT_END
object after the returned object.
To unload an object loaded in this way use unload_datafile_object
:
unload_datafile_object (data_object);
See the example: examples/chap_10/ex_3_2
A handy feature of Allegro's packfile routines is the
ability to read a datafile object as if it were a file
on disk. To do this you simply use the encoded
filename format datafile_filename#object_name
, for
example:
fp = pack_fopen ("datafile.dat#MUSIC", F_READ);
Then if fp
is not NULL you can read from it as you
would read from a normal packfile. You can't write to
the file. To close it you use fclose
as normal.
See the example: examples/chap_10/ex_3_3