Loading Cocos2D Sprite Frame Animations from Plist Files

John WordsworthiOS Development7 Comments

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. If not, you can read up on them through the provided links.

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.

Introduction to CCSpriteFrameCache.
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;

Image showing an example plist file for adding animations to CCAnimationCache automatically

Example plist file for adding animations to CCAnimationCache automatically (click to enlarge)

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’.

An early screenshot of Knight Terrors; please ignore the prototype icons at the bottom of the screen!

John WordsworthLoading Cocos2D Sprite Frame Animations from Plist Files

7 Comments on “Loading Cocos2D Sprite Frame Animations from Plist Files”

    1. John Wordsworth

      That’s fantastic – thanks for the comment.

      I’ve recently finished some code that fires CCActions when you ‘start’ specific CCAnimation frames (so you can have a character fire a bullet on ‘attack frame 7′ for instance). When I’ve tidied that up I’ll be sure to either share it back on google code or submit it as a possible extension. However, we have been busy with client work recently – which has put a pause button on the game development for a while (just like GameDevStory!).

  1. Kirian

    Hi, John!
    Thanx a lot for this extenstion!

    P.S. You probably wanted to log out a warning about null frame in line 31 in case then “frame == nil”.

    Good luck!

    1. John Wordsworth

      Hi Kirian, nice spot – many thanks. I’ve got a couple of other minor edits to make to this post very soon, so I’ll do these as a batch when I next sit down and go through my blog change list.

      Thanks for commenting and best of luck with your Cocos2D projects!

      1. SeruK

        I know this post is old, and that this extension is actually included in Cocos 1.1, but your CCLOG on line 31 doesn’t actually check if the frame is nil; it just complains no matter what*.

        * kind of like my mother-in-law! Hawhawhaw!
        Nah, just kidding she’s great.

  2. Jady

    Hi John Wordsworth, would you help tell me the tool’s name which generate the animation file? (Basic-knight-def.plist)
    I think it is very useful, and how can we get?
    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *