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:
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:
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.
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:
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.
Now that we have defined the background and play area, let's create the tiles and their respective sprites. The code is as follows:
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:
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:
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:
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:
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.
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:
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 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.
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:
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.
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:
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:
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:
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.
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:
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
:
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:
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:
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:
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.
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:
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.