- Cocos2d-x Game Development Blueprints
- Karan Sequeira
- 1799字
- 2021-07-16 13:47:57
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:

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.
- Python科學計算(第2版)
- Visual Basic程序設計(第3版):學習指導與練習
- Julia機器學習核心編程:人人可用的高性能科學計算
- AngularJS深度剖析與最佳實踐
- 人臉識別原理及算法:動態人臉識別系統研究
- Building Minecraft Server Modifications
- Hands-On Natural Language Processing with Python
- Java程序設計
- Python Data Analysis Cookbook
- C/C++程序員面試指南
- Python全棧數據工程師養成攻略(視頻講解版)
- 從Excel到Python數據分析:Pandas、xlwings、openpyxl、Matplotlib的交互與應用
- LabVIEW數據采集
- Keil Cx51 V7.0單片機高級語言編程與μVision2應用實踐
- Eclipse開發(學習筆記)