Book Image

Cocos2d-X Game Development Blueprints

By : Karan Sequeira
Book Image

Cocos2d-X Game Development Blueprints

By: Karan Sequeira

Overview of this book

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

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.