In a game that a friend and I are working on (Knight Terrors) we wanted a system to pre-load animations into CCAnimationFrameCache without having to hardcode any of that configuration. This means that our designer and artist, Jackson Matthews, can add and remove frames from a creature’s animation without having to come back to me with a frame list to paste back into the code. I will assume you have knowledge of CCSpriteFrameCache and CCAnimations within cocos2d before embarking on this.
Important Note. This feature has been part of cocos2d since v1.1 and may have changed since. This is kept here for historical purposes only and to provide some understanding to why this feature was written.
If you are new to sprite animation with Cocos2D, you might want to read this tutorial: How to Animate Sprites in Cocos2D.
Background: We’re currently developing a game that makes heavy use of Cocos2D for the iPhone / iPod / iPad. I’m building up to writing a tutorial on how we implemented a system to allow actions to trigger when a given animation frame is reached for a given entity. However, as that’s not quite there yet - I wanted to share something a little bit simpler, but something that I consider to be very useful…
CCAnimationCacheExtensions: We developed a pretty simple extension for CCAnimationCache that will allow you to build CCAnimations from the information contained in a plist file that contains a list of animation frames and a delay value for a selection of animations. In our model, we created a plist file for each creature, and this plist file is modelled in a specific way, as depicted below;
As shown in image of the plist file above, we have a standard plist file with a Dictionary root node. Within that, we have a dictionary named ‘animations’ which we will use to store all of the animations that we want to load with our code. In this dictionary, we then hold another dictionary for each animation we want to load, with the key being the name of the animation that we want to load into the cache, and the value of that animation being a another dictionary. Each dictionary at this level represents a CCAnimation, containing a frames array that lists the name of each frame you want to be part of the animation and a delay number which represents the time delay between switching frames.
Important note: the frames you list in your animations plist file must already exist in the CCSpriteFrameCache before you load in this plist file.
Loading this plist file into the CCAnimationCache is achieved by calling [[CCAnimationCache sharedAnimationCache] addAnimationsWithFile:@“path.plist”]. We add this functionality to CCAnimationCache without editing any part of the cocos2d library by using an Objective-C Category, which allows us to add additional methods to a class defined elsewhere without editing the original files (keeping our changes upgrade safe).
The code we use to load these animations is as follows;
CCAnimationCacheExtensions.h
#import
#import "cocos2d.h"
@interface CCAnimationCache (ISExtensions)
-(void)addAnimationsWithDictionary:(NSDictionary *)dictionary;
-(void)addAnimationsWithFile:(NSString *)plist;
@end
CCAnimationCacheExtensions.m
#import "CCAnimationCacheExtensions.h"
@implementation CCAnimationCache (ISExtensions)
/** Add animations to the cache from an NSDictionary that contains an 'animations' element at it's root. */
-(void)addAnimationsWithDictionary:(NSDictionary *)dictionary
{
NSDictionary *animations = [dictionary objectForKey:@"animations"];
if ( animations == nil ) {
CCLOG(@"ISCCAnimationCacheExtensions: No animations found in provided dictionary.");
return;
}
NSArray* animationNames = [animations allKeys];
for( NSString *name in animationNames ) {
NSDictionary* animationDict = [animations objectForKey:name];
NSArray *frameNames = [animationDict objectForKey:@"frames"];
NSNumber *delay = [animationDict objectForKey:@"delay"];
CCAnimation* animation = nil;
if ( frameNames == nil ) {
CCLOG(@"ISCCAnimationCacheExtensions: Animation '%@' found in dictionary without any frames - cannot add to animation cache.", name);
continue;
}
NSMutableArray *frames = [NSMutableArray arrayWithCapacity:[frameNames count]];
for( NSString *frameName in frameNames ) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:frameName];
CCLOG(@"ISCCAnimationCacheExtensions: Animation '%@' refers to frame '%@' which is not currently in the CCSpriteFrameCache. This frame will not be added to the animation.", name, frameName);
if ( frame != nil ) {
[frames addObject:frame];
}
}
if ( [frames count] == 0 ) {
CCLOG(@"ISCCAnimationCacheExtensions: None of the frames for animation '%@' were found in the CCSpriteFrameCache. Animation is not being added to the AnimationCache.", name);
continue;
} else if ( [frames count] != [frameNames count] ) {
CCLOG(@"ISCCAnimationCacheExtensions: An animation in your dictionary refers to a frame which is not in the CCSpriteFrameCache. Some or all of the frames for the animation '%@' may be missing.", name);
}
if ( delay != nil ) {
animation = [CCAnimation animationWithFrames:frames delay:[delay floatValue]];
} else {
animation = [CCAnimation animationWithFrames:frames];
}
[[CCAnimationCache sharedAnimationCache] addAnimation:animation name:name];
}
}
/** Read an NSDictionary from a plist file and parse it automatically for animations. */
-(void)addAnimationsWithFile:(NSString *)plist
{
NSString *directory = [plist stringByDeletingLastPathComponent];
NSString *file = [plist lastPathComponent];
NSString *path = [[NSBundle mainBundle] pathForResource:file ofType:nil inDirectory:directory];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
if ( dict == nil ) {
CCLOG(@"ISCCAnimationCacheExtensions: Couldn't load animations from plist file.");
} else {
[self addAnimationsWithDictionary:dict];
}
}
@end
The *addAnimationsWithFile:(NSString )plist method simply loads a plist file into an NSDictionary and then passes it to the other method, which does the grunt work. The method *addAnimationsWithDictionary:(NSDictionary )dictionary does the work of iterating through the relevant elements of the NSDictionary that was read from the plist file and creating a CCAnimation object for each inner dictionary that contains valid ‘frames’ (and optionally, a delay).
Playing an animation from the cache is then a case of creating a CCAnimate action and running it on the sprite you want to animate, like so;
CCAnimation *animation = [[CCAnimationCache sharedAnimationCache] animationByName:@"knight-walk-left01.png"];
if ( animation != nil ) {
CCAction *action = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:animation restoreOriginalFrame:NO]];
[self.sprite runAction:action];
}
I hope you find this useful. If you have any questions about how it works, please don’t hesitate to get in touch below.
If you’re interested, here’s a shot of the code in action, in our upcoming iOS game ‘Knight Terrors’.