- Cocos2d-x Game Development Blueprints
- Karan Sequeira
- 3538字
- 2021-07-16 13:47:55
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:
- Finding the tile/s to be removed
- Removing the tile/s with an awesome effect
- Finding and shifting the tiles above into the recently vacated space with an awesome effect
- Adding new tiles
- Adding score and bonus
- 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.
- 算法零基礎(chǔ)一本通(Python版)
- Magento 2 Theme Design(Second Edition)
- Windows Presentation Foundation Development Cookbook
- Learning Laravel 4 Application Development
- Windows Phone 7.5:Building Location-aware Applications
- 細(xì)說Python編程:從入門到科學(xué)計(jì)算
- 代替VBA!用Python輕松實(shí)現(xiàn)Excel編程
- Access 2010數(shù)據(jù)庫(kù)應(yīng)用技術(shù)實(shí)驗(yàn)指導(dǎo)與習(xí)題選解(第2版)
- IDA Pro權(quán)威指南(第2版)
- Struts 2.x權(quán)威指南
- Magento 2 Beginners Guide
- Java高級(jí)程序設(shè)計(jì)
- MySQL數(shù)據(jù)庫(kù)應(yīng)用實(shí)戰(zhàn)教程(慕課版)
- Building Apple Watch Projects
- 趣學(xué)數(shù)據(jù)結(jié)構(gòu)