Cocos2d-x Game Development Blueprints

4 (2 reviews total)
By Karan Sequeira
  • 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. A Colorful Start

About this book

Packed with comprehensive projects, this book takes a detailed look at a few of the industry's most popular games. This book will show you how to use Cocos2d-x to build games using its core components. You will learn how to incorporate game physics, and import custom models and animations. Next, you will see how to include effects such as particles and sounds. With a brief introduction to the upcoming HTML5 platform using Cocos2d-html5, the book goes on to tackle the many different concepts that comprise game development today. You will learn how to build worlds with meshes, a terrain, user interaction, physics, and more. You will start by developing a puzzle game, and then progress on to games that are increasingly complex. Along the way, you'll learn how to build gaming favorites similar to games such as Flappy Bird, Tilt to Live, Jumpy Clown, Angry Birds, and Tower Defense.

Publication date:
July 2015
Publisher
Packt
Pages
392
ISBN
9781783985265

 

Chapter 1. A Colorful Start

We start the journey of game development in Cocos2d-x with a simple, fun, and colorful game. Let's call it ColourSmash. This will serve as a revision of the basic features of the engine and will get us started with some fun development. We will be using the HTML5 version of Cocos2d for this project—Cocos2d-html5-v2.2.3 to be specific. So all the coding in this chapter, and the next, will be done in JavaScript.

Here's what you'll learn in this chapter:

  • How and where to use cc.Scene and cc.Layer

  • How to optimize rendering with a batch node

  • How to use actions and easing

  • How to get touches

  • How to schedule callbacks

 

An overview of ColourSmash


The game we will discuss here is one of the many famous puzzle games found online. The basic idea of the game is simple: click on a colored tile and all adjacent tiles of the same color disappear. As you may have guessed, the game world consists of a set of colorful tiles. You keep clicking tiles and they disappear. What makes it fun is a bunch of tiles disappearing together yields you a bonus!

This is a glimpse of what we should have at the end of this chapter:

 

Setting up the environment for Cocos2d-html5


Before we actually begin developing this game, let's spend some time setting up an environment for our HTML5 games. Since these are HTML5 games, they will essentially run in a web browser—be it on a computer or a mobile device. You are free to run these games in a browser of your choice. My personal favorite is Google Chrome!

Since we will be using JavaScript to develop the game, I would definitely recommend brushing up on some basic JavaScript coding principles before diving into the code. You can visit the following URL for some help:

http://jstherightway.org/

Moving on, here is the list of software that you will require to run the tests/samples in the Cocos2d-html5 source as well as the Cocos2d-html5 games we'll develop:

The first software on the list is, of course, the source of the Cocos2d-html5 engine, without which we won't be able to move a single web-based muscle.

The next software on the list is WebStorm, which is a powerful JavaScript editor that provides features such as code completion, code refactoring, debugging, and many others. Of course, this does not impact our chapter in anyway. You can choose to use any editor of your choice.

The last item on the list is WampServer, which is optional and is valid only for developers working on a Windows machine. WampServer provides a web server that can serve files over HTTP. This is especially useful if you want to test your game on wireless devices connected to the same network as your development machine.

Once you've downloaded the above software, move to the next section where we will create a sample project.

 

Creating a project in Cocos2d-html5


Creating a project in Cocos2d-html5 is as simple as copying a readymade template and renaming it to what you want to call your game. If you've downloaded and extracted the archive of the Cocos2d-html5 source, navigate to the template folder inside it. Your file explorer should show you something like this:

If you run the index.html file you see in the template folder, you'll realize that it is nothing but a hello world project. This is exactly what we need to get us started. To create a new project in Cocos2d-html5, follow these steps:

  1. Copy all the files within the template folder.

  2. Create a folder in the root directory of the Cocos2d-html5 engine. The engine's root with the folder for ColourSmash is shown in the following screenshot:

  3. Now, paste all the files that you copied from the template folder into the folder you just created.

That's it! You have created your first project in Cocos2d-html5. Now, it's time to get acquainted with the different kinds of files located in the project's directory.

 

Getting acquainted with the project structure


The main backbone files required for any Cocos2d-html5 game are the index.html, main.js, and cocos2d.js files. These are exactly the files you will see in your project's root directory. In addition to these three files, you can also see a file called build.xml and another file called cocos2d-jsb.js. Take a look at the following table for a brief description of what these files are responsible for:

File

Description

index.html

This is the main HTML page that will be displayed on any device's browser. This is where your game will run. This is also the file you must double-click on in order to launch the game in a browser.

main.js

This file could be compared to the AppDelegate class from Cocos2d-x. It is the starting point of our game and is where the director is informed of which the game's first scene should be.

cocos2d.js

This file is the main configuration file and is the only source file that gets linked to your HTML page. This file is responsible for invoking the engine's main loader. Any sources that go into your game need to be listed in this file.

cocos2d-jsb.js

This JavaScript source file is required to boot JavaScript bindings and is not required for Cocos2d-html5. You could delete this file if you don't intend on using JavaScript bindings.

build.xml

You can compress your entire game's source code into a single file using the Google Closure Compiler that is provided under the tools directory of the Cocos2d-html5 source. This file is the input to the Ant build command that invokes the compiler. To run this, you will need Ant and JRE installed on your machine.

resources.js

This file contains a JSON object that lists all the resources that you will need to run the game. You must ensure that each and every resource (image, plist, font, audio, and so on) is included in this file. Not doing so will most certainly crash your application.

The engine will then load all the files listed in this file into the browser's memory. Therefore, you will still need to load the textures, sprite frames and animations into their respective caches.

Now that we have a basic understanding of the structure of a typical Cocos2d-html5 project, it is time to create our very first scene in ColourSmash.

 

Creating the first scene – the main menu


Our first scene happens to be the opening screen of the game—the main menu. It is what the users will see when the game has finished loading. This scene is defined in mainmenu.js in the source for this project. This is what it looks like:

var MainMenu = cc.Layer.extend({

  init:function () {
    this._super();

    return true;
  }
});

var MainMenuScene = cc.Scene.extend({
  onEnter:function () {
    this._super();
    var layer = new MainMenu();
    layer.init();
    this.addChild(layer);
  }
});

As you can see, we have two entities here; an object called MainMenu, which is of type cc.Layer, and a class called MainMenuScene, which extends the cc.Scene class. Understandably, if you've worked on Cocos2d-x before, this is the way the various screens within a game are structured into scenes and layers. If you do need to brush up on some Cocos2d node graph basics, feel free to pay a visit to the following link:

http://www.cocos2d-x.org/wiki/Scene_Graph

Now that we have created a cc.Scene class and a cc.Layer class, it's time to create some basic UI elements. We add a title and a play button to our main menu and add some color to the background. Hence, the following code is added to the init function of MainMenu:

// create a coloured layer as background
var background = cc.LayerColor.create(cc.c4b(25, 0, 51, 255), this.screenSize.width, this.screenSize.height);
this.addChild(background);
 
// create a label to display the name of the game
var titleLabel = cc.LabelTTF.create("ColourSmash", "Comic Sans MS", 64);
titleLabel.setPosition(cc.p(this.screenSize.width * 0.5, this.screenSize.height * 0.8));
this.addChild(titleLabel, 1);
 
// create a play button to move to the game world
var playButton = cc.MenuItemSprite.create(cc.Sprite.create(s_Play));
playButton.setCallback(this.onPlayClicked, this);
playButton.setPosition(cc.p(this.screenSize.width * 0.5, this.screenSize.height * 0.5));
 
// create a menu that will contain the button above
var menu = cc.Menu.create(playButton);
menu.setPosition(0,0);
this.addChild(menu, 1);

Notice how the callback is set for the play button. In Cocos2d-html5, we need not specify the kind of function pointer that we're setting into the callback. Since all functions are objects in JavaScript, we need to pass the handler function, and the class it belongs to, as parameters to the setCallback function. The handler function that will take us to the GameWorld is given as follows:

onPlayClicked:function(){
  // ask the director to change the running scene
  cc.Director.getInstance().replaceScene(cc.TransitionFade.create(0.5, new GameWorldScene()));
}

Note

An important difference in the API of Cocos2d-x and Cocos2d-html5 is the absence of selector types in the latter. Whether a function has to be passed as a schedule_selector, a callfunc_selector, or a menu_selector, the API requires only the reference to the function.

The code for the play button's callback is pretty straightforward. It just tells the director to replace the current scene with GameWorldScene and include a smooth fade transition.

That completes a simple version of our MainMenu, but how will this scene be displayed in the first place? To answer that question, we navigate to the bottom of the main.js file and pass an object of our MainMenuScene class as an argument to the cocos2dApp class' constructor:

var myApp = new cocos2dApp(MainMenuScene);

This will set MainMenuScene as the first scene that will be displayed when the web application has loaded.

Tip

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.

 

Moving on to the game world


We will add another scene to represent the actual game play, which will contain its own cc.Layer called GameWorld. This class is defined in the gameworld.js file in the source bundle for this chapter. Every scene that you define must be added to the list of sources in cocos2d.js and build.xml if you plan on using the closure compiler to compress your source files.

For this game, all we need is a small, white, square-shaped image like this:

We will use this white image and manually set different RGB values for the sprites to create our grid of colorful tiles. Don't forget to add this to the resources.js file so that it is preloaded and can be used in the game.

Now that we have our sprites, we can actually start building our grid. Let's define a few constants before we do that. You're right, JavaScript does not have a concept of constants, however, for the purposes of our understanding, we will name and consider these quantities as constants. Here is the declaration of the constants in the gameworld.js file:

var MAX_COLOURS = 4;
// maximum number of colours we can use
var TILE_SIZE = 32;
// size in points of each tile (same as tile.png)
var NUM_COLS = 14;
// maximum number of columns
var NUM_ROWS = 20;
// maximum number of rows
var GAMEPLAY_OFFSET = cc.p(TILE_SIZE/2, TILE_SIZE);
// offset so that game is not stuck to the bottom-left
var SCORE_PER_TILE = 10;
// score when a tile is cleared
var BONUS = [50, 40, 30, 20, 10];
// number of tiles used to trigger bonuses eg. Bonus if 50 tiles collected in one shot
 
// define an object that we can use an enumeration for our colour types
var E_COLOUR_TYPE = {
  E_COLOUR_NONE:0,
  E_COLOUR_RED:1,
  E_COLOUR_GREEN:2,
  E_COLOUR_BLUE:3,
  E_COLOUR_YELLOW:4
};

We have defined a constant GAMEPLAY_OFFSET. This is a convenient variable that specifies how many points should be added to our grid so that it appears in the center of the game world. We have also defined another quantity E_COLOUR_TYPE, which will act as enum to represent our color types. Since JavaScript is a weak-typed language, we cannot really create enumerations like in C++, which is a strong-typed language. The best we can do is to simulate a normal JavaScript object so that we can have the convenience of an enum, as done in the preceding code snippet.

Declaring and initializing the variables

Let's declare the members of the GameWorld class and define the init method that will be called when this scene is created:

// member variable declarations
// save screenSize for fast access
screenSize:null,
// array to represent the colour type for each tile
tileData:null,
// array to hold each tile's sprite
tileSprites:null,
// batch rendering
spriteBatchNode:null,
// arrays to support game logic
tilesToRemove:null,
tilesToShift:null,
// score and time
score:0,
scoreLabel:null,
time:0,
timeLabel:null,
// buttons and popups
pauseButton:null,
popup:null,
isGameOver:false,

init:function () {
  this._super();

  this.screenSize = cc.Director.getInstance().getWinSize();

  this.tilesToRemove = [];
  this.tilesToShift = [];
  this.createBackground();
  this.createTileData();
  this.createTileSprites();
  this.createHUD();
  this.doCountdownAnimation();

  return true;
},

Right on top, we have two arrays named tileData and tileSprites to hold our data and sprites respectively. Then, we have our sprite batch node that will be used for optimized rendering. Next, you can see arrays that we will use to find and remove tiles when a user makes a move. Last but not the least, we have our HUD elements and menu buttons.

Creating the background

Let's begin filling up the GameWorld by creating the background, which will contain the play area for the game, the title of the game, and a pause button. The code is as follows:

createBackground:function(){
  // same as main menu
  var background = cc.LayerColor.create(cc.c4b(25, 0, 51, 255), this.screenSize.width, this.screenSize.height);
  this.addChild(background);
 
  // generate vertices for the gameplay frame
  var vertices = [];
  vertices[0] = cc.pAdd(GAMEPLAY_OFFSET, cc.p(-1, -1));
  vertices[1] = cc.pAdd(GAMEPLAY_OFFSET, cc.p(-1, (NUM_ROWS * TILE_SIZE)+1));
  vertices[2] = cc.pAdd(GAMEPLAY_OFFSET, cc.p((NUM_COLS * TILE_SIZE)+1, (NUM_ROWS * TILE_SIZE)+1));
  vertices[3] = cc.pAdd(GAMEPLAY_OFFSET, cc.p((NUM_COLS * TILE_SIZE)+1, -1));
  // use new DrawingPrimitive class
  var gamePlayFrame = cc.DrawNode.create();
  // pass vertices, fill colour, border width and border colour to get a nice bordered, coloured rectangle
  gamePlayFrame.drawPoly(vertices, cc.c4f(0.375, 0.375, 0.375, 1), 2, cc.c4f(0.4, 0, 0, 1));
  // must add the DrawNode else it won't be drawn at all
  this.addChild(gamePlayFrame);
 
  // label to show the title of the game
  var titleLabel = cc.LabelTTF.create("ColourSmash", "Comic Sans MS", 52);
  titleLabel.setPosition(cc.p(this.screenSize.width * 0.5, this.screenSize.height * 0.95));
  this.addChild(titleLabel);
 
  // menu containing a button to pause the game
  this.pauseButton = cc.MenuItemSprite.create(cc.Sprite.create(s_Pause));
  this.pauseButton.setCallback(this.onPauseClicked, this);
  this.pauseButton.setPosition(cc.p(this.screenSize.width * 0.9, this.screenSize.height * 0.95));
  this.pauseButton.setEnabled(false);
  var pauseMenu = cc.Menu.create(this.pauseButton);
  pauseMenu.setPosition(cc.POINT_ZERO);
  this.addChild(pauseMenu,1);
},

We've used a cc.LayerColor class to create a simple, colored background that is of the same size as the screen. Next, we make use of the new primitive drawing class called cc.DrawNode. This class is much faster and simpler than the cc.DrawingPrimitive class. We will use it to draw a filled rectangle with a colored border of some thickness. This rectangle will act as a visual container for our tiles.

To do this, we generate an array of vertices to represent the four points that compose a rectangle and pass it to the drawPoly function along with the color to fill, border width, and border color. The cc.DrawNode object is added to GameWorld just like any other cc.Node. This is one of the major differences between the cc.DrawNode and the older cc.DrawingPrimitives. We don't need to manually draw our primitives on every frame inside the draw function either. This is handled by the cc.DrawNode class. In addition, we can run most kinds of cc.Action objects the cc.DrawNode. We will discuss more on actions later. For now, all that's left is to add a label for the game's title and a pause button to launch the pause menu.

Creating the tiles

Now that we have defined the background and play area, let's create the tiles and their respective sprites. The code is as follows:

createTileData:function(){
        this.tileData = [];
        // generate tile data randomly
        for(var i = 0; i < (NUM_COLS * NUM_ROWS); ++i){
            this.tileData[i] = 1 + Math.floor(Math.random() * MAX_COLOURS);
        }
    },

Based on a random value, one of the four predefined color types are chosen from the E_COLOUR_TYPE enum and saved into the tileData array. The code is as follows:

createTileSprites:function(){
  // create the batch node passing in path to the texture & initial capacity
  // initial capacity is slightly more than maximum number of sprites
  // this is because new tiles may be added before old tiles are removed
  this.spriteBatchNode = cc.SpriteBatchNode.create(s_Tile, NUM_COLS * NUM_ROWS + NUM_ROWS);
  this.addChild(this.spriteBatchNode);
 
  this.tileSprites = [];
  for(var i = 0; i < (NUM_COLS * NUM_ROWS); ++i){
  this.createTileSprite(i);
  }
},

Looking at the createTileSprites function, we have created a cc.SpriteBatchNode object and added it to the game world. The cc.SpriteBatchNode class offers a great way to optimize rendering, as it renders all its child sprites in one single draw call. For a game like ours where the grid is composed of 280 sprites, we save on 279 draw calls! The only prerequisite of the cc.SpriteBatchNode class is that all its children sprites use the same texture. Since all our tile sprites use the same image, we fulfill this criterion.

We create the sprite batch node and pass in the path to the image and the initial capacity as parameters. You can see that the initial capacity is slightly more than the maximum number of tiles in the grid. This is done to prevent unnecessary resizing of the batch node later in the game.

Tip

It's a good idea to create a sprite batch node with a predefined capacity. If you fail to do this, the sprite batch node class will have to allocate more memory at runtime. This is computationally expensive, since the texture coordinates will have to be computed again for all existing child sprites.

Great! Let's write the createTileSprite function where we will create each sprite object, give them a color, and give them a position:

createTileSprite:function(tileId){
  // create sprite with the image
  this.tileSprites[tileId] = cc.Sprite.create(s_Tile);
  // set colour based on the tile's data
  this.tileSprites[tileId].setColor(this.getColourForTile(this.tileData[tileId]));
  // set colour based on the tile's index
  this.tileSprites[tileId].setPosition(this.getPositionForTile(tileId));
  // save the index of the tile as user data
  this.tileSprites[tileId].setUserData(tileId);
  // add the sprite to the batch node
  this.spriteBatchNode.addChild(this.tileSprites[tileId]);
},

The createTileSprite function, which is called in a loop, creates a sprite and sets the respective position and color. The position is calculated based on the tile's ID within the grid, in the function getPositionForTile. The color is decided based on the E_COLOUR_TYPE value of the corresponding cell in the tileData array in the getColourForTile function.

Please refer to the code bundle for this chapter for the implementation of these two functions. Notice how the tileId value for each tile sprite is saved as user data. We will make good use of this data a little later in the chapter.

Creating the Heads-Up Display

A Heads-Up Display (HUD) is the part of the game's user interface that delivers information to the user. For this game, we have just two pieces of information that we need to tell the user about, that is, the score and the time left. As such, we initialize these variables and create respective labels and add them to GameWorld. The code is as follows:

createHUD:function(){
  // initialise score and time
  this.score = 0;
  this.time = 60;
 
  // create labels for score and time
  this.scoreLabel = cc.LabelTTF.create("Score:" + this.score, "Comic Sans MS", 18);
  this.scoreLabel.setPosition(cc.p(this.screenSize.width * 0.33, this.screenSize.height * 0.875));
  this.addChild(this.scoreLabel);
  this.timeLabel = cc.LabelTTF.create("Time:" + this.time, "Comic Sans MS", 18);
  this.timeLabel.setPosition(cc.p(this.screenSize.width * 0.66, this.screenSize.height * 0.875));
  this.addChild(this.timeLabel);
},

The countdown timer

A countdown timer is quite common in many time-based games. It serves the purpose of getting the user charged-up to tackle the level, and it also prevents the user from losing any time because the level started before the user could get ready.

Let's take a look at the following code:

doCountdownAnimation:function(){
  // create the four labels
  var labels = [];
  for(var i = 0; i < 4; ++i)
  {
    labels[i] = cc.LabelTTF.create("", "Comic Sans MS", 52);
    // position the label at the centre of the screen
    labels[i].setPosition(cc.p(this.screenSize.width/2, this.screenSize.height/2));
    // reduce opacity so that the label is invisible
    labels[i].setOpacity(0);
    // enlarge the label
    labels[i].setScale(3);
    this.addChild(labels[i]);
  }
 
  // assign strings
  labels[0].setString("3");
  labels[1].setString("2");
  labels[2].setString("1");
  labels[3].setString("Start");
 
  // fade in and scale down at the same time
  var fadeInScaleDown = cc.Spawn.create(cc.FadeIn.create(0.25), cc.EaseBackOut.create(cc.ScaleTo.create(0.25, 1)));
  // stay on screen for a bit
  var waitOnScreen = cc.DelayTime.create(0.75);
  // remove label and cleanup
  var removeSelf = cc.RemoveSelf.create(true);
 
  for(var i = 0; i < 4; ++i)
  {
    // since the labels should appear one after the other,
    // we give them increasing delays before they appear
    var delayBeforeAppearing = cc.DelayTime.create(i);
    var countdownAnimation = cc.Sequence.create(delayBeforeAppearing, fadeInScaleDown, waitOnScreen, removeSelf);
    labels[i].runAction(countdownAnimation);
  }
 
  // after the animation has finished, start the game
  var waitForAnimation = cc.DelayTime.create(4);
  var finishCountdownAnimation = cc.CallFunc.create(this.finishCountdownAnimation, this);
  this.runAction(cc.Sequence.create(waitForAnimation, finishCountdownAnimation));
},
 
finishCountdownAnimation:function(){
  // start executing the game timer
  this.schedule(this.updateTimer, 1);
  // finally allow the user to touch
  this.setTouchEnabled(true);
  this.pauseButton.setEnabled(true);
},

We declare an array to hold the labels and run a loop to create the four labels. In this loop, the position, opacity, and scale for each label is set. Notice how the scale of the label is set to 3 and opacity is set to 0. This is because we want the text to scale down from large to small and fade in while entering the screen. Finally, add the label to GameWorld. Now that the labels are created and added the way we want, we need to dramatize their entry and exit. We do this using one of the most powerful features of the Cocos2d-x engine—actions!

Note

Actions are lightweight classes that you can run on a node to transform it. Actions allow you to move, scale, rotate, fade, tint, and do much more to a node. Since actions can run on any node, we can use them with everything from sprites to labels and from layers to even scenes!

We use the cc.Spawn class to create our first action, fadeInScaleDown. The cc.Spawn class allows us to run multiple finite time actions at the same time on a given node. In this case, the two actions that need to be run simultaneously are cc.FadeIn and cc.ScaleTo. Notice how the cc.ScaleTo object is wrapped by a cc.EaseBackOut action. The cc.EaseBackOut class is inherited from cc.ActionEase. It will basically add a special easing effect to the cc.ActionInterval object that is passed into it.

Tip

Easing actions are a great way to make transformations in the game much more aesthetically appealing and fun. They can be used to make simple actions look elastic or bouncy, or give them a sinusoidal effect or just a simple easing effect. To best understand what cc.EaseBackOut and other cc.ActionEase actions do, I suggest that you check out the Cocos2d-x or Cocos2d-html5 test cases.

Next, we create a cc.DelayTime action called waitOnScreen. This is because we want the text to stay there for a bit so the user can read it. The last and final action to be run will be a cc.RemoveSelf action. As the name suggests, this action will remove the node it is being run on from its parent and clean it up. Notice how we have created the array as a function variable and not a member variable. Since we use cc.RemoveSelf, we don't need to maintain a reference and manually delete these labels.

Tip

The cc.RemoveSelf action is great for special effects in the game. Special effects may include simple animations or labels that are added, animate for a bit, and then need to be removed. In this way, you can create a node, run an action on it, and forget about it!

Examples may include simple explosion animations that appear when a character collides in the game, bonus score animations, and so on.

These form our three basic actions, but we need the labels to appear and disappear one after another. In a loop, we create another cc.DelayTime action and pass an incremental value so that each label has to wait just the right amount of time before its fadeInScaleDown animation begins. Finally, we chain these actions together into a cc.Sequence object named countdownAnimation so that each action is run one after another on each of the labels. The cc.Sequence class allows us to run multiple finite time actions one after the other on a given node.

The countdown animation that has just been implemented can be achieved in a far more efficient way using just a single label with some well designed actions. I will leave this for you as an exercise (hint: make use of the cc.Repeat action).

Once our countdown animation has finished, the user is ready to play the game, but we need to be notified when the countdown animation has ended. Thus, we add a delay of 4 seconds and a callback to the finishCountdownAnimation function. This is where we schedule the updateTimer function to run every second and enable touch on the game world.

Let's get touchy...

Touch events are broadcasted to all cc.Layers in the scene graph that have registered for touch events by calling setTouchEnabled(true). The engine provides various functions that offer different kinds of touch information.

For our game, all we need is a single touch. So, we shall override just the onTouchesBegan function that provides us with a set of touches. Notice the difference in the name of the function versus Cocos2d-x API. Here is the onTouchesBegan function from the gameworld.js file:

onTouchesBegan:function (touches, event) {
  // get touch coordinates
  var touch = cc.p(touches[0].getLocation().x, touches[0].getLocation().y);
  // calculate touch within the grid
  var touchWithinGrid = cc.pSub(touch, GAMEPLAY_OFFSET);
  // calculate the column touched
  var col = Math.floor(touchWithinGrid.x / TILE_SIZE);
  // calculate the row touched
  var row = Math.floor(touchWithinGrid.y / TILE_SIZE);
  // calculate the id of the touched tile
  var touchedTile = row * NUM_COLS + col;
 
  // simple bounds checking to ignore touches outside of the grid
  if(col < 0 || col >= NUM_COLS || row < 0 || row >= NUM_ROWS)
  return;
 
  // disable touch so that the subsequent functions have time to execute
  this.setTouchEnabled(false);
  this.findTilesToRemove(col, row, this.tileData[touchedTile]);
  this.updateScore(touch);
  this.removeTilesWithAnimation();
  this.findTilesToShift();
},

Once we have got the point of touch, we calculate exactly where the touch has occurred within the grid of tiles and subsequently the column, row, and exact tile that has been touched. Equipped with this information, we can actually go ahead and begin coding the core gameplay.

An important thing to notice is how touch is disabled here. This is done so that the subsequent animations are given enough time to finish. Not doing this would result in a few of the tiles staying on screen and leaving blank spaces. You are encouraged to comment this line to see exactly what happens in this case.

The core gameplay

The core gameplay will consist of the following steps:

  1. Finding the tile/s to be removed

  2. Removing the tile/s with an awesome effect

  3. Finding and shifting the tiles above into the recently vacated space with an awesome effect

  4. Adding new tiles

  5. Adding score and bonus

  6. Ending the game when the time has finished

We will go over each of these separately and in sufficient detail, starting with the recursive logic to find which tiles to remove. To make it easy to understand what each function is actually doing, there are screenshots after each stage.

Finding the tiles

The first step in our gameplay is finding the tiles that should be cleared based on the tile that the user has touched. This is done in the findTilesToRemove function as follows:

findTilesToRemove:function(col, row, tileColour){
  // first do bounds checking
  if(col < 0 || col >= NUM_COLS || row < 0 || row >= NUM_ROWS)
  return;
 
  // calculate the ID of the tile using col & row
  var tileId = row * NUM_COLS + col;
 
  // now check if tile is of required colour
  if(this.tileData[tileId] != tileColour)
  return;
 
  // check if tile is already saved
  if(this.tilesToRemove.indexOf(tileId) >= 0)
  return;
 
  // save the tile to be removed
  this.tilesToRemove.push(tileId);
 
  // check up
  this.findTilesToRemove(col, row+1, tileColour);
 
  // check down
  this.findTilesToRemove(col, row-1, tileColour);
 
  // check left
  this.findTilesToRemove(col-1, row, tileColour);
 
  // check right
  this.findTilesToRemove(col+1, row, tileColour);
},

The findTilesToRemove function is a recursive function that takes a column, row, and target color (the color of the tile that the user touched). The initial call to this function is executed in the onTouchesBegan function.

A simple bounds validation is performed on the input parameters and control is returned in case of any invalidation. Once the bounds have been validated, the ID for the given tile is calculated based on the row and column the tile belongs to. This is the index of the specific tile's data in the tileData array. The tile is then pushed into the tilesToRemove array if its color matches the target color and if it hasn't already been pushed. What follows then are the recursive calls that check for matching tiles in the four directions: up, down, left, and right.

Before we proceed to the next step in our gameplay, let's see what we have so far. The red dot is the point the user touched and the tiles highlighted are the ones that the findTilesToRemove function has found for us.

Removing the tiles

The next logical step after finding the tiles that need to be removed is actually removing them. This happens in the removeTilesWithAnimation function from the gameworld.js file:

removeTilesWithAnimation:function(){
  for(var i = 0; i < this.tilesToRemove.length; ++i)
  {
    // first clear the tile's data
    this.tileData[this.tilesToRemove[i]] = E_COLOUR_TYPE.E_COLOUR_NONE;
    // the tile should scale down with easing and then remove itself
    this.tileSprites[this.tilesToRemove[i]].runAction(cc.Sequence.create(cc.EaseBackIn.create(cc.ScaleTo.create(0.25, 0.0)), cc.RemoveSelf.create(true)));
    // nullify the tile's sprite
    this.tileSprites[this.tilesToRemove[i]] = null;
  }
  // wait for the scale down animation to finish then bring down the tiles from above
  this.spriteBatchNode.runAction(cc.Sequence.create(cc.DelayTime.create(0.25), cc.CallFunc.create(this.bringDownTiles, this)));
},

The first order of business in this function would be to clear the data used to represent the tile, so we set it to E_COLOUR_NONE. Now comes the fun part—creating a nice animation sequence for the exit of the tile. This will consist of a scale-down animation wrapped by a neat cc.EaseBackIn ease effect.

Now, all we need to do is nullify the tile's sprite since the engine will take care of removing and cleaning up the sprite for us by virtue of the cc.RemoveSelf action. This animation will take time to finish, and we must wait, so we create a sequence consisting of a delay (with a duration the same as the scale-down animation) and a callback to the bringDownTiles function. We run this action on the spriteBatchNode object.

Let's see what the game looks like after the removeTilesWithAnimation function has executed:

Finding and shifting tiles from above

As you can see in the preceding screenshot, we're left with a big hole in our gameplay. We now need the tiles above to fall down and fill this hole. This happens in the findTilesToShift function from the gameworld.js file:

findTilesToShift:function(){
  // first sort the tiles to be removed, in descending order
  this.tilesToRemove.sort(function(a, b){return b-a});

  // for each tile, bring down all the tiles belonging to the same column that are above the current tile
  for(var i = 0; i < this.tilesToRemove.length; ++i)
  {
    // calculate column and row for the current tile to be removed
    var col = Math.floor(this.tilesToRemove[i] % NUM_COLS);
    var row = Math.floor(this.tilesToRemove[i] / NUM_COLS);

    // iterate through each row above the current tile
    for(var j = row+1; j < NUM_ROWS; ++j)
    {
      // each tile gets the data of the tile exactly above it
      this.tileData[(j-1) * NUM_COLS + col] = this.tileData[j * NUM_COLS + col];
      // each tile now refers to the sprite of the tile exactly above it
      this.tileSprites[(j-1) * NUM_COLS + col] = this.tileSprites[j * NUM_COLS + col];
      // null checking...this sprite may have already been nullified by removeTilesWithAnimation
      if(this.tileSprites[(j-1) * NUM_COLS + col])
      {
        // save the new index as user data
        this.tileSprites[(j-1) * NUM_COLS + col].setUserData((j-1) * NUM_COLS + col);
        // save this tile's sprite so that it is animated, but only if it hasn't already been saved
        if(this.tilesToShift.indexOf(this.tileSprites[(j-1) * NUM_COLS + col]) == -1)
        this.tilesToShift.push(this.tileSprites[(j-1) * NUM_COLS + col]);
      }
    }
    // after shifting the whole column down, the tile at the top of the column will be empty
    // set the data to -1...-1 means empty
    this.tileData[(NUM_ROWS-1) * NUM_COLS + col] = -1;
    // nullify the sprite's reference
    this.tileSprites[(NUM_ROWS-1) * NUM_COLS + col] = null;
  }
},

Before actually shifting anything, we use some JavaScript trickery to quickly sort the tiles in descending order. Now within a loop, we find out exactly which column and row the current tile belongs to. Then, we iterate through every tile above the current tile and assign the data and sprite of the above tile to the data and sprite of the current tile.

Before saving this tile's sprite into the tilesToShift array, we check to see if the sprite hasn't already been nullified by the removeTilesWithAnimation function. Notice how we set the user data of the tile's sprite to reflect its new index. Finally, we push this sprite into the tilesToShift array, if it hasn't already been pushed.

Once this is done, we will have a single tile right at the top of the grid that is now empty. For this empty tile, we set the data to -1 and nullify the sprite's reference. This same set of instructions continues for each of the tiles within the tilesToRemove array until all tiles have been filled with tiles from above. Now, we need to actually communicate this shift of tiles to the user through a smooth bounce animation. This happens in the bringDownTiles function in the gameworld.js file as follows:

bringDownTiles:function(){
  for(var i = 0; i < this.tilesToShift.length; ++i)
  {
    // the tiles should move to their new positions with an awesome looking bounce
    this.tilesToShift[i].runAction(cc.EaseBounceOut.create(cc.MoveTo.create(0.25, this.getPositionForTile(this.tilesToShift[i].getUserData()))));
  }
  // wait for the movement to finish then add new tiles
  this.spriteBatchNode.runAction(cc.Sequence.create(cc.DelayTime.create(0.25), cc.CallFunc.create(this.addNewTiles, this)));
},

In the bringDownTiles function, we loop over the tilesToShift array and run a cc.MoveTo action wrapped by a cc.EaseBounceOut ease action. Notice how we use the user data to get the new position for the tile's sprite. The tile's index is stored as user data into the sprite so that we could use it at any time to calculate the tile's correct position.

Once again, we wait for the animation to finish before moving forward to the next set of instructions. Let's take a look at what the game world looks like at this point. Don't be surprised by the +60 text there, we will get to it soon.

Adding new tiles

We have successfully managed to find and remove the tiles the user has cleverly targeted, and we have also shifted tiles from above to fill in the gaps. Now we need to add new tiles so the game can continue such that there are no gaps left. This happens in the addNewTiles function in the gameworld.js file as follows:

addNewTiles:function(){
  // first search for all tiles having value -1...-1 means empty
  var emptyTileIndices = [], i = -1;
  while( (i = this.tileData.indexOf(-1, i+1)) != -1){
  emptyTileIndices.push(i);
  }
 
  // now create tile data and sprites
  for(var i = 0; i < emptyTileIndices.length; ++i)
  {
  // generate tile data randomly
  this.tileData[emptyTileIndices[i]] = 1 + Math.floor(Math.random() * MAX_COLOURS);
  // create tile sprite based on tile data
  this.createTileSprite(emptyTileIndices[i]);
  }
 
  // animate the entry of the sprites
  for(var i = 0; i < emptyTileIndices.length; ++i)
  {
    // set the scale to 0
    this.tileSprites[emptyTileIndices[i]].setScale(0);
    // scale the sprite up with a neat easing effect
    this.tileSprites[emptyTileIndices[i]].runAction(cc.EaseBackOut.create(cc.ScaleTo.create(0.125, 1)));
  }
 
  // the move has finally finished, do some cleanup
  this.cleanUpAfterMove();
},

We start by finding the indices where new tiles are required. We use some JavaScript trickery to quickly find all the tiles having data -1 in our tileData array and push them into the emptyTileIndices array.

Now we need to simply loop over this array and randomly generate the tile's data and the tile's sprite. However, this is not enough. We need to animate the entry of the tiles we just created. So, we scale them down completely and then run a scale-up action with an ease effect.

We have now completed a single move that the user has made and it is time for some cleanup. Here is the cleanUpAfterMove function of gameworld.js:

cleanUpAfterMove:function(){
  // empty the arrays
  this.tilesToRemove = [];
  this.tilesToShift = [];
  // enable touch so the user can continue playing, but only if the game isn't over
  if(this.isGameOver == false)
  this.setTouchEnabled(true);
},

In the cleanup function, we simply empty the tilesToRemove and tilesToShift arrays. We enable the touch so that the user can continue playing. Remember that we had disabled touch in the onTouchesBegan function. Of course, touch should only be enabled if the game has not ended.

This is what the game world looks like after we've added new tiles:

Adding score and bonus

So the user has taken the effort to make a move, the tiles have gone, and new ones have arrived, but the user hasn't been rewarded for this at all. So let's give the user some positive feedback in terms of their score and check if the user has made a move good enough to earn a bonus.

All this magic happens in the updateScore function in the gameworld.js file as follows:

updateScore:function(point){
  // count the number of tiles the user just removed
  var numTiles = this.tilesToRemove.length;
 
  // calculate score for this move
  var scoreToAdd = numTiles * SCORE_PER_TILE;
 
  // check if a bonus has been achieved
  for(var i = 0; i < BONUS.length; ++i)
  {
    if(numTiles >= BONUS[i])
    {
      // add the bonus to the score for this move
      scoreToAdd += BONUS[i] * 20;
      break;
    }
  }
 
  // display the score for this move
  this.showScoreText(scoreToAdd, point);
  // add the score for this move to the total score
  this.score += scoreToAdd;
  // update the total score label
  this.scoreLabel.setString("Score:" + this.score);
  // run a simple action so the user knows the score is being added
  // use the ease functions to create a heart beat effect
  this.scoreLabel.runAction(cc.Sequence.create(cc.EaseSineIn.create(cc.ScaleTo.create(0.125, 1.1)), cc.EaseSineOut.create(cc.ScaleTo.create(0.125, 1))));
},

We calculate the score for the last move by counting the number of tiles removed in the last move. Remember that this function is called right after the findTilesToRemove function in onTouchesBegan, so tilesRemoved still has its data. We now compare the number of tiles removed with our bonus array BONUS, and add the respective score if the user managed to remove more than the predefined tiles to achieve a bonus.

This score value is added to the total score and the corresponding label's string is updated. However, merely setting the string to reflect the new score is not enough in today's games. It is very vital to get the users' attention and remind them that they did something cool or earned something awesome. Thus, we run a simple and subtle scale-up/scale-down animation on the score label. Notice how the ease actions are used here. This results in a heartbeat effect on the otherwise simple scaling animation.

We notify the score achieved in each move to the user using the showScoreText function:

// this function can be used to display any message to the user
// but we will use it to display the score for each move
showScoreText:function(scoreToAdd, point){
  // create the label with the score & place it at the respective point
  var bonusLabel = cc.LabelTTF.create("+" + scoreToAdd, "Comic Sans MS", 32);
  bonusLabel.setPosition(point);
  // initially scale it down completely
  bonusLabel.setScale(0);
  // give it a yellow colour
  bonusLabel.setColor(cc.YELLOW);
  this.addChild(bonusLabel, 10);
 
  // animate the bonus label so that it scales up with a nice easing effect
  bonusLabel.runAction( cc.Sequence.create(cc.EaseBackOut.create(cc.ScaleTo.create(0.125, 1)),
  cc.DelayTime.create(1),
  // it should stay on screen so the user can read it
  cc.EaseBackIn.create(cc.ScaleTo.create(0.125, 0))
  // scale it back down with a nice easing effect
  cc.RemoveSelf.create(true) ));
  // its task is finished, so remove it with cleanup
},

The preceding function can be used to display any kind of text notification to the user. For the purpose of our game, we will use it only to display the score in each move. The function is quite simple and precise. It creates a label with the string passed as a parameter and places it at the position passed as a parameter. This function also animates the text so it scales up with some easing, stays for some time so the user registers it, scales down again with easing, and finally removes the text.

It seems as if we have almost finished our first game, but there is still a vital aspect of this game that is missing—the timer. What was the point of running a scheduler every second? Well let's take a look.

Updating the timer

We scheduled the timer as soon as the countdown animation had finished by calling the updateTimer function every second, but what exactly are we doing with this updateTimer function?

Let's take a look at the code:

updateTimer:function(){
  // this is called every second so reduce the time left by 1
  this.time --;
  // update the time left label
  this.timeLabel.setString("Time:" + this.time);
 
  // the user's time is up
  if(this.time<= 0)
  {
    // game is now over
    this.isGameOver = true;
    // unschedule the timer
    this.unschedule(this.updateTimer);
    // stop animating the time label
    this.timeLabel.stopAllActions();
    // disable touch
    this.setTouchEnabled(false);
    // disable the pause button
    this.pauseButton.setEnabled(false);
    // display the game over popup
    this.showGameOverPopup();
  }
  else if(this.time == 5)
  {
    // get the user's attention...there are only 5 seconds left
    // make the timer label scale up and down so the user knows the game is about to end
    // use the ease functions to create a heart beat effect
    var timeUp = cc.Sequence.create(cc.EaseSineIn.create(cc.ScaleTo.create(0.125, 1.1)), cc.EaseSineOut.create(cc.ScaleTo.create(0.125, 1)));
    // repeat this action forever
    this.timeLabel.runAction(cc.RepeatForever.create(timeUp));
  }
},

At the start of the function, the time variable is decremented and the respective label's string is updated. Once the time is up, the isGameOver flag is enabled. We don't need the scheduler to call the updateTimer function anymore, so we unschedule it. We disable touch on the GameWorld layer and disable the pause button. Finally, we show a game-over popup.

We add a little more fun into the game by rapidly scaling up and down the time label when there are 5 seconds or less left in the game. Again, the ease actions are used cleverly to create a heartbeat effect. This will not only inform the users that the game is about to end, but also get them to hurry up and score as many points as possible.

This completes the flow of the game. The only thing missing is the pause popup, which is created in the showPausePopup function that gets called when the handler for the pauseButton object is executed. Both the pause and game-over popups contain two or more buttons that serve to restart the game or navigate to the main menu. The logic for creating these popups is pretty simplistic, so we won't spend time going over the details. Also, there are a few cool things to look at in the code for the MainMenu class in the mainmenu.js file. Some liveliness and dynamics have been added to an otherwise static screen. You should refer to your code bundle for the implementation.

 

Summary


You've learned a lot in this chapter by building ColourSmash. By now, you should be familiar with the structure and relationship of the cc.Scene and cc.Layer. This same structure will be used in the forthcoming projects, both in Cocos2d-html5 and Cocos2d-x.

We also made extensive use of a wide variety of actions, thereby understanding how powerful they make the Cocos2d-x engine. You have also understood the importance of easing while using actions. You learned how to register touches and how to run simple schedulers. There is still a lot to learn and a lot more fun ahead.

Our next game is similar to an extremely famous game that took the world by storm. You're surely in for a treat!

About the Author

  • Karan Sequeira

    Karan Sequeira is a budding game developer with 4 years of experience in the mobile game development industry. He started out as a JavaScript programmer, developing HTML5 games, and fell in love with C++ when he moved to Cocos2d-x. He has a handful of games on the iOS and Android app stores and is currently working for an organization in the in-flight entertainment industry. He is extremely passionate about learning new technologies and dreams about building an artificially intelligent super computer that can fall in love.

    Browse publications by this author

Latest Reviews

(2 reviews total)
Good
The book touches upon some of the most important aspects of Cocos2dx. As a pro game developer, one needs to be aware of such capabilities of a worthy game engine, such as Cocos2dx. Though the book lacks in an depth approach to each particular situation, but in that case it'd more of a gospel than a book. ;D . So anyone looking forth to developing with Cocos2dx can sufficiently bank upon this book.