On to the game world

You already set the environment up in the last two sections. Now we need to code in some gameplay. So we add towers that the dragon must dodge and add some gravity as well as touch controls. All this action happens in our second scene, which goes by the name GameWorld and is defined in the gameworld.js file. The following are the member variables of gameworld.js:

// variables
screenSize:null,
spriteBatchNode:null,
score:0,
scoreLabel:null,
mustAddScore:false,
tutorialSprite:null,
popup:null,
castleRoof:0,
hasGameStarted:false,
// managers
towerManager:null,
dragonManager:null,
fairytaleManager:null,

You might remember some of the variables declared in the preceding code from the previous chapter. In addition, you can see some variables that record the position of the castle roof, if the game has started and if a score needs to be added. Finally, you will find three variables that will reference our respective managers.

In our first game, we coded all the game logic straight into the GameWorld class. This time, we will create separate manager classes for each feature of the game. We have already discussed the FairytaleManager. Soon, we'll discuss the TowerManager and DragonManager classes. The init function of GameWorld is as follows:

init:function () {
  this._super();
  // enable touch
  this.setTouchEnabled(true);
  // store screen size for fast access
  this.screenSize = cc.Director.getInstance().getWinSize();
 
  // create and add the batch node
  this.spriteBatchNode = cc.SpriteBatchNode.create(s_SpriteSheetImg, 256);
  this.addChild(this.spriteBatchNode, E_ZORDER.E_LAYER_BG + 1);
 
  // set the roof of the castle
  this.castleRoof = 100;
  // create & init all managers
  this.towerManager = new TowerManager(this);
  this.towerManager.init();
  this.dragonManager = new DragonManager(this);
  this.dragonManager.init();
  this.fairytaleManager = new FairytaleManager(this);
  this.fairytaleManager.init();
  this.createHUD();
 
  this.scheduleUpdate();
  return true;
},

First and foremost, enable touch and create the batch node into which you will add all your game's sprites. Next, set the castleRoof variable to 100. This means the roof of the castle is considered to be 100 pixels from the bottom of the screen. Then, you create and initialize the three main managers, create the HUD, and schedule the update function.

The HUD for this game is quite simple. You can find the logic for it in the createHUD function. It consists of a score label and a sprite for the tutorial that looks like this:

On to the game world

The core gameplay

The core gameplay that will take place in our GameWorld scene consists of the following steps:

  • Create
    • Creating the dragon
    • Creating the towers
    • Creating the fairy tale environment
  • Update
    • Applying gravity and force to the dragon
    • Scrolling towers and keep them coming
    • Updating the fairy tale environment
  • Collision detection

Creating the dragon

Let's define a few global "constants" that we will use repeatedly and the constructor of DragonManager in the dragonManager.js file:

var MAX_DRAGON_SPEED = -40;
var FLAP_FORCE = 13;
var ANIMATION_ACTION_TAG = 123;
var MOVEMENT_ACTION_TAG = 121;

function DragonManager(gameWorld)
{
  // save reference to GameWorld
  this.gameWorld = gameWorld;
  this.screenSize = gameWorld.screenSize;
  // initialise variables
  this.dragonSprite = null;
  this.dragonSpeed = cc.POINT_ZERO;
  this.dragonPosition = cc.POINT_ZERO;
  this.mustApplyGravity = false;
}

The constructor maintains a reference to GameWorld and screenSize, and it initializes the variables needed to create and update the dragon. Notice how mustApplyGravity has been set to false. This is because we don't want the dragon crashing into the castle walls as soon as the game starts. We wait for the user's touch before applying gravity.

Let's take a look at the following code:

DragonManager.prototype.init = function() {
  // create sprite and add to GameWorld's sprite batch node
  this.dragonSprite = cc.Sprite.createWithSpriteFrameName("dhch_1");
  this.dragonPosition = cc.p(this.screenSize.width * 0.2, this.screenSize.height * 0.5);
  this.dragonSprite.setPosition(this.dragonPosition);
  this.gameWorld.spriteBatchNode.addChild(this.dragonSprite, E_ZORDER.E_LAYER_PLAYER);
 
  // fetch flying animation from cache & repeat it on the dragon's  sprite
  var animation = cc.AnimationCache.getInstance().getAnimation("dragonFlying");
  var repeatedAnimation = cc.RepeatForever.create(cc.Animate.create(animation));
  repeatedAnimation.setTag(ANIMATION_ACTION_TAG);
  this.dragonSprite.runAction(repeatedAnimation);
  .
  .
  .
};

The init function is responsible for creating the dragon's sprite and giving the default hovering motion that we saw on the MainMenu screen. So, dragonSprite is created and added to spriteBatchNode of the GameWorld and positioned appropriately. We then fetch the flying animation from the cc.AnimationCache and run it repeatedly on dragonSprite. Consequently, the hovering motion runs just like on the MainMenu screen.

Setting tags for actions is a great way to avoid maintaining references to the various actions you may have running on a node. This way, you can get the object of a particular action by calling yourNode.getActionByTag(actionsTag) on the node running the action. You can also use yourNode.stopActionByTag(actionsTag) to stop the action without a reference to the respective action.

Creating the towers

At the top of the towerManager.js file, we will create a small Tower object to maintain the upper and lower sprites for the tower as well as its position:

function Tower(position)
{
    this.lowerSprite = null;
    this.upperSprite = null;
    this.position = position;
}

Next, we define the constructor of TowerManager as follows:

var VERT_GAP_BWN_TOWERS = 300;
function TowerManager(gameWorld)
{
  // save reference to GameWorld
  this.gameWorld = gameWorld;
  this.screenSize = gameWorld.screenSize;
  // initialise variables
  this.towers = [];
  this.towerSpriteSize = cc.SIZE_ZERO;
  this.firstTowerIndex = 0;
  this.lastTowerIndex = 0;
}

The upper and lower sprites for each tower will be separated vertically so that the user can help the dragon pass through. This gap is represented by VERT_GAP_BWN_TOWERS. The TowerManager constructor maintains a reference to GameWorld and records the value of screenSize. It also initializes the towers' array and the size for a tower's sprite. The last two variables are convenience variables that will point to the first and last tower, respectively.

Let's take a look at the following code:

TowerManager.prototype.init = function() {
  // record size of the tower's sprite
  this.towerSpriteSize = cc.SpriteFrameCache.getInstance().getSpriteFrame("opst_02").getOriginalSize();
 
  // create the first pair of towers
  // they should be two whole screens away from the dragon
  var initialPosition = cc.p(this.screenSize.width*2, this.screenSize.height*0.5);
  this.firstTowerIndex = 0;
  this.createTower(initialPosition);
  // create the remaining towers
  this.lastTowerIndex = 0;
  this.createTower(this.getNextTowerPosition());
  this.lastTowerIndex = 1;
  this.createTower(this.getNextTowerPosition());
  this.lastTowerIndex = 2;
};

We start by recording the size of a tower's sprite. Then, the first tower is placed a good distance away from the dragon. You don't want to overwhelm users as soon as they hit play. The distance in this case is two times the screens width. We then create three towers by calling the createTower function and passing in a position calculated in the getNextTowerPosition function. Observe how the firstTowerIndex and lastTowerIndex variables are set. Don't worry, you'll understand this soon.

Let's take a look at the following code:

TowerManager.prototype.createTower = function(position) {
  // create a new tower and add it to the array
  var tower = new Tower(position);
  this.towers.push(tower);

  // create lower tower sprite & add it to GameWorld's batch node
  tower.lowerSprite = cc.Sprite.createWithSpriteFrameName("opst_02");
  tower.lowerSprite.setPositionX(position.x);
  tower.lowerSprite.setPositionY( position.y + VERT_GAP_BWN_TOWERS * -0.5 + this.towerSpriteSize.height * -0.5 );
  this.gameWorld.spriteBatchNode.addChild(tower.lowerSprite, E_ZORDER.E_LAYER_TOWER);

  // create upper tower sprite & add it to GameWorld's batch node
  tower.upperSprite = cc.Sprite.createWithSpriteFrameName("opst_01");
  tower.upperSprite.setPositionX(position.x);
  tower.upperSprite.setPositionY( position.y + VERT_GAP_BWN_TOWERS * 0.5 + this.towerSpriteSize.height * 0.5 );
  this.gameWorld.spriteBatchNode.addChild(tower.upperSprite, E_ZORDER.E_LAYER_TOWER);
};

First, you create a Tower object and push it into the towers' array. You then create the lower and upper sprites for the tower. Both lower and upper sprites will have the same x coordinate but different y coordinates. The bit of arithmetic here basically creates a vertical gap between the towers and adjusts them according to their anchor points. Finally, the sprites are added into spriteBatchNode of GameWorld. Now, let's spend some time understanding the getNextTowerPosition function, as this will be used to dynamically generate the positions of the towers making our dragon's journey that much more challenging and fun.

Let's take a look at the following code:

TowerManager.prototype.getNextTowerPosition = function() {
  // randomly select either above or below last tower
  var isAbove = (Math.random() > 0.5);
  var offset = Math.random() * VERT_GAP_BWN_TOWERS * 0.75;
  offset *= (isAbove) ? 1:-1;

  // new position calculated by adding to last tower's position
  var newPositionX = this.towers[this.lastTowerIndex].position.x + this.screenSize.width*0.5;
  var newPositionY = this.towers[this.lastTowerIndex].position.y + offset;

  // limit the point to stay within 30-80% of the screen
  if(newPositionY >= this.screenSize.height * 0.8)
    newPositionY -= VERT_GAP_BWN_TOWERS;
  else if(newPositionY <= this.screenSize.height * 0.3)
    newPositionY += VERT_GAP_BWN_TOWERS;

  // return the new tower position
  return cc.p(newPositionX, newPositionY);
};

First, we choose whether the tower calling this function should be positioned above or below the last tower. Then, an offset or gap is calculated with a random factor of VERT_GAP_BWN_TOWERS. This offset is added to the last tower's y coordinate and half of the screen's width is added to the last tower's x coordinate to get the position for the next tower. By last tower I mean the tower that is currently most to the right or last in line. Finally, we clamp the y coordinate to stay between 30-80 percent of the screen. Otherwise, we would have to fly our dragon out of the screen or straight into the castle. I'm sure the dragon would not like the latter.

I shall skip the environment creation since we have already covered it for the MainMenu screen.

The update loop

The game will be updated after every tick in the update function of GameWorld because we called scheduleUpdate in the init function of GameWorld. This is where we need to call the update functions of our respective manager classes. The code is as follows:

update:function(deltaTime) {
  // update dragon
  this.dragonManager.update();
  // update towers only after game has started
  if(this.hasGameStarted)
  this.towerManager.update();
  // update environment
  this.fairytaleManager.update();
  this.checkCollisions();
},

We must update all our managers and check collisions after every tick. That is what our update loop will consist of. Notice how the update function of the TowerManager class is called only once the game has started. This is because we still want the environment and the dragon to be active while users comprehend the tutorial. But we don't want the towers to start appearing before users have had enough time to understand what they need to do.

Updating the dragon

The update function of the DragonManager class will be responsible for applying gravity, updating the dragon's position, and checking for collisions between the dragon and the roof of the castle.

Let's take a look at the code:

DragonManager.prototype.update = function() {
  // calculate bounding box after applying gravity
  var newAABB = this.dragonSprite.getBoundingBox();
  newAABB.setY(newAABB.getY() + this.dragonSpeed.y);

  // check if the dragon has touched the roof of the castle
  if(newAABB.y <= this.gameWorld.castleRoof)
  {
    // stop downward movement and set position to the roof of the castle
    this.dragonSpeed.y = 0;
    this.dragonPosition.y = this.gameWorld.castleRoof + newAABB.getHeight() * 0.5;

    // dragon must die
    this.dragonDeath();
    // stop the update loop
    this.gameWorld.unscheduleUpdate();
  }
  // apply gravity only if game has started
  else if(this.mustApplyGravity)
  {
    // clamp gravity to a maximum of MAX_DRAGON_SPEED & add it
    this.dragonSpeed.y = ( (this.dragonSpeed.y + GRAVITY) < MAX_DRAGON_SPEED ) ? MAX_DRAGON_SPEED : (this.dragonSpeed.y + GRAVITY);
  }

  // update position
  this.dragonPosition.y += this.dragonSpeed.y;
  this.dragonSprite.setPosition(this.dragonPosition);
};

We start by calling the getBoundingBox() function of the dragonSprite class that will return a cc.Rect to represent the sprite's bounding box. We use this bounding box to check for collisions with the roof of the castle. If a collision has occurred, we stop the dragon from falling and instead position it right on top of the castle roof. We then tell the dragon to die by calling dragonDeath and unscheduling the update function of the GameWorld class. If no collision is found, the game should continue normally. So, apply some gravity to the dragon's speed. Finally, update the dragon's position based on the speed.

Updating the towers

The update function of the TowerManager class is responsible for scrolling the towers from right to left and repositioning them once they leave the left edge of the screen. The code is as follows:

TowerManager.prototype.update = function(){
  var tower = null;
  for(var i = 0; i < this.towers.length; ++i)
  {
    tower = this.towers[i];
    // first update the position of the tower
    tower.position.x -= MAX_SCROLLING_SPEED;
    tower.lowerSprite.setPosition(tower.position.x, tower.lowerSprite.getPositionY());
    tower.upperSprite.setPosition(tower.position.x, tower.upperSprite.getPositionY());
 
    // if the tower has moved out of the screen, reposition them at the end
    if(tower.position.x < this.towerSpriteSize.width * -0.5)
    {
      this.repositionTower(i);
      // this tower now becomes the tower at the end
      this.lastTowerIndex = i;
      // that means some other tower has become first
      this.firstTowerIndex = ((i+1) >= this.towers.length) ? 0:(i+1);
    }
  }
};

The update function of the TowerManager class is quite straightforward. You start by moving each tower MAX_SCROLLING_SPEED pixels to the left. If a tower has gone outside the left edge of the screen, reposition it at the right edge. Pay attention to how the lastTowerIndex and firstTowerIndex variables are set. The last tower is important to us because we need to know where to place subsequent towers. The first tower is important to us because we need it for collision detection.

Collision detection

Our dragon would really love to just keep flying and not run into anything, but that doesn't mean we don't check for collisions. The code is as follows:

checkCollisions:function() {
  // first find out which tower is right in front
  var frontTower = this.towerManager.getFrontTower();

  // fetch the bounding boxes of the respective sprites
  var dragonAABB = this.dragonManager.dragonSprite.getBoundingBox();
  var lowerTowerAABB = frontTower.lowerSprite.getBoundingBox();
  var upperTowerAABB = frontTower.upperSprite.getBoundingBox();

  // if the respective rects intersect, we have a collision
  if(cc.rectIntersectsRect(dragonAABB, lowerTowerAABB) || cc.rectIntersectsRect(dragonAABB, upperTowerAABB))
  {
    // dragon must die
    this.dragonManager.dragonDeath();
    // stop the update loop
    this.unscheduleUpdate();
  }
  else if( Math.abs(cc.rectGetMidX(lowerTowerAABB) - cc.rectGetMidX(dragonAABB)) <= MAX_SCROLLING_SPEED/2 )
  {
    // increment score once the dragon has crossed the tower
    this.incrementScore();
  }
},

Since the dragon can only collide with the tower that is in the front, there is no point checking for collisions with all the towers. After asking the TowerManager class for the tower in the front, we proceed to read the bounding box rectangles for the dragon and the tower's lower and upper sprites. We then check for an intersection between the dragon and the tower's rectangles. If a collision is found, we tell the dragon to die by calling the dragonDeath method of the DragonManager class and unscheduling the update function of the GameWorld class. If the dragon manages to clear the tower, we increment the score by one in the incrementScore function.

Flying the dragon

Now that we have created our dragon, the towers and the environment we are ready to begin playing when the user taps the screen. We record touches in the onTouchesBegan function as follows:

onTouchesBegan:function (touches, event) {
  this.hasGameStarted = true;
  // remove the tutorial only if it exists
  if(this.tutorialSprite)
  {
    // fade it out and then remove it
    this.tutorialSprite.runAction(cc.Sequence.create(cc.FadeOut.create(0.25), cc.RemoveSelf.create(true)));
    this.tutorialSprite = null;
  }
  // inform DragonManager that the game has started
  this.dragonManager.onGameStart();
  // fly dragon...fly!!!
  this.dragonManager.dragonFlap();
},

Right at the beginning of the function, we set the hasGameStarted flag to true. Remember how we need this flag to be true in order to call the update function of the TowerManager class? We proceed to fade out and remove the tutorial sprite. The if condition is there to prevent the tutorial sprite from being removed repeatedly on every touch. We must also inform the DragonManager class that the game has begun so that it can start applying gravity to the dragon. Finally, every touch must push the dragon a bit upwards in the air, so we call the dragonFlap function of the DragonManager class, as shown here:

DragonManager.prototype.dragonFlap = function() {
  // don't flap if dragon will leave the top of the screen
  if(this.dragonPosition.y + FLAP_FORCE >= this.screenSize.height)
  return;

  // add flap force to speed
  this.dragonSpeed.y = FLAP_FORCE;

  cc.AudioEngine.getInstance().playEffect(s_Flap_mp3);
};

Our dragon is really charming, so we won't ever let him go off the screen. Hence, we return from this function if a flap will cause the dragon to exit the top of the screen. If all is okay, we simply add some force to the vertical component of the dragon's speed. That is pretty much all it takes to simulate the simplest form of gravity on an object. We call the playEffect function to play an effect. We will discuss HTML5 audio in our last section. For now, all you need to know is that the engine takes care of playing the sound for us so that it works almost everywhere.

Farewell dear dragon

Well, a collision has occurred and our dragon must now miserably fall to his death. If only you had played better and helped him through a few more towers. Let's see how our dragon dies in the dragonDeath function:

DragonManager.prototype.dragonDeath = function() {
  // fall miserably to the roof of the castle
  var rise = cc.EaseSineOut.create(cc.MoveBy.create(0.25, cc.p(0, this.dragonSprite.getContentSize().height)));
  var fall = cc.EaseSineIn.create(cc.MoveTo.create(0.5, cc.p(this.screenSize.width * 0.2, this.gameWorld.castleRoof)));
  // inform GameWorld that dragon is no more :(
  var finish = cc.CallFunc.create(this.gameWorld.onGameOver, this.gameWorld);
  // stop the frame based animation...dragon can't fly once its dead
  this.dragonSprite.stopAllActions();
  this.dragonSprite.runAction(cc.Sequence.create(rise, fall, finish));

  cc.AudioEngine.getInstance().playEffect(s_Crash_mp3);
};

The preceding function is called when the dragon touches the castle roof in the update function of the DragonManager class and also when the dragon collides with a tower in the checkCollisions function of the GameWorld class. At this stage, the update function of the GameWorld class has been unscheduled. So we animate the dragon to fall to the castle roof with some easing and call the onGameOver function of the GameWorld class after that has happened. We also play a horrendous sound effect.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.42.134