Search This Blog

Friday, November 19, 2010

Cocos2D iphone tutorial: Die, Grossini, Die! – Part II

(Click here Part I)


Previously, we described the fundamentals of a Cocos2D game (or in this case, 'show') and explained how everything is set up. Let's describe the game, now.

If you came here directly, Part I is here.

The image on the left is the splash screen that I use for the game. I replaced the default Default.png cocos2d image with this one. You will notice Grossini has a nice 3D effect here. That's just a simple Bevel effect on a layer. You can do this in GIMP, Photoshop or many other editors. The source for this classy looking Grossini image (well, just the regular image with a stick drawn on the right is here)


GameScreen


When you click on Play in the MainMenu, it asks CCDirector to load the "GameScreen". This is it.

GameScreen.h


[objc]
///
// GameScreen.h
// Fighter
//
// Created by Arjun on 11/18/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface GameScreen : CCLayer {
CCSprite *game_background;
}

+(id) scene;
@end

[/objc]

Nothing much here. Auto generated, for the most part. The only thing I added here is the game_background CCSprite. The reason I did this is I will be scrolling the background smoothly as Grossini jumps around. Putting it here makes it simple to access it from the jumpSprite function that is called once in 60 seconds.

GameScreen.m


First the full code. Click below to expand

[objc collapse="true"]
//
// GameScreen.m
// Fighter
//
// Created by Arjun on 11/18/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import "GameScreen.h"
#import "MainMenu.h"
#import "CCParticleMyBlood.h"
#import "SimpleAudioEngine.h"

CCLabel *pause_label;
CCMenuItem *pause_continue_menu;
CCMenuItem *pause_exit_menu;
CCSpriteSheet *danceSheet;
CCSprite *danceSprite;
CCParticleMyBlood *myparticle;
float xv,yv;
CCSprite *k[5];

@implementation GameScreen

+(id) scene {
// ‘scene’ is an autorelease object.
CCScene *scene = [CCScene node];
// ‘layer’ is an autorelease object.
CCLayer *layer = [GameScreen node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}
-(id) init {
if( (self=[super init] ))
{

CCMenuItem *pause_menu = [CCMenuItemImage itemFromNormalImage:@"pause.png" selectedImage:@"pause.png" target:self selector:@selector(pauseGame:)];
CCMenu *menu = [CCMenu menuWithItems: pause_menu, nil];
menu.position = ccp(460, 15);
[self addChild:menu z:100];

game_background = [CCSprite spriteWithFile:@"mountains large.png"];
game_background.anchorPoint = ccp(0, 0);
game_background.position = ccp(0, 0);
[self addChild:game_background z:0];

// create the sprite sheet
CCSpriteSheet *danceSheet = [CCSpriteSheet spriteSheetWithFile:@"grossini_dance_atlas.png"];
[self addChild:danceSheet z:10];

// create the sprite
danceSprite = [CCSprite spriteWithTexture:danceSheet.texture rect:CGRectMake(0, 0, 85, 121)];
danceSprite.anchorPoint=ccp(0,0);
[danceSheet addChild:danceSprite];

danceSprite.position = ccp(40,20);
xv = 1;
yv=6;
myparticle=nil;
// create the animation
CCAnimation *danceAnimation = [CCAnimation animationWithName:@"dance" delay:0.2f];

int frameCount = 0;
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 5; x++) {
CCSpriteFrame *frame = [CCSpriteFrame frameWithTexture:danceSheet.texture rect:CGRectMake(x*85,y*121,85,121) offset:ccp(0,0)];
[danceAnimation addFrame:frame];

frameCount++;

if (frameCount == 14)
break;
}
}

// create the action
CCAnimate *danceAction = [CCAnimate actionWithAnimation:danceAnimation];
CCRepeatForever *repeat = [CCRepeatForever actionWithAction:danceAction];

// knives

for (int i=0; i<5; i++)
{
k[i]=[CCSprite spriteWithFile:@"knife.png"];
k[i].anchorPoint=ccp(0,0);
k[i].position = ccp (80 + 80*i, 280);
[self addChild:k[i] z:0];
}

// run the action
[danceSprite runAction:repeat];
[self schedule:@selector(jumpSprite) interval:1/60];

}
return self;
}

-(void) jumpSprite
{
static int waitForHitToClear=0;
static int timer=0;
static float bk_f=0.0f;

bk_f -=0.5;
if (bk_f <= -480*2) {bk_f=0;}
//NSLog(@"***********@BK_F:%f",bk_f);
game_background.position = ccp(bk_f, 0);

int sx = [danceSprite boundingBox].size.width;
int sy = [danceSprite boundingBox].size.height;

// Now Check if the man hit a dagger
for (int i=0; i<5; i++)
{
CGRect headFrame = CGRectMake(danceSprite.boundingBox.origin.x+sx/2-10,danceSprite.boundingBox.origin.y+sy-30,
20,10);

//CCLOG (@"KNIFE%@", NSStringFromRect(k[i].boundingBox));
//CCLOG (@"HEAD%@", NSStringFromRect(headFrame));

if (CGRectIntersectsRect(k[i].boundingBox, headFrame) && (!waitForHitToClear) && (yv>0))
{

//NSLog (@"HIT A KNIFE %d",i+1);

yv=-yv;
int sx = [danceSprite boundingBox].size.width;
int sy = [danceSprite boundingBox].size.height;

id spin_action =[CCRotateBy actionWithDuration:0.5 angle:360];

float delta_y = arc4random()%50;
delta_y = -delta_y;
//NSLog(@"Delta Y %f",delta_y);
//NSLog (@"NEW POSITION:%f", k[i].position.y+ delta_y);
if (k[i].position.y < 220) {delta_y = -delta_y;}
id move_action =[CCMoveBy actionWithDuration:1.5 position:ccp(0,delta_y)];

[k[i] runAction:[CCSequence actions:spin_action, move_action, nil]];

[[SimpleAudioEngine sharedEngine] playEffect:@"die.wav"];
if (myparticle !=nil) { [self removeChild:myparticle cleanup:YES]; }
myparticle = [[CCParticleMyBlood alloc]init];
myparticle.texture = [[CCTextureCache sharedTextureCache] addImage:@"blood.png"];
myparticle.position = ccp(danceSprite.position.x+sx/2,danceSprite.position.y+sy/2);
[self addChild:myparticle z:9];
myparticle.autoRemoveOnFinish = YES;

waitForHitToClear=1;

} //if

}// for

if (waitForHitToClear)
{
timer++;
if (timer==30)
{
timer=0; waitForHitToClear=0;
}
} //if waitForHitToClear

if ((danceSprite.position.y >= 200) || (danceSprite.position.y<20))
{

yv=-yv;
}
if ((danceSprite.position.x <0) || (danceSprite.position.x > 480)) {xv=-xv;}
danceSprite.position=ccp(danceSprite.position.x+xv, danceSprite.position.y+yv);

}

-(void) pauseGame: (id)sender
{

[[CCDirector sharedDirector] pause];
pause_label = [CCLabel labelWithString:@"Game Paused" fontName:@"Marker Felt" fontSize:64];

// ask director the the window size
CGSize size = [[CCDirector sharedDirector] winSize];

// position the label on the center of the screen
pause_label.position = ccp( size.width /2 , size.height/2 + 100);

// add the label as a child to this Layer
[self addChild: pause_label];

CCMenuItem *continue_button = [CCMenuItemImage itemFromNormalImage:@"continue.png" selectedImage:@"continue button pressed.png"
target:self selector:@selector(GameContinue:)];

CCMenuItem *exit_button = [CCMenuItemImage itemFromNormalImage:@"exit.png" selectedImage:@"exit button pressed.png"
target:self selector:@selector(GameMain:)];

pause_continue_menu = [CCMenu menuWithItems: continue_button,nil];
pause_exit_menu = [CCMenu menuWithItems: exit_button,nil];

pause_continue_menu.position = ccp(size.width/2-80, size.height/2);
pause_exit_menu.position = ccp(size.width/2+80, size.height/2);
//[menu alignItemsVerticallyWithPadding:12.5f];

[self addChild:pause_continue_menu z:100];
[self addChild:pause_exit_menu z:100];

}

-(void) GameContinue: (id) sender {

[self removeChild: pause_label cleanup:YES];
[self removeChild: pause_continue_menu cleanup:YES];
[self removeChild: pause_exit_menu cleanup:YES];

[[CCDirector sharedDirector] resume];
}

-(void) GameMain: (id) sender {

[[CCDirector sharedDirector] resume];
// [danceSprite stopAllActions];
// [danceSheet removeChild:danceSprite cleanup:YES];
// [self removeChild:danceSheet cleanup:YES];

[[CCDirector sharedDirector] replaceScene:[CCZoomFlipXTransition
transitionWithDuration:1 scene:[MainMenu node]]];

}
@end

[/objc]

Now lets go through it in parts

(mumble)local Globals


[objc]
#import "GameScreen.h"
#import "MainMenu.h"
#import "CCParticleMyBlood.h"
#import "SimpleAudioEngine.h"

CCLabel *pause_label;
CCMenuItem *pause_continue_menu;
CCMenuItem *pause_exit_menu;
CCSpriteSheet *danceSheet;
CCSprite *danceSprite;
CCParticleMyBlood *myparticle;
float xv,yv;
CCSprite *k[5];
[/objc]

Some globals I will use later. I could have avoided globals, but globals are the sins programmers love to use, but hate to talk about. Sorta like goto. (Auuugh?)
So anyway.


Well, these are local globals, but I don't need them. they could be class members. I don't really know why I put them here. Anyway.

Of interest, note "CCSpriteSheet" and "CCSprite". Basically, cocos2d gives me a convenient way to load a series of images into a CCSpriteSheet. Grossini's dance is actually a set of 14 images. All those 14 images are packed in a single PNG file equally spaced from each other. We load that sheet into CCSpriteSheet and then there is an easy way to cull out an animation sequence from them. More on that later. Finally, the verbosely labelled array of "k" is basically 5 knives that will each hold a sprite image of a knife. Rest is self-explanatory.

Init code


Now let's explore GameScreen's init function.

[objc]

CCMenuItem *pause_menu = [CCMenuItemImage itemFromNormalImage:@"pause.png" selectedImage:@"pause.png" target:self selector:@selector(pauseGame:)];
CCMenu *menu = [CCMenu menuWithItems: pause_menu, nil];
menu.position = ccp(460, 15);
[self addChild:menu z:100];
[/objc]

Okay, I want a pause option. To do that, I have a small pause icon on the lower right corner of the screen. When you press that, it pauses the game. That's what this does. It creates a CCMenuItem from the image, adds it to the CCMenu object and displays it on the screen. Remember, cocos2d co-ords have (0,0) at the lower left, so (450,15) means to the right and almost bottom.

Adding a background image


[objc]
game_background = [CCSprite spriteWithFile:@"mountains large.png"];
game_background.anchorPoint = ccp(0, 0);
game_background.position = ccp(0, 0);
[self addChild:game_background z:0];

[/objc]

we add a nice background image to this scene. I've explained this earlier when we described how to add a background to the main menu. Note the filename "mountains large.png". This is a PNG that is thrice the size of the iphone screen. As the game timer ticks, I am going to move the image to the left to expose more of it.

The concept of a continuous Background Scroller


You will see the scrolling code later, but first, I took a picture of mountains from openclipart.org and cropped it to 480x320. Lets call it BKGRND. Then, in a photo editor, I increased the canvas size to triple its size in width (ie
480*3x320). Next to BKGRND, I added another BKGRND image and flipped it horizontally. Next to it, I added back the original image.

So the entire image sized 480*3x320 comprised of:

BKGRND+fliphorizontal(BKGRND)+BKGRND

I saved this as mountains large.png. When I first display it, cocos2d only displays what fits (it does not auto-fit the entire image). Now, all I am going to do is change the "game_background.position" x value to make it scroll, and when it reaches the end, I will reset it to zero. The concept of concatenating the images like above is so that the transition from the end to the start of the image looks smooth.

(Updated on Nov 22: I have since discovered, background scrolling can be handled more elegantly with a simple runWithAction MoveBy  instead of manually changing position each time. Infact, I also implemented Parallax scrolling along with it - the test Parallax Demo that comes with cocos2d is a great reference)

Creating Grossini


[objc]
// create the sprite sheet
CCSpriteSheet *danceSheet = [CCSpriteSheet spriteSheetWithFile:@"grossini_dance_atlas.png"];
[self addChild:danceSheet z:10];

// create the sprite
danceSprite = [CCSprite spriteWithTexture:danceSheet.texture rect:CGRectMake(0, 0, 85, 121)];
danceSprite.anchorPoint=ccp(0,0);
[danceSheet addChild:danceSprite];

danceSprite.position = ccp(40,20);
xv = 1;
yv=6;
[/objc]

Okay, you will find this code in many places on the net. This part's credit goes to GetGames.com. Let me explain it -its very simple:

  • First, I load the entire sprite sheet. If you want to see the sheet, it looks like this



  • Next, I instantiate a danceSprite object and point it to the first character in the sheet. Each sprite within the sheet is an 85x121 block, so I basically cut out the first 85x121 block from the sheet and assign it to danceSprite. I then add danceSprite to the Scene. So you get Grossini on the screen with his head turned around.

  • I also set up 2 variables, xv and yv - they will contain values to move Grossini's position as he dances. More on it later.

[objc]
myparticle=nil;
// create the animation
CCAnimation *danceAnimation = [CCAnimation animationWithName:@"dance" delay:0.1f];

int frameCount = 0;
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 5; x++) {
CCSpriteFrame *frame = [CCSpriteFrame frameWithTexture:danceSheet.texture rect:CGRectMake(x*85,y*121,85,121) offset:ccp(0,0)];
[danceAnimation addFrame:frame];

frameCount++;

if (frameCount == 14)
break;
}
[/objc]

Next, I create a CCAnimation object. I then tell it that when it starts animating the entire animation needs to be complete in 0.1 seconds. To make CCAnimation do its work, we need to add frames to it to iterate through. Where are our frames? In the PNG that we showed above in a 3x5 row which was loaded in danceSheet. So next up, all we need to do is set up a loop that extracts each image in that 2x2 image sheet and feed it as a new frame to CCAnimation. That's what the above code does. since the sprites are all 85x121, its easy to set a double for loop to point to the right image within it. Finally, the last row has one image less, so we break out of the loop when we reach image count 14. If we did not, we would see a blank image in the animation which would cause flicker.

Kapisch?

Dance, Grossini, Dance!


[objc]

// create the action
CCAnimate *danceAction = [CCAnimate actionWithAnimation:danceAnimation];
CCRepeatForever *repeat = [CCRepeatForever actionWithAction:danceAction];
// run the action
[danceSprite runAction:repeat];
[self schedule:@selector(jumpSprite) interval:1/60];

[/objc]

Finally, we use the convienent CCAnimate and CCRepeatForever constructs to tell cocos2d to animate the dance frames forever. Then, we use runAction to start the process of Gossini's never ending dance.

But now you will find Grossini stand at one place and dance. Cool and all, but lets also make him jump across the screen as he dances. That's up next - and that's what the last line did - it set up a timer to call jumpSprite 1/60 times a second.
In many tutorials, you will find people directly using NSTimer. DON'T. Use cocos2d schedule. This is because if you use NSTimer, CCDirector will have no idea about it. So if you paused the game, for example, using CCDirector pause, the timer would still be active and you will manually have to stop it. Why not let cocos2d do the work for you?

Making the Background actually Scroll


[objc]
static int waitForHitToClear=0;
static int timer=0;
static float bk_f=0.0f;

bk_f -=0.5;
if (bk_f <= -480*2) {bk_f=0;}
//NSLog(@"***********@BK_F:%f",bk_f);
game_background.position = ccp(bk_f, 0);

int sx = [danceSprite boundingBox].size.width;
int sy = [danceSprite boundingBox].size.height;

[/objc]

Take a look at the static float bk_f. Here is what I do, each time the game timer calls this function, I change the game background's X position to this value. In effect, I am moving the image left by 0.5 points each time. So what this means now is that in each iteration, you see a little more of the larger background image on the right. Then, I check if its reached its end. If it has, I reset this offset to 0. There goes, continuous scrolling:-)

Now I understand there are more efficient ways to do it. I've read about using CCamera as well as tiled maps - but for my simple needs this was the easiest. I will try the other methods later.
Checking if Grossini hit a knife

[objc]
for (int i=0; i<5; i++)
{
CGRect headFrame = CGRectMake(danceSprite.boundingBox.origin.x+sx/2-10,danceSprite.boundingBox.origin.y+sy-30,
20,10);
[/objc]

Okay, cocos2d has a very advanced collision engine using a physics engine called Box2d (There is also chipmunk). I've not used either in this project. I am going to fall back to the same collision logic I used for Bricks. See if frames intersect. So let's look at the line above. As it turns out, cocos2d Sprites have a convenient property called "boundingBox" that gives the frame around the sprite. But in my case, I want to see if Grossini's HEAD hits the knife. Easiest way to do this is compute the top center of the sprite (which is danceSprite.boundingBox.origin.x+sx/2,danceSprite.boundingBox.origin.y+sy) and make a rectangle of 20 pixels around it as my collision frame check - that's approximately his head (I did not measure precisely, just guessed).

Now that we have computed the approximate rectangle that encapsulates Grossini's head, we can use CGRectIntersectsRect with the knives to see if he hit 'em. Also, we want Grossini to hit it on the way up, not on the way down (if he is headed down, he really is hitting the butt of the knife, so let's spare him some pain...)

[objc]
if (CGRectIntersectsRect(k[i].boundingBox, headFrame) && (!waitForHitToClear) && (yv>0))
{

NSLog (@"HIT A KNIFE %d",i+1);

yv=-yv;
int sx = [danceSprite boundingBox].size.width;
int sy = [danceSprite boundingBox].size.height;

id spin_action =[CCRotateBy actionWithDuration:0.5 angle:360];

float delta_y = arc4random()%50;
delta_y = -delta_y;
NSLog(@"Delta Y %f",delta_y);
NSLog (@"NEW POSITION:%f", k[i].position.y+ delta_y);
if (k[i].position.y < 220) {delta_y = -delta_y;}
id move_action =[CCMoveBy actionWithDuration:1.5 position:ccp(0,delta_y)];

[k[i] runAction:[CCSequence actions:spin_action, move_action, nil]];

[[SimpleAudioEngine sharedEngine] playEffect:@"die.wav"];
if (myparticle !=nil) { [self removeChild:myparticle cleanup:YES]; }
myparticle = [[CCParticleMyBlood alloc]init];
myparticle.texture = [[CCTextureCache sharedTextureCache] addImage:@"blood.png"];
myparticle.position = ccp(danceSprite.position.x+sx/2,danceSprite.position.y+sy/2);
[self addChild:myparticle z:9];
myparticle.autoRemoveOnFinish = YES;

waitForHitToClear=1;

} //if
}//for
[/objc]

So, that's what we do here. If the head hit any of the 5 knives, we do the following:
a) Make the knife spin with a cutesy animation (I use CCRotateBy and turn it a full 360)
b) Then, I randomly move the knife down by a value (or up, if it reaches to a point)

You will notice that I use a function called CCSequence with runAction. CCSequence is an easy way to define multiple actions and execute then sequentially. In my case, I defined that the knife will first spin and then it will move to a random Y co-ordinate.

And there is the great part. In cocos2d, while a sprite is animating, you can perform collision checks! This is unlike the UIKit CGTransform function and really really useful. So when you play the game, you will find Grossini hitting a knife while its moving and still spilling blood

Finally, if Grossini hit a knife, I reverse the Y direction, and play my custom particle explosion that I explained in part I. the "autoRemoveFinish" is an easy way to dealloc the particle when animation is complete.

[objc]

if (waitForHitToClear)
{
timer++;
if (timer==30)
{
timer=0; waitForHitToClear=0;
}
} //if waitForHitToClear

if ((danceSprite.position.y >= 200) || (danceSprite.position.y<20))
{

yv=-yv;
}
if ((danceSprite.position.x <0) || (danceSprite.position.x > 480)) {xv=-xv;}
danceSprite.position=ccp(danceSprite.position.x+xv, danceSprite.position.y+yv);

}
[/objc]

The last part of jumpSprite basically checks if we just made a hit. If we made a hit, we wait for around half a second (JumpSprite is called 60 times a second, and we increment timer by 1 in each call after a hit - its a static var. So if it reaches 30, half a second has expired). We do this to avoid 'wrong collisions' - as the sprite moves away after a hit, it may still be intersecting with the knife but its not a hit again. So either you compute your movement increments precisely, or just put in a logical timer that avoids such issues.

Also, I check if Grossini hits the screen borders, if so, we reverse path as well.

Implementing the Pause


All games need a pause layer. Remember, in GameScreen we added a pause button. Now let's see what happens when you click it. It called a function called pauseGame. This is it.

An easy way to implement pause is to create a "new scene" and push it on top of the Game Scene. But this also replaces the full game background. I don't want that. I want pause to show up in the Game Screen itself as a transparent layer with some buttons. As far as I know, doing it this way is more work, but the effect is much better.

[objc]
[[CCDirector sharedDirector] pause];
pause_label = [CCLabel labelWithString:@"Game Paused" fontName:@"Marker Felt" fontSize:64];

// ask director the the window size
CGSize size = [[CCDirector sharedDirector] winSize];

// position the label on the center of the screen
pause_label.position = ccp( size.width /2 , size.height/2 + 100);

// add the label as a child to this Layer
[self addChild: pause_label];

CCMenuItem *continue_button = [CCMenuItemImage itemFromNormalImage:@"continue.png" selectedImage:@"continue button pressed.png"
target:self selector:@selector(GameContinue:)];

CCMenuItem *exit_button = [CCMenuItemImage itemFromNormalImage:@"exit.png" selectedImage:@"exit button pressed.png"
target:self selector:@selector(GameMain:)];

pause_continue_menu = [CCMenu menuWithItems: continue_button,nil];
pause_exit_menu = [CCMenu menuWithItems: exit_button,nil];

pause_continue_menu.position = ccp(size.width/2-80, size.height/2);
pause_exit_menu.position = ccp(size.width/2+80, size.height/2);
//[menu alignItemsVerticallyWithPadding:12.5f];

[self addChild:pause_continue_menu z:100];
[self addChild:pause_exit_menu z:100];

}
[/objc]

Notice [[CCDirector sharedDirector] pause]. That's all it takes to pause a current scene. Everything freezes. How cool.

Rest is easy to understand since I've explained it before. The only change is CCMenuItemImage. Remember, the menu in the Main Menu scene was text. Here, I created a continue and exit button and am using those instead of text. I have different images for when you press a button and when you release it. If you press "Continue", the pause layer disappears and the game continues. If you press exit, the game finishes.

Game Continue


[objc]

-(void) GameContinue: (id) sender {

[self removeChild: pause_label cleanup:YES];
[self removeChild: pause_continue_menu cleanup:YES];
[self removeChild: pause_exit_menu cleanup:YES];

[[CCDirector sharedDirector] resume];
}
[/objc]

If you choose to continue, I just remove all the litter pause brings on the screen and invoke resume of CCDirector. The game is back on like it never knew it was stopped.

Game Exit


Just remember here that you cannot call replaceScene unless you have a running scene. And here, we have paused it. Based on my tests, I had to first resume the scene and then replace it. If you don't resume first, it crashes.



[objc]</span>
<pre>-(void) GameMain: (id) sender {

[[CCDirector sharedDirector] resume];
[[CCDirector sharedDirector] replaceScene:[CCZoomFlipXTransition
transitionWithDuration:1 scene:[MainMenu node]]];
}
@end
[/objc]

First I resume CCDirector. If I did not, replaceScreen does not work.

And that's all Folks


Hope you enjoyed it.

Source Code


Grab it from here.

Bye.

3 comments: