Animation

Our game will contain a main character and several animated animals. Each of these entities will require one or more animations for each of their behaviors and each of the directions they could be facing during those behaviors. For instance, the main character could have “idle,” “walking,” and “jumping” animations in the “north,” “south,” “east,” and “west” directions.

Once our artist has rendered each of these, we can assemble them together into a sprite texture similar to the unique tile strip texture we will use for the tile engine map. However, these animations are much more complex than the unique tile strip. See Figure 4-3, for instance, for the images we need to represent a walking emu chick.

Emu chick walking animation

Figure 4-3. Emu chick walking animation

As you can see, the emu chick sprite texture has three animations: walking west, walking north, and walking south (we omitted walking east because it can be created by flipping the walking west animation horizontally, which saves precious texture memory).

To make things more complicated, the animations are set up in such a way that some frames should be used multiple times in a single animation sequence. Specifically, the walking west animation should be displayed in the frame sequence 0, 1, 2, 3, 4. If we had a more complex animation, this format would support something similar to 0, 1, 0, 2, 0, 3…, where the frame indexes are not sequential.

Additionally, we could want some frames to take longer than others, so we should specify how long (in milliseconds) each frame should take: 200, 300, 200, 100, 200, 100, 100, 200. This adds up to an animation sequence that takes 1,400 ms (or 1.4 s) to complete.

To complicate things further, some animations should be looped until we change to a different animation, whereas other animations should play only once and stop. In addition, some animations should specify another animation to switch to as soon as they have completed.

Animation and Sprite Classes

However, our game logic should not have to deal with all of the intricacies of animation programming. We need to encapsulate the animation logic inside an Animation class.

To begin, we need a way to specify the animation sequences that an Animation object will represent. We could hardcode all of the animation logic, but that would be a lot of work with all of the animals and such, and we would have to throw it away the next time we wrote a game. We could also create a proprietary file format, but then we’d need to write a tool that could take in all of the animation sequences and write them to the file, which is beyond the scope of this book.

Instead, we will use something familiar to Mac developers: the .plist file. You already used a .plist file to set up certain parameters for your Xcode project. Here we will use a .plist file to contain all of our animation sequence data, and we will write an initialization function in our Animation class that will read in that data.

Property list

We can use Xcode to create a .plist file for animation purposes. We will use the emuchick.png file as our texture. Our goal is to describe the “walking left,” “walking up,” “walking down,” and “walking right” animations:

  1. From within Xcode, select File→New File to open the New File dialog.

  2. From the left column select Other and in the right window select Property List.

  3. Type Animations.plist for the name and click the Finish button.

  4. Select the Animations.plist file within Xcode. The .plist editor view will appear.

All items in a property list are added to the root dictionary item, in key-value pairs. You can structure your data by nesting dictionaries or arrays. We are going to structure our animation data by using a dictionary for each sprite texture.

We will begin by adding a dictionary named emuchick.png:

  1. Right-click the Root row and select Add Row.

  2. Replace the “New Item” text with “emuchick.png” in the Key column.

  3. Change the Row type to Dictionary by clicking the Type column (it is set to String by default) and selecting Dictionary.

Now we can start adding rows to the emuchick.png dictionary to describe the animations found in this sprite texture:

  1. Add a row to the emuchick.png dictionary by right-clicking and selecting Add Row. Make sure you added the row to our dictionary, not the root: the new row should be indented farther to the right than the emuchick.png row is in the Key column.

  2. Name the new row “frameCount” and set its type to Number.

  3. Set the value for frameCount to 15, since there are 15 frames in the emuchick.png texture.

Next, we want to store information about all of the different animations that can be found in this sprite. For each separate animation, we will create a new dictionary below the emuchick.png dictionary:

  1. Add another row to the emuchick.png dictionary by right-clicking and selecting Add Row again.

  2. Name this one “walkLeft” and set its type to Dictionary. It will represent an animation of the emu chick walking to the left.

Each animation will need to specify a sequence of frames and the animation period for each of those frames. For “walkLeft”, we want to use a frame sequence of 0,1,2,3,4 with 100 ms for each frame:

  1. Add two rows to the walkLeft dictionary and name them “anim” and “time”. Leave their types as String.

  2. In “anim”, set the value to “0,1,2,3,4”, with no spaces.

  3. In “time”, set the value to “100,100,100,100,100”, also with no spaces.

Great; now we have the first sequence done. For the next two, we can actually copy and paste the walkLeft dictionary and rename it to save us some time:

  1. Right-click on “walkLeft” and select Copy.

  2. Left-click on “emuchick.png”, then right-click and select Paste (it is important to have it highlighted by left-clicking before pasting, or your new copy will end up in the wrong place). Double-check that the new copy is a child of emuchick.png; it should be at the same indentation as the original walkLeft dictionary.

  3. Rename the first new copy to “walkup” and change the “anim” value to “5,6,7,8,9”.

  4. Rename the second copy to “walkdown” and change its “anim” value to “10,11,12,13,14”.

Now we need a walking right animation. But the sprite has no frames of the emu chick walking to the right. Instead, following our plan when we asked the artist to draw the images, we’ll flip the left-facing frames for use in the walking right animation:

  1. Copy and paste the walkLeft dictionary onto emuchick.png once more.

  2. This time, add a row to walkRight, named “flipHorizontal”.

  3. Set its type to Boolean and toggle the checkbox that shows up in the Value column. It should be marked with a check, which means the value is set to true.

The last piece of data we need to store about the emuchick.png animations is an offset value for all of the frames. When we want to draw the emu chick, we don’t want to have to figure out how big the frame is and draw from the upper-left corner; instead, we set that value here so that we can automatically offset the graphics. In this case, we want the origin to be at 16,6 within the frame:

  1. Add one more row to the emuchick.png dictionary.

  2. Name this one “anchor” and keep its type as String.

  3. Set its value to “16,6”.

Now we have a .plist file that contains all of the data to represent a series of animations used by an entity. Next, we will create an Animation class that will allow us to use this data in our game.

Consider, however, that we do not want to allocate a new Animation object for each of our entities. For instance, let’s say we have three siren salamanders walking around in our level. Just as they are all using the same GLTexture for their image data, they should also use the same Animation object for their animation data. You should consider an Animation object as a resource such as a texture.

However, certain aspects of the animation should belong to each Salamander entity: specifically, the current animation sequence being rendered, and the time since the start of the animation.

To facilitate this, we will also create a Sprite class. This class will keep track of the animation data, as well as the exact frame and start time of the animation being rendered.

Animation class

Although the Animation class will represent the animation used by an object in our game, it’s possible that a single object could have multiple behaviors. In the preceding example, the emu chick had only a walking animation, but it could also have had an idle animation or a sleeping animation.

We call each of these behaviors an animation sequence, and our Animation class will keep track of multiple AnimationSequence objects.

In the preceding example, the Animation object used by the emu chick would have only one AnimationSequence.

We will start by defining the AnimationSequence class, followed by the Animation class:

//Animation.h
@interface AnimationSequence : NSObject
{
    @public
    int frameCount;
    float* timeout;
    CGRect* frames;
    bool flipped;
    NSString* next;
}

- (AnimationSequence*) initWithFrames:(NSDictionary*) animData
                       width:(float) width
                       height:(float) height;

@end

@interface Animation : NSObject {
    NSString* image;
    NSMutableDictionary* sequences;
    CGPoint anchor;
}

- (Animation*) initWithAnim:(NSString*) img;
- (void) drawAtPoint:(CGPoint) point
         withSequence:(NSString*) sequence
         withFrame:(int) frame;

-(int) getFrameCount:(NSString*) sequence;
-(NSString*) firstSequence;

-(AnimationSequence*) get:(NSString*) sequence;

@end

The AnimationSequence class keeps track of the number of frames, the time that each frame should be displayed, and (similar to the Tile class) a CGRect to represent the subsection of the GLTexture that represents each frame. It also keeps a Boolean to determine whether the frame should be drawn flipped on the horizontal axis.

The Animation class keeps a string that represents the name of the GLTexture for use with our ResourceManager’s getTexture: function, as well as an NSMutableDictionary to keep track of our AnimationSequences. To make things easy, we will use a string as the key value. When we want to access the walking animation sequence, all we have to do is call [sequences valueForKey:"walking"]. Finally, we keep an anchor point that allows us to draw the animation at an offset.

The AnimationSequence class we defined had only one function, an initialization method, which will parse the animation sequence data from a .plist file:

//Animation.mm
@implementation AnimationSequence

- (AnimationSequence*) initWithFrames:(NSDictionary*) animData
                       width:(float) width
                       height:(float) height
{
    [super init];
    NSArray* framesData = [[animData valueForKey:@"anim"]
                            componentsSeparatedByString:@","];
    NSArray* timeoutData = [[animData valueForKey:@"time"]
                             componentsSeparatedByString:@","];
                            //will be nil if "time" is not present.
    bool flip = [[animData valueForKey:@"flipHorizontal"] boolValue];
    self->next = [[animData valueForKey:@"next"] retain];
    frameCount = [framesData count];
    frames = new CGRect[frameCount];
    flipped = flip;
    for(int i=0;i<frameCount;i++){
        int frame = [[framesData objectAtIndex:i] intValue];
        int x = (frame * (int)width) % 1024;
        int row = (( frame * width ) - x) / 1024;
        int y = row * height;
        frames[i] = CGRectMake(x, y, width, height);
    }
    timeout = NULL;
    if(timeoutData){
        timeout = new float[frameCount];
        for(int i=0;i<frameCount;i++){
            timeout[i] = [[timeoutData objectAtIndex:i] floatValue] / 1000.0f;
            if(i > 0) timeout[i] += timeout[i-1];
        }
    }
    return self;
}

- (void) dealloc {
    delete frames;
    if(timeout) delete timeout;
    [self->next release];
    [super dealloc];
}

@end

We begin by grabbing the array of animation frames labeled “anim” from inside the animData dictionary. We do this by passing @"anim" as the key into the valueForKey: function of NSDictionary, which returns a string value. In the same line, we split that string into substrings by separating each section marked by a comma using the componentsSeparatedByString: method of NSString. We do the same thing on the next line, only we grab the “time” entry instead.

We also grab the “flipHorizontal” entry and convert it to a bool value from which we directly initialize our flipped member variable. After that, we get the “next” entry to determine what the next animation sequence should be: if the value is nil, we will simply stop animating; otherwise, we will continue into the next animation sequence automatically.

Next, we want to create a CGRect that represents the section of the GLTexture each frame represents. We know the width and height of the CGRect because they were passed into our function and each frame is the same size. All we need to do is calculate the x and y offsets based on the frame number of each animation.

We index the framesData array using the objectAtIndex: function and convert each element to an integer to get the frame index. Next, we calculate the x and y offsets based on the frame height and width. Remember, we need to wrap the frames if the width or height is larger than 1,024.

After we are done creating the frame rectangles, we initialize our timeout array. After indexing the timeoutData array and converting each element into an int (the same as we did earlier), we divide the result by 1,000. We mentioned earlier that the iPhone counts time by seconds, although it allows fractional values. Because the time periods in our .plist are in milliseconds, dividing by 1000.0f converts from integer milliseconds to floating-point seconds, which is what we want.

Now we can implement the Animation class. Recall that the Animation class is a resource that multiple Sprite classes will use; it should not store any data used to render a particular instance of an animation, only the list of AnimationSequences. The drawing function should have parameters that specify exactly which frame of which AnimationSequence is currently being requested so that multiple Sprites can render the same animation at different locations of the screen and of the animation.

Let’s start by looking at the initialization function:

//Animation.mm
- (Animation*) initWithAnim:(NSString*) img {
    NSData* pData;
    pData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle]
                pathForResource:@"Animations" ofType:@"plist"]];
    NSString *error;
    NSDictionary* animData;
    NSPropertyListFormat format;
    animData = [NSPropertyListSerialization propertyListFromData:pData
                mutabilityOption:NSPropertyListImmutable
                format:&format
                errorDescription:&error];

    animData = [animData objectForKey:img];

    GLTexture *tex = [g_ResManager getTexture:img];
    image = img;

    float frameWidth, frameHeight;

    if([animData objectForKey:@"frameCount"]){
        int frameCount = [[animData objectForKey:@"frameCount"] intValue];
        frameWidth = [tex width] / (float)frameCount;
        frameHeight = [tex height];
    }

    if([animData objectForKey:@"frameSize"]){
        NSArray* wh = [[animData objectForKey:@"frameSize"]
                        componentsSeparatedByString:@"x"];
        frameWidth = [[wh objectAtIndex:0] intValue];
        frameHeight = [[wh objectAtIndex:1] intValue];
    }

    //anchor is the position in the image that is considered the center. In
    //pixels. Relative to the bottom left corner. Will typically be positive.
    //all frames in all sequences share the same anchor.
    NSString* anchorData = [animData valueForKey:@"anchor"];
    if(anchorData){
        NSArray* tmp = [anchorData componentsSeparatedByString:@","];
        anchor.x = [[tmp objectAtIndex:0] floatValue];
        anchor.y = [[tmp objectAtIndex:1] floatValue];
    }

    NSEnumerator *enumerator = [animData keyEnumerator];
    NSString* key;
    sequences = [NSMutableDictionary dictionaryWithCapacity:1];

    while ((key = [enumerator nextObject])) {

        NSDictionary* sequencedata = [animData objectForKey:key];
        if (![sequencedata isKindOfClass:[NSDictionary class]]) continue;

        AnimationSequence* tmp = [[AnimationSequence alloc]
                              initWithFrames:sequencedata
                              width:frameWidth
                              height:frameHeight];

        [sequences setValue:tmp forKey:key];
        [tmp release];
    }

    [sequences retain];

    return self;
}

The initWithAnim: function accepts the name of an image as its only parameter. This will be used to grab the associated animation from within the Animations.plist file, as well as the GLTexture by the same name.

We start by using an NSData pointer to grab the contents of the Animations.plist file. Next, we convert it into an NSDictionary for easy access of the data inside using the propertyListFromData: method of the NSPropertyListSerialization utility class.

However, the NSDictionary we just got contains all of the animations in the .plist file, and we want only the animation that corresponds to the image name passed into our initialization function. We grab the subsection with [animData objectForKey:img], reassigning it to our NSDictionary pointer. We don’t need to worry about a memory leak while doing this because the original NSDictionary returned by propertyListFromData: is put into an auto-release pool before it’s handed to us.

Now that we have only the data that pertains to this animation, we will extract the Animation and AnimationSequence information from it. For the Animation class, we need the data that applies to all of the AnimationSequences, including the frame height and width and the anchor offset.

The rest of the entries into the dictionary comprise AnimationSequence data. However, we don’t know what their names are or how many there will be, so we need to use an NSEnumerator to iterate across all of the entries. We do this by calling keyEnumerator: on the animData dictionary.

We also need a place to store the AnimationSequences we create, so we initialize our sequences variable as an empty NSMutableDictionary. Since we are calling alloc on sequences here, we need to remember to release it in the dealloc function, as well as all of the entries inside it.

Next, we start the while loop to iterate through all of the keys in our animData dictionary. The entries are not in a particular order, so we need to be sure to skip any entries that we have already read (and therefore are not AnimationSequence dictionaries). We know that any entry that is an NSDictionary will represent an AnimationSequence, so we check the type of entry using isKindOfClass:. If the entry is not an NSDictionary, we skip it.

Finally, we know we have an AnimationSequence dictionary. We grab it from animData using objectForKey: and then send it into a newly allocated AnimationSequence object. Once the AnimationSequence is initialized, we can store it into our sequences dictionary. Since we called alloc on the AnimationSequence when we were creating it, we need to call release. Rest assured, however, that it was retained by NSDictionary when we inserted it into sequences.

And now the rendering function:

//Animation.mm
- (void) drawAtPoint:(CGPoint) point
         withSequence:(NSString*) sequence
         withFrame:(int) frame
{
    AnimationSequence* seq = [sequences valueForKey:sequence];
    CGRect currframe = seq->frames[frame];
    [[g_ResManager getTexture:image]
        drawInRect:CGRectMake(
                point.x+(seq->flipped?currframe.size.width:0)-anchor.x,
                point.y-anchor.y,
                seq->flipped?-currframe.size.width:currframe.size.width,
                currframe.size.height)
         withClip:currframe
        withRotation:0];
}

Compared to the initialization function, rendering is fairly simple. We first grab the AnimationSequence as detailed by the sequence parameter. We then extract the CGRect that represents the portion of a GLTexture that contains the frame we want from the AnimationSequence and store it as currFrame.

Next, we grab the GLTexture from the ResourceManager and call drawInRect:, creating the destination rectangle using the utility function CGRectMake and using currFrame as our source rectangle. Creating the destination rectangle is a little tricky because we have to consider whether the animation is flipped and whether there is an anchor offset. Fortunately, the math is simple, as you can see.

Sprite class

Now that we have an Animation, we can create a Sprite class that will allow us to use it. The Sprite class needs to keep track of what animation it is associated with, which AnimationSequence is currently being drawn, when that sequence started, and what the current frame is.

Because the Sprite class will be used directly in our game code, we are going to make an autorelease constructor instead of the typical init function. We will also need a rendering function that accepts a position to draw the sprite, an update function that will calculate when the current frame needs to change based on the timeout values from the current AnimationSequence, and finally, a way to set which sequence we want to display:

//Sprite.h
@interface Sprite : NSObject {
    Animation* anim;
    NSString* sequence;
    float sequence_time;
    int currentFrame;
}

@property (nonatomic, retain) Animation* anim;
@property (nonatomic, retain) NSString* sequence;

+ (Sprite*) spriteWithAnimation:(Animation*) anim;
- (void) drawAtPoint:(CGPoint) point;
- (void) update:(float) time;

@end

Notice the @property tag we are using for anim and sequence variables. It will cause a special setter function to be called whenever we assign anything to anim or to sequence. Since retain is one of the keywords, it will automatically be retained when we set these.

Although the default setter is acceptable for anim, we want to specify our own setter function for sequence because whenever the sequence changes we need to reset the sequence_time and currentFrame back to zero. We do this by overloading the setter function named setSequence in the following code.

The implementation of Sprite follows:

//Sprite.m
+ (Sprite*) spriteWithAnimation:(Animation*) anim {
    Sprite* retval = [[Sprite alloc] init];

    retval.anim = anim;
    retval.sequence = [anim firstSequence];

    [retval autorelease];
    return retval;
}

- (void) drawAtPoint:(CGPoint) point {
    [anim drawAtPoint:point withSequence:sequence withFrame:currentFrame];
}

- (void) update:(float) time{
    AnimationSequence* seq = [anim get:sequence];

    if(seq->timeout == NULL){
        currentFrame++;
        if(currentFrame >= [anim getFrameCount:sequence]) currentFrame = 0;
    } else {
        sequence_time += time;
        if(sequence_time > seq->timeout[seq->frameCount-1]){
            sequence_time -= seq->timeout[seq->frameCount-1];
        }
        for(int i=0;i<seq->frameCount;i++){
            if(sequence_time < seq->timeout[i]) {
                currentFrame = i;
                break;
            }
        }
    }
}

- (void) setSequence:(NSString*) seq {
    [seq retain];
    [self->sequence release];
    self->sequence = seq;
    currentFrame = 0;
    sequence_time = 0;
}

The constructor function needs to be statically accessible (you call it before an object exists, not on an object), so we use + instead of in the definition. It starts by allocating and initializing a Sprite object.

We then set the animation to the anim variable sent in the parameter. Remember that we declared this as a retain parameter, so it will automatically be retained when we set this, meaning we need to release it in our dealloc function.

We start the first AnimationSequence by using a utility function to ask the Animation which we should use first and then setting it to sequence. This is also a property, but it will be calling our custom setter, the setSequence: function.

Finally, we add the new Sprite to an autorelease pool and return it.

The rendering function is very simple: just pass the point to draw at, the current sequence, and the current frame into the Animation drawAtPoint: function we discussed earlier.

The update function needs to start by grabbing a pointer to the AnimationSequence we are currently running. Next, we need to check that it has a list of frame times; if it does not, we simply increment the frame every update loop. If we reach the end of the AnimationSequence, we start over from the first Animation.

If there is frame time information, we need to know whether the current frame should be incremented. The time parameter of the update function represents the amount of time that has elapsed since the last call to update, so we can simply add this to the sequence_time variable and check whether it is larger than the timeout value of the current frame. We calculate the current frame based on the total sequence_time elapsed.

The setSequence: function is our overloaded setter function, as we already mentioned. A normal setter function has two jobs: retain the new object being assigned to, and release the old one. Our custom setter will also initialize the currentFrame and sequence_time to zero so that the new sequence starts at the beginning.

Get iPhone Game Development now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.