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.
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.
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.
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:
From within Xcode, select File→New File to open the New File dialog.
From the left column select Other and in the right window select Property List.
Type
Animations.plist
for the name and click the Finish button.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
:
Right-click the Root row and select Add Row.
Replace the “New Item” text with “emuchick.png” in the Key column.
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:
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 theemuchick.png
row is in the Key column.Name the new row “frameCount” and set its type to Number.
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:
Add another row to the
emuchick.png
dictionary by right-clicking and selecting Add Row again.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:
Add two rows to the
walkLeft
dictionary and name them “anim” and “time”. Leave their types as String.In “anim”, set the value to “0,1,2,3,4”, with no spaces.
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:
Right-click on “walkLeft” and select Copy.
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 originalwalkLeft
dictionary.Rename the first new copy to “walkup” and change the “anim” value to “5,6,7,8,9”.
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:
Copy and paste the
walkLeft
dictionary ontoemuchick.png
once more.This time, add a row to
walkRight
, named “flipHorizontal”.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:
Add one more row to the
emuchick.png
dictionary.Name this one “anchor” and keep its type as String.
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.
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
AnimationSequence
s. 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 AnimationSequence
s. The drawing function
should have parameters that specify exactly which frame of which
AnimationSequence
is currently
being requested so that multiple Sprite
s 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 AnimationSequence
s, 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 AnimationSequence
s 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.
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.