Now it is time to add some animation to our sprites. One thing that should be stressed about animation is that it is only as complicated as you make it. In this recipe we will use very simple animation to create a compelling effect. We will create a scene where bats fly around a creepy looking castle. I've also added a cool lightning effect based on the technique used to make the swords glow in the previous recipe.
Please refer to the project RecipeCollection01
for full working code of this recipe. Also note that some code has been omitted for brevity.
Execute the following code:
//SimpleAnimObject.h @interface SimpleAnimObject : CCSprite { int animationType; CGPoint velocity; } @interface Ch1_AnimatingSprites { NSMutableArray *bats; CCAnimation *batFlyUp; CCAnimation *batGlideDown; CCSprite *lightningBolt; CCSprite *lightningGlow; int lightningRemoveCount; } -(CCLayer*) runRecipe { //Add our PLIST to the SpriteFrameCache [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"simple_bat.plist"]; //Add a lightning bolt lightningBolt = [CCSprite spriteWithFile:@"lightning_bolt.png"]; [lightningBolt setPosition:ccp(240,160)]; [lightningBolt setOpacity:64]; [lightningBolt retain]; //Add a sprite to make it light up other areas. lightningGlow = [CCSprite spriteWithFile:@"lightning_glow.png"]; [lightningGlow setColor:ccc3(255,255,0)]; [lightningGlow setPosition:ccp(240,160)]; [lightningGlow setOpacity:100]; [lightningGlow setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }]; [lightningBolt addChild:lightningGlow]; //Set a counter for lightning duration randomization lightningRemoveCount = 0; //Bats Array Initialization bats = [[NSMutableArray alloc] init]; //Add bats using a batch node. CCSpriteBatchNode *batch1 = [CCSpriteBatchNode batchNodeWithFile:@"simple_bat.png" capacity:10]; [self addChild:batch1 z:2 tag:TAG_BATS]; //Make them start flying up. for(int x=0; x<10; x++){ //Create SimpleAnimObject of bat SimpleAnimObject *bat = [SimpleAnimObject spriteWithBatchNode:batch1 rect:CGRectMake(0,0,48,48)]; [batch1 addChild:bat]; [bat setPosition:ccp(arc4random()%400+40, arc4random()%150+150)]; //Make the bat fly up. Get the animation delay (flappingSpeed). float flappingSpeed = [self makeBatFlyUp:bat]; //Base y velocity on flappingSpeed. bat.velocity = ccp((arc4random()%1000)/500 + 0.2f, 0.1f/flappingSpeed); //Add a pointer to this bat object to the NSMutableArray [bats addObject:[NSValue valueWithPointer:bat]]; [bat retain]; //Set the bat's direction based on x velocity. if(bat.velocity.x > 0){ bat.flipX = YES; } } //Schedule physics updates [self schedule:@selector(step:)]; return self; } -(float)makeBatFlyUp:(SimpleAnimObject*)bat { CCSpriteFrameCache * cache = [CCSpriteFrameCache sharedSpriteFrameCache]; //Randomize animation speed. float delay = (float)(arc4random()%5+5)/80; CCAnimation *animation = [[CCAnimation alloc] initWithName:@"simply_bat_fly" delay:delay]; //Randomize animation frame order. int num = arc4random()%4+1; for(int i=1; i<=4; i+=1){ [animation addFrame:[cache spriteFrameByName:[NSString stringWithFormat:@"simple_bat_0%i.png",num]]]; num++; if(num > 4){ num = 1; } } //Stop any running animations and apply this one. [bat stopAllActions]; [bat runAction:[CCRepeatForever actionWithAction: [CCAnimate actionWithAnimation:animation]]]; //Keep track of which animation is running. bat.animationType = BAT_FLYING_UP; return delay; //We return how fast the bat is flapping. } -(void)makeBatGlideDown:(SimpleAnimObject*)bat { CCSpriteFrameCache * cache = [CCSpriteFrameCache sharedSpriteFrameCache]; //Apply a simple single frame gliding animation. CCAnimation *animation = [[CCAnimation alloc] initWithName:@"simple_bat_glide" delay:100.0f]; [animation addFrame:[cache spriteFrameByName:@"simple_bat_01.png"]]; //Stop any running animations and apply this one. [bat stopAllActions]; [bat runAction:[CCRepeatForever actionWithAction: [CCAnimate actionWithAnimation:animation]]]; //Keep track of which animation is running. bat.animationType = BAT_GLIDING_DOWN; } -(void)step:(ccTime)delta { CGSize s = [[CCDirector sharedDirector] winSize]; for(id key in bats){ //Get SimpleAnimObject out of NSArray of NSValue objects. SimpleAnimObject *bat = [key pointerValue]; //Make sure bats don't fly off the screen if(bat.position.x > s.width){ bat.velocity = ccp(-bat.velocity.x, bat.velocity.y); bat.flipX = NO; }else if(bat.position.x < 0){ bat.velocity = ccp(-bat.velocity.x, bat.velocity.y); bat.flipX = YES; }else if(bat.position.y > s.height){ bat.velocity = ccp(bat.velocity.x, -bat.velocity.y); [self makeBatGlideDown:bat]; }else if(bat.position.y < 0){ bat.velocity = ccp(bat.velocity.x, -bat.velocity.y); [self makeBatFlyUp:bat]; } //Randomly make them fly back up if(arc4random()%100 == 7){ if(bat.animationType == BAT_GLIDING_DOWN){ [self makeBatFlyUp:bat]; bat.velocity = ccp(bat.velocity.x, -bat.velocity.y); } else if(bat.animationType == BAT_FLYING_UP){ [self makeBatGlideDown:bat]; bat.velocity = ccp(bat.velocity.x, -bat.velocity.y); } } //Update bat position based on direction bat.position = ccp(bat.position.x + bat.velocity.x, bat.position.y + bat.velocity.y); } //Randomly make lightning strike if(arc4random()%70 == 7){ if(lightningRemoveCount < 0){ [self addChild:lightningBolt z:1 tag:TAG_LIGHTNING_BOLT]; lightningRemoveCount = arc4random()%5+5; } } //Count down lightningRemoveCount -= 1; //Clean up any old lightning bolts if(lightningRemoveCount == 0){ [self removeChildByTag:TAG_LIGHTNING_BOLT cleanup:NO]; } } @end
This recipe shows us how to structure animation based classes through the use of SimpleAnimObject
:
Animated object class structure:
When switching from one animation to another it is often important to keep track of what state the animated object is in. In our example we use
SimpleAnimObject
, which keeps an arbitraryanimationType
variable. We also maintain a velocity variable that has a Y scalar value that is inversely proportional to the animation frame delay:@interface SimpleAnimObject : CCSprite { int animationType; CGPoint velocity; }
Depending on how in-depth you want your animation system to be you should maintain more information such as, for example, a pointer to the running
CCAnimation
instance, frame information, and physical bodies.
As you get more involved with Cocos2d game development you will become more and more tempted to use asynchronous actions
for gameplay logic and AI. Derived from the CCAction
class, these actions can be used for everything from moving a CCNode
using CCMoveBy
to animating a CCSprite
using CCAnimate
. When an action is run, an asynchronous timing mechanism is maintained in the background. First time game programmers often over-rely on this feature. The extra overhead required by this technique can multiply quickly when multiple actions are being run. In the following example we have used a simple integer timer that allows us to
regulate how long lightning lasts onscreen:
//Randomly make lightning strike if(arc4random()%70 == 7){ if(lightningRemoveCount < 0){ [self addChild:lightningBolt z:1 tag:TAG_LIGHTNING_BOLT]; lightningRemoveCount = arc4random()%5+5; } } //Count down lightningRemoveCount -= 1; //Clean up any old lightning bolts if(lightningRemoveCount == 0){ [self removeChildByTag:TAG_LIGHTNING_BOLT cleanup:NO]; }
Synchronous timers like the one shown in the preceding code snippet are often, but not always, preferable to asynchronous actions. Keep this in mind as your games grow in size and scope.