Book Image

Cocos2d Game Development Blueprints

By : Jorge Jordán
Book Image

Cocos2d Game Development Blueprints

By: Jorge Jordán

Overview of this book

Table of Contents (15 chapters)
Cocos2d Game Development Blueprints
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Your first game – RunYetiRun


The purpose of this game is the following: a monster avalanche has begun while our yeti friend was trying to scare some trekkers (not to be confused with Star Trek fans) and he has to escape on a sledge from the dangerous snowballs rolling down the mountain.

Maybe you won't believe me but the instructions we have seen during the previous section are almost enough on their own to develop this game. We will need only one scene, where we will place a mountain background image, the snowballs, and the yeti. The snowballs' movement will be controlled by actions, the yeti will move thanks to touches on the screen, and we will place a score label at the top to track how many snowballs we have avoided successfully. Only two new things will be necessary to develop this game: managing collisions and playing sounds.

Creating the CCScene class

A CCScene (http://www.cocos2d-swift.org/docs/api/Classes/CCScene.html) is a class that inherits from CCNode and whose main purpose is to contain the behavior and the nodes of a single scene of the game. Commonly, you will split your games into different scenes, so, for instance, you will have a scene for the main menu, a scene for the game itself, and another scene for the pause menu. As our first game won't have a main screen or pause menu, all the logic will be in GameScene.

We don't need the scenes included in our project by default, so feel free to delete them. We create our scene as a new class, so in Xcode, be sure the group Classes is selected in the project navigator before selecting File | New | File… and you will see the dialog shown in the following screenshot:

Cocos2d provides a template to create new classes inherited from CCNode, but as we have discussed in the previous section, we will derive our scene from CCScene:

  1. Click on Next and replace the default CCNode with CCScene as the parent class.

  2. Click on Next again, call it GameScene, and be sure that the RunYetiRun target is selected before clicking on Create.

At this point, our newly created GameScene is empty and will do nothing, but we just need to add the +(GameScene *) scene method and implement –(id) init to give it the characteristics of a common scene.

Go ahead and replace GameScene.h with the following code:

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

@interface GameScene : CCScene {

}

+(GameScene *) scene;

@end

Do the same with GameScene.m with the following lines:

#import "GameScene.h"

@implementation GameScene

+(GameScene *) scene {
    return [[self alloc] init];
}

-(id) init {
    self = [super init];
    if (!self) {
        return(nil);
    }
    return self;
}

@end

The last thing we need to do is to update AppDelegate to start GameScene in spite of IntroScene. To achieve it, replace the contents of AppDelegate.m:

#import "AppDelegate.h"
#import "GameScene.h"

@implementation AppDelegate

-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [self setupCocos2dWithOptions:@{
    CCSetupShowDebugStats: @(YES),
  }];

  return YES;
}

-(CCScene *)startScene
{
  // The initial scene will be GameScene
  return [GameScene scene];
}

@end

Now that HelloWorldScene and IntroScene are no longer needed, you can delete them if you haven't already done so. If you run the project now, you will only see the debug stats on a black screen, but don't worry, we are going to fix this with a few lines, so just keep reading!

Adding the first CCSprite class

It would be true to say that the CCSprite (http://www.cocos2d-swift.org/docs/api/Classes/CCSprite.html) class is one of the most commonly used when developing games, as this is what games are: a bunch of sprites moving and interacting on a screen.

Basically, a CCSprite class is a class derived from CCNode and its purpose is to represent and manage sprites, which are objects made up of a 2D image or a subrectangle of an image. To create a sprite, you first need to add the image you want to use to the Resources group, so complete the following steps:

  1. Unzip the code files of this chapter from the code bundle and go back to Xcode.

  2. Right-click on the Resources group and select Add Files to "RunYetiRun"….

  3. It will open a dialog where you will select the yeti.png image from the RunYetiRun folder.

  4. Be sure that Copy items into destination group's folder (if needed) is selected and click on Add.

Note

Downloading the example code

You can download the example code files from your account at http://www.packtpub.com for all the Packt Publishing books you have purchased. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

The next step is to declare the private sprite variable we will use to manage our yeti friend, so in GameScene.m replace @implementation GameScene with the following lines of code:

@implementation GameScene
{
    // Declaring a private CCSprite instance
    CCSprite *_yeti;
}

Add the following lines before return self; in the init method:

// Creating the yeti sprite using an image file and adding it to the scene
_yeti = [CCSprite spriteWithImageNamed:@"yeti.png"];
[self addChild:_yeti];

Note

When specifying filenames, you need to take care, as they are case-sensitive. In addition, the desired image format is .png as it's more efficient than .jpg and .jpeg.

The addChild method adds a new child to the specified container (GameScene in this case) and this is how we add new nodes to the scene.

Ok, now it is time to run the game and see what we've just done:

The sprite has been placed in the bottom-left of the screen that corresponds to the coordinate (0,0), and due to the fact that the sprite texture is centered on the sprite's position, we can only see the top-right part of the yeti:

Anchor points

This center point of a sprite is known as the anchor point, an attribute used by nodes to perform all the transformations and position changes around it. By default, the anchor point in a CCNode class (don't forget our GameScene derives from it) is placed in the bottom-left corner, that is, (0,0); but with a CCSprite, the anchor point is placed at the very center of their texture (0.5, 0.5), which is why the majority of the yeti is not displayed on the screen.

For our game, we want our yeti to be placed at the same distance from the top and the bottom of the screen and near the right side, so add the following lines of code just before the return self; instruction:

    // Positioning the yeti centered
    CGSize screenSize = [CCDirector sharedDirector].viewSize;
    _yeti.position = CGPointMake(screenSize.width * 3 / 4, screenSize.height / 2);

At this point, I would like to highlight two important things. For those who have been working with Cocos2d v2, you should note that the former [CCDirector sharedDirector].winSize has been deprecated and we should use [CCDirector sharedDirector].viewSize instead.

This method returns the size of the view in points, so we will use this line to retrieve the width and height of the device screen. This way we can place nodes using relative positions in the future.

Note

When developing games for all devices (iPhone and iPad) it is recommended to work with relative positions. This way the location of nodes in the viewing window will be the same across all devices.

The CCDirector class is a singleton class: a globally accessible class that can only be instantiated once at any time that handles the main screen, and is responsible for executing the different scenes. Due to its nature, to access it you have to call the static method sharedDirector as we just saw in the previous code lines.

We place the yeti sprite with CGPointMake (coord_x, coord_y).

Note that we are indicating the sprite position after adding it to the scene, which means that we can modify it whenever we want; it's not necessary to do it before [self addChild:_yeti]. Run the game again and you will see the yeti in the correct position:

Ok, I must admit that it looks more like a space yeti than a mountain one, but it's just a matter of background.

Placing things in context

Yetis can't live in space; they need mountains, wind, and snow to be happy, and that's why we will put him in the environment he likes. In this case, we will follow almost the same steps that we did to add the yeti but with a few differences.

Let's add the background image to the project so we can use it in the future:

  1. In the project navigator, select the Resources group.

  2. Right-click and select Add Files to "RunYetiRun"….

  3. Look for the background.png file in the RunYetiRun folder, select it, and click on Add.

Then add the following lines to GameScene.m after the _yeti.position = CGPointMake(screenSize.width * 3 / 4, screenSize.height / 2) instruction:

// Adding the background image
CCSprite *background = [CCSprite spriteWithImageNamed:@"background.png"];
background.position = CGPointMake(screenSize.width / 2, screenSize.height / 2);
[self addChild:background z:-1];

The first two lines are already known to you: we are creating a sprite using the background image we have just added to the project and positioning it in the middle of the screen. However, there is an intriguing z argument when sending the message addChild to the current scene. It's the z-order and it represents the depth of the node; in other words it indicates the order in which the node will be drawn.

You can think of it as the layer organization in image processing software, where you can specify which layer is shown over the rest. The default value for z-order is 0, which is why we specified the -1 value because we want our background to be placed behind other layers. Can you see what would happen if you change -1 to 0? In this case, the background should be called foreground because it is now placed over our yeti. As we have specified the default value, the node will be drawn in the order we add it to the scene, so the poor yeti will be buried under snow even before breaking free from the avalanche!

So, once you know this, you can decide the most convenient way of adding sprites to your scene: specifying the z-order value or adding the nodes in the desired order. As this game is pretty simple, we just need to add the sprites in order of appearance, although in complex scenes you will want to keep them in a specific order (for example, when loading enemies that you want to be covered by some objects on the scene), but that is another matter.

The following image represents the resultant layers depending on the z-order value: background (z-order = -1), sprites (z-order = 0), and score label (z-order > 0):

Ok, enough talking. Time to run and see the yeti chilling out on his sledge without being conscious of what is going to happen.

Now that we have a background and a sprite, we should talk about the different resolutions of a universal game (runnable on both iPhone and iPad). As you already know, there are several iOS devices on the market with different screen sizes and resolutions, so we need to adapt our visual sources to each of them. Take a look at the following table as a reference of the different specifications:

 

iPhone

iPhone Retina

iPhone 5

iPad

iPad Retina

Devices

iPhone 1G-3GS

iPhone 4, 4S

iPhone 5, 5C, 5S

iPad

iPad Air

 

iPod Touch 1G-3G

iPod Touch 4G

iPod Touch 5G

iPad 2

iPad Mini Retina

Resolution

480 x 320

960 x 640

1136 x 640

1024 x 768

2048 x 1536

Cocos2d file suffix

file.png

file-hd.png

file-iphone5hd.png

file-ipad.png

file-ipadhd.png

At the time of writing this book, there are five different resolution families with five different filenames; not bad considering they support more than 15 devices. It means that if you want your game to be displayed in the native resolution of all these devices, you will need to provide five images with the particular suffix. So, for example, if you want your game to be played properly on the first iPhone and iPod Touch generation, you will need file.png, but if you want it to be also displayed with the expected resolution on all the iPhone family, you will need file-hd.png and file-iphone5hd.png.

One important thing included in the current Cocos2d version is that support for Retina displays is enabled by default, as Apple began to make it mandatory; it makes no sense to disable it.

The aim of this convention is to avoid programmatically downscaling the image, which adversely affects the game's performance. You should never upscale an image because it won't look very engaging, but you can try to downscale a high-resolution image to support all the resolutions. However, it's not recommended due to the amount of memory and CPU cycles a non-Retina device would waste performing this action.

Note

When designing image files, it is recommended to do it to the highest resolution and then downscale it for the lower ones.

As you may realize, the image suffixes used in Cocos2d games aren't the same as those used in iOS apps (they use the @2x convention). It can be used for Retina files, but Cocos2d doesn't recommend that.

For all of these reasons, and as we don't want to reposition nodes or lose image quality, each time we add an image in this chapter, we should add five different images. At the moment, we lack four images for the yeti sprite and four more images for the background:

  1. Right-click on Resources and select Add Files to "RunYetiRun"….

  2. You'll find yeti-hd.png, yeti-iphone5hd.png, yeti-ipad.png, yeti-ipadhd.png, background-hd.png, background-iphone5hd.png, background-ipad.png, and background-ipadhd.png in the RunYetiRun folder, so select these eight files and click on Add.

If you want to see how the yeti looks on other devices, go ahead and run it now.

Time for CCAction

We don't want our yeti to slide down the mountain in just one direction; what will happen if there is an obstacle? We will enable touch detection to move him up and down to dodge all the snowballs rolling down the mountain.

First of all, we need our game to manage touches, so add the following line in the init method, just before return self:

// Enabling user interaction
self.userInteractionEnabled = TRUE;

Implement touchBegan by adding the following code lines:

-(void)touchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
    // Moving the yeti to the touched position
    CGPoint touchLocation = [touch locationInNode:self];
    [self moveYetiToPosition:touchLocation];
}

Before going further, in GameScene.h, declare moveYetiToPosition by adding the following lines just after the +(GameScene *) scene; instruction:

-(void) moveYetiToPosition:(CGPoint)nextPosition;

Implement it on GameScene.m:

-(void) moveYetiToPosition:(CGPoint)nextPosition{

    // Preventing the yeti going out of the landscape
    CGSize screenSize = [CCDirector sharedDirector].viewSize;

    float yetiHeight = _yeti.texture.contentSize.height;

    if (nextPosition.y > screenSize.height – 3 * yetiHeight/2) {
        nextPosition.y = screenSize.height – 3 * yetiHeight/2;
    } else if (nextPosition.y < yetiHeight) {
        nextPosition.y = yetiHeight;
    }

    // We don't want to worry about the x coordinates
    nextPosition.x = _yeti.position.x;

    // The constant yeti speed
    float yetiSpeed = 360.0;

    // We want the yeti to move on a constant speed
    float duration = ccpDistance(nextPosition, _yeti.position) / yetiSpeed;

    // Move the yeti to the touched position
    CCActionMoveTo *actionMove = [CCActionMoveTo actionWithDuration:duration position:CGPointMake(_yeti.position.x, nextPosition.y)];

    [_yeti runAction:actionMove];
}

Ok, it's a big piece of code, but don't worry, you will understand it in no time at all. The first thing we are doing is enabling the scene to manage touches thanks to the userInteractionEnabled property.

We were discussing a few pages ago why CCLayers are not used any more, because now all the nodes inherit from CCResponder and can handle touch events, so we just need to enable this feature and implement touchBegan to respond to any interaction as soon as it happens.

In touchBegan, we just get the location of the touch event and pass it to the moveYetiPosition method, where all the magic is going to happen. We are moving the yeti up and down but we don't want him to go off the background and look odd, so if we detect a new touch in positions close to the upper or lower edges of the background (the trees and the gray layer on the bottom), we will replace it with the maximum and minimum y coordinates the yeti can move to. These maximum and minimum coordinates will be three times the half height of the yeti on the upper screen edges, and the yeti height on the lower screen edge, as the sprite anchor is placed at the center of the image. Then we update the x coordinate of nextPosition to the yeti's x coordinate because we want to focus only on vertical displacement. That way it doesn't matter where on the screen we touch, as the movement will only take into account the vertical distance.

So, once we know the next position, we just need to focus on the movement itself. In Cocos2d there are a large number of methods to perform all the actions we will need in a game. At this moment, we will just focus on movement actions, specifically on CCMoveTo. This action moves the sprite to the next position in a specified duration and can be concurrently called, resulting in a movement that will be the sum of the different movements. There is a similar action called CCMoveBy that generates a movement by the next position using relative coordinates, but we want to move to an absolute position to be sure we are avoiding the snowballs. As we want our yeti to always move at the same speed (360), we will need to update the duration of the action using basic physics. Do you remember the formula used to calculate the speed?

speed = distance/time

We already know the speed and distance values, but we need to calculate how much time (duration) it will take to move the sprite to the next position at a speed that equals 360. The ccpDistance method calculates the distance between the yeti and the next position and that's how we know the distance to be covered.

The last line [_yeti runAction:actionMove]; triggers the action and without it there won't be movement at all, as it sends the message runAction with the action we just created to the node we want to move.

Just one thing before testing these new changes: to make our code fancier we should follow the healthy habit of declaring constant variables whenever we use constant values, so delete the line float yetiSpeed = 360.0; and add the following code to GameScene.m after the GameScene.h import:

// The constant yeti speed
#define yetiSpeed 360.0;

Also, declare two private float variables to keep the top and bottom limits of the available screen stored. In GameScene.m, add the following lines after CCSprite *_yeti;:

// Available screen limits
float _topLimit;
float _bottomLimit;

Initialize these values in the init method, just after enabling user interaction:

// Initializing playable limits
_topLimit = screenSize.height - 3 * _yeti.texture.contentSize.height/2;
_bottomLimit = _yeti.texture.contentSize.height;

On the moveYetiToPosition method, modify the following lines:

    if (nextPosition.y > screenSize.height - 3 * yetiHeight/2) {
        nextPosition.y = screenSize.height - 3 * yetiHeight/2;
    } else if (nextPosition.y < yetiHeight) {
        nextPosition.y = yetiHeight;
    }

Add these new code lines:

    if (nextPosition.y > _topLimit) {
        nextPosition.y = _topLimit;
    } else if (nextPosition.y < _bottomLimit) {
        nextPosition.y = _bottomLimit;
    }

Delete the following no longer needed lines from moveYetiToPosition:

// Preventing the yeti going out of the landscape
CGSize screenSize = [CCDirector sharedDirector].viewSize;
float yetiHeight = _yeti.texture.contentSize.height;

Ok, enough code for now. Run the project and see how our yeti moves happily up and down!

However, if you touch the screen several times, you will see strange movement behavior: the yeti moves further than expected. Don't worry, it's due to the nature of the CCMoveTo class itself. When I introduced this action, I specified that it could be concurrently called resulting in a movement that will be the sum of the individual movements, but in our case it makes our yeti look crazy. To take control of the movement, we will take advantage of one important method when calling actions: stopActionByTag.

Actions under control

In Cocos2d, we can trigger and stop actions whenever we want, so we can control what is happening all the time. After running the movement action on the yeti, we realize that it's not behaving as we wanted: it's chaining movements and the sprite is not placed on the desired position. We can solve it thanks to the action-canceling methods available in CCNode (don't forget CCSprite inherits from this class):

  • stopAction:(CCAction * action)

  • stopActionByTag:(NSInteger)

  • stopAllActions

The first removes a specified action, the second removes the action specifying a tag number, and the last cancels every action running on the node. In our case, we don't want to stop all actions because we may want to execute another action in the future, so we should stop the action with the stopActionByTag:(NSInteger) tag.

We just need to make two changes in our code. In moveYetiToPosition, add the following lines before running actionMove:

// Controlling actions
[actionMove setTag:0];

At the very beginning of touchBegan, add:

// Controlling actions
[_yeti stopActionByTag:0];

Great, the yeti is now moving smoothly on the snow, but it's time to make some noise and begin a big avalanche, isn't it?

Throwing some snowballs

As soon as an avalanche begins, a lot of snowballs roll down the mountain. We will represent the snowballs with an array filled with sprites that will vertically cover the entire available screen: there will be as many snowballs as can fit within the height of the background. Also, as we are developing this game for any kind of iOS device, the size of the snowball array will depend on the height of the device, so it will be indicated during the initialization of the scene.

Let's do these changes step by step. First, in GameScene.m, declare the variables for both the array of snowballs and number of snowballs by adding the following lines after float _bottomLimit:

// Declare snowballs array
NSMutableArray *_snowBalls;

// Declare number of snowballs
int _numSnowBalls;

We are using NSMutableArray, the iOS class used to create modifiable arrays of objects, and not CCArray as it has been deprecated. During the lifetime of the previous Cocos2d version, many people suggested stopping using CCArray because its benefits were limited, but it had many restrictions, which is why it's not available anymore.

For the moment we are declaring all the global variables as private as we don't need to share them with other scenes, and we will keep this approach until we need to publish some variables.

The next step is to initialize the variables we just declared, but first we will need an image to create the snowball sprites:

  1. In Xcode, right-click on the Resources group in the project navigator.

  2. Select Add Files to "RunYetiRun"….

  3. Look for snowball0.png, snowball1.png, and snowball2.png (and the corresponding –hd.png, -iphone5hd.png, -ipad.png, and -ipadhd.png) included in the RunYetiRun folder and click on Add.

Now let's calculate how many snowballs fit on the screen. In the init method, add the following lines just after enabling user interaction:

    // Creating a temporal sprite to get its height
    CCSprite *tempSnowBall = [CCSprite spriteWithImageNamed:@"snowball0.png"];
    float snowBallHeight = tempSnowBall.texture.contentSize.height;

    // Calculate number of snowballs that fits in the screen
    _numSnowBalls = (screenSize.height - 3 * _yeti.texture.contentSize.height/2) / snowBallHeight;

We are creating a temporal sprite with the snowball texture and then we are getting its height by accessing the texture content size. Once we know the image height, we can divide the available screen size by the image height, and we will get a number that will correspond to the number of items the snowball array will have.

Note that when calculating the available screen size, we are taking into account the whole screen that will be filled with snowballs; in other words, we don't calculate it as _topLimit - _bottomLimit as this variables refers to the anchor point of the yeti sprite and we want to take into account the top of its texture for the top limit and the bottom of its texture for the bottom limit when calculating the available screen. That's why the available screen is (screenSize.height - 3 * _yeti.texture.contentSize.height/2).

We can now initialize the snowball array. Add the following lines just before return self;:

// Initialize array with capacity
_snowBalls = [NSMutableArray arrayWithCapacity:_numSnowBalls];

for (int i = 0; i < _numSnowBalls;i++) {
      CCSprite *snowBall = [CCSprite spriteWithImageNamed:[NSString stringWithFormat:@"snowball%i.png", i % 3]];
      // Add the snowball to the scene
      [self addChild:snowBall];

      // Add the snowball to the array
      [_snowBalls addObject:snowBall];
}
[self initSnowBalls];

First we use the arrayWithCapacity method to specify the number of items our array will have and, using a for loop, we create a snowball sprite that we add to the scene and the array. If you look closely, we are formatting the filename to initialize each ball with one of the three images we have available. Also pay attention to the addChild line we need to add each sprite to the scene, because if we don't do it the sprites won't appear. The final line in the loop is simple: we are just adding another node to the array.

At this point, there is no visual update and you may be getting a compilation error. Don't worry, we're going to fix it right now, so let's begin by initializing the snowball positions.

Rolling down the hill

In this step, we will initialize the snowballs and make them roll down, trying to hit the yeti. Perhaps this task looks easy—and it is—but it will be a good chance to explain some new concepts.

First of all, we will wrap this initialization in an instance method, which is why you should include the following line in GameScene.h just after declaring moveYetiToPosition:

-(void) initSnowBalls;

Going back to GameScene.m, you need to implement it by adding:

-(void) initSnowBalls {

    CCSprite *tempSnowBall = [_snowBalls objectAtIndex:0];

    // Position y for the first snowball
    int positionY = _bottomLimit;

    // Calculate the gaps between snowballs to be positioned proportionally
    CGSize screenSize = [CCDirector sharedDirector].viewSize;
    float blankScreenSize = (screenSize.height - 3 * _yeti.texture.contentSize.height/2) - _numSnowBalls * tempSnowBall.contentSize.height;
    float gap = blankScreenSize / (_numSnowBalls - 1);

    for (int i = 0; i < _snowBalls.count; i++){

        CCSprite *snowBall = [_snowBalls objectAtIndex:i];

        // Put the snow ball out of the screen
        CGPoint snowBallPosition = CGPointMake(-snowBall.texture.contentSize.width / 2, positionY);
        positionY += snowBall.contentSize.height + gap;
        snowBall.position = snowBallPosition;

        [snowBall stopAllActions];
    }

    [self schedule:@selector(throwSnowBall:) interval:1.5f];
}

At the beginning of the method, we are retrieving one snowball as a temporal variable to get its height, because we want to place each snowball in a different y position but always inside the playable screen. That's why the initial positionY variable is equal to _bottomLimit as it's the lowest point the yeti will reach (remember anchor points?) and we will use it to know where to place the next snowball.

As we want to position each snowball proportionally to the available screen, we first calculate how much space will be kept blank when all the snowballs are drawn. It is the available screen height minus the height of the number of snowballs on the screen.

Then we divide this blank space by one snowball less than the total, because we don't want to leave a gap when placing the first snowball. This gap variable will be used later to calculate the next snowball position.

Then, with a for loop we iterate over the array, retrieving each sprite and performing the following actions:

  1. Initialize a new CGPoint, placing it half the width of the snowball out of the screen (on the left) and in the y position calculated.

  2. Update the positionY variable by increasing it by a snowball height plus the gap.

  3. Assign the new position to the snowball.

  4. Stop every running action on the snowball, as we want it to lie still.

The following image represents the resultant screen after performing the preceding steps:

Now that we have initialized the snowballs and they are waiting to attack, it is time to throw them at the yeti! We will do it thanks to the last line:

[self schedule:@selector(throwSnowBall:) interval:1.5f];

It means that we are scheduling the method throwSnowBall to execute once every 1.5 seconds. If you pay attention to the syntax of the previous instruction, you can see that we specify the method we want to schedule through selector, which is the name of the method including colons and parameter names.

There are several ways of scheduling selectors. These include specifying a delay or a set number of repetitions:

  • schedule:interval: Schedules a method that will be triggered after the number of seconds specified as the interval

  • schedule:interval:repeat:delay: Similar to the previous method but it also allows us to specify the number of repetitions and a desired initial delay in seconds

In this chapter, we will use the first version.

Now you need to implement throwSnowBall, so paste the following piece of code into GameScene.m:

-(void) throwSnowBall:(CCTime) delta {
    for (int i = 0; i < _numSnowBalls; i++){

        // Get a random number between 0 and the size of the array
        int randomSnowBall = arc4random_uniform(_numSnowBalls);

        // Select the snowball at the random index
        CCSprite *snowBall = [_snowBalls objectAtIndex:randomSnowBall];

        // Don't want to stop the snow ball if it's already moving
        if ([snowBall numberOfRunningActions] == 0) {

            // Specify the final position of the snowball
            CGPoint nextSnowBallPosition = snowBall.position;
            nextSnowBallPosition.x = [CCDirector sharedDirector].viewSize.width + snowBall.texture.contentSize.width / 2;

            // Move the snowball to its next position out of the screen
            CCActionMoveTo *throwSnowBallAction = [CCActionMoveTo actionWithDuration:1 position:nextSnowBallPosition];

            // Reset the position of the snowball to reuse it
            CCActionCallBlock *callDidThrown = [CCActionCallBlock actionWithBlock:^{

                CGPoint position = snowBall.position;
                position.x = -snowBall.texture.contentSize.width / 2;
                snowBall.position = position;
            }];

            // Execute the movement and the reset in a sequence
            CCActionSequence *sequence = [CCActionSequence actionWithArray:@[throwSnowBallAction, callDidThrown]];
            [snowBall runAction:sequence];

            // To avoid moving more than one snowball at the same time
            break;
        }
    }
}

Don't be scared of this block of harmless code; at least you are not the yeti who is about to be buried by tons of snow! This method covers the snowball's movement and the recovery of its initial position. Let's look at it line by line.

As with every time we perform an action on the snowballs array, we loop into it thanks to the global variable _numSnowBalls. We want to throw one snowball at a time but we don't want to do it sequentially because it would be too easy for the yeti to learn the sequence. To make things harder, we will randomly decide what snowball to throw, and it is as easy as using the arc4random_uniform(_numSnowBalls) mathematical function included in the stdlib.h library (after iOS 4.3) to obtain a random number between 0 and _numSnowballs. You can also use two more approaches:

int randomSnowBall = arc4random() % _numSnowballs;
int randomSnowBall = CCRANDOM_0_1() * _numSnowballs;

Both of them calculate a random number within our array size but arc4random_uniform gives a more uniform distribution of the random results.

With the random number calculated, we take the snowball sprite corresponding to this array index, and before doing anything else, we check if it has a running action. This check is done by sending the message numberOfRunningActions to the node and it will help us to avoid stopping snowballs in the middle of their movement, in case we get the same sprite in the next interval. The numberOfRunningActions message returns the number of actions running plus the actions scheduled to run, that way we know what is happening.

In the next two lines, we are going to calculate the final position we want the snowball moved to using its initial position. We just need to modify the x coordinate because we want it to stop as soon as it goes off the screen on the right-hand side, and it corresponds to the screen plus half of the sprite's width.

Once we know the snowball's final position, we just need to decide the duration of the movement to set up the throwSnowBallAction action. As we don't want to make it too hard, the snowballs will take 1 second to cover the entire screen width, enough for the yeti to avoid them. Note that this time we don't need to calculate the duration dynamically because the displacement always has the same distance.

In this case, we are not executing the action right now because we need to perform some updates after this action finishes. We will solve it by concatenating one action after throwSnowBallAction, but in this case it won't be CCActionMoveTo but CCActionCallBlock. The CCActionCallBlock class (called CCCallBlock in Cocos2d v2) allows you to set up an action with a typical Objective-C block and we are going to take advantage of this feature to update the snowballs' positions as soon as they fade from view.

CCActionCallBlock *callDidThrown = [CCActionCallBlock actionWithBlock:^{

      CGPoint position = snowBall.position;
      position.x = -snowBall.texture.contentSize.width / 2;
      snowBall.position = position;
}];

The block of code gets the current snowball position (off the right-hand side of the screen) and sets its x coordinate back to the left, off the screen too, because we want to reset its position. Then we just need to concatenate throwSnowBallAction and callDidThrown to make the snowball cross the screen from left to right and vice versa. We will do it thanks to CCActionSequence (called CCSequence in Cocos2d v2).

CCActionSequence *sequence = [CCActionSequence actionWithArray:@[throwSnowBallAction, callDidThrown]];
[snowBall runAction:sequence];

break;

The sequence allows us to execute an array of actions (even an array of just one action) that will take place one after the other. That's why we include our CCActionMoveTo and CCActionCallBlock classes, but nothing will happen until we ask the sprite to run the sequence action. If you have been working with arrays in either Objective-C or previous Cocos2d versions, you will notice that the array we've sent to the sequences has not a nil value in the last position; in Cocos2d v3, this technique is not needed anymore.

If you are asking yourself why I put a break instruction at the end, it's just to prevent throwing more than one snowball each time throwSnowBall is called.

Ok, that's a lot of writing this time but it was necessary to explain this behavior. Let's amuse ourselves a little by running the game and looking at the snowballs passing over and over. Relaxing, isn't it?

If you need to know why we can't see the snowball returning to its initial position although we didn't set it to be invisible, the reason is inside CCActionCallBlock: we set snowBall.position = position; and it moves the sprite to this position instantly without intermediate updates.

Managing collisions

At the moment, our yeti is beginning to think that something is going wrong and he is right, but lucky him that the snowballs can't hit him…yet.

We just need to introduce collisions to the logic of our game to make him fear for his life. So come on, let's make the snowballs collide with the yeti.

First, before writing more code, it's important to know that Cocos2d allows you to schedule a method that will be called in every frame. This method is (void) update:(CCTime)delta and we can take advantage of it to make common checks for collision detection.

Previous versions of Cocos2d required a call to [self scheduleUpdate] to activate this feature, but in Cocos2d v3 it's not needed any more, you just need to implement the update method with your desired logic and it will be called automatically.

In our case, implement it in GameScene.m by adding the following block of code:

-(void) update:(CCTime)delta {
    for (CCSprite *snowBall in _snowBalls){
        // Detect collision
        if (CGRectIntersectsRect(snowBall.boundingBox, _yeti.boundingBox)) {
            [snowBall setVisible:FALSE];
        }
    }
}

This method iterates the snowball array and tries to find if its texture rectangle intersects with the yeti's rectangle. When a collision is detected, we will set the snowball to invisible and let it continue its movement out of view. We could use other solutions but for now we are happy with just making the obstacle invisible.

Come on, run the project and check this behavior!

This image is a mock-up, because when a snowball collides with the yeti, it disappears and it's impossible to distinguish this situation from the initial one when no snowball is visible. Don't worry, let's do some things to make it more credible.

First, declare the following method in GameScene.h:

-(void) manageCollision;

Call it by adding the following in GameScene.m, when the collision has been detected, after [snowBall setVisible:FALSE];:

// Managing collisions
[self manageCollision];

As a final step, implement it by adding the following lines to GameScene.m:

-(void) manageCollision {

    _yeti.color = [CCColor redColor];
    CCAction *actionBlink = [CCActionBlink actionWithDuration:0.9 blinks:3];
    CCActionCallBlock *callDidBlink = [CCActionCallBlock actionWithBlock:^{
      // Recover the visibility of the snowball and its tint
      [_yeti setVisible:TRUE];
      _yeti.color = [CCColor whiteColor];
    }];

    CCActionSequence *sequence = [CCActionSequence actionWithArray:@[actionBlink, callDidBlink]];
    [_yeti runAction:sequence];

}

From the preceding code, you will understand it quickly. We are tinting the yeti with a predefined red color in the CCColor class (if you take a look at the CCColor class reference, you will see that there are several predefined colors we can use). After that, we are creating a CCActionBlink class that consists of making the sprite blink a fixed number of times during the specified interval. However, we need to set the yeti's visibility to TRUE at the end of the blink action as otherwise it finishes with the disappearance of the yeti, and we don't want that… at least not at this very moment! To achieve it, we concatenate CCActionCallBlock, where we update the visibility after the blink action and set its tint color to white again. Easy, right? I decided to execute the red tint plus blink action as it's commonly used in video games to represent when a character has been hit.

There is only one thing left, we need to recover the visibility of the snowballs once they arrive at the end of their movement! Can you guess where we are going to do it? Do you remember the CCActionCallBlock instance named callDidThrown inside the throwSnowBall method? You just need to add the following line at the end of the block:

// Recovering the visibility of the snowball
[snowBall setVisible:TRUE];

If you run the game now, you will feel that a collision is happening.

But what happens if the yeti avoids a snowball—nothing? Not even a banana or whatever yetis eat? Let's at least give him a bunch of points!

Adding labels

How can a game exist without a score? Gamers need an incentive to keep playing again and again and usually it consists of beating their own score record, so we will learn in this section how to display a label on the screen and update it dynamically.

Cocos2d provides two classes that inherit from CCSprite to visualize labels:

  • CCCLabelTTF

  • CCLabelBMFont

We are going to focus on the first one along this chapter.

CCLabelTTF

The CCLabelTTF class displays labels using a rendered TrueType font texture.

Note

The TrueType font is a vector-based font format that was designed to represent all the style variants of a letter. If you want to know which of these fonts are available in iOS, visit http://iosfonts.com.

They are simple but they have a performance handicap if you need to update a label frequently, because every time you modify its text you are creating a new CCLabelTTF instance. In this game, we are going to update the score label each time the yeti avoids a snowball, but that doesn't matter as I will teach you how to display this kind of label so you will be able to use them whenever it's convenient.

In this step, we will need an integer variable to track the score, a label, and a Boolean variable, which we will use as a flag to know when to increase the score. So first of all, let's declare the following variables in GameScene.m:

    // Label to show the score
    CCLabelTTF *_scoreLabel;

    // Score count
    int _gameScore;

    // Collisions flag
    BOOL _collisionDetected;

Now we should initialize them, so add the following code to the init method:

    // Initialize score count
    _gameScore = 0;

    // Initialize score label
    _scoreLabel = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"SCORE: %i", _gameScore] fontName:@"Chalkduster" fontSize:15];
    _scoreLabel.color = [CCColor orangeColor];
    _scoreLabel.position = CGPointMake(screenSize.width, screenSize.height);
    [self addChild:_scoreLabel];

    // Initialize the collision flag to false
    _collisionDetected = FALSE;

Initializing an integer and a Boolean variable is easy enough, so I will not waste your time explaining it. Let's focus on how we are initializing the score label. We are creating a new CCLabelTTF specifying its font name, its size, and its text. For the font, I chose Chalkduster because of its childish look, but you can use any of the available fonts in iOS, which you can find at http://iosfonts.com. Note that labelWithString accepts NSString so we can include both numbers and text in the label. Also, I set its color to orange so we can differentiate the label from the background.

Run the game and you will see the brand-new label in the top-right corner:

Wait, don't throw this book into a fire! I set the score offscreen deliberately because I wanted to introduce one important feature of anchor points. The anchor point attribute is useful to align textures, as in this particular case, so we will take advantage of it to right-align the score label. To achieve that, we just need to modify the position of its anchor point. So, going back to the init method, you just need to add:

    // Right-aligning the label
    _scoreLabel.anchorPoint = CGPointMake(1.0, 1.0);

Then execute:

    [self addChild:scoreLabel];

This way, when the label size increases due to scores of three or four digits, the label will grow to the left, keeping all digits visible.

Updating labels

In this section, you will learn how to implement the logic to update labels. For this purpose, we will take into account the Boolean flag we declared previously, which we will use to identify whether to increase the score or not.

It will take a couple of changes and a new method. In GameScene.h, add the following line to declare a method we will use to update the score:

-(void) increaseScore;

Going back to GameScene.m, as we want to identify when a collision happens, there is nothing better than using the methods we have created previously. In the manageCollision method, add the following line at the very beginning:

_collisionDetected = TRUE;

That way we will be able to identify moment-by-moment whether the yeti has been hit.

In the throwSnowBall method, you may remember we created a CCActionCallBlock to update the snowball's position when its movement has finished. Find this line of code:

[snowBall setVisible:TRUE];

Before the preceding code, add the following lines of code:

// Evaluating if a collision happened
if (!_collisionDetected){
      [self increaseScore];
}

Then we will implement the increaseScore method. You just need to add:

-(void) increaseScore{
    _gameScore += 10;
    [_scoreLabel setString:[NSString stringWithFormat:@"SCORE: %i", _gameScore]];
}

As a final step, we will need to reset the Boolean flag to false and we will make this change after the whole snowball sequence is done. Add the following line just before break;:

_collisionDetected = FALSE;

Ok, let's see what we have done so far. As soon as we detect a collision, we update the Boolean flag to TRUE and we will use it at the end of the snowball movement to know if we have to increase the label. If the flag's value is FALSE, it means that the yeti has survived the attack of one snowball and we will execute increaseScore, rewarding the yeti with some points to make him happy.

With this method, we increase the score by 10 points and update the label using the setString method, keeping the same format as when we created it.

Now that we have a flag to control when a collision happens, we will use it to minimize the times the method manageCollision is called. It's been happening repeatedly, because when a snowball collides with the yeti, although we are making it invisible, it's complying with the conditions of the collision. And this is happening every frame until the snowball passes the yeti completely, with the consequent number of calls to manageCollision. To avoid this behavior, go to the update method and replace the following condition:

if (CGRectIntersectsRect(snowBall.boundingBox, _yeti.boundingBox)) {

with a new one:

if (CGRectIntersectsRect(snowBall.boundingBox, _yeti.boundingBox) && !_collisionDetected) {

Let's go! Run the game and you will see how our yeti earns points when he survives!

Making some noise

Video games are like movies: they need music and sound effects to transmit the emotions they want the players to feel. Good movies usually come with amazing soundtracks that make you remember a particular scene, and this also applies to good video games. But you may be thinking, "I'm not a musician, nor a producer!" Don't worry, the Internet has plenty of sites where you can find free and paid resources you can use in your games, but first let's introduce a new class for playing audio in Cocos2d.

OALSimpleAudio

The OALSimpleAudio class is the new class included in Cocos2d v3 to play audio files. If you have developed games using previous versions, you will need to forget SimpleAudio as it's not used any more, but don't worry, the new class is easier to use and more powerful. One feature to highlight is that it plays all formats supported by iOS, and this is important when thinking about creating audio resources. For this chapter, I created one background song and two sound effects, and I will show you how I made them and what tools I used.

Voice memos

I used this app, included in iOS by default, to record a growl made by myself, because the format in which the audio file is exported, .m4a, is supported by iOS and is as easy to use as touching one button and sending the file to your computer. If you have a different mobile device, I'm sure it includes an audio recorder that you can use too. This way in a few minutes I got growl.m4a, which the yeti will play whenever a snowball hits it. I know it's not the best audio quality, but I wanted to use it just to show you this possibility.

Audacity

Next I thought that the snowballs needed a sound too, because they are not quiet when they are rolling down a mountain. I went to http://soundbible.com/ and looked for a realistic avalanche sound, then downloaded a public domain 8-second file, but it was too long for the movement of the snowballs, whose duration is only one second, so I used the audio-editing tool Audacity (http://audacity.sourceforge.net) to cut a piece of the desired duration. This is a very useful and easy-to-use tool for audio production. It allows you to record from the internal microphone or from an external sound card, cut and paste, adding and deleting tracks. In this case, I used Audacity just to create a new audio file from the original, selecting a 1-second piece and exporting it as a .mp3 file (avalanche.mp3).

GarageBand

This software is integrated in OS X by default and is a professional tool for music production, so it's perfect for our game's background music. However, it requires a little more experience to take advantage of its whole potential. I created a track using some preset banjo loops, making sure that the beginning and the end match because this file will be played in a loop until the end of the game.

I decided to add another track with a wind sound so it feels more like being on a mountain, and I found a sound made by Mark DiAngelo (http://soundbible.com/1810-Wind.html) that I think is perfect. I just had to export the song (background_music.mp3) and that's it!

This is how a GarageBand project looks. As you can see there are two tracks: one is for the banjo and the other one contains the wind effect.

Audio resources

I said at the beginning of this section that the Internet is full of places where you can find resources to use in your games, and here are some examples of websites where you can download them:

Playing audio files

Now that we know how to create and get audio files, it's time to play them in our own game.

First you will need to add the audio resources to the project:

  1. Right-click on the Resources group in the project navigator on the left.

  2. Select Add Files to "RunYetiRun"….

  3. In the RunYetiRun folder, you will find background_music.mp3, avalanche.mp3, and growl.m4a. Select them and click on Add.

Then, in the init method, just before return self;, add the following line:

// Playing background music
[[OALSimpleAudio sharedInstance] playBg:@"background_music.mp3" loop:YES];

In this way, we are specifying that we want to play background music that will loop infinitely.

The next step is playing a sound when a snowball starts moving, so go to the throwSnowBall method and add the following code just before creating CCActionSequence:

// Playing sound effects
[[OALSimpleAudio sharedInstance] playEffect:@"avalanche.mp3"];

It will play an audio effect, in this case the sound of the snowball rolling down and trying to hit the yeti.

Finally, we want to make the yeti growl when it's hit and it's as easy as adding the following line of code at the very beginning of manageCollision:

// Playing sound effects
[[OALSimpleAudio sharedInstance] playEffect:@"growl.m4a"];

In this case, we will play an audio effect in the same way we just did with the snowball sound.

It's possible that you want to preload sound effects to avoid the little lag happening when you play a sound for the first time. To achieve this you can add the following lines to the init method:

[[OALSimpleAudio sharedInstance] preloadEffect:@"avalanche.mp3"];
[[OALSimpleAudio sharedInstance] preloadEffect:@"growl.m4a"];

Now you can run the game and enjoy the new sounds!

Game over

Now that our yeti is in a rush avoiding snowballs and trying to win as many points as it can, it's time to set a game over condition. The game will finish as soon as the score reaches a specified value, and when it happens we will trigger the actions needed to stop the game.

As we are going to compare the current score with a score target, we need to declare it. In GameScene.m, add the following line after BOOL _collisionDetected;:

// Score target
int _gameOverScore;

Initialize its value before [self initSnowBalls];:

// Initialize the score target
_gameOverScore = 150;

There is nothing new here, we are just declaring an integer and initializing its value to 150. You can set the value that you want: the higher the score, the more chances the snowballs will get to hit the yeti!

Now we must detect when the score target has been reached, and this task is best performed as soon as the score increases, in other words, in the increaseScore method. Go there and add the following lines after _gameScore += 10;:

    // If we reach the score target, the game is over
    if (_gameScore >= _gameOverScore){
        [self gameOver];
        return;
    }

If the condition is true, the score has been reached, then we call the gameOver method and exit from increaseScore.

We just need to declare and implement the new method. In GameScene.h, add the following line:

-(void) gameOver;

Back in GameScene.m, implement it by adding the following lines:

-(void) gameOver{

    CGSize screenSize = [CCDirector sharedDirector].viewSize;

    // Initializing and positioning the game over label
    CCLabelTTF *gameOverLabel = [CCLabelTTF labelWithString:@"LEVEL COMPLETE!" fontName:@"Chalkduster" fontSize:40];

    gameOverLabel.color = [CCColor greenColor];
    gameOverLabel.position = CGPointMake(screenSize.width/2, screenSize.height/2);

    [self addChild:gameOverLabel];

    // Removing score label
    [self removeChild:_scoreLabel];

    // Stop throwing snowballs
    [self unscheduleAllSelectors];

    // Disable touches
    self.userInteractionEnabled = FALSE;

    // Stop background music and sound effects
    [[OALSimpleAudio sharedInstance] stopEverything];
}

This method just needs to highlight the removeChild call. It removes the label from the scene and forces the cleanup of all running and scheduled actions.

Run the code and try to achieve 150 points without being touched!