Animation is a pretty simple thing to do. Take a series of pictures and display them with a wait interval in between each and you get the appearance of motion. You can move the pictures around the screen, rotate them, stretch/squish them, rotate their colors, and so on - but these are things that can be done in lots of ways - not necessarily as a part of the animation. At its most basic, we have pictures and times, and thats it.
Let's look at one part of an animation, the "frame" or "cel":
class CEL { public: BITMAP *bitmap; float duration; }; |
There isn't much more that we need. We have a pointer to a bitmap, and an amount of time we're going to display the bitmap for, when we're displaying the animation. Make note that I'm using a float to store duration - you may want to store it a different way, but I'll go over this below. Now we'll look at the animation class:
class ANIMATION { public: CEL *cel; int num_cels, current_cel; float time_elapsed; bool repeating, done; }; |
As far as getting a basic animation going, we don't need any more than this. Most of the variables are self-explanatory. The time_elapsed variable can be used to track how much time has passed since the animation started, or it could be used to track how much time has gone by for current_cel. I prefer the second interpretation. Now we need to look at our time conventions.
If we were using Game Timing - The Old Way (check out my Game Timing with Allegro tutorial for info), we could assume the time increases at a constant rate of 1 unit. My understanding of The Old Way is not yet complete, so what CEL::duration translates into isn't obvious to me. The best thing to do is to just try some numbers and just tweak them until they look good. Don't forget to change the two variables to integers.
Using Game Timing - My Way, we can't assume the time increases at any fixed rate; we need to use the amount of time passed since the last frame. I tend to think of motion as a function of motion-units per second, so I tend to use seconds as the unit for CEL::duration and ANIMATION::time_elapsed. On low-end machines you may want to stick with the ints and use 1000ms in place of 1sec and alter your math based on that, as integer math is faster than floating point math when you get to a certain class of cpu.
Now that we have some conventions, we can make an update function:
void ANIMATION::update (double d_time) { if (done) return; time_elapsed += d_time; // if we're past the end of the current cel while (time_elapsed > cel[current_cel]->duration && !done) { // back up for the next cel time_elapsed -= cel[current_cel]->duration; current_cel++; // if we went past the end of the animation if (current_cel >= num_cels) { if (repeating) { current_cel %= num_cels; } else { done = true; } } } } |
If you're using The Old Way of timing, then you won't need an argument to the function, and you can change time_elapsed+=d_time into time_elapsed++. In either case, the idea the code construes is the same. Note that we can simplify the code for The Old Way, knowing we're only going to update by 1 every time. I leave that up to you to do though, because I don't use The Old Way. Its not hard to do. Let's step through the code.
First we check to see if the animation is currently running; if not, don't bother doing anything. Next we update how much time has passed. Now we need to know if we've run past the end of the current cel. If we haven't, we're done. If we have, we need to update the time passed to reflect how far past the end of the cel we went, then we update the current cel. If we haven't gone past the end of the cel list, we're done. If we have, we need to check to see if the animation is repeating. If it is, we just go back to the start. If it isn't, then the animation is done playing. We're going to keep updating time_elapsed and current_cel until we finally catch up, or run past the end of a non-repeating animation.
Drawing: There are many ways to do this; one way is to allow access to the current cel's BITMAP, another is to make a drawing function for the animation class. I personally would like to leave the drawing method in the hands of the user, but you can put switches and whatnot into your own animation class for your special effects.
Reverse Play: Again, there isn't just one method of doing this. Without changing any code, you could make a seperate 'reverse' animation where the frame bitmaps and times were changed around to emulate reverse-ness. You could also code it right into the animation class, by adding a reverse boolean and handling the update function differently if it were true.
Ping-Pong Play: This effect is done when you want an animation that will repeat itself backwards when it is done. If you had 4 cels that you wanted to ping-pong play, the cel order would likely look like {0, 1, 2, 3, 2, 1} if you were to use the above code as-is. You could code this right into the animation class as well, by adding a pingpong boolean and changing the update function. If you implemented reverse play before attempting ping-pong, you'll probably have an easier time of it.
Before we can code this, we need to come up with a file format. I tend to use text files to start out with so I can hand-edit my work; this method also doesn't require a special editor. I am going to use this method in the following examples.
We need to know, we can find in our classes: animation - cel count, repeating flag; cel - bitmap filename, time duration. I like commenting in my text files so that I can read it faster, but that would require a parser. We'll keep it simple for now. Here's a sample animation file:
walk.ani:
num_cels 4 repeating true "left_foot.bmp" .100 "center.bmp" .100 "right_foot.bmp" .100 "center.bmp" .100 |
Notice that it uses the ping-pong method I mentioned above, without the need for extra code. Now we need to read the file in:
void ANIMATION::load (const char *filename) { FILE *infile = fopen (filename, "r"); // get the number of cels fscanf (infile, "num_cels %4", &num_cels); // check repeating flag char temp_str[160]; fscanf (infile, "repeating %s", temp_str); if (strcmp (temp_str, "true") == 0) { repeating = true; } else { repeating = false; } // allocate memory for cel list cel = new CEL [num_cels]; for (int i=0; i<num_cels; i++) { // get cel info from file fscanf (infile, "%s %f", temp_str, cel[i].duration); // load bitmap cel[i].bitmap = load_bitmap (temp_str, NULL); } fclose (infile); } |
I don't think there's anything to explain that the comments don't cover, except for the warnings and advice. The most obvious thing is that there isn't any error checking at all. Error checking is very important, you could make a typing mistake in your ani file and this wouldn't catch it, or your ani and bitmap files could be corrupted and it would slip by and who knows what would happen then.
Notice that I'm not getting or setting a palette when I load_bitmap. I'm assuming I'm going to be using a high- or true-color mode and that the bitmaps will be converted properly. Be sure you handle your paletting properly in your class; if you're making it generic, make sure it supports all configurations.
I haven't gone over the constructor or destructor, but you should be initializing everything to 0 and NULL in the constructor, and destroying the bitmap and freeing up the memory cel list in the destructor. Be sure to add this functionality in your own implementation. I also haven't mentioned that when you draw, if you have differently sized bitmaps, it may not work as you expect - it will always anchor to the upper left unless you specify otherwise.
Let's look at different ways we can store the animation on disk. If you used the above method as is, you have to be sure the ani file and images are all in the program's base directory. If you have more than 2 animations with more than 3 cels each, then the directory will get cluttered really quick. One thing you could do is store the animations in their own directories; you'll only need to do a little work to get the proper directory handle to use when loading the images. Another is to just pack each animation into its own datafile - I'm going to research this in the near future.
I've thought of a few bad ideas, and just so you know about them before you try them, I'll voice my opinions on why I think they're bad. One thing you could do is use a single framesheet and make the ani file contain the rectangles surrounding each cel. I personally think this is a little more complicated than needed, however. The size of a single BITMAP object is 128bytes, not including the image data. Unless you're looking at a computer with a 20mb harddrive, you really don't need to worry about 'saving space' by doing this. You won't get much back.
Another bad idea is to make your own format that stores everything all in one binary file. Unless you're very sure of yourself, you're going to make alot of mistakes and end up taking 10 times longer to get a simple animation working. You're also going to need to make an editor in order to create or change your animations. Personally I like to get up and running quickly - but an editor will eventually help you out. I mention the editor because it slows down development time when you just want to get something quick and dirty done.
Well, that's it for now. If I find the datafile method works out well, I'll update this tutorial to cover it. Have fun with animation!