Tutorial: Creating a tank battle style game with Cocos Creator (Part 2)
Note: Part 1 of this tutorial is located here.
Introduction and getting help
You are welcome to post to the Cocos Creator category on the Cocos forums to talk about any issues you encounter during the development process. In this tutorial, you will start to learn to write a relatively complete tank war game. Please prepare your development environment before starting this tutorial. If you are having trouble with our tutorial, please take a look at the following documentation:
Preparing the development environment
Let’s begin
1. Write a script to work with game data
We need to first create a script that will read and write our games data. By reading the game data, we can know the current game progress, character status, etc., and by writing data, we can control the character to perform tasks, save the game progress and so on.
First, create a gameData folder in the ./assets/scripts/
directory, mainly for saving game data scripts. Second, create a GameConfig.js
script, under the gameData
folder, which will be used to save the games configuration data. Example:
var GameConfig = {
PlayerNum: 1, // number of players
};
module.exports = GameConfig;
The module.exports
statement is scripting code for the Javascript language that allows other scripts to read and write data from other scripts. You can read more about this in the Modular Script documentation.
Create a GameConst.js
script under the gameData folder. This script is mainly used to record various constant data in the game. Example code:
var GameEnum = require("./GameEnum");
var GameConst = {
GidToTileType:[
GameEnum.TileType.tileNone,
GameEnum.TileType.tileNone, GameEnum.TileType.tileNone, GameEnum.TileType.tileGrass, GameEnum.TileType.tileGrass, GameEnum.TileType.tileSteel, GameEnum.TileType.tileSteel,
GameEnum.TileType.tileNone, GameEnum.TileType.tileNone, GameEnum.TileType.tileGrass, GameEnum.TileType.tileGrass, GameEnum.TileType.tileSteel, GameEnum.TileType.tileSteel,
GameEnum.TileType.tileWall, GameEnum.TileType.tileWall, GameEnum.TileType.tileRiver, GameEnum.TileType.tileRiver, GameEnum.TileType.tileKing, GameEnum.TileType.tileKing,
GameEnum.TileType.tileWall, GameEnum.TileType.tileWall, GameEnum.TileType.tileRiver, GameEnum.TileType.tileRiver, GameEnum.TileType.tileKing, GameEnum.TileType.tileKing,
GameEnum.TileType.tileKing, GameEnum.TileType.tileKing, GameEnum.TileType.tileNone, GameEnum.TileType.tileNone, GameEnum.TileType.tileNone, GameEnum.TileType.tileNone,
GameEnum.TileType.tileKing, GameEnum.TileType.tileKing, GameEnum.TileType.tileNone, GameEnum.TileType.tileNone, GameEnum.TileType.tileNone, GameEnum.TileType.tileNone
],
Dirction: ["up","left","down","right"],
DirctionRex: [/up/,/left/,/down/,/right/],
EnemyTankTypes: [
{
name: "armor",
score: 500,
speed: 0.4
},
{
name: "fast",
score: 250,
speed: 0.2
},
{
name: "normal",
score: 100,
speed: 0.4
}
],
armorTankNum: 4,
fastTankNum: 3,
normalTankNum: 2,
PlayerTankReviveTimes: 5,
EnemyTankAmount: 20
};
module.exports = GameConst;
Next, create a GameEnum.js
script under the gameData folder. This script will be mainly used to record various enumeration data in the game. Example code:
var GameEnum = {
TankFlag: cc.Enum({
Player: 0,
Enemy: 1
}),
TileType: cc.Enum({
tileNone: 0,
tileGrass: 1,
tileSteel: 2,
tileWall: 3,
tileRiver: 4,
tileKing: 5
}),
TileGroup: cc.Enum({
default: 0,
playerBullet: 1,
playerTank: 2,
enemyTank: 3,
enemyBullet: 4
})
};
module.exports = GameEnum;
In Cocos Creator, open the menu Project → Project Settings…, select the Group Manager interface, make the following settings changes and click Save when finished:
cc.game.groupList
is responsible for recording the group data in this game.
2. Layout of the game start interface
First, preview the game start interface as it is currently:
Use the following image to set the Canvas component of the Canvas node:
Set the game start scene according to the design resolution. Add a node by referring to the node tree in the following figure:
The name of the Atlas resource is required for this interface to start.
Create Start.js
in the ./assets/
directory and add it to the Canvas node. This script is primarily responsible for handling user actions on the game start interface. Example code:
var GameConfig = require("./gameData/GameConfig");
cc.Class({
extends: cc.Component,
properties: {
gamePlayerCount: {
default: null,
type: cc.ToggleContainer
},
},
start () {
this.initGameLoginView();
},
// initialize the game start interface
initGameLoginView () {
switch (GameConfig.PlayerNum) {
case 1:
this.gamePlayerCount.node.getChildByName("Toggle OnePlayer").getComponent(cc.Toggle).isChecked = true;
break;
case 2:
console.log(this.gamePlayerCount.node);
this.gamePlayerCount.node.getChildByName("Toggle DoublePlayer").getComponent(cc.Toggle).isChecked = true;
break;
}
},
// Select a callback for the number of players
onGamePlayerNumToggleChecked (event, CoustomEventData) {
switch (CoustomEventData) {
case "onePlayer":
GameConfig.PlayerNum = 1;
break;
case "doublePlayer":
GameConfig.PlayerNum = 2;
break;
}
},
});
The Property Inspector interface binding for this user component:
The Property Inspector interface for binding the Button component to the Button PlayGame node will look like this:
The script events shown will be explained in Section 4.
The Property Inspector interface for binding the Toggle component to the Toggle OnePlayer node will look like this:
The Property Inspector interface for the Toggle DoublePlayer node will look like this:
Adding the CCBlockInputEvents component to the Nodes Login Game node prevents accidental access to the game’s battle interface. The Property Inspector panel will look like this:
After the above steps are complete, the starting interface for this game is complete. We can move on to the next steps.
3. Write an object pool management script
During this game, there will be constant demand for node destruction and creation. This includes the creation and destruction of bullets. Node creation and destruction each have a series of functions that need to be executed in-order to completely erase the data of the node object. Executing this code affects game performance. When a large number of nodes are repeatedly created and destroyed, game performance is degraded. In Cocos Creator, the CCNodePool
object pool module is designed to provide a solution to this problem. When it is used, node creation and destruction will not be completely executed. When the node is removed from the scene graph, the node is also cached. In the node pool, the next time you use it, the performance is much faster than before because of the caching that happens. Create NodePoolManager.js
in the ./assets/scripts/components
directory. Use the CCNodePool
in the same manner as demonstrated here:
let NodePoolManager = {
_nodePools:[],
_nodePoolNames:["player_bullet","player_tank","enemy_tank","enemy_bullet"],
initNoedPools: function () {
for (let i = 0; i < this._nodePoolNames.length; i++) {
this.createNodePool(this._nodePoolNames[i]);
}
},
createNodePool: function (name){
if (!this.getNodePool(name) && !this.getNodeElement(name)) {
let nodePool = new cc.NodePool(name);
this._nodePools.push(nodePool);
return nodePool;
}
else {
return null;
}
},
getNodePool: function (name) {
if (this._nodePools.length > 0) {
for (let i = 0; i < this._nodePools.length; i++) {
if (this._nodePools[i].poolHandlerComp === name) {
return this._nodePools[i];
}
}
return null;
}
},
getNodeElement (name) {
let nodePool = this.getNodePool(name);
if (nodePool) {
let nodeElement = nodePool.get();
return nodeElement;
}
else {
return null;
}
},
putNodeElemenet (name, element) {
let nodePool = this.getNodePool(name);
if (nodePool) {
nodePool.put(element);
}
},
};
module.exports = NodePoolManager;
4. Initializing the battle scene
Create a Nodes Play GameP node under the Canvas node, which will mainly be responsible for rendering the core battle interface. The page layout and the node tree for this node is as follows:
After creating the nodes and adjusting the layout of the interface, create Game.js
in the ./assets/
directory and add it to the Canvas node. This script is mainly used to perform operations, such as, game initialization, processing game flow, and processing player input and output. Example code, we also write the property parameters that the project will use in the script:
var GameEnum = require("./gameData/GameEnum");
var GameConst = require("./gameData/GameConst");
var GameConfig = require("./gameData/GameConfig");
var NodePoolManager = require("./components/NodePoolManager");
cc.Class({
extends: cc.Component,
properties: {
loginGame: {
default: null,
type: cc.Node
},
playGame: {
default: null,
type: cc.Node
},
tankWarMap:{
default: null,
type: cc.TiledMap
},
tankSpriteAtlas: {
default: null,
type: cc.SpriteAtlas
},
anyTank: {
default: null,
type: cc.Prefab
},
bullet: {
default: null,
type: cc.Prefab
},
gameMenu: {
default: null,
type: cc.Node
},
enemyTankBornPosition: {
default: [],
type: cc.Vec2
},
playerTankBornPosition: {
default: [],
type: cc.Vec2
},
_initialRound: 1,
_enemyTankAmount: 0,
_playerTankReviveTimes: 0,
_playing: false,
_playerTank: [],
_gameScore: 0,
},
});
The Property Inspector shows how to bind this component:
The resource path of Node AnyTank is ./assets/res/prefab/Node AnyTank.prefab
, and the Property Inspector panel of the layout and the CCSprite
components is as follows:
The resource path for Node Battle is ./assets/res/prefab/Node Buttle.prefab
, and the Property Inspector panel for layout and the CCSprite
components is as follows:
Add the onPlayGameButtonClicked
function to Game.js
to start the click event for the Button PlayGame button. Example:
onPlayGameButtonClicked () {
// Hide the game start interface
this.loginGame.opacity = false;
this.loginGame.y += this.node.height;
// Display the game battle interface
this.playGame.opacity = 255;
// Play the game start sound, SoundManager.js will be written in Part 5
this.node.getComponent("SoundManager").playStartGameEffectSound();
// Perform game initialization
this.initGame();
// Execute the game to start
this.startGame();
},
Next, the initGame
function can be created as:
initGame() {
// object pool initialization
NodePoolManager.initNoedPools();
},
Next, the startGame
function can be created as:
startGame () {
// Mark the game to start fighting
this._playing = true;
// Initialize the game menu information
this.initGameMenuInfo();
// Empty the tank
this.clearTanks();
// According to the size of EnemyTankAmount to generate the corresponding number of enemy player tanks
for (let i = 0; i < GameConst.EnemyTankAmount; i++) {
// Only generate two enemy tanks at a time
if (i < 2) {
// Generate enemy tanks at the specified location
this.createEnemyTank(this.enemyTankBornPosition[i]);
}
}
// According to the size of PlayerNum to generate the corresponding
// number of player tanks
for (let i = 0; i < GameConfig.PlayerNum; i++) {
// Generate player tank in the specified location
let playerTank = this.createPlayerTank(this.playerTankBornPosition[i]);
// Save the object data of the player tank to _playerTank
this._playerTank.push(playerTank);
}
// Initialize the game listener
this.initListenHandlers();
},
Next, the initGameMenuInfo
function can be created as:
initGameMenuInfo() {
this.gameMenu.getChildByName("Label Tank Remaining Number").getComponent(cc.Label).string = this._enemyTankAmount = GameConst.EnemyTankAmount;
this.gameMenu.getChildByName("Label Revive Times Number").getComponent(cc.Label).string = this._playerTankReviveTimes = GameConst.PlayerTankReviveTimes;
},
Next, the clearTanks
function can be created as:
clearTanks () {
this._playerTank = [];
for (let i = 0; i < this.tankWarMap.node.getChildByName("tank").childrenCount; i++) {
NodePoolManager.putNodeElemenet(this.tankWarMap.node.getChildByName("tank").children[i].group, this.tankWarMap.node.getChildByName("tank").children[i]);
}
}
Next, the createEnemyTank
function can be created as:
createEnemyTank (position) {
// Determine whether the game is in the battle process
if (this.node.getComponent("Game")._playing) {
// Get the tank node object in the cache from the object pool
this.enemyTank = NodePoolManager.getNodeElement(cc.game.groupList[3]);
// Create a new tank node object when there are no available tank node
// objects in the cache
if (!this.enemyTank) {
this.enemyTank = cc.instantiate(this.anyTank);
}
// Place the node at the specified coordinates
this.enemyTank.setPosition(position);
// Add the tank node to the map
this.tankWarMap.node.getChildByName("tank").addChild(this.enemyTank);
}
},
Next, the createPlayerTank
function can be created as:
createPlayerTank (position) {
// Determine whether the game is in the battle process
if (this.node.getComponent("Game")._playing) {
// Get the tank node object in the cache from the object pool
this.playerTank = NodePoolManager.getNodeElement(cc.game.groupList[2]);
// Create a new tank node object when there are no available tank node
// objects in the cache
if (!this.playerTank) {
this.playerTank = cc.instantiate(this.anyTank);
}
// Place the node at the specified coordinates
this.playerTank.setPosition(position);
// Add the tank node to the map
this.tankWarMap.node.getChildByName("tank").addChild(this.playerTank);
// will create a successful tank node object is thrown
return this.playerTank;
}
},
Next, the initListenHandlers
function can be created as:
initListenHandlers () {
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onPlayerKeyDownCallback, this);
},
The code for onPlayerKeyDownCallback
is not being fully implemented now. Section 5 will continue to build out this code. For now, use:
onPlayerKeyDownCallback () {},
Before the player’s control logic for the tank is perfected, after executing the above code, the game runs as follows:
While the game battle interface was rendered correctly, the tanks also appeared in the designated positions.
5. Tank initialization and management
In this game, different types of tanks need to be generated at the same time. We will make a base type of tank node into a Prefab resource, and then we add this Prefab the ability to transform types and styles at any time.
First, create TankManager.js
script in the ./assets/scripts/components/tank
directory. Example:
var GameEnum = require("../../gameData/GameEnum");
var GameConst = require("../../gameData/GameConst");
var NodePoolManager = require("../NodePoolManager");
cc.Class({
extends: cc.Component,
editor:{
// executeInEditMode: true
},
properties: {
isAuto: false,
isCanMove: true,
tankSpriteAtlas: {
default: null,
type: cc.SpriteAtlas
},
tankFlag: {
default: GameEnum.TankFlag.Player,
type: GameEnum.TankFlag,
notify: function () {
this.updateTank(this.tankFlag);
}
},
enemyTankSpriteFrames: {
default: [],
type: cc.SpriteFrame
},
playerTankSpriteFrames: {
default: [],
type: cc.SpriteFrame
},
tankDirection: {
default: GameConst.Dirction[0],
notify: function () {
this.updateTankSpriteFrame(this.tankDirection);
}
},
actionSpeed: {
default: 0.05,
type: cc.Float,
range: [0,3,0.01]
},
changeDirectionStep: {
default: 10,
type: cc.Integer,
range: [0,100,1]
},
bullet: {
default: null,
type: cc.Prefab
},
_bornPosition: null,
},
// LIFE-CYCLE CALLBACKS:
// onLoad () {},
start () {
if (!CC_EDITOR) {
// Initialize the automatic walking of the tank
this.initTankAutoActionManager();
}
},
onEnable () {
if (this.node.group === cc.game.groupList[2]) {
// Play the opening animation of the tank
this.node.getComponent(cc.Animation).play();
}
},
// Initialize the various parameters of the tank
initTank(tankFlag, direction, group, auto, position) {
// Set the tank ID (according to GameEnum.TankFlag)
this.tankFlag = tankFlag;
// Set the tank wind direction
this.tankDirection = direction;
// Set the tank node grouping
this.node.group = group;
// Set whether to allow automatic behavior
this.isAuto = auto;
// Record the initial coordinates of the birth of the node
this._bornPosition = position;
// Set the node coordinates
this.node.setPosition(position);
// Change the node sprite map according to the direction of the node
this.updateTankSpriteFrame(direction);
},
// Update the data bound to the tank
updateTank (type) {
// Create a new variable to receive a new sprite map object
var newSpriteFrame = null;
// Only tanks of type Enemy need to be replaced with different textures
if (type === GameEnum.TankFlag.Enemy) {
// Randomly get the serial number of the new map
var spriteFrameIndex = Math.floor(Math.random() * this.enemyTankSpriteFrames.length);
// Get a random map
newSpriteFrame = this.enemyTankSpriteFrames[spriteFrameIndex];
// Get the tank configuration matching the map name from the current
// tank type
for (let i = 0; i < GameConst.EnemyTankTypes.length; i++) {
// Matching process
if (newSpriteFrame.name.indexOf(GameConst.EnemyTankTypes[i].name) !== -1) {
// Get score data
this._score = GameConst.EnemyTankTypes[i].score;
// Get speed data
this.actionSpeed = GameConst.EnemyTankTypes[i].speed;
}
}
}
else {
// Set as the default tank wizard map
newSpriteFrame = this.playerTankSpriteFrames[0];
}
// Let the CCSprite component render the new texture
this.node.getComponent(cc.Sprite).spriteFrame = newSpriteFrame;
},
// Update the map of the tank wizard
updateTankSpriteFrame (newDirection) {
// Get the rendering component
var tankSprite = this.node.getComponent(cc.Sprite);
// Record the name of the old map
var oldSpriteFrameName = tankSprite.spriteFrame.name;
// Create a new variable to record the name of the new map
var newSpriteFrameName = null;
// Traverse the map name array
for (let i = 0; i < GameConst.DirctionRex.length; i++) {
// Find an item from the map name array that matches the old map and
// return a new map name
newSpriteFrameName = oldSpriteFrameName.replace(GameConst.DirctionRex[i], newDirection);
// If the name of the new and old maps are not the same, then jump out
// of this loop
if (newSpriteFrameName !== oldSpriteFrameName) {
break;
}
}
// According to the name of the new map to get the corresponding map
// resource in the map
if (this.tankSpriteAtlas.getSpriteFrame(newSpriteFrameName)) {
tankSprite.spriteFrame = this.tankSpriteAtlas.getSpriteFrame(newSpriteFrameName);
}
},
// Initialize the tank automatic behavior
initTankAutoActionManager() {
// Turn on the timer
this.schedule(this.timerCallBack, this.actionSpeed);
},
// Timer callback
timerCallBack () {
// Determine whether automatic behavior is allowed
if (this.isAuto) {
// Change direction after every n steps
if (!this._changeDirectionStep || this._changeDirectionStep < 0) {
// change direction
this.changeTankDirection();
// Update the current number of steps
this._changeDirectionStep = this.changeDirectionStep;
}
else {
// Automatically fire bullets when there are only half of the steps
if (this._changeDirectionStep === Math.floor(this.changeDirectionStep / 2)) {
// fire bullets
this.lauchBullet(cc.game.groupList[4]);
}
// If the tank encounters an obstacle in the map
if (!cc.find("Canvas").getComponent("TiledMapManager").onTileMovedEvent(this.node)) {
// Change the direction of the tank
this.changeTankDirection();
}
// The number of steps is automatically reduced by one
this._changeDirectionStep--;
}
// Let the tank move through the map
cc.find("Canvas").getComponent("TiledMapManager").onTileMovedEvent(this.node);
}
},
// Change the direction of the tank itself
changeTankDirection () {
// Get a random serial number
var newDircetionIndex = Math.floor(Math.random() * GameConst.Dirction.length);
// Set the direction of the tank
this.node.getComponent("TankManager").tankDirection = GameConst.Dirction[newDircetionIndex];
},
// 发射子弹
lauchBullet (group) {
// Get the bullet object cache from the object pool
var bullet = NodePoolManager.getNodeElement(group);
// if there is no object in cache
if (!bullet) {
// Instantiate a bullet node
bullet = cc.instantiate(this.bullet);
}
// Set the direction of the bullet
bullet._direction = this.node.getComponent("TankManager").tankDirection;
// Add bullets to the scene
cc.find("Canvas").getComponent("Game").tankWarMap.node.getChildByName("bullet").addChild(bullet);
// Perform the initialization of the bullet node
bullet.getComponent("BulletManager").initBullet(this.node, group);
// Play sound effects, this section will be explained in Section 9.
cc.find("Canvas").getComponent("SoundManager").playEffectSound("shoot", false);
},
// Stop the timer of the current component
onUnschedule () {
this.unschedule(this.timerCallBack);
},
onDisable () {
// Close the current node's listen event in the ColliderManager component
this.node.targetOff(this.node.getComponent("ColliderManager"));
}
});
Next, add a script component to the Node AnyTank node of the Node AnyTank prefab. Its Property inspector should be as follows:
Also, we can add an animation to the birth of the player tank:
Follow these steps:
- create a child node, Sp Shield under the Node AnyTank node
- add a
CCSprite
component - find the shield_0 texture in the
./assets/res/texture/
directory - add it to the SpriteFrame property panel of the
CCSprite
component above
Render the node as follows:
After that:
- remove the active check of the Sprite Shield node
- return to the Node AnyTank node
- add the
CCAnimation
component to it - create an
animationClip
resource in the./assets/res/anima/
directory, named start, which is mainly used to play the tank Admission animation.
The Property inspector panel for the CCAnimation
component is as follows:
The start sequence animation is set in the Animation editor. If you don’t know how to use the Animation editor, you can refer to this document: Animation system:
After the above steps are complete, the game behaves as follows:
6. Control the tank moving in the map
We need to use the global system events in Cocos Creator to control the player’s input and output operations. Global system events refer to various global events that are not related to the node tree. They are uniformly distributed by cc.systemEvent
. Currently, the following events are supported:
- keyboard events
- device gravity sensing events.
In this project, there are a few rules:
Player 1:
- controls the tank moving up, left, down, and right by pressing w, a, s, and d on the keyboard.
- launches bullets by pressing the Space key.
Player 2:
- controls the tank moving up, left, down, and right by pressing the arrow keys on the keyboard.
- launches bullets by pressing the PgUp key.
We need to assign the keys to the proper event to be fired. Example:
onPlayerKeyDownCallback (event) {
switch (event.keyCode) {
case cc.macro.KEY.w:
// This is the listener event belonging to player 1
if (!this._playerTank[0]) return;
// Set the steering of the player 1 tank
this._playerTank[0].getComponent("TankManager").tankDirection = GameConst.Dirction[0];
// Transfer the tank object after the state change to the tank map
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[0]);
break;
case cc.macro.KEY.a:
if (!this._playerTank[0]) return;
this._playerTank[0].getComponent("TankManager").tankDirection = GameConst.Dirction[1];
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[0]);
break;
case cc.macro.KEY.s:
if (!this._playerTank[0]) return;
this._playerTank[0].getComponent("TankManager").tankDirection = GameConst.Dirction[2];
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[0]);
break;
case cc.macro.KEY.d:
if (!this._playerTank[0]) return;
this._playerTank[0].getComponent("TankManager").tankDirection = GameConst.Dirction[3];
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[0]);
break;
case cc.macro.KEY.up :
// This is the listener event belonging to player 1
if (!this._playerTank[1]) return;
// set the sterring for player 1
this._playerTank[1].getComponent("TankManager").tankDirection = GameConst.Dirction[0];
// Transfer the tank object after the state change to the tank map
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[1]);
break;
case cc.macro.KEY.left:
if (!this._playerTank[1]) return;
this._playerTank[1].getComponent("TankManager").tankDirection = GameConst.Dirction[1];
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[1]);
break;
case cc.macro.KEY.down:
if (!this._playerTank[1]) return;
this._playerTank[1].getComponent("TankManager").tankDirection = GameConst.Dirction[2];
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[1]);
break;
case cc.macro.KEY.right:
if (!this._playerTank[1]) return;
this._playerTank[1].getComponent("TankManager").tankDirection = GameConst.Dirction[3];
this.node.getComponent("TiledMapManager").onTileMovedEvent(this._playerTank[1]);
break;
case cc.macro.KEY.space:
if (!this._playerTank[0]) return;
// Player 1 fires a bullet
this._playerTank[0].getComponent("TankManager").lauchBullet(cc.game.groupList[1]);
break;
case cc.macro.KEY.pageup:
if (!this._playerTank[1]) return;
// Player 2 fires a bullet
this._playerTank[1].getComponent("TankManager").lauchBullet(cc.game.groupList[1]);
break;
}
},
Next, the BulletManager.js
function can be created as:
var NodePoolManager = require("../NodePoolManager");
cc.Class({
extends: cc.Component,
properties: {
lauchStep: {
default: 1,
type: cc.Float,
range: [0,1,0.01]
}
},
// LIFE-CYCLE CALLBACKS:
// onLoad () {},
start () {
},
// Initialize bullets
initBullet (tank, group) {
// Set the node grouping
this.node.group = group;
// Set the node coordinates
this.node.position = tank.position;
// Start the timer
this.schedule(this.timerCallBack, this.lauchStep);
},
timerCallBack (direction) {
// Set the node activity boundary
if (this.node.position.x === -208 || this.node.position.x === 204 || this.node.position.y === -204 || this.node.position.y === 208) {
// Remove the node from the node tree and cache the node object into
// the object pool
NodePoolManager.putNodeElemenet(this.node.group, this.node);
return;
}
else {
// Let the bullet move in the map
cc.find("Canvas").getComponent("TiledMapManager").onTileMovedEvent(this.node);
}
},
onDisable () {
// stop the timer
this.unschedule(this.timerCallBack);
}
// update (dt) {},
});
Next, let the tank move on the map. We need to create a map management script, TiledMapManager.js
in the ./assets/scripts/components/
directory. This script is mainly responsible for the behavior of the tank on the map. Example:
var NodePoolManager = require("./NodePoolManager");
var GameEnum = require("./gameData/GameEnum");
var GameConst = require("../gameData/GameConst");
cc.Class({
extends: cc.Component,
properties: {
mainTiledMap: {
default: null,
type: cc.TiledMap
},
tiledMapAssetSet: {
default: [],
type: cc.TiledMapAsset
},
tiledMapAssets: {
default: [],
type: cc.TiledMapAsset
},
_interimPos:[],
},
start () {
// Initialize the map data
this.initTiledMapData();
},
initTiledMapData () {
// initialize layer_0、layer_1
this.mainLayer = this.mainTiledMap.getLayer("layer_0");
this.secondaryLayer = this.mainTiledMap.getLayer("layer_1");
},
// used to transform the movement of the node in the coordinates of the
// node into a map coordinate movement
onTileMovedEvent (tileNode, callback) {
// If the tank is not allowed to move, do not execute internal logic
if (tileNode.getComponent("TankManager") && !tileNode.getComponent("TankManager").isCanMove) {
return;
}
// Create a variable for the initial coordinates of the cache node
// under the node axis
var startPos = cc.v2(tileNode.position.x, tileNode.position.y);
// Create a variable to cache the direction of the tank
var tankDirection = null;
// If the node is not a tank
if (!tileNode.getComponent("TankManager")) {
// Set the direction
tankDirection = tileNode._direction;
}
// If the node is a tank
else {
// Set the direction
tankDirection = tileNode.getComponent("TankManager").tankDirection;
}
// Reset the coordinates of the node according to the direction obtained
if (tankDirection === GameConst.Dirction[0]) {
startPos.y += tileNode.height;
}
else if(tankDirection === GameConst.Dirction[2]){
startPos.y -= tileNode.height;
}
else if (tankDirection === GameConst.Dirction[1]) {
startPos.x -= tileNode.width;
}
else if (tankDirection === GameConst.Dirction[3]) {
startPos.x += tileNode.width;
}
// Convert node coordinates to map coordinates
var tilePos = this.getTilePositionAt(tileNode, startPos);
// Set the map boundary
if (tilePos.y <= this.mainLayer.getLayerSize().height - 1 && tilePos.x <= this.mainLayer.getLayerSize().width - 1
&& tilePos.y >= 0 && tilePos.x >= 0) {
// Get the tile ID corresponding to the current map coordinates
var tileGID = this.mainLayer.getTileGIDAt(tilePos);
// Get the type corresponding to the current tile according to
// GameConst.GidToTileType, and then compare with GameEnum.TileType,
// the corresponding changes of different types of tiles are different
// Here deal with tileWall type tiles
if (GameConst.GidToTileType[tileGID] === GameEnum.TileType.tileWall) {
// According to the grouping of nodes to perform different logic,
// here the node grouping is tankBullet
if (tileNode.group === cc.game.groupList[1] || tileNode.group === cc.game.groupList[4]) {
// Reset the tile
this.mainLayer.setTileGIDAt(0, tilePos.x, tilePos.y);
// Remove the tank from the node tree and put the node object
// into the object pool
NodePoolManager.putNodeElemenet(tileNode.group, tileNode);
}
// tank
else if (tileNode.group === cc.game.groupList[2] || tileNode.group === cc.game.groupList[3]) {
return false;
}
}
// Here deal with tileWall type tiles
else if (GameConst.GidToTileType[tileGID] === GameEnum.TileType.tileSteel) {
if (tileNode.group === cc.game.groupList[1] || tileNode.group === cc.game.groupList[4]) {
// Play the steel sound, this part is explained in Section 9.
cc.find("Canvas").getComponent("SoundManager").playEffectSound("steel", false);
NodePoolManager.putNodeElemenet(tileNode.group, tileNode);
}
// tank
else if (tileNode.group === cc.game.groupList[2] || tileNode.group === cc.game.groupList[3]) {
return false;
}
}
else if (GameConst.GidToTileType[tileGID] === GameEnum.TileType.tileRiver) {
if (tileNode.group === cc.game.groupList[2] || tileNode.group === cc.game.groupList[3]) {
return false;
}
}
else if (GameConst.GidToTileType[tileGID] === GameEnum.TileType.tileKing) {
if (tileNode.group === cc.game.groupList[1] || tileNode.group === cc.game.groupList[4]) {
// Execute the game end logic
cc.find("Canvas").getComponent("Game").onGameOverEvent();
NodePoolManager.putNodeElemenet(tileNode.group, tileNode);
}
// tank
else if (tileNode.group === cc.game.groupList[2] || tileNode.group === cc.game.groupList[3]) {
return false;
}
}
// Set the converted node coordinates to the node
tileNode.position = startPos;
if (typeof callback === "function") {
// Execute callback function
callback();
}
//return node object
return tileNode;
}
else {
return false;
}
},
getTilePositionAt (tileNode, position) {
// Convert the node coordinates of the node into world coordinates
var worldPosition = tileNode.parent.convertToWorldSpaceAR(position);
// Get the height and height data of the map node
var mapSize = this.node.getContentSize();
// or the width and height of the map
var tileSize = this.mainTiledMap.getTileSize();
// Calculate the coordinates of the node in the map, and round down
var x = Math.floor(worldPosition.x / tileSize.width);
var y = Math.floor((mapSize.height - worldPosition.y) / tileSize.height);
return cc.v2(x, y);
},
// update (dt) {},
});
Add the component to the Canvas node, below is the Property inspector panel for the component, and the TiledMapAsset
resource is in the ./assets/res/map
directory:
After completing this part of the game, the gameplay is something similar to this:
7. Add collision logic for bullets and tanks
Cocos Creator provides a Collision System to handle the collisions between the bullet and the tank. We set the groups grouping in the first section of this tutorial and now we can edit ColliderManager.js
. Example:
var GameEnum = require("../gameData/GameEnum");
var NodePoolManager = require("../components/NodePoolManager");
cc.Class({
extends: cc.Component,
properties: {
},
onLoad () {
// Get the global collision system management object of the game
var colliderManager = cc.director.getCollisionManager();
// Open collision management
colliderManager.enabled = true;
// draw the border of the collision box
// colliderManager.enabledDebugDraw = true;
},
// Callback when triggering a collision
onCollisionEnter (other, self) {
// Remove the node from the node tree and cache the node object into
// the object pool
NodePoolManager.putNodeElemenet(this.node.group, this.node);
// Grouping different processing different logic
if (this.node.group === cc.game.groupList[2]) {
// The number of rebirths of the player tank is reduced by one.
--cc.find("Canvas").getComponent("Game")._playerTankReviveTimes;
// Update the game information on the right side of the battle interface
cc.find("Canvas").getComponent("Game").updateGameMenuInfo(this.node, GameEnum.TankFlag.Player);
// Play playerTankBoom sound, the sound system is explained in Section 9.
cc.find("Canvas").getComponent("SoundManager").playEffectSound("playerTankBoom", false);
}
else if (this.node.group === cc.game.groupList[3]) {
// The number of enemy tanks is reduced by one.
--cc.find("Canvas").getComponent("Game")._enemyTankAmount;
// Update the game information on the right side of the battle interface
cc.find("Canvas").getComponent("Game").updateGameMenuInfo(this.node, GameEnum.TankFlag.Enemy);
// Calculate the score the player gets in the game
cc.find("Canvas").getComponent("Game").updateScore(this.node.getComponent("TankManager")._score);
// Play enemyTankBoom sound effect
cc.find("Canvas").getComponent("SoundManager").playEffectSound("enemyTankBoom", false);
}
},
// The callback executed at the end of the collision
onCollisionExit (other, self) {
if (this.node.group === cc.game.groupList[0]) {
}
else if (this.node.group === cc.game.groupList[0]) {
}
},
initGroup () {}
// update (dt) {},
});
We only need to add this component to the Node AnyTank node of the Node AnyTank prefab, and the Node Buttle node of the Node Buttle prefab. Then add the CCBoxCollider
component to the node with the ColliderManager.js
script component. The Property inspector interface is as follows:
After completing this part of the game, the collision between the bullet and the tank can occur. Shall we see the effect:
8. Game decision making
After the tank collides, decisions will be made to decide what shape the player is in. We need to add the following function to Game.js
:
updateGameMenuInfo (targetNode, tankTag) {
// Update the game information on the right side of the battle interface
if (cc.find("Canvas").getComponent("Game")._enemyTankAmount > 0 && cc.find("Canvas").getComponent("Game")._playerTankReviveTimes > 0) {
//Create a new tank
this.createNewTank(targetNode, tankTag);
}
else if (cc.find("Canvas").getComponent("Game")._enemyTankAmount > 0 && cc.find("Canvas").getComponent("Game")._playerTankReviveTimes === 0){
// Perform the game failure logic
this.onGameOverEvent("lose");
}
else if (cc.find("Canvas").getComponent("Game")._enemyTankAmount === 0 && cc.find("Canvas").getComponent("Game")._playerTankReviveTimes > 0) {
// Go to the next level
this.onNextRoundEvent();
}
else if (cc.find("Canvas").getComponent("Game")._enemyTankAmount === 0 && cc.find("Canvas").getComponent("Game")._playerTankReviveTimes === 0){
// Go to the next level
this.onNextRoundEvent();
}
},
Next, the createNewTank
function can be created as:
createNewTank (targetNode, tankTag) {
// Create different tanks by type
if (tankTag === GameEnum.TankFlag.Enemy) {
cc.find("Canvas").getComponent("Game").gameMenu.getChildByName("Label Tank Remaining Number").getComponent(cc.Label).string = cc.find("Canvas").getComponent("Game")._enemyTankAmount;
cc.find("Canvas").getComponent("Game").createEnemyTank(targetNode.getComponent("TankManager")._bornPosition);
}
else if (tankTag === GameEnum.TankFlag.Player) {
cc.find("Canvas").getComponent("Game").gameMenu.getChildByName("Label Revive Times Number").getComponent(cc.Label).string = cc.find("Canvas").getComponent("Game")._playerTankReviveTimes;
cc.find("Canvas").getComponent("Game").createPlayerTank(targetNode.getComponent("TankManager")._bornPosition);
}
},
Next, the onGameOverEvent
function can be created as:
// Game end processing
onGameOverEvent (command) {
// Update the game state
this._playing = false;
// Perform different logic according to the specified
if (command === "win") {
// Play begin sound
this.node.getComponent("SoundManager").playEffectSound("begin", false);
console.log("[Game Win]");
}
else if (command === "lose") {
// Play gameOver sound effect
cc.find("Canvas").getComponent("SoundManager").playEffectSound("gameOver", false);
console.log("[Game Lose]");
}
// Restart the game
this.restartGame();
},
Next, the onNextRoundEvent
function can be created as:
// Go to the next level
onNextRoundEvent () {
// Update the level value
++this._initialRound;
// The level value can not be greater than the number of maps
// built into the game
if (this._initialRound <= this.node.getComponent("TiledMapManager").tiledMapAssets.length) {
// Replace map resources
this.tankWarMap.tmxAsset = this.node.getComponent("TiledMapManager").tiledMapAssets[this._initialRound - 1];
// Update the level value text information
this.gameMenu.getChildByName("Label Round Number").getComponent(cc.Label).string = this._initialRound;
// Initialize the map data
this.node.getComponent("TiledMapManager").initTiledMapData();
// Play the game to start sound
this.node.getComponent("SoundManager").playStartGameEffectSound();
// Execute the game start logic
this.startGame();
}
else {
this.onGameOverEvent("win");
}
},
Next, the restartGame
function can be created as:
// restarting the game
restartGame () {
// Update the game state
this._playing = false;
// Logout listener event
this.unListenHandlers();
// Clear the player tank array cache
this._playerTank = [];
// Turn off all sound effects of the game
this.node.getComponent("SoundManager").stopAll();
// Load game scene
cc.director.loadScene("game");
},
Now, the game can return to the game start interface when the game fails, and enter the next level when the game is leveled up.
9. Adding background music and sound management
We control the various audio playback operations of the game through the SoundManager.js
script. Example:
cc.Class({
extends: cc.Component,
editor: {
menu:"CustomComponent/AudioControl",
},
properties: {
backGroupSound: {
default: null,
type: cc.AudioClip
},
loop: true,
soundsVolume: {
default: 1,
range: [0,1,0.01],
notify: function() {
this.setSoundsVolume();
}
},
effectsVolume: {
default: 1,
range: [0,1,0.01],
notify: function () {
this.setEffectsVolume();
}
},
audioClipPool: {
default: [],
type: cc.AudioClip
},
_isPlaying: false,
_audioId: null,
_effectId: null,
},
// Play background music
playBackGroupSound (callback) {
if (this.backGroupSound) {
// Pause the music being played
cc.audioEngine.stopAll();
// Play music
this._audioId = cc.audioEngine.play(this.backGroupSound, this.loop, this.soundsVolume);
// Execute callback when playback is complete
if (callback && typeof callback === "function") {
cc.audioEngine.setFinishCallback(this._audioId, callback);
}
}
},
// According to different instructions to play different sound effects
playEffectSound (command, loop = this.loop, callback) {
if (command !== null && command !== undefined || this.audioClipPool.length > 0) {
switch (command) {
case "begin":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[0], loop);
break;
case "nmoving":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[1], loop);
break;
case "moving":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[2], loop);
break;
case "shoot":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[3], loop);
break;
case "steel":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[4], loop);
break;
case "enemyTankBoom":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[5], loop);
break;
case "playerTankBoom":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[6], loop);
break;
case "gameOver":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[7], loop);
break;
case "pause":
case "resume":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[8], loop);
break;
case "bouns":
this._effectId = cc.audioEngine.playEffect(this.audioClipPool[9], loop);
break;
default:
console.error("Command is invalid");
}
}
if (typeof callback === "function") {
// Execute callback when playback is complete
cc.audioEngine.setFinishCallback(this._effectId, callback);
}
},
// pause music
pauseMusic () {
cc.audioEngine.pauseAll();
},
// resue music
resumeMusic () {
cc.audioEngine.resumeAll();
},
// set sound volume
setSoundsVolume() {
if (this._audioId) {
cc.audioEngine.setVolume(this.soundsVolume);
}
},
// set effects volume
setEffectsVolume () {
if (this._effectId) {
cc.audioEngine.setEffectsVolume(this.effectsVolume);
}
},
// stop all
stopAll () {
cc.audioEngine.stopAll();
this._audioId = null;
this._effectId = null;
},
// play game effects sounds
playStartGameEffectSound () {
this.playEffectSound("begin", false, ()=>{
this.playEffectSound("nmoving", true);
});
},
});
Add it to the Canvas node. The Property inspector panel looks like this:
Add a sliding callback to the two CCSlider
components, Slider BGM Volume and Slider Effect Volume, so that they can adjust the volume each time they slide. Add the onSliderSwitchEvent
function to the Game.js
script with the following code:
// Volume control slider
onSliderSwitchEvent (slider, CustomEventData) {
// Create a new variable cache slider corresponding value
let volume = slider.progress;
// Perform different logic according to the slider's custom parameters
if (CustomEventData === "bgm") {
// Set the background music volume
this.node.getComponent("SoundManager").soundsVolume = volume;
}
else if (CustomEventData === "effect") {
// Set the effect volume
this.node.getComponent("SoundManager").effectsVolume = volume;
}
},
Bind the script to the sliding callbacks of the Slider BGM Volume and Slider Effect Volume components. The Property inspector panel looks like this:
This completes the audio control operation of the game.
10. Add game pause, resume, and replay features
Add the onPauseOrResumeGameEvent
function to the Game.js
script to control the progress of the game. The code is as follows:
// Pause or resume the game
onPauseOrResumeGameEvent () {
// Change the label of the game battle in progress
this._playing = !this._playing;
// Non-combat time
if (!this._playing) {
// Play pause sound
cc.find("Canvas").getComponent("SoundManager").playEffectSound("pause", false);
// Suspend all tanks
for (let i = 0; i< this.tankWarMap.node.getChildByName("tank").childrenCount; i++) {
// Forbidden tank movement
this.tankWarMap.node.getChildByName("tank").children[i].getComponent("TankManager").isCanMove = false;
//pause timer
cc.director.getScheduler().pauseTarget(this.tankWarMap.node.getChildByName("tank").children[i].getComponent("TankManager"));
}
//Change the button text
this.gameMenu.getChildByName("Button Game Pause").getChildByName("Label Button").getComponent(cc.Label).string = "resume";
console.log("[Game Pause]");
}
else {
// Play recovery sound
cc.find("Canvas").getComponent("SoundManager").playEffectSound("resume", false);
// Restore all tanks
for (let i = 0; i< this.tankWarMap.node.getChildByName("tank").childrenCount; i++) {
// Allow tanks to move
this.tankWarMap.node.getChildByName("tank").children[i].getComponent("TankManager").isCanMove = true;
// Recovery timer
cc.director.getScheduler().resumeTarget(this.tankWarMap.node.getChildByName("tank").children[i].getComponent("TankManager"));
}
// change the button text
this.gameMenu.getChildByName("Button Game Pause").getChildByName("Label Button").getComponent(cc.Label).string = "pause";
console.log("[Game Resume]");
}
},
Set onPauseOrResumeGameEvent
to the click callback of the Button Game Pause button. The Property inspector interface is as follows:
Create a new CCButton
component node named Button Game Restart. Bind it to the restartGame
function in Game.js
as the clickback end of the callback. The Property inspector interface is as follows:
This completes the process control for this game.
11. Conclusion
We have now created TankWar. Take a look at the game performance (here the number of enemy tanks is adjusted to 5 for the convenience of testing):
There are still a lot of optimizations that can take place in this game. You can download the complete project