Topic : Allegro Vivace
Author : Filipe Medeiros
Page : << Previous 6  Next >>
Go to page :

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.
    So 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 `src/5.1/single' and `src/5.1/multiple' 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. This could increase the amount of flicker seen though, unless you use a double buffer.

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.
    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, here are some I made earlier: `src/5.2/a' and `src/5.2/b'. If you do not manage it on your own, look at how I did it 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 is `src/5.2/c'; you might want to run it to see what it does, and then try to mimic its behaviour with your own program.

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.

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

Page : << Previous 6  Next >>