Cocos2d Game Development Blueprints

By Jorge Jordán
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Sprites, Sounds, and Collisions

About this book

Cocos2d is one of the most popular 2D game engines for the iOS platform. It is packed with powerful features that set it apart from other game engines. It can be used to build games, apps, and other cross-platform GUI-based interactive programs.

This practical guide will develop your skills in designing and developing iOS games across several genres. You will start with managing collisions through building a horizontal scroll game for both iPhone and iPad, you will then go through building a shooting game. After that, you will learn how to use the accelerometer to control movement, load particle systems such as explosions, draw primitives, and create parallax effects. We also cover essential elements of the Cocos2d package including sprite sheets, multiple image file loading to animate a sprite, building physics behavior with Chipmunk, using filters and listeners, and providing powerful AI, non-playable characters, and pathfinder algorithms with a cool tower defence game.

Publication date:
January 2015
Publisher
Packt
Pages
440
ISBN
9781783987887

 

Chapter 1. Sprites, Sounds, and Collisions

In the first chapter of this book, I will introduce you to the basics of developing games, specifically how to do it with Cocos2d. I'm sure that most of you have heard technical terms such as sprites and collisions before but how many of you know how to create and work with them? Throughout this chapter, I will teach you enough to answer this question and I will guide you through building a horizontal scrolling game similar to Jetpack Joyride and Flappy Bird that will run on both iPhone and iPad.

In this chapter, we will look at the following:

  • How to create a Cocos2d game that will run on iPhone and iPad

  • How to enable touch detection

  • How to manage collisions

  • How to load images and sounds

  • How to play sound effects

  • How to create score labels and update them

 

Creating a new Cocos2d project


In this book, we work with the latest Xcode version (5.1.1 at the time of writing), Cocos2d v3.0.0, and iOS7.

If you are familiar with Xcode, creating a new project should be easy enough to do alone but I would like to explain what the project creation process looks like in Cocos2d v3.

First of all:

  1. Create a new Cocos2d project in Xcode by selecting File | New | Project….

  2. Click on the cocos2d v3.x template under iOS and you should see the two icons shown in the following screenshot:

  3. Choose the cocos2d iOS template and click on Next.

  4. Call it RunYetiRun and make sure Universal is chosen on the Device Family menu before clicking on Next.

  5. Select the place where you want your project to be saved and click on Create.

I would like you to focus on the project navigator, the section on the left that contains the folders and files that will give life to our game:

This pane contains a hierarchy of files and folders in your Cocos2d project that does not reflect the real location of the files on your hard drive. In other words, the pane shows the files in your Xcode project and lets you organize these files in a way that is unique to Xcode.

Folders in the navigator are called groups and are used to organize the classes, images, and the rest of the files, but moving them will not affect their real location on your hard drive. In fact, moving files in Finder will mean Xcode won't be able to find them anymore, so we must pay attention when reorganizing our project's files.

At this point, you only need to know about the existence of the Resources and Classes groups. The first of them is where you will place your images, properties, and audio files, and the second, as its name suggests, contains the classes of the project. Let's take a look at the latter:

If you have experience working with the previous version of Cocos2d, you should notice that the former HelloWorldLayer class has been replaced by HelloWorldScene and IntroScene, and if you take a look at these classes, you will discover that they inherit from CCScene instead of CCLayer.

Just a quick note: the use of CCScene is mainly grouping nodes in a scene, which was the purpose of CCLayer in Cocos2d v2, but don't worry about it as we will discuss this new feature later in this chapter. For now you just need to know how a new Cocos2d v3 project looks.

In Xcode, look at the project window in the center, where you will see the default properties of the new project:

As you can see, there are three sections on this screen: Identity, Deployment Info, and App Icons, but we will just pay attention to the second one. In this section, you can configure:

  • Deployment Target: The earlier iOS version that will be able to install and run the game, which is 5.0.

  • Devices: The family of devices that will be able to install and run the game. It will show the property we chose during the project creation process.

  • Main Interface: Allows you to select the main XIB file.

  • Device Orientation: The orientations supported by your game. We want it to be landscape as we are going to develop a horizontal game.

  • Status Bar Style: The way the status bar will be shown.

We don't need to modify these properties because they meet our requirements, so let's run the project.

 

Run Xcode Run


Now that we have our newly created project, let's see how it looks. Click on Run at the top-left of the Xcode window and it will run the project in the iOS Simulator, which defaults to an iOS 6.1 iPhone:

Voilà! You've just built your first Hello World example with Cocos2d v3, but before going further, let's take a look at the code to understand how it works.

Note

We will be using iOS Simulator to run the game unless otherwise specified.

Understanding the default project

We are going to take an overview of the classes available in a new project, but don't worry if you don't understand everything; the objective of this section is just to get familiar with the look of a Cocos2d game.

If you open the main.m class under the Supporting Files group, you will see:

int main(int argc, char *argv[]) {

    @autoreleasepool {
        int retVal = UIApplicationMain(argc, argv, nil, @"AppDelegate");
        return retVal;
    }
}

As you can see, the @autorelease block means that ARC is enabled by default on new Cocos2d projects so we don't have to worry about releasing objects or enabling ARC.

ARC is the acronym for Automatic Reference Counting and it's a compiler iOS feature to provide automatic memory management of objects. It works by adding code at compile time, ensuring every object lives as long as necessary, but not longer.

On the other hand, the block calls AppDelegate, a class that inherits from CCAppDelegate which implements the UIApplicationDelegate protocol. In other words, the starting point of our game and the place to set up our app is located in AppDelegate, like a typical iOS application.

If you open AppDelegate.m, you will see the following method, which is called when the game has been launched:

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

    return YES;
}

Here, the only initial configuration specified is to enable the debug stats, specifying the option CCSetupShowDebugStats: @(YES), that you can see in the previous block of code.

The number on the top indicates the amount of draw calls and the two labels below are the time needed to update the frame and the frame rate respectively.

Note

The maximum frame rate an iOS device can have is 60 and it's a measure of the smoothness a game can attain: the higher the frame rate, the smoother the game.

You will need to have the top and the bottom values in mind as the number of draw calls and the frame rate will let you know how efficient your game will be.

The next thing to take care of is the startScene method:

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

This method should be overriden to indicate the first scene we want to display in our game. In this case, it points to IntroScene where the init method looks like the following code:

- (id)init
{
    // Apple recommends assigning self with super's return value
    self = [super init];
    if (!self) {
        return(nil);
     }

    // Create a colored background (Dark Gray)
    CCNodeColor *background = [CCNodeColor nodeWithColor:[CCColor colorWithRed:0.2f green:0.2f blue:0.2f alpha:1.0f]];
    [self addChild:background];

    // Hello world
    CCLabelTTF *label = [CCLabelTTF labelWithString:@"Hello World" fontName:@"Chalkduster" fontSize:36.0f];
    label.positionType = CCPositionTypeNormalized;
    label.color = [CCColor redColor];
    label.position = ccp(0.5f, 0.5f); // Middle of screen
    [self addChild:label];

    // Helloworld scene button
    CCButton *helloWorldButton = [CCButton buttonWithTitle:@"[ Start ]" fontName:@"Verdana-Bold" fontSize:18.0f];
    helloWorldButton.positionType = CCPositionTypeNormalized;
    helloWorldButton.position = ccp(0.5f, 0.35f);
    [helloWorldButton setTarget:self selector:@selector(onSpinningClicked:)];
    [self addChild:helloWorldButton];

    // done
    return self;
}

This code first calls the initialization method for the superclass IntroScene by sending the [super init] message. Then it creates a gray-colored background with a CCNodeColor class, which is basically a solid color node, but this background won't be shown until it's added to the scene, which is exactly what [self addChild:background] does. The red "Hello World" label you can see in the previous screenshot is an instance of the CCLabelTTF class, whose position will be centered on the screen thanks to label.position = ccp(0.5f, 0.5f).

Note

Cocos2d provides the cpp(coord_x, coord_y) method, which is a precompiler macro for CGPointMake and both can be used interchangeably.

The last code block creates CCButton that will call onSpinningClicked once we click on it.

Don't worry about all these new classes we have been talking about up to now as we will discuss them further in later sections. This source code isn't hard at all, but what will happen when we click on the Start button? Don't be shy, go back to the iOS Simulator and find out!

If you take a look at the onSpinningClicked method in IntroScene.m, you will understand what happened:

- (void)onSpinningClicked:(id)sender
{
    // start spinning scene with transition
    [[CCDirector sharedDirector] replaceScene:[HelloWorldScene scene]
        withTransition:[CCTransition transitionPushWithDirection:CCTransitionDirectionLeft duration:1.0f]];
}

This code presents the HelloWorldScene scene replacing the current one (InitScene) and it's being done by pushing HelloWorldScene to the top of the scene stack and using a horizontal scroll transition that will last for 1.0 second. Let's take a look at the HelloWorldScene.m to understand the behavior we just experienced:

@implementation HelloWorldScene
{
    CCSprite *_sprite;
}

- (id)init
{
    // Apple recommends assigning self with super's return value
    self = [super init];
    if (!self) {
        return(nil);
    }

    // Enable touch handling on scene node
    self.userInteractionEnabled = YES;

    // Create a colored background (Dark Gray)
    CCNodeColor *background = [CCNodeColor nodeWithColor:[CCColor colorWithRed:0.2f green:0.2f blue:0.2f alpha:1.0f]];
    [self addChild:background];

    // Add a sprite
    _sprite = [CCSprite spriteWithImageNamed:@"Icon-72.png"];
    _sprite.position = ccp(self.contentSize.width/2,self.contentSize.height/2);
    [self addChild:_sprite];

    // Animate sprite with action
    CCActionRotateBy* actionSpin = [CCActionRotateBy actionWithDuration:1.5f angle:360];
    [_sprite runAction:[CCActionRepeatForever actionWithAction:actionSpin]];

    // Create a back button
    CCButton *backButton = [CCButton buttonWithTitle:@"[ Menu ]" fontName:@"Verdana-Bold" fontSize:18.0f];
    backButton.positionType = CCPositionTypeNormalized;
    backButton.position = ccp(0.85f, 0.95f); // Top Right of screen
    [backButton setTarget:self selector:@selector(onBackClicked:)];
    [self addChild:backButton];

    // done
    return self;
}

This piece of code is very similar to the one we saw in IntroScene.m, which is why we just need to focus on the differences. If you look at the top of the class, you can see how we are declaring a private instance for a CCSprite class, which is also a subclass of CCNode, and its main role is to render 2D images on the screen.

Note

The CCSprite class is one of the most-used classes in Cocos2d game development, as it provides a visual representation and a physical shape to the objects in view.

Then, in the init method, you will see the instruction self.userInteractionEnabled = YES, which is used to enable the current scene to detect and manage touches by implementing the touchBegan method, which will be covered in detail later in this book.

The next thing to highlight is how we initialize a CCSprite class using an image, positioning it in the center of the screen. If you read a couple more lines, you will understand why the icon rotates as soon as the scene is loaded. We create a 360-degree rotation action thanks to CCRotateBy that will last for 1.5 seconds. But why is this rotation repeated over and over? This happens thanks to CCActionRepeatForever, which will execute the rotate action as long as the scene is running.

The last piece of code in the init method doesn't need explanation as it creates a CCButton that will execute onBackClicked once clicked. This method replaces the scene HelloWorldScene with IntroScene in a similar way as we saw before, with only one difference: the transition happens from left to right.

Did you try to touch the screen? Try it and you will understand why touchBegan has the following code:

-(void) touchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint touchLoc = [touch locationInNode:self];

    // Move our sprite to touch location
    CCActionMoveTo *actionMove = [CCActionMoveTo actionWithDuration:1.0f position:touchLoc];
    [_sprite runAction:actionMove];
}

This is one of the methods you need to implement to manage touch. The others are touchMoved, touchEnded, and touchCancelled. When the user begins touching the screen, the sprite will move to the registered coordinates thanks to a commonly used action: CCActionMoveto. This action just needs to know the position that we want to move our sprite to and the duration of the movement.

Now that we have had an overview of the initial project code, it is time to go deeper into some of the classes we have shown. Did you realize that CCNode is the parent class of several classes we have seen? You will understand why if you keep reading.

 

CCNode


Sometimes called a scene hierarchy, the scene graph is a hierarchy of every Cocos2d node that's currently active.

We call them nodes because they inherit from the class CCNode (http://www.cocos2d-swift.org/docs/api/Classes/CCNode.html), the base class to display objects in Cocos2d.

The usual picture of a scene graph is a root CCScene node with several node children such as CCButton, CCSprite, and CCLabelTTF. For example, the graph of an initial Cocos2d project looks like the following:

One important feature of CCNode in the current Cocos2d version is that it inherits from CCResponder, which allows all the objects derived from CCNode to handle user touches and mouse events.

I said that CCNode is the base class to display objects, but what are the classes that inherit from it? You can see the CCNode class hierarchy in the following graph:

As I mentioned in the previous section, scenes will no longer derive from CCLayer (as has been the case in previous Cocos2d versions) and if you look closely at the CCNode class hierarchy in the preceding graph, you will realize that this class doesn't appear in it. It means that CCLayer has been deprecated and doesn't exist anymore, due to the fact that CCNode now supports user interaction, which was basically the purpose of CCLayer besides grouping nodes, a task perfectly accomplished by CCScene.

One important CCNode property is positionType. This property makes reference to how a node's position will be interpreted and can take three different values:

  • CCPositionUnitPoints: The node's position will be set in points. This is the default value.

  • CCPositionUnitScaled: The node's position will be scaled, for example, to be supported by iPhone and iPad.

  • CCPositionUnitNormalized: The node's position will be relative to its parent node.

Another important property is contentSizeType, which means that the size of the node's content will be interpreted in different ways depending on its value:

  • CCSizeUnitPoints: This is the default value and means that the size will be set in points

  • CCSizeUnitScaled: The node's content size will be scaled by the factor indicated

  • CCSizeUnitNormalized: The size will be relative to its parent container size

  • CCSizeUnitInset: The size of the node's content will be the same as its parent node, but it will be inset by the indicated value

  • CCSizeUnitInsetScaled: The size of the node's content will be the same as its parent node, but it will be inset by the indicated value that will be multiplied by a scale factor

This class is one of the most important because our games will have plenty of classes that derive from it, such as CCButton (inherits from CCControl), CCParallaxNode, CCParticleSystemBase, CCSprite, CCLabelTTF (inherits from CCSprite), CCLabelBMFont (inherits from CCSpriteBatchNode), or CCScene.

 

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!

 

1-star challenge – accurate collision detection


The collision detection we implemented is a little inaccurate due to the yeti's texture. The CGRectIntersectsRect class is working properly but the collision is being triggered as soon as the snowball enters into contact with the snow under the sledge. Now that you know how to calculate the distance between two points, try to detect the collisions when the distance between the snowball and the anchor point of the yeti is small enough.

The solution

To achieve this new collision detection, we just need to modify one line. Go to the update method and modify the condition:

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

By the new one using ccpDistance:

if(ccpDistance(snowBall.position, _yeti.position) <= _yeti.contentSize.width/2) {

As we want to know the distance between the snowball and the yeti, we check it in the for loop and we just calculate the distance between their positions. I decided that if this distance is lower than half the yeti, it's close enough to be a collision.

 

2-star challenge – having three lives


We have developed a way to end the game using a top score to reach, but you know most of the time games end because you've lost all your lives. I want you to take what you learned when creating the score label on the screen and put a lives label in the top-left corner of the screen that will represent the lives left. This label will show the number of lives as a countdown starting from 3, and this counter will decrease when a snowball hits the yeti. Then, as soon as the counter arrives at 0, you will stop the game and show a red GAME OVER label in the center of the screen, similar to the previous LEVEL COMPLETED label.

The solution

This is very similar to what we did to make the score count and game over labels. To achieve this, you will need an integer variable for the lives counter and a label to show its value. So in GameScene.m, add the following lines after int _gameOverScore;:

int _numLives;
CCLabelTTF *_livesLabel;

Initialize their values by adding the following code lines to the init method, just before return self;:

_numLives = 3;


_livesLabel = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"Lives: %i", _numLives] fontName:@"Chalkduster" fontSize:15];

_livesLabel.color = [CCColor orangeColor];
_livesLabel.anchorPoint = CGPointMake(0.0, 1.0);
_livesLabel.position = CGPointMake(0, screenSize.height);

[self addChild:_livesLabel];

We are setting the number of lives counter to 3 and initializing the label with the appropriate text formatted to show this counter. Then we configure the label to be orange colored and we set its anchor point to the top-left because we want it left and top-aligned. Once we have set the anchor point, we can set its position to the top-left corner of the screen, and don't forget to add it to the scene.

Until now we've just initialized the variables, so let's make them work! As we want the counter to decrease when a snowball hits the yeti, we will perform this update on the manageCollision method. Go there and add the following lines at the very beginning of the method:

_numLives--;
[_livesLabel setString:[NSString stringWithFormat:@"Lives: %i", _numLives]];
if (_numLives == 0) {
    [self gameOverLives];
    return;
}

The first thing to do is decrease the counter and update the label. Then, if the counter arrives at 0, we need to finish the game and exit from the method. We just need to declare the gameOverLives method by adding the following line to GameScene.h:

-(void) gameOverLives;

Implement the method in GameScene.m:

-(void) gameOverLives{

    CGSize screenSize = [CCDirector sharedDirector].viewSize;
    // Initializing and positioning the game over label
    CCLabelTTF *gameOverLabel = [CCLabelTTF labelWithString:@"GAME OVER" fontName:@"Chalkduster" fontSize:50];

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

    [self addChild:gameOverLabel];

    [self removeChild:_livesLabel];

    // 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 is very similar to the one we implement to finish the game due to the achievement of the score target, which is why I will just focus on the differences. In this case, we create a red label with the text GAME OVER and put it in the center of the screen. Then we remove both score and lives labels and stop scheduled selectors and sounds. That's it!

 

Summary


At the beginning of this chapter, we had our first contact with a Cocos2d project. We created a new project and took an overview of it, understanding some of the classes that are part of this framework.

Then we were introduced to CCNode, one of the main classes, as it's the class parent for some of the most important objects we will use when developing games. Also, we showed how the scene graph should look, with a parent CCScene node as root and the different children nodes hanging from it.

You learned how to create a scene and add sprites to it, taking into account their anchor point to position them in the desired place. You also learned to add a background image with a customized z-order to be sure it lies behind the rest of the nodes.

I explained to you the characteristics to bear in mind when developing a game for both iPhone and iPad devices, such as screen resolution and relative positions.

Then you enabled user interaction to handle touches, which you used to know where to move the yeti using the CCActionMoveTo action. At the same time, you learned how to keep a sprite within the screen limits.

You learned how to set up a bunch of enemies, snowballs in this case, in an array, initializing their positions to keep them out of view waiting to attack. At this point, you learned how to get one snowball randomly and to apply a horizontal movement until it reached the right-hand edge of the screen.

Also, you handled the collisions between the snowballs and the yeti and took advantage of it to create a tint and blink action to execute when a collision happens.

To give some playability to the game, you learned how to place labels using their anchor points and how to initialize and update their text.

Toward the end of the chapter, you learned where you can get some audio sources and how you can create your own. Once you had the background music and the sound effects, you learned how to use them in your game.

In the last section of the chapter, you learned how to declare a condition to finish the game and which actions are needed to illustrate the game is over.

Now that we know how to load and move sprites, let's take them further to create complex objects and realistic effects.

About the Author

  • Jorge Jordán

    Jorge Jordán is an iOS indie developer who's passionate about how things work since his childhood. This is the reason why he graduated in computer science and became a Java developer. After buying his first iPhone, he became deeply interested in its technology and spent his spare time learning how to develop apps for Apple's smartphones.

    Over time, he founded www.insaneplatypusgames.com, where he tries to make his dreams and games come true. He is also a member of the tutorial team at www.raywenderlich.com.

    Also, he has worked on a book titled Cocos2d Game Development Blueprints, Packt Publishing.

    In his free time, he loves to play video games, play bass guitar, and watch TV series.

    You can follow him on Twitter; his Twitter handle is @jjordanarenas.

    Browse publications by this author