Author : Filipe Medeiros
Page : << Previous 15 Next >>
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.
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.
10.2.1 The grabber
(not yet written)
10.2.2 The DAT utility
(not yet written)
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.
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.
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'
Page : << Previous 15 Next >>