Allegro Vivace

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.


[ Next: | Up:Top ]

Legal issues

Distribution

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.

Disclaimer

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.


[ Next: | Previous:Legal issues | Up:Top ]

1. Introduction


[ Next: | Up:Introduction ]

1.1 About the tutorial

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.


[ Next: | Previous:About the tutorial | Up:Introduction ]

1.2 Aim

The aim of this tutorial is to guide newcomers to game programming and Allegro through the process of writing a simple game.


[ Next: | Previous:Aim | Up:Introduction ]

1.3 Target audience

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.


[ Next: | Previous:Target audience | Up:Introduction ]

1.4 Requirements

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.


[ Previous:Requirements | Up:Introduction ]

1.5 Before we start...

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!


[ Next: | Previous:Introduction | Up:Top ]

2. Getting, installing and using Allegro


[ Next: | Up:Getting installing and using Allegro ]

2.1 What is Allegro?

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/


[ Next: | Previous:What is Allegro | Up:Getting installing and using Allegro ]

2.2 Where to find 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.


[ Next: | Previous:Where to find Allegro | Up:Getting installing and using Allegro ]

2.3 How to install Allegro

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.


[ Next: | Previous:How to install Allegro | Up:Getting installing and using Allegro ]

2.4 Testing the installation and building the Vivace example programs

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.


[ Previous:Testing the installation | Up:Getting installing and using Allegro ]

2.5 Using Allegro

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.


[ Next: | Previous:Getting installing and using Allegro | Up:Top ]

3. A basic game structure


[ Next: | Up:A basic game structure ]

3.1 What does a game need to do?

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.

  1. Initialisation

    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.

  2. Main game loop

    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:

    1. Input -- getting input from the player
    2. Processing -- moving things around, responding to the input
    3. Output -- sending information back to the player, usually by putting it on the screen but sometimes by other means, for example playing sounds
  3. After the game

    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.


[ Next: | Previous:What does a game need to do | Up:A basic game structure ]

3.2 Proposed structure

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.


[ Previous:Proposed structure | Up:A basic game structure ]

3.3 Multi-file projects

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.

Justification

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.

Suggested system

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.

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.


[ Next: | Previous:A basic game structure | Up:Top ]

4. Introducing graphics

Note

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.


[ Next: | Up:Introducing graphics ]

4.1 Selecting a graphics mode

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.


[ Next: | Previous:Selecting a graphics mode | Up:Introducing graphics ]

4.2 Drawing things


[ Next: | Up:Drawing things ]

4.2.1 The BITMAP struct

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).


[ Next: | Previous:The BITMAP struct | Up:Drawing things ]

4.2.2 Plotting pixels

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.


[ Next: | Previous:Plotting pixels | Up:Drawing things ]

4.2.3 Some other primitives

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.


[ Previous:Some other primitives | Up:Drawing things ]

4.2.4 Writing text

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.


[ Next: | Previous:Drawing things | Up:Introducing graphics ]

4.3 Palette manipulation

Note

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.


[ Next: | Up:Palette manipulation ]

4.3.1 Palette explanation

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.


[ Next: | Previous:Palette explanation | Up:Palette manipulation ]

4.3.2 Changing a single logical colour's physical appearance

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.


[ Next: | Previous:Changing a palette entry | Up:Palette manipulation ]

4.3.3 Changing the entire palette

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.


[ Previous:Changing the entire palette | Up:Palette manipulation ]

4.3.4 Fading in and out

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.


[ Previous:Palette manipulation | Up:Introducing graphics ]

4.4 Simple animation


[ Next: | Up:Simple animation ]

4.4.1 What is animation?

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.


[ Next: | Previous:What is animation | Up:Simple animation ]

4.4.2 Making things appear to move

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.


[ Previous:Making things appear to move | Up:Simple animation ]

4.4.3 Reducing flicker

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).


[ Next: | Up:Reducing flicker ]

4.4.3.1 Synchronising to the vertical retrace

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.


[ Next: | Previous:Synchronising to the vertical retrace | Up:Reducing flicker ]

4.4.3.2 Maximising drawn time

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.


[ Next: | Previous:Maximising drawn time | Up:Reducing flicker ]

4.4.3.3 Optimising drawing order

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.


[ Next: | Previous:Optimising drawing order | Up:Reducing flicker ]

4.4.3.4 Double buffering

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.


[ Next: | Previous:Double buffering | Up:Reducing flicker ]

4.4.3.5 Dirty rectangles

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.


[ Previous:Dirty rectangles | Up:Reducing flicker ]

4.4.3.6 Alternate line blitting

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.


[ Next: | Previous:Introducing graphics | Up:Top ]

5. Making several things happen at once


[ Next: | Up:Making several things happen at once ]

5.1 Moving more circles

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.


[ Next: | Previous:More circles | Up:Making several things happen at once ]

5.2 Now moving a square as well

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.


[ Next: | Previous:Squares too | Up:Making several things happen at once ]

5.3 Keeping track of things

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.


[ Next: | Up:Keeping track of things ]

5.3.1 Dynamic allocation

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.


[ Previous:Dynamic allocation | Up:Keeping track of things ]

5.3.2 Linked lists

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.


[ Next: | Up:Linked lists ]

5.3.2.1 Singly linked lists

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.


[ Next: | Previous:Singly linked lists | Up:Linked lists ]

5.3.2.2 Doubly linked lists

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.


[ Next: | Previous:Doubly linked lists | Up:Linked lists ]

5.3.2.3 Circularly linked lists

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.


[ Previous:Circularly linked lists | Up:Linked lists ]

5.3.2.4 Using linked lists

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.


[ Previous:Keeping track of things | Up:Making several things happen at once ]

5.4 Object Oriented Programming

This section has not yet been written, sorry.


[ Next: | Previous:Making several things happen at once | Up:Top ]

6. User input

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.


[ Next: | Up:User input ]

6.1 Keyboard input


[ Next: | Up:Keyboard input ]

6.1.1 Why the standard PC keyboard routines are useless

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.


[ Next: | Previous:Why the standard keyboard routines are useless | Up:Keyboard input ]

6.1.2 Allegro's keyboard 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.


[ Next: | Previous:Allegro keyboard routines | Up:Keyboard input ]

6.1.3 Moving something using the keyboard

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.


[ Previous:Moving something using the keyboard | Up:Keyboard input ]

6.1.4 Letting the user choose which keys to use

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.


[ Next: | Previous:Keyboard input | Up:User input ]

6.2 Joystick input


[ Next: | Up:Joystick input ]

6.2.1 Digital readings

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.


[ Next: | Previous:Digital readings | Up:Joystick input ]

6.2.2 The fire buttons

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.


[ Previous:The fire buttons | Up:Joystick input ]

6.2.3 Analogue readings and calibration

[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:

  1. Ask the user to centre joystick, then wait for them to acknowledge (for example, by pressing a key)
  2. Call the initialise_joystick function to note the centre position
  3. Ask the user to push it to the top left and acknowledge
  4. Call the calibrate_joystick_tl function
  5. Ask the user to pull it to the bottom right and acknowledge
  6. Call the 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.


[ Next: | Previous:Joystick input | Up:User input ]

6.3 Mouse input

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.


[ Next: | Up:Mouse input ]

6.3.1 Point-and-click

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.


[ Next: | Up:Point and click ]

6.3.1.1 Initialising the mouse

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.


[ Next: | Previous:Initialising the mouse | Up:Point and click ]

6.3.1.2 Reading the mouse

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 */
}


[ Next: | Previous:Reading the mouse | Up:Point and click ]

6.3.1.3 Displaying the mouse pointer

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.


[ Previous:Displaying the mouse pointer | Up:Point and click ]

6.3.1.4 Controlling the mouse pointer

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.


[ Previous:Point and click | Up:Mouse input ]

6.3.2 Direct mouse control

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.


[ Next: | Previous:Mouse input | Up:User input ]

6.4 Generic input

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.


[ Previous:Generic input | Up:User input ]

6.5 Ways of interpretting input

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.


[ Next: | Previous:User input | Up:Top ]

7. More 2D graphics

This section isn't written yet. When it is, it will talk about:


[ Next: | Previous:More 2D graphics | Up:Top ]

8. Sound


[ Next: | Up:Sound ]

8.1 Sound configuration


[ Next: | Up:Sound configuration ]

8.1.1 Initialising sound drivers

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.


[ Previous:Initialising sound drivers | Up:Sound configuration ]

8.1.2 Using a configuration file

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).


[ Next: | Previous:Sound configuration | Up:Sound ]

8.2 Digital sound


[ Next: | Up:Digital sound ]

8.2.1 Loading sound files

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.


[ Next: | Previous:Loading sound files | Up:Digital sound ]

8.2.2 Playing samples

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:

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.


[ Next: | Previous:Playing samples | Up:Digital sound ]

8.2.3 Adjusting a playing sample's parameters

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.


[ Next: | Previous:Adjusting sample parameters | Up:Digital sound ]

8.2.4 Unloading samples

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.


[ Previous:Unloading samples | Up:Digital sound ]

8.2.5 Voice functions

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


[ Previous:Digital sound | Up:Sound ]

8.3 MIDI music


[ Next: | Up:MIDI music ]

8.3.1 Loading MIDI music

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.


[ Next: | Previous:Loading MIDI music | Up:MIDI music ]

8.3.2 Playing MIDI music

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.


[ Next: | Previous:Playing MIDI music | Up:MIDI music ]

8.3.3 Controlling MIDI music

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:

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.


[ Previous:Controlling MIDI music | Up:MIDI music ]

8.3.4 Unloading MIDI music

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.


[ Next: | Previous:Sound | Up:Top ]

9. Timers


[ Next: | Up:Timers ]

9.1 Uses of timers

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.


[ Next: | Previous:Uses of timers | Up:Timers ]

9.2 Setting up timer callbacks


[ Next: | Up:Setting up timer callbacks ]

9.2.1 Initialising the timer system

Before you can use any of the timer functions, you must initialise Allegro's timer system. As noted earlier on, many other parts of Allegro need this anyway, so perhaps you have already initialised it. These other components include the mouse pointer updating code and the MIDI player.

To initialise the timer system, just call:

install_timer();

After making this call, the libc delay function will no longer work; you should use Allegro's rest function, which operates in almost the same way.


[ Next: | Previous:Initialising the timer system | Up:Setting up timer callbacks ]

9.2.2 Locking and volatility

Allegro's timer routines are driven by interrupts. These are generated on certain events, and interrupt the execution of your program. At this point, the CPU starts running the code of an ISR (interrupt service routine). Allegro hooks itself in here in various ways so that it is informed whenever certain interrupts occur -- in this case, the timer interrupt.

Allegro's timer interrupt handler calls various functions you specify, called callbacks, at certain intervals (which you also specify). The key point is that these callbacks are called inside an interrupt.

For various technical reasons, it's a very bad idea to try to page things to and from disk during an interrupt. This means that you must ensure that your callbacks are never paged out to disk -- if they were, they'd need paging back in during the interrupt. You must also ensure that any data they touch -- global variables, etc -- is never paged out to disk. You do this by locking the memory in which they are located. Some functions are provided by djgpp to do this, and Allegro provides some nicer wrappers for them in the form of the following macros:

END_OF_FUNCTION (function_name);
LOCK_FUNCTION (function_name);
LOCK_VARIABLE (variable_name);

LOCK_VARIABLE locks a static or global variable and LOCK_FUNCTION locks a function. LOCK_VARIABLE can work on its own, but LOCK_FUNCTION needs you to mark the end of the function in question using END_OF_FUNCTION. If you forget the END_OF_FUNCTION you'll get some odd-looking (but fairly self-explanatory) compiler warnings and linker errors.

The other issue that comes up when dealing with interrupts is volatility. Any decent compiler will try to optimise the code it generates. In doing so, it will normally assume that if you don't write to a variable then its value won't change. Normally this is true, but if there's an interrupt-called function that modifies the variable then the variable's value might change without the compiler noticing. To solve this problem you must mark all such variables as being volatile. volatile is a keyword in the C language that directs compilers to make no assumptions about the contents of the variable -- in particular, it will never cache the variable's value in a register.

You don't want to make all of your variables volatile, of course. If you did this you'd be restricting the compiler's ability to optimise your code, and would probably end up with slower code. Try to only make variables volatile when they're written to or read from in an interrupt context (that is, inside a timer routine or other interrupt callback).


[ Next: | Previous:Locking and volatility | Up:Setting up timer callbacks ]

9.2.3 Installing timer callbacks

When you've initialised the timer system, locked your callback and any data it touches, and marked any such data as volatile, you can at last tell Allegro to call it.

install_int (callback, msecs);

Replace callback with the name of a function returning void that takes no parameters (in C) or a variable number (in C++). msecs is the time between calls to your function.

Here's an example function for C:

volatile int counter = 0;      /* We'll increase this in the callback,
                                  so it must be volatile. */
void my_callback_func()
{
    counter++;                 /* Nice and simple */
}
END_OF_FUNCTION (my_callback_func);   /* Note the syntax here */

Note that the callback function must be very simple. It must not take a long time, it must not call any C library functions, it must not do anything fancy like calling DOS routines. If it calls Allegro routines, they must be reentrant ones that obey these same rules.

If we were using C++, we'd have to change the function definition to:

void my_callback_func(...)

otherwise we'd get an error.

Next, here's the code we'd use to lock the things mentioned above:

LOCK_FUNCTION (my_callback_func);    /* Lock the function */
LOCK_VARIABLE (counter);             /* It touches this variable,
                                        so we lock it too. */

Finally, the following line asks Allegro to call our function ten times per second (once per hundred milliseconds):

install_int (my_callback_func, 100);

Note that even though my_callback_func is a function, we don't write parentheses (( and )) after its name -- if we did, the function would be called and its (non-existant) return value would be passed to install_int. We're passing the entry point of the function to install_int, so we don't write the parentheses.

Incidentally, you can call install_int again later on to change the rate of calls to the function.

For more control over your callback functions, you can use the install_int_ex function. The parameter to this function is given in hardware ticks, but you can use some macros to convert to this format from more useful units -- seconds or milliseconds per call, or calls per second or minute. See the Allegro documentation for this function for full details, and more information on what timer callbacks should and should not try to do.


[ Previous:Installing timer callbacks | Up:Setting up timer callbacks ]

9.2.4 Removing timer callbacks

Removing timer callbacks is simple -- just call this function:

remove_int (my_callback_proc);

In theory, you could now unlock the memory of the function, but in practice there's little point.


[ Next: | Previous:Setting up timer callbacks | Up:Timers ]

9.3 Limitations of timers

Listed here are some things you should and should not do when writing and using timer callbacks (and interrupt callbacks in general).

Do:

Don't:


[ Previous:Limitations of timers | Up:Timers ]

9.4 Examples of timers


[ Next: | Up:Examples of timers ]

9.4.1 Timing a game

This one's fairly simple. We set up a global variable to contain the number of seconds elapsed. We make our timer callback, which increases that number. We lock the counter and the callback routine. Then, when we want to start timing we install the callback routine to be called once per second. After that we can read from the (volatile!) counter variable to find out how much time has elapsed.

When the period we're timing has ended, we can either take a copy of the counter, or just remove the callback. If we do remove it, the counter won't be changed any more.

As a variation, we could implement a time limit. In this case we'd set the counter to the number of seconds allowed (for example, to complete a level). The callback would now be made to decrement that value. In the main game loop we'd check on each cycle whether or not the counter is positive. If it's not, the player has run out of time. At this stage we ought to remove the callback routine, to stop the number decreasing further.

See examples/chap_09/ex_4_1.


[ Next: | Previous:Timing a game | Up:Examples of timers ]

9.4.2 Measuring the frame rate

This, too, is pretty simple.

First of all, we make two volatile global variables -- one called last_fps and the other frame_counter. Then we make a timer callback that simply copies frame_counter to last_fps and then sets frame_counter to zero. We now lock both variables and the timer callback, then install the callback to be executed once per second.

Now we can display the variable last_fps on the screen somewhere, or log it, or do what we like with it. At the moment it will be zero. All we need to do to make it count the frames in each second is to make our graphics routines increment the frame_counter variable once per frame.

This works because the callback routine is called every second, and it copies the value into last_fps, zeroing frame_counter. Next we draw some frames, and as we do so the frame_counter variable increases. One second after its previous call, the callback is called again, and it copies the number of frames drawn in that second from frame_counter to last_fps.

See examples/chap_09/ex_4_2.


[ Previous:Measuring the frame rate | Up:Examples of timers ]

9.4.3 Regulating game speed

This topic is my nomination for FAQ of the year on the Allegro mailing list. It's very important. Games that do not run at the same speed on different computers or under different circumstances annoy me. I don't mean that the frame rate should be the same on all computers -- that's not possible, of course. I'm referring to the actual game speed -- the rate of movement of the game characters, for instance.

To make this happen, you want ideally to move the characters at regular intervals. Inside a timer callback? No way. That's far too complicated; remember, timer callbacks must be simple. Besides, think how much of a problem it would be to try to lock everything touched by such a callback!

The best thing we can do realistically is to make a timer increment a variable, which holds the number of game cycles which should have elapsed by now. Then we can make the game loop compare this to the number of game cycles which really have elapsed. If we're behind target, we need to call the game logic function one or more times, until we're back on target. If not, we don't need to do any more game cycles at the moment.

If we are up-to-date then we can go off and do other things, like display a frame of graphics. Displaying graphics is often a slow process, partly because it's just a slow thing to do and partly because it often involves waiting around for the VBI, using vsync. While we're drawing graphics, and waiting for the VBI, our timer callback will still be called at the right intervals. We won't be dealing with that immediately, of course; it will have to wait until after we've done the graphics. But when we have done the graphics, we know exactly how many game updates we need to do to get back on target.

So, in summary, we set up two counters -- one being increased at a constant rate (the target game cycle rate) by a timer callback, and the other being increased once per actual game cycle, by the function that performs a game cycle. Initially these counters start at zero. On every pass through our main game loop, we first draw a frame of graphics. This will take a relatively long time, in most games. Then we test whether the target game cycle is greater than the actual game cycle (it probably is), and if so we keep doing game cycles until it is no longer the case. Then we loop again.

See examples/chap_09/ex_4_3.


[ Previous:Timers | Up:Top ]

10. Datafiles

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.


[ Next: | Up:Datafiles ]

10.1 Concept

By now you can see that a game can require many different files; there is the executable file the users run, then there are the graphics, which could be in several large files with many graphics on each, or could be each in its own file (I tend to use the former approach). We also have the sound effects (one file for each) and the music (one file for each piece).

That's just the generic files that nearly all games need. On top of that, each game may have other files describing for example level designs, enemy AI, or other data needed. In total there can be a large number of files.

Due to clustering arrangements under DOS, each file must be stored in a number of blocks of a certain size. On partitions of just under 1Gb these clusters are about 16K each. Most files won't fully fill their last cluster, and so the remaining space in it is wasted -- on average, assuming evenly distributed file lengths, this would be 8K per file, but it's probably more in practice. This wasted space can mount up, especially if you store many files which are much smaller than even one cluster. If you have Windows 95, try typing dir /s/v in your djgpp\zoneinfo directory, and comparing `bytes' to `bytes allocated'. After doing this you might consider deleting the contents of that directory, or at least zipping it up -- you probably don't need it.

Having many files with .PCX, .WAV or .MID extensions also encourages 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.


[ Next: | Previous:Concept | Up:Datafiles ]

10.2 Creating a datafile

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.


[ Next: | Up:Creating a datafile ]

10.2.1 The grabber

(not yet written)


[ Previous:The grabber | Up:Creating a datafile ]

10.2.2 The DAT utility

(not yet written)


[ Previous:Creating a datafile | Up:Datafiles ]

10.3 Using a datafile in your program

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.


[ Next: | Up:Using a datafile ]

10.3.1 Loading an entire datafile

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


[ Next: | Previous:Loading an entire datafile | Up:Using a datafile ]

10.3.2 Individually loading datafile components

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


[ Previous:Individually loading datafile components | Up:Using a datafile ]

10.3.3 Reading a datafile component as a normal packfile

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


[ Up:Top ]

Index

Menu

This section is generated automatically, but I haven't bothered to mark any index entries yet, so it's empty at the moment.

Table of Contents