一、前言
筆者閑來無事,某天github閑逛,看到了游戲引擎的專題,引起了自己的興趣,于是就自己搗騰了一下Cocos2dx-JS。
由于是學(xué)習(xí),所謂紙上得來終覺淺,只是看文檔看sample看demo,并不會(huì)讓自己有多大的提升,于是一開始就計(jì)劃寫一個(gè)小游戲,以作為自己完成這個(gè)階段學(xué)習(xí)的一個(gè)標(biāo)志,也算是目標(biāo)導(dǎo)向吧。
完整源碼移步Github: https://github.com/RogerKang/JasonAmbition
Online Demo: http://www.rogerkang.site:3000 (由于需要WebGL的支持,請(qǐng)使用較新版本的Chrome或者Firefox打開)
本游戲的圖片素材以及特效素材來自騰訊的《全民飛機(jī)大戰(zhàn)》,僅供學(xué)習(xí)參考,請(qǐng)勿用于其他用途。
筆者水平有限,本身也不是專業(yè)游戲開發(fā)者,如有錯(cuò)漏,還請(qǐng)多多指教。
二、Cocos2dx-JS
Cocos2dx的介紹請(qǐng)移步官網(wǎng): http://www.cocos2d-x.org/
目前Cocos2dx支持3種開發(fā)語言:C++、Lua、 JavaScript,其中C++和Lua是使用較為廣泛開發(fā)語言,而JS的支持最早是作為獨(dú)立的項(xiàng)目進(jìn)行開發(fā)的,在3.7之后并入了Cocos2d-x的主repository,成為了官方支持的正式開發(fā)語言。
使用JS開發(fā)Cocos2dx游戲的優(yōu)點(diǎn):
- 跨平臺(tái),是唯一可以到達(dá)web端的分支,同時(shí)也支持編譯成iOS和Android原生APP
- 開發(fā)效率高,開發(fā)難度相對(duì)較低
缺點(diǎn):
- 運(yùn)行效率相對(duì)較低
- JS-binding在功能和文檔以及開發(fā)者的使用經(jīng)驗(yàn)上仍然落后于C++和Lua
閱讀本文之前,你最好具備以下知識(shí)基礎(chǔ):
- 基本的JavaScript技能
- Cocos2dx-JS的基本知識(shí)
三、游戲簡(jiǎn)介


這個(gè)游戲是一個(gè)簡(jiǎn)單的飛行射擊游戲,主要使用鼠標(biāo)點(diǎn)擊頁面(移動(dòng)設(shè)備就是手指點(diǎn)擊屏幕)控制飛機(jī)的飛行,從而實(shí)現(xiàn)攻擊敵人,回避攻擊,獲取增強(qiáng)效果物品等常見的行為。
具體細(xì)節(jié)的可以到Online Demo去看一看。
四、整體設(shè)計(jì)
1. Game Scene
游戲總共有3個(gè)scene;Menu、About、Game,可以理解為不同的功能放在不同的場(chǎng)合之中,其中最重要的是菜單(Menu)場(chǎng)景和游戲(Game)場(chǎng)景,又以游戲場(chǎng)景最為核心,當(dāng)我們控制飛機(jī)進(jìn)行游戲的時(shí)候,實(shí)際上就是在于游戲場(chǎng)景在進(jìn)行交互,此時(shí)我們看到的飛機(jī),敵人,子彈,特效,背景等等一切,都是游戲場(chǎng)景在承載。
2. Game Layer
Layer的層次比Scene低一級(jí),通常是一個(gè)scene會(huì)包含一個(gè)或者多個(gè)layer(當(dāng)然也可以一個(gè)都沒有)。
通常不同的內(nèi)容可以放在不同的layer中,比如游戲場(chǎng)景里面,就使用了一個(gè)獨(dú)立的layer來顯示飛機(jī)的血量和得分。
本游戲只有3個(gè)layer,GameLayer,GameInfo 和 GameOverLayer,顧名思義,一個(gè)作為游戲場(chǎng)景的承載,一個(gè)顯示飛機(jī)的血量和得分,一個(gè)在飛機(jī)死亡的時(shí)候顯示相關(guān)的信息。
3. Game Object
使用了面向?qū)ο蟮脑O(shè)計(jì)來組織游戲的各個(gè)對(duì)象:
Plane:玩家控制的飛機(jī)
Enemy:敵人的基類,提供了公共行為接口,包括回收與重用的接口
Bullet:子彈的基類,提供了公共行為接口,包括回收與重用的接口
./Enemies:擴(kuò)展Enemy基類,不同的敵人有自己的行為
./Bullets:擴(kuò)展Bullet基類,不同的子彈的效果自己定義
./Boss:擴(kuò)展Enemy基類,實(shí)際上就是特殊的敵人作為Boss登場(chǎng)
4. Manager
設(shè)計(jì)了若干個(gè)manager來組織游戲的各個(gè)方面:
BulletManager:每幀更新子彈的位置,檢測(cè)碰撞和傷害,回收超出屏幕的子彈
EnemyManager:控制敵人和Boss的產(chǎn)生,檢測(cè)敵人與飛機(jī)的碰撞,控制敵人的攻擊,回收敵人
ItemManager:控制游戲中隨機(jī)增強(qiáng)item的產(chǎn)生,并應(yīng)用其效果
MusicManager:音效的控制
ParticleManager:特效的控制,包括被攻擊的特效,item的特效,Boss特殊攻擊的特效
StageManger:控制游戲階段,比如常規(guī)階段,boss階段等等
五、啟動(dòng)游戲
啟動(dòng)游戲,我們需要設(shè)置game stage的配置以及初始化各個(gè)manager,并且以Menu Scene作為第一個(gè)scene:
var defaultStage = [{name:"normal", duration: 20}, {name:"mega", duration: 20}, {name:"boss", duration:100000}]; //game stage配置
cc.game.onStart = function(){
cc.view.adjustViewPort(false);
cc.view.setDesignResolutionSize(512, 768, cc.ResolutionPolicy.SHOW_ALL); // 設(shè)置分辨率
cc.view.resizeWithBrowserSize(true);
//load resources
cc.LoaderScene.preload(g_resources, function () {
cc.game.musicManager = new MusicManager(); //初始化各個(gè)manager
cc.game.bulletManager = new BulletManager();
cc.game.enemyManager = new EnemyManager();
cc.game.itemManager = new ItemManager();
cc.game.particleManager = new ParticleManager();
cc.game.stageManager = new StageManager(defaultStage);
cc.director.runScene(new MenuScene()); //首先啟動(dòng)Menu Scene
cc.director.setDisplayStats(false);
}, this);
};
cc.game.run(); //運(yùn)行游戲
var trace = function() {
//cc.log(Array.prototype.join.call(arguments, ", "));
};
六、Menu Scene
Menu場(chǎng)景只需要一個(gè)背景圖片,一段BGM,以及一個(gè)按鈕開始游戲即可
var MenuScene = cc.Scene.extend({
ctor:function(){
this._super();
var layer = new cc.Layer();
this.addChild(layer);
var winSize = cc.director.getWinSize();
cc.game.winSize = winSize;
var bgImage = new cc.Sprite("res/bg.png"); //背景圖片
bgImage.x=winSize.width/2;
bgImage.y = winSize.height/2;
layer.addChild(bgImage);
var gameName = new cc.LabelTTF("Jason's Ambition", "Arial",58, cc.Size(500, 200), cc.TEXT_ALIGNMENT_CENTER, cc.VERTICAL_TEXT_ALIGNMENT_TOP); //標(biāo)題文本
gameName.x = winSize.width/2;
gameName.y = winSize.height+100;
var gameNameAction = cc.moveTo(3, cc.p(winSize.width/2, winSize.height-200)); //讓標(biāo)題動(dòng)起來
gameName.runAction(gameNameAction);
layer.addChild(gameName);
this.scheduleOnce(function(){
this.showMenu();
}.bind(this),3); //3秒之后顯示按鈕
cc.game.musicManager.playStarWar(); //播放BGM
},
showMenu:function(){
var startGameButton = new cc.MenuItemImage("res/startgame2.png", "res/startgame2.png", this.startGame, this); //創(chuàng)建按鈕并綁定回調(diào)事件
var menu = new cc.Menu(startGameButton);
this.addChild(menu);
},
startGame: function(){
trace('Game start..');
cc.director.runScene(new AboutScene()); //點(diǎn)擊按鈕后跳到About Scene
}
});
在這個(gè)場(chǎng)景里面,我們加載了背景圖片,播放了BGM,設(shè)置了會(huì)移動(dòng)的標(biāo)題,并在標(biāo)題停止移動(dòng)后顯示開始按鈕,在按鈕上綁定回調(diào)事件,點(diǎn)擊后跳轉(zhuǎn)到About Scene。
七、About Scene
About Scene只是一段文本,3秒后跳轉(zhuǎn)到游戲場(chǎng)景。
var AboutScene = cc.Scene.extend({
ctor:function(){
this._super();
var winSize = cc.director.getWinSize();
var layer = new cc.Layer();
this.addChild(layer);
var aboutText = "Jason, a shameless man.\nHe want to kidnap all girls around the earth.\n";
aboutText += "Hero, you have to stop him!";
var aboutLabel = new cc.LabelTTF(aboutText, "Arial",24, cc.Size(500, 200), cc.TEXT_ALIGNMENT_CENTER, cc.VERTICAL_TEXT_ALIGNMENT_TOP);
aboutLabel.x = winSize.width/2;
aboutLabel.y = winSize.height-300;
layer.addChild(aboutLabel);
this.scheduleOnce(function(){
var gameScene = new GameScene();
cc.director.runScene(gameScene); //啟動(dòng)游戲場(chǎng)景,游戲開始
cc.game.gameScene = gameScene;
},3);
}
});
八、Game Scene
Game Scene的設(shè)計(jì)是最為復(fù)雜的,因?yàn)槭怯螒蚪换サ膱?chǎng)所,所以其之上需要處理的事情也最多。
我們需要一點(diǎn)一點(diǎn)說。
GameScene的執(zhí)行流程:(簡(jiǎn)書不支持流程圖,有興趣的同學(xué)可以把下面的代碼復(fù)制到支持flowchart流程圖的Markdown編輯器里面查看,注意使用flow代碼段包裹,例如https://maxiang.io)
st=>start: Start Game Scene
end=>end: End Game
addBackGround=>operation: Add scrolling background
addPlane=>operation: Add plane
listenClick=>operation: Listen to click operation
continueGame=>condition: Plane dead or Boss dead?
addEnemy=>operation: Add new enemies
enemyAttack=>operation: Enemy attack
bulletReuse=>operation: Reuse out bullets
bulletHit=>operation: Check collision between planes and bullets
bulletUpdate=>operation: Update bullets' action
enenmyCollide=>operation: Check the collision between enemies and plane
enemyOut=>operation: Reuse outing enemies
updateInfo=>operation: Update game information
genenrateItem=>operation: Generate items
itemCollide=>operation: Check the collision between items and plane
allyEffect=>operation: Apply items' effect to plane
planeBlood=>condition: Plane's dead?
gameOver=>operation: Game Over
st->addBackGround->addPlane->listenClick->continueGame
continueGame(yes)->addEnemy->enemyAttack->bulletReuse->bulletHit->bulletUpdate->enenmyCollide->enemyOut
enemyOut->updateInfo->genenrateItem->itemCollide->allyEffect->planeBlood
planeBlood(yes)->gameOver
planeBlood(no)->continueGame
continueGame(no)->gameOver
gameOver->end
1. 幀
首先解釋幀的概念。
對(duì)于一個(gè)游戲而言,連貫的畫面其實(shí)是有快速切換的幀造成的,而每一幀之間會(huì)有細(xì)小的變化,連續(xù)播放幀就會(huì)形成動(dòng)作。而幀之間的變化,就是我們的代碼需要計(jì)算和處理的。
簡(jiǎn)而言之,如果你需要一個(gè)物體移動(dòng)到某個(gè)地方,那你的代碼就應(yīng)該在每一幀里面對(duì)他的坐標(biāo)進(jìn)行一些改變,從而實(shí)現(xiàn)移動(dòng)這個(gè)效果(當(dāng)然,任何一個(gè)優(yōu)秀的游戲引擎,這種基本的動(dòng)畫是不需要我們自己去做幀間的處理的,我們只需要調(diào)用一次,這個(gè)效果就會(huì)在它所進(jìn)行的時(shí)間內(nèi)在每幀進(jìn)行更新)。
在Scene的ctor函數(shù)里面調(diào)用this.scheduleUpdate 方法,就會(huì)使用默認(rèn)的update方法去進(jìn)行幀間計(jì)算,我們?cè)趗pdate方法里面需要做的事情:
- 更新飛機(jī)的位置
- 更新敵人的位置
- 更新子彈的位置
- 更新item的位置
- 判斷飛機(jī)與敵人、子彈、item的碰撞,并應(yīng)用效果
- 等等
其中,實(shí)際上我們只需要真的去在每一幀都處理的只有“更新子彈的位置” 以及 “判斷飛機(jī)與敵人、子彈、item的碰撞,并應(yīng)用效果”其余的都只需要在他們真的需要更新的時(shí)候聲明一次action,之后的每幀,cocos都會(huì)幫我們?nèi)ヒ苿?dòng),而不需要我們?cè)偃ビ?jì)算改怎么移動(dòng)。
如果說“判斷飛機(jī)與敵人、子彈、item的碰撞,并應(yīng)用效果”的處理是理所當(dāng)然的,那么為什么“更新子彈的位置”不能交給cocos去處理?事實(shí)上大多數(shù)的子彈的位置也都是有cocos去應(yīng)用action,我們只需要在子彈發(fā)生的時(shí)候告訴cocos接下來怎么去移動(dòng)子彈就可以了,但是有例外的情況:追蹤彈。它的移動(dòng)和敵人相關(guān),因此在敵人的位置、存活發(fā)生變化的時(shí)候可能需要改變它的移動(dòng),這個(gè)時(shí)候就需要我們?cè)趲g進(jìn)行處理:計(jì)算出新的移動(dòng)方向,告訴cocos取消之前的action,應(yīng)用新的action。
update函數(shù)的執(zhí)行時(shí)間與游戲的幀率是息息相關(guān)的,因?yàn)橛螒虻拿恳粠际歉鶕?jù)這個(gè)函數(shù)的計(jì)算結(jié)果來產(chǎn)生的,如果它的執(zhí)行時(shí)間太長(zhǎng),就會(huì)導(dǎo)致幀率下降。
舉個(gè)例子,如果update的執(zhí)行時(shí)間為0.1秒,那么這個(gè)游戲最高也只可能是10fps,因?yàn)樵?秒內(nèi)它最多執(zhí)行10次,給出10次的結(jié)果,從而cocos也只能由此渲染10幀的畫面。
總而言之,如果你想實(shí)現(xiàn)什么效果,那就在update function里面計(jì)算這個(gè)效果在每一幀里面的作用,然后讓cocos去把幀渲染出來,聯(lián)合起來就可以實(shí)現(xiàn)你要的效果。
2. 游戲背景
首先,作為一個(gè)飛行射擊游戲,我們需要一個(gè)可以滾動(dòng)的背景。自然,準(zhǔn)備一張高度足夠的圖片,讓它從上往下勻速下移是可以實(shí)現(xiàn)這個(gè)效果的,但是實(shí)際上應(yīng)該不會(huì)有人會(huì)這樣做。
在這里,實(shí)際上我們只需要一張圖片,這張圖片剛好可以覆蓋整個(gè)游戲的可是窗口就可以了:

如果你仔細(xì)觀察,你會(huì)發(fā)現(xiàn),這張圖片的上面和下面是可以接合的,也就是說,如果你在這張圖片的正上方再放一張同樣的圖片,你會(huì)發(fā)現(xiàn)這兩張圖片的景色、河流都是剛好接合起來,看不出是兩種圖片,而像是一張圖片。
事實(shí)上,我們正式利用這樣的圖片來實(shí)現(xiàn)背景的無限滾動(dòng)。
我們只需要在一開始就放一張圖片鋪滿顯示窗口,然后在窗口的正上方再放一張一樣的圖片,讓它們同時(shí)勻速往下移動(dòng),等到第一張圖片完全離開窗口,而第二種剛好鋪滿窗口的時(shí)候,在下一幀恢復(fù)它們的初始位置,不斷重復(fù)這個(gè)過程,那么看起來背景就是在不斷的移動(dòng)。
_scrollBackground:function(){
this.bgImage1.y -= Math.ceil(this.winSize.height*0.001); //下移兩張圖片的位置
this.bgImage2.y -= Math.ceil(this.winSize.height*0.001);
if(this.bgImage1.y<-(this.winSize.height/2)){ //當(dāng)?shù)谝粡垐D片的y小于負(fù)的二分之一屏幕高度,就是它剛好離開視窗的時(shí)候,此時(shí)恢復(fù)兩張圖片的初始位置
this.bgImage1.y = this.winSize.height/2; //第一張置于中間
this.bgImage2.y = this.winSize.height/2+this.winSize.height; //第二張放著第一張的上面
}
}
3. 添加/移動(dòng) 飛機(jī)
飛機(jī)本身只是一個(gè)Sprite:
var Plane = cc.Sprite.extend({
image:null,
bullet:[],
status:null,
blood:5000,
layer:null,
gameScene:null,
shotInterval:200,
allowShot:true,
allowMissile:true,
allowUltra:true,
hitEffect:null,
addedEffects:[],
laserHit:true,
ctor:function(gameScene, layer){
this._super("res/main_plane.png"); //初始化飛機(jī)的圖片
this.gameScene = gameScene;
this.layer = layer;
this.addedEffects = [];
this.allowShot = true;
this.allowMissile = true;
this.allowUltra = true;
this.laserHit = true;
}
}
在Game Scene中添加飛機(jī):
var plane = new Plane(this, layer);
plane.scale = 0.5;
this.targetX = plane.x = winSize.width/2;
this.targetY = plane.y = winSize.height/2;
layer.addChild(plane,7); //給game scene的layer添加飛機(jī)
this.plane = plane;
這樣在game scene上就會(huì)出現(xiàn)飛機(jī)的圖片,但是僅僅只是這樣是不夠的,我們需要這個(gè)飛機(jī)可以根據(jù)我們的點(diǎn)擊進(jìn)行移動(dòng),原理也很簡(jiǎn)單,就是獲取點(diǎn)擊的坐標(biāo),然后讓飛機(jī)移動(dòng)過去,這里我們需要監(jiān)聽兩個(gè)事件:
- Touch
- MouseDown
在game scene里監(jiān)聽事件:
if("touches" in cc.sys.capabilities){
cc.eventManager.addListener({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
onTouchBegan: this._onTouchBegan.bind(this) //處理觸摸的回調(diào)
}, this);
} else {
cc.eventManager.addListener({
event: cc.EventListener.MOUSE,
onMouseDown: this._onMouseDown.bind(this) //處理鼠標(biāo)點(diǎn)擊的回調(diào)
}, this);
}
在回調(diào)里使用點(diǎn)擊的坐標(biāo)讓飛機(jī)移動(dòng):
_onTouchBegan:function(touch, event){
var clickX = touch.getLocation().x;
var clickY = touch.getLocation().y;
this.targetX = clickX;
this.targetY = clickY;
var speedo = Math.sqrt(Math.pow(clickX-this.plane.x,2)+Math.pow(clickY-this.plane.y,2))/300;
var moveAction = cc.moveTo(speedo, cc.p(clickX, clickY));
if(this.action!=null)
this.plane.stopAllActions();
this.plane.runAction(moveAction);
this.action = moveAction;
},
_onMouseDown: function(event){
var clickX = event.getLocationX();
var clickY = event.getLocationY();
this.targetX = clickX;
this.targetY = clickY;
var speedo = Math.sqrt(Math.pow(clickX-this.plane.x,2)+Math.pow(clickY-this.plane.y,2))/300;
var moveAction = cc.moveTo(speedo, cc.p(clickX, clickY));
if(this.action!=null)
this.plane.stopAllActions();
this.plane.runAction(moveAction);
this.action = moveAction;
}
這樣,我們點(diǎn)哪里,飛機(jī)就會(huì)移動(dòng)到哪里。
但是此時(shí)的飛機(jī)只能夠移動(dòng),并不能攻擊。
4. 添加/管理敵人
考慮到敵人可能有多種多樣,每種敵人會(huì)有自己的子彈、運(yùn)動(dòng)軌跡,所以我們先定義一個(gè)基類,里面定義了敵人的通用接口,具體的實(shí)現(xiàn)在每個(gè)敵人的之類里面完成。
var Enemy = cc.Sprite.extend({
bullets:null,
scene:null,
layer:null,
score:null,
blood:null,
explosionHarm:null,
//ctor:function(image){
//
// this._super(image);
//
//
//},
appear:function(){}, //敵人出場(chǎng)
attack:function(){}, //攻擊
explode:function(){}, //爆炸
hurt:function(){}, //被攻擊受到傷害
move:function(){}, //敵人移動(dòng)
reuse:function(scene, layer){ //reuse和unuse方法用于敵人的緩存回收,這部分將獨(dú)立說明
trace("reuse enemy:");
this.ctor(scene, layer);
},
unuse:function(){
this.layer.removeChild(this);
}
});
Alpha(一個(gè)Enemy的子類,是一個(gè)具體的敵人)的部分實(shí)現(xiàn):
var Alpha = Enemy.extend({
image:"res/enemy/alpha.png",
allowShot:true,
blood:null,
score:100,
explosionHarm:100,
ctor:function(scene, layer){ //初始化Alpha,并調(diào)用appear方法,使得敵人在游戲場(chǎng)景中登場(chǎng)
this.allowShot = true;
this.scene = scene;
this.gameScene = this.scene;
this.layer = layer;
this._super(this.image);
this.blood = 50;
this.threshold = 50;
this.appear();
layer.removeChild(this);
if(scene.enemies.indexOf(this)!=-1)
scene.enemies.splice(scene.enemies.indexOf(this), 1);
layer.addChild(this, 2);
scene.enemies.push(this); //用一個(gè)數(shù)組存儲(chǔ)所有的敵人,在之后的攻擊判定、清理回收等會(huì)用到
},
appear:function(){ //登場(chǎng),在可是窗口的上方的一個(gè)隨機(jī)位置里面出現(xiàn)
this.scale = 0.5;
this.x = Math.round(Math.random()*cc.game.winSize.width);
this.y = cc.game.winSize.height+100;
this.move(); //登場(chǎng)后移動(dòng),移動(dòng)到可視窗口下方的某個(gè)隨機(jī)位置
},
move:function(){
var moveAction = cc.moveTo(12, cc.p(Math.random()*cc.game.winSize.width,-400));
this.runAction(moveAction); //移動(dòng)敵人
}
}
這樣,如果我們創(chuàng)建一個(gè)Alpha的實(shí)例,那么它就會(huì)出現(xiàn)在游戲場(chǎng)景,并且從游戲窗口的上方的某個(gè)位置勻速地向下移動(dòng)。
EnemyManager
為了更好地管理敵人的生成和回收,我們創(chuàng)建了一個(gè)enemyManager來管理敵人的添加、移除回收、碰撞檢測(cè):
var EnemyManager = cc.Class.extend({
allowAddNewEnemy: true,
privateSprite:null,
enemyList:[
Alpha,
Beta,
Gamma
],
enemyFrequency:1,
ctor: function () {
this.privateSprite = new cc.Sprite();
this.allowAddNewEnemy = true;
},
addEnemy: function (scene, layer, frequency) { //負(fù)責(zé)添加新敵人
if (this.allowAddNewEnemy == false)
return;
//new Alpha(scene, layer);
//trace("New enemy");
this.allowAddNewEnemy = false;
var enemyType = this.enemyList[new Date().getTime() % this.enemyList.length];
if(cc.pool.hasObject(enemyType)){
cc.pool.getFromPool(enemyType, scene, layer);
trace("reuse enemy");
}else{
new enemyType(scene, layer);
}
scene.scheduleOnce(function () {
trace("allow new enemy");
this.allowAddNewEnemy = true;
}
.bind(this),this.enemyFrequency);
},
addBoss : function(scene, layer){ //添加BOSS
new Jason(scene, layer);
},
removeEnemy:function(scene, layer, enemy, index){ //從場(chǎng)景里面刪除一個(gè)敵人
scene.score+=enemy.score;
layer.removeChild(enemy);
scene.enemies.splice(index, 1);
cc.pool.putInPool(enemy);
},
enemyAttack:function(scene){ //命令所有敵人進(jìn)行一次攻擊
for(var i =0;i<scene.enemies.length;i++){
scene.enemies[i].attack();
}
},
_collide:function(x1, y1, x2, y2, threshold){ //碰撞檢測(cè)
if((Math.pow((x1-x2),2)+Math.pow((y1-y2),2))>Math.pow(threshold, 2))
return false;
else
return true;
},
collisionCheck:function(scene, layer, plane){ //如果敵人受到攻擊,且血量低于0,讓敵人爆炸,同時(shí)回收敵人
var enemies = scene.enemies;
for(var i = 0;i<enemies.length;i++){
var checkEnemy = enemies[i];
if(this._collide(checkEnemy.x, checkEnemy.y, plane.x, plane.y, checkEnemy.threshold) == true){
plane.blood -= checkEnemy.explosionHarm;
scene.score += checkEnemy.score;
checkEnemy.explode();
this.removeEnemy(scene, layer, checkEnemy, i);
i--;
}
}
},
outEnemyCheck : function(scene, layer){ //如果敵人飛出了游戲場(chǎng)景,也回收敵人
var enemies = scene.enemies;
for(var i = 0;i<enemies.length;i++){
var checkEnemy = enemies[i];
if(checkEnemy.x>-200 && checkEnemy.x<cc.game.winSize.width+200 && checkEnemy.y>-200 && cc.game.winSize.height+200)
;
else {
trace("unuse out ennemy");
this.removeEnemy(scene, layer, checkEnemy, i);
i--;
}
}
}
});
5. 子彈系統(tǒng)
飛機(jī)與敵人都可以發(fā)射子彈,我們需要解決的問題是:
- 子彈的生成、發(fā)射(位置,角度)、速度、傷害,動(dòng)畫
- 子彈與飛機(jī)/敵人的碰撞檢測(cè)
- 子彈的回收
Bullet基類
與Enemy類似地,基類只定義了接口以及共同的方法:
var Bullet = cc.Class.extend({
BulletOwner:null,
type:null,
plane:null,
layer:null,
action:null,
bullets:null,
scene:null,
ctor:function(type, plane, layer, scene){ //根據(jù)給出的type初始化bullet
this.type = type;
this.plane = plane;
this.layer = layer;
this.scene = scene;
this.bullets = [];
this._shot(); //初始化完畢后馬上發(fā)射子彈,_shot方法應(yīng)該在子彈之類里面具體實(shí)現(xiàn)
},
_validate:function(){ //檢測(cè)子彈是否離開了可視窗口
var valid = false;
for(var i =0;i<this.bullets.length;i++){
var childBullet = this.bullets[i];
if(childBullet.x>=-20 && childBullet.x<=cc.game.winSize.width+20 && childBullet.y>=0 && childBullet.y<=cc.game.winSize.height+20){
valid = true;
break;
}else{
valid = false;
//break;
}
}
return valid;
},
reuse:function(type, plane, layer, scene){ //reuse和unuse用于回收、重新利用子彈
this.ctor(type, plane, layer, scene);
},
unuse:function(){
for(var i =0;i<this.bullets.length;i++){
this.bullets[i].stopAllActions();
this.layer.removeChild(this.bullets[i]);
}
},
_shot:function(){}
});
BasicBullet
BasicBullet是一個(gè)具體實(shí)現(xiàn)的子彈之類,設(shè)置了子彈的圖片、數(shù)量、發(fā)射位置,實(shí)現(xiàn)了_shot方法:
var BasicBullet = Bullet.extend({
BulletType:{
"basic" : "res/bullet/bullet_basic.png" //子彈的圖片
},
BulletLocation : {
"basic" : [{x:0, y:0}] //數(shù)組的元素?cái)?shù)量代表子彈的數(shù)量,x和y坐標(biāo)代表某個(gè)子彈的發(fā)射位置
},
BulletOwner:"enemy",
harm : 5, //子彈的傷害
_shot:function(){ //_shot方法的作用:創(chuàng)建子彈Sprite,添加到scene中,調(diào)用runBulletCustomAction使得子彈開始運(yùn)動(dòng)
for(var i =0;i<this.BulletLocation[this.type].length;i++){
var bulletLocation = this.BulletLocation[this.type][i];
var newBullet = new cc.Sprite(this.BulletType[this.type]);
newBullet.scale = 0.15;
newBullet.x = this.plane.x+bulletLocation.x;
newBullet.y = this.plane.y+bulletLocation.y;
newBullet.harm = this.harm;
this.runBulletCustomAction(newBullet);
this.layer.addChild(newBullet, 1);
this.bullets.push(newBullet);
}
this.layer.bullets.push(this);
},
runBulletCustomAction:function(newBullet){ //實(shí)現(xiàn)了每種子彈自己的運(yùn)動(dòng)方式,不同的子彈的不同行為在這里定義
var action = cc.moveTo(4, cc.p(newBullet.x, -(cc.director.getWinSize().height+300)));
var rotation = cc.rotateBy(10,3600, 3600);
newBullet.runAction(cc.spawn(action,rotation));
}
});
實(shí)際的子彈種類會(huì)有多種,每個(gè)子彈的飛行軌跡也都各有不同。BasicBullet的飛行軌跡從初始化就已經(jīng)決定了,以后不再改變,直到發(fā)生碰撞或者離開窗口被回收。
但是也可以實(shí)現(xiàn)更復(fù)雜的飛行軌跡,比如跟蹤彈(MissileBullet)就會(huì)跟蹤離它最近的敵人的位置,這個(gè)子彈的運(yùn)動(dòng)就需要每幀更新):
MissileBullet
var MissileBullet = Bullet.extend({
updatePerFrames:true, //是否需要每幀更新,對(duì)于MissileBullet來說是true
allowSwitchEnemy:false, //是否允許在原追蹤敵人毀滅后更換瞄準(zhǔn)目標(biāo)
/* ... */
aimedEnemy:function(bulletX, bulletY, oldX, oldY, bullet){ //瞄準(zhǔn)敵人,每幀都會(huì)計(jì)算當(dāng)前離追蹤彈最接近的敵人是哪一個(gè),在allowSwitchEnemy為true時(shí)會(huì)在敵人摧毀后重新計(jì)算,否則將會(huì)沿著之前的軌跡方向飛行
var enemyX = 0;
var enemyY = 0;
var foundEnemy = false;
if(this.allowSwitchEnemy == true || bullet.lockEnemy == false ) {
if (this.scene.enemies != null && this.scene.enemies.length > 0) {
var enemyList = this.scene.enemies;
var leastDistance = 1000000000;
var leastX = bulletX;
var leastY = cc.game.winSize.height + 200;
;
for (var i = 0; i < enemyList.length; i++) {
var checkEnemy = enemyList[i];
if (checkEnemy.x > 0 && checkEnemy.x < cc.game.winSize.width && checkEnemy.y > 0 && checkEnemy.y < cc.game.winSize.height)
;
else
continue;
var distance = Math.pow((bulletX - checkEnemy.x), 2) + Math.pow((bulletY - checkEnemy.y), 2);
if (distance < leastDistance) {
foundEnemy = true;
bullet.lockEnemy = true;
bullet.enemy = checkEnemy;
leastDistance = distance;
leastX = checkEnemy.x;
leastY = checkEnemy.y;
}
}
enemyX = leastX;
enemyY = leastY;
}
}else{
if(bullet.enemy != null && this.scene.enemies.indexOf(bullet.enemy) > -1 ){
foundEnemy = true;
enemyX = bullet.enemy.x;
enemyY = bullet.enemy.y;
}
}
if(foundEnemy != true){
var deltaX = bulletX - oldX;
var deltaY = bulletY - oldY;
var deltaD = Math.sqrt(Math.pow(deltaX,2)+Math.pow(deltaY,2));
enemyX = bulletX + this.speed*deltaX/deltaD;
enemyY = bulletY + this.speed*deltaY/deltaD;
}
var rotationAngle = Math.atan((enemyY - bulletY)/(enemyX - bulletX))*360/(2*Math.PI);
if((enemyX - bulletX)>=0)
;
else
rotationAngle += 180;
if(rotationAngle<0)
rotationAngle +=360;
rotationAngle = 90 - rotationAngle;
//trace((enemyY - bulletY)+","+(enemyX - bulletX)+","+rotationAngle);
//trace(enemyY+","+(bulletY)+","+(oldY));
var duration = Math.sqrt(Math.pow(enemyX - bulletX, 2) + Math.pow(enemyY - bulletY, 2))/this.speed;
//trace("bullet speed:"+duration);
return {x:enemyX, y:enemyY, duration:duration, rotationAngle:rotationAngle};
},
perFramesUpdate:function(scene, layer, plane, bullet){ //每幀更新,每一幀都根據(jù)瞄準(zhǔn)的敵人的位置調(diào)整子彈的運(yùn)動(dòng)軌跡
if(bullet.initial == true) {
bullet.scheduleOnce(function(){
this.initial = false;
}.bind(bullet), 0.2);
return ;
}
//if(Math.abs(bullet.x - bullet.oldX)>10 || Math.abs(bullet.y - bullet.oldY)>10)
//if(bullet.moveAction!=null)
// bullet.moveAction.speed(10000);
//
if(bullet.recentStop == null || bullet.recentStop == false) {
bullet.stopAllActions();
bullet.recentStop = true;
bullet.scheduleOnce(function(){
this.recentStop = false;
}.bind(bullet), 0.02);
}
else
return ;
//if(bullet.moveAction!=null)
// trace("speed:"+bullet.moveAction.getSpeed());
//
//bullet.stopAllActions();
var enemyLocation = this.aimedEnemy(bullet.x, bullet.y, bullet.oldX, bullet.oldY, bullet);
bullet.oldX = bullet.x;
bullet.oldY = bullet.y;
//trace("now:"+bullet.x+","+bullet.y);
//trace(enemyLocation.x+","+enemyLocation.y);
var action = cc.moveTo(enemyLocation.duration, cc.p(enemyLocation.x, enemyLocation.y));
//var action = cc.moveTo(1, cc.p(bullet.x+1000, bullet.y+1000));
//action.easing(cc.easeIn(20));
//bullet.setRotationSkewX(enemyLocation.rotationAngle);
var rotationAction = cc.rotateTo(0.01,enemyLocation.rotationAngle);
//trace("angel:"+enemyLocation.rotationAngle);
bullet.runAction(cc.spawn(action, rotationAction));
bullet.moveAction = action;
}
})
發(fā)射子彈
在飛機(jī)和敵人上發(fā)射子彈,是通過調(diào)用plane._shot()和enemy.attack()來進(jìn)行的,他們的實(shí)現(xiàn)其實(shí)是類似的:
飛機(jī)的_shot方法里有:
if (this.allowShot) {
//cc.game.musicManager.playEffect("bullet");
if (cc.pool.hasObject(DefaultBullet)) { //從回收池里取出DefaultBullet實(shí)例
cc.pool.getFromPool(DefaultBullet, "normal", this, this.layer, this.gameScene);
//trace("reuse bullets");
} else { //沒有就new一個(gè)DefaultBullet的實(shí)例
new DefaultBullet("normal", this, this.layer, this.gameScene);
}
this.allowShot = false;
this.scheduleOnce(function () { //每0.2秒設(shè)計(jì)一次
this.allowShot = true;
}
.bind(this), 0.2);
}
碰撞檢測(cè)
飛機(jī)發(fā)射的子彈和敵人之間,以及敵人的子彈與飛機(jī)之間,需要進(jìn)行碰撞檢測(cè),如果發(fā)射了碰撞,就需要回收子彈,計(jì)算傷害(并且根據(jù)血量摧毀敵人/飛機(jī),同時(shí)顯示特效等)。
碰撞檢測(cè)的方法有很多,這里我們只使用了最簡(jiǎn)單的方法:計(jì)算子彈和機(jī)體的距離,就是根據(jù)兩個(gè)目標(biāo)的x軸和y軸運(yùn)用勾股定理計(jì)算距離,當(dāng)距離小于一定的閾值(threshold)時(shí)判定為發(fā)生碰撞。
碰撞一共有3種:
- 子彈與機(jī)體
- 敵人與飛機(jī)
- 物品與飛機(jī)
這3種碰撞分別由3個(gè)manager來執(zhí)行檢查:BulletManager,EnemManager,ItemManager。
以BulletManager的子彈與機(jī)體的碰撞檢測(cè)為例:
collisionCheck:function(scene, layer){ //分別進(jìn)行飛機(jī)與敵人的子彈,飛機(jī)的子彈與敵人的碰撞檢測(cè),計(jì)算傷害,在血量為0時(shí)調(diào)用機(jī)體的explode方法,回收敵人,同時(shí)進(jìn)行子彈回收
var playerX= scene.plane.x;
var playerY= scene.plane.y;
var bullets = layer.bullets;
for(var i =0;i<bullets.length;i++) {
var checkBullet = bullets[i];
if (checkBullet.BulletOwner == "enemy") {
for(var k =0;k<checkBullet.bullets.length;k++){
var childBullet = checkBullet.bullets[k];
if (this._collide(playerX, playerY, childBullet.x, childBullet.y, 50) == true) {
layer.removeChild(childBullet);
checkBullet.bullets.splice(k,1);
k--;
//var blinkAction = cc.blink(1,3);
//scene.plane.runAction(blinkAction);
scene.plane.showHitEffect(childBullet.harm);
if(checkBullet.bullets.length==0){
cc.pool.putInPool(checkBullet);
layer.bullets.splice(i, 1);
i--;
}
}
}
}
}
for(var s = 0;s<scene.enemies.length;s++){
//trace("s:"+s);
var checkEnemy = scene.enemies[s];
var enemyX = checkEnemy.x;
var enemyY = checkEnemy.y;
for(var i =0;i<bullets.length;i++) {
var checkBullet = bullets[i];
if (checkBullet.BulletOwner == "player") {
for(var k =0;k<checkBullet.bullets.length;k++){
var childBullet = checkBullet.bullets[k];
var bulletX = childBullet.x;
var bulletY = childBullet.y;
if (this._collide(enemyX, enemyY, childBullet.x, childBullet.y, checkEnemy.threshold) == true) {
layer.removeChild(childBullet);
checkBullet.bullets.splice(k,1);
k--;
//var blinkAction = cc.blink(1,3);
//scene.plane.runAction(blinkAction);
checkEnemy.showHitEffect(childBullet.harm, {x:bulletX, y:bulletY});
if(checkBullet.bullets.length==0){
cc.pool.putInPool(checkBullet);
layer.bullets.splice(i, 1);
i--;
}
}
}
}
}
// place out of inside loop, otherwise will case s++ run multiple times!
trace(checkEnemy.blood);
if(checkEnemy.blood<=0){
checkEnemy.explode();
cc.game.enemyManager.removeEnemy(scene, layer, checkEnemy, s);
s--;
}
//check ennemy done
}
},
_collide:function(x1, y1, x2, y2, threshold){ //用勾股定理計(jì)算舉例,對(duì)比threshold判定碰撞
if((Math.pow((x1-x2),2)+Math.pow((y1-y2),2))>Math.pow(threshold, 2))
return false;
else
return true;
}
子彈回收
碰撞了的子彈就會(huì)被回收,還有一種情況需要回收:沒發(fā)生碰撞但是飛出了可視窗口。
這種情況在BulletManager里面進(jìn)行了每幀檢測(cè)和處理:
manageBullet:function(layer){
if(layer.bullets.length>0)
for(var i=0;i<layer.bullets.length;i++){
var bullet = layer.bullets[i];
if(bullet._validate()==true)
;
else{
cc.pool.putInPool(bullet);
layer.bullets.splice(i,1);
i--;
}
}
}
6. 物品(Item)系統(tǒng)
物品系統(tǒng)是飛行射擊游戲一個(gè)常見的系統(tǒng),具體來說就是可以控制飛機(jī)去吃一些隨機(jī)生成的物品,從而獲得一些額外的增強(qiáng)效果。
筆者同樣給游戲添加了這個(gè)系統(tǒng)。
ItemManager
var ItemManager = cc.Class.extend({
scene:null,
layer:null,
plane:null,
items:{
missile:"res/items/missile.png", //物品以及對(duì)于圖片
blood:"res/items/blood.png",
shield:"res/items/shield.png",
bigBang:"res/items/bigBang.png",
shine:"res/items/shine.png",
ultra:"res/items/ultra.png"
},
itemName:["missile", "blood", "shield", "bigBang", "shine", "ultra"], //物品名
basicGap: 2,
randomGap: 3,
allowAddItem:true,
floatingItems:[],
privateSprite:null,
durableEffectFunc:[],
allowDurableEffect:true,
ctor:function(){ //ItemManager初始化
this.privateSprite = new cc.Sprite();
this.durableEffectFunc = []; // remove durable effect when re-init game
this.floatingItems = [];
this.allowDurableEffect = true;
this.allowAddItem = true;
}
}
添加物品
添加物品其實(shí)和添加敵人的做法是類似的,就是在一個(gè)隨機(jī)的位置上添加一個(gè)物品,并讓它飛行。與敵人不同之處在于,物品只可能與飛機(jī)發(fā)生碰撞,既不會(huì)與敵人也不會(huì)與子彈發(fā)生碰撞。
generateItem:function(scene, layer, plane){
if(this.allowAddItem == false)
return ;
if(this.allowAddItem == true) {
var item = this.itemName[new Date().getTime() % this.itemName.length]; //隨機(jī)產(chǎn)生物品
//item = this.itemName[1];
var itemSprite = new cc.Sprite(this.items[item]);
itemSprite.itemName = item;
itemSprite.scale = 0.3;
itemSprite.x = Math.round(Math.random() * cc.game.winSize.width);
itemSprite.y = cc.game.winSize.height + 100;
var moveAction = cc.moveTo(6, cc.p(Math.random() * cc.game.winSize.width, -400));
itemSprite.runAction(moveAction); //移動(dòng)物品
layer.addChild(itemSprite, 5);
scene.items.push(itemSprite);
this.allowAddItem = false;
scene.scheduleOnce(function(){ //物品產(chǎn)生的間隔為基礎(chǔ)間隔+隨機(jī)時(shí)間
this.allowAddItem = true;
}.bind(this), Math.random()*this.randomGap+this.basicGap);
}
檢測(cè)物品碰撞
_collide:function(x1, y1, x2, y2, threshold){
if((Math.pow((x1-x2),2)+Math.pow((y1-y2),2))>Math.pow(threshold, 2))
return false;
else
return true;
},
collisionCheck:function(scene, layer, plane){
var itemList = scene.items;
for(var i=0;i<itemList.length;i++){
var checkItem = itemList[i];
if(this._collide(checkItem.x, checkItem.y, plane.x, plane.y, 50) == true){
this.takeEffect(scene, checkItem.itemName, plane); //如果物品與飛機(jī)發(fā)生了碰撞,那么就應(yīng)用物品的效果
layer.removeChild(checkItem);
scene.items.splice(i, 1);
i--;
}
}
}
物品效果
物品效果分兩種:即時(shí)效果和延時(shí)效果,前者有加血、一次性范圍攻擊等,后者有護(hù)罩、增強(qiáng)型子彈等。
takeEffect:function(scene, itemName, plane){
switch(itemName){ //根據(jù)物品的名稱選擇要加的效果
case "blood": cc.game.itemManager.addPlaneEffect(scene, "blood", "res/particle/wsparticle_revival01.ccbi", plane, 2);break;
case "shield":cc.game.itemManager.addPlaneEffect(scene, "shield", "res/particle/wsparticle_buff01.ccbi", plane, 10);break;
case "missile":cc.game.itemManager.addPlaneEffect(scene, "missile","res/particle/wsparticle_tailinga.ccbi" , plane, 10);break;
case "bigBang":cc.game.itemManager.addPlaneEffect(scene, "bigBang","res/particle/wsparticle_item_boom_02.ccbi" , plane, 10);break;
case "shine":cc.game.itemManager.addPlaneEffect(scene, "shine","res/particle/wsparticle_universallylight2.ccbi" , plane, 10);break;
case "ultra":cc.game.itemManager.addPlaneEffect(scene, "ultra","res/particle/wsparticle_super02.ccbi" , plane, 10);break;
}
},
addPlaneEffect:function(scene, effectName, effectCCBI, plane, duration){
var timeStamp = new Date().getTime();
var locationFunc = null;
var followTarget = null;
switch (effectName){ //根據(jù)效果的名稱決定效果特效的起始位置
case "bigBang":locationFunc = function(x, y){ //bigBang的特效位置是固定的
return {
x:cc.game.winSize.width/2,
y:cc.game.winSize.height/2
};
};followTarget = false;break;
default : locationFunc = function(x, y){ //其余的與飛機(jī)當(dāng)前位置相關(guān)
return {
x:x,
y:y
};
};followTarget = true;break; //followTarget 為true就以為則特效藥跟隨飛機(jī)
}
var ccbNode = cc.game.particleManager.showParticle(effectName, scene, plane, locationFunc,followTarget, duration); //使用粒子系統(tǒng)來展示物品的應(yīng)用特效
if(followTarget == true){ //需要跟隨的效果加入到飛機(jī)的addedEffects數(shù)組中
plane.addedEffects.push({name:effectName, effect:ccbNode, timeStamp:timeStamp});
scene.scheduleOnce(function(){
//this.removeChild(ccbNode);
this.plane.removeAddedEffect(timeStamp);
}.bind(scene), duration);
}
this.instantEffect(scene, effectName, plane); //即時(shí)效果和延時(shí)效果使用不同的處理
this.durableEffect(scene, effectName, plane);
},
instantEffect:function(scene, effectName,plane){ //即時(shí)效果
switch (effectName){
case "bigBang" : this.bigBang(scene, effectName,plane);break;
case "blood" : this.addBlood(scene, effectName,plane);break;
}
},
durableEffect:function(scene, effectName,plane){ //延時(shí)效果
switch (effectName){
case "shine" : this.shineField(scene, effectName,plane);break; //shine效果需要添加每幀處理方法
}
},
shineField:function(scene, effectName,plane){ //對(duì)于有些效果,除了需要添加到飛機(jī)的addedEffects上,還需要給出一個(gè)效果function來在每幀執(zhí)行引用效果,例如shine特效,會(huì)計(jì)算飛機(jī)周圍一定距離的敵人,并給予一定的傷害
this.durableEffectFunc.push({
effectName:"shine",
effectFunc:function (scene, layer, plane) {
var enemyList = scene.enemies;
for (var i = 0; i < enemyList.length; i++) {
var checkEnemy = enemyList[i];
if (((Math.pow((plane.x-checkEnemy.x),2)+Math.pow((plane.y-checkEnemy.y),2))<Math.pow(300, 2)) == true) {
checkEnemy.showHitEffect(10);
//checkEnemy.blood -= 10;
//
//if (checkEnemy.blood < 0)
// checkEnemy.blood = 0;
}
}
}});
scene.scheduleOnce(function(){
for(var j =0 ;j<this.durableEffectFunc.length;j++){
if(this.durableEffectFunc[j].effectName == "shine"){
this.durableEffectFunc.splice(j, 1);
break;
}
}
}.bind(this), 10);
}
7. 特效系統(tǒng)
特效系統(tǒng)負(fù)責(zé)產(chǎn)生并管理兩種特效:Particle System和幀動(dòng)畫。
Particle System是設(shè)定一些規(guī)則,使用紋理圖片隨機(jī)產(chǎn)生一些粒子特效,這些效果是隨機(jī)的,不可重復(fù)的;
幀動(dòng)畫是使用一些圖片連續(xù)播放產(chǎn)生一些特效效果,這些特效每一次播放都是相同的。
本游戲中,飛機(jī)被擊中時(shí)產(chǎn)生的火花效果是Particle System產(chǎn)生的,而物品效果,Boss的激光以及機(jī)體爆炸則是幀動(dòng)畫。
Particle Manager的作用:
- 在給定的位置上顯示特效
- 回收結(jié)束的特效
Particle Manager中有3個(gè)show方法,分別處理3種特效:
- showParticle:物品特效
- showFire:機(jī)體被擊中的特效
- showLaser:Boss的激光射擊特效
在需要顯示特效的時(shí)候,例如子彈碰撞檢測(cè)判定為擊中、敵人血量低于0要爆炸、Boss要發(fā)射激光攻擊、飛機(jī)吃到物品等等,就會(huì)調(diào)用Particle Manager相應(yīng)的方法來顯示特效。
Particle System
產(chǎn)生Particle System特效的方法:讀取plist文件
Particle Manager:
hitEffect = new cc.ParticleSystem("res/particle2/particle.plist");
hitEffect.duration = -1;
hitEffect.setAutoRemoveOnFinish(true);
hitEffect.x = plane.x;
hitEffect.y = plane.y;
plane.gameScene.addChild(hitEffect);
plane.hitEffect = hitEffect;
plane.scheduleOnce(function(){
plane.hitEffect = null;
//plane.gameScene.removeChild(hitEffect);
hitEffect.x = -1000;
hitEffect.y = -1000;
this.fireEffect.push(hitEffect);
}.bind(this), 1);
想要制作自定義的特效,可以去這個(gè)網(wǎng)站制作:
http://www.onebyonedesign.com/flash/particleeditor/
幀動(dòng)畫
對(duì)于本游戲的幀動(dòng)畫,有兩個(gè)存儲(chǔ)格式:plist,ccbi。
其中ccbi文件是cocos2dx所支持的特效文件,在C++和Lua使用廣泛,但是JS中暫時(shí)找不到使用sample,在官方文檔中也找不到相應(yīng)的API,在stackoverflow中也沒有相應(yīng)的解答。
但是筆者在cocos2dx-js的最新版本的源碼中找到了ccbi相關(guān)的模塊,所以最終是通過閱讀這個(gè)模塊的源碼來得知cocos2dx-js是如何讀取ccbi文件的,希望可以給讀者一些參考。
Particle Manager:
particleList:{
hit:"/res/particle/wsparticle_hit_01.ccbi",
explode:"/res/particle/wsparticle_hit_02.ccbi",
missile:"res/particle/wsparticle_tailinga.ccbi",
blood:"res/particle/wsparticle_revival01.ccbi",
shield: "res/particle/wsparticle_buff01.ccbi",
bigBang: "res/particle/wsparticle_item_boom_02.ccbi",
shine: "res/particle/wsparticle_universallylight2.ccbi",
ultra:"res/particle/wsparticle_super02.ccbi",
bomb:"/res/particle/wsparticle_hit_03.ccbi",
warning:"/res/particle/wsparticle_warning.ccbi"
};
var nodeLibrary = new cc.NodeLoaderLibrary();
nodeLibrary.registerDefaultCCNodeLoaders();
reader =new cc.BuilderReader(new cc.BuilderReader(nodeLibrary,null, null,null));
var ccbNode = reader.readNodeGraphFromFile(this.particleList[particleName], null, null, new cc.BuilderAnimationManager());
var location = locationFunc(target.x, target.y);
ccbNode.x = location.x;
ccbNode.y = location.y;
scene.addChild(ccbNode);
reader.ccbNode = ccbNode;
reader.ccbNode.scheduleOnce(function(){
this.readerCache[particleName].push(reader);
//reader.ccbNode.stopAllActions();
if(particleName == "missile") {
// for missile particle re-show bug
reader.ccbNode.x = -400;
reader.ccbNode.y = -400;
}
else
scene.removeChild(reader.ccbNode);
}.bind(this), endurance);
8. Stage系統(tǒng)
所謂stage系統(tǒng),其實(shí)就是指游戲的階段管理。
眾所周知,一個(gè)游戲一般不會(huì)一上來就是打Boss,也不會(huì)一上來就是最高難度,會(huì)有一個(gè)階段的推進(jìn)。
所以筆者設(shè)計(jì)了Stage系統(tǒng)來控制游戲的階段過渡。
Stage Manager
var StageManager = cc.Class.extend({
stage:0,
stageList:null,
nextStage:true,
stageFunc:null,
preStageFunc:null,
ctor:function(stageList){ //初始化,stageList包含游戲的階段信息,stageManager將會(huì)使用這些信息來切換stage
this.stageList = stageList;
this.nextStage = true;
this.stage = -1;
this.stageFunc={};
this.preStageFunc = {};
this.stageFunc["normal"] = this.normalStage;
this.stageFunc["mega"] = this.megaStage;
this.stageFunc["boss"] = this.bossStage;
this.preStageFunc["boss"] = this.preBossStage;
},
manageGameStage:function(scene, layer, plane){ //stage切換方法,每個(gè)stage都有自己的執(zhí)行方法,主要是調(diào)整了一些運(yùn)行配置,諸如敵人產(chǎn)生間隔等
if(this.nextStage == true){ //nextStage 為true時(shí)切換至下一個(gè)stage
if(this.stage<this.stageList.length-1){
this.stage++;
//console.log("Change stage to:"+this.stageList[this.stage].name);
if(this.preStageFunc[this.stageList[this.stage].name] != null)
this.preStageFunc[this.stageList[this.stage].name](scene, layer, plane);
this.stageFunc[this.stageList[this.stage].name](scene, layer, plane);
this.nextStage = false;
scene.scheduleOnce(function(){
this.nextStage = true;
}.bind(this), this.stageList[this.stage].duration);
}else{
this.endStage(scene);
}
}else{
this.stageFunc[this.stageList[this.stage].name](scene, layer, plane);
}
},
endStage:function(scene){ //默認(rèn)的結(jié)束stage,游戲結(jié)束
//console.log("End stage.");
//cc.director.pause();
new GameOverLayer(scene);
},
normalStage:function(scene, layer, plane){ //常規(guī)階段
scene.plane._shot();
cc.game.enemyManager.addEnemy(scene, layer, 0.2);
cc.game.enemyManager.enemyAttack(scene);
cc.game.bulletManager.manageBullet(layer);
cc.game.bulletManager.collisionCheck(scene, layer);
cc.game.bulletManager.updateBulletLocation(scene, layer, plane);
cc.game.enemyManager.collisionCheck(scene, layer, plane);
cc.game.enemyManager.outEnemyCheck(scene, layer);
scene.plane.update();
scene.gameInfo.update();
if(scene.boss!=null)
scene.boss.update();
if(scene.laserCheck == true)
scene.boss.laserCheck(scene, layer, plane);
cc.game.itemManager.generateItem(scene, layer, plane);
cc.game.itemManager.collisionCheck(scene, layer, plane);
cc.game.itemManager.applyDurableEffect(scene, layer, plane);
if(plane.blood<=0) {
trace("dead");
//cc.director.pause();
cc.game.stageManager.nextStage = true;
}
},
megaStage:function(scene, layer, plane){ //mega階段
cc.game.enemyManager.enemyFrequency = 1;
cc.game.stageManager.normalStage(scene, layer, plane);
},
preBossStage:function(scene, layer, plane){ //preBoss階段,Boss出來前顯示一個(gè)警告
cc.game.enemyManager.addBoss(scene, layer); //添加一個(gè)Boss
cc.game.enemyManager.enemyFrequency = 3;
cc.game.particleManager.showParticle("warning", scene, plane, function(x, y){
return {
x:cc.game.winSize.width/2,
y:cc.game.winSize.height/2
};
},false, 4);
},
bossStage:function(scene, layer, plane){ //boss階段
cc.game.stageManager.normalStage(scene, layer, plane);
},
});
Stage 配置
在main.js里面,啟動(dòng)游戲之前使用stageList配置來配置游戲的stage。
var defaultStage = [{name:"normal", duration: 20}, {name:"mega", duration: 20}, {name:"boss", duration:100000}]; //20秒normal stage,20秒mega stage,然后一直是Boss stage
cc.game.onStart = function(){
cc.view.adjustViewPort(false);
cc.view.setDesignResolutionSize(512, 768, cc.ResolutionPolicy.SHOW_ALL);
cc.view.resizeWithBrowserSize(true);
//load resources
cc.LoaderScene.preload(g_resources, function () {
cc.game.musicManager = new MusicManager();
cc.game.bulletManager = new BulletManager();
cc.game.enemyManager = new EnemyManager();
cc.game.itemManager = new ItemManager();
cc.game.particleManager = new ParticleManager();
cc.game.stageManager = new StageManager(defaultStage);
cc.director.runScene(new MenuScene());
cc.director.setDisplayStats(false);
}, this);
};
cc.game.run();
var trace = function() {
//cc.log(Array.prototype.join.call(arguments, ", "));
};
九、 Android/iOS 原生App
Cocos2dx-JS最大的特點(diǎn)就是跨平臺(tái),既可以以html的web形式發(fā)布,也可以編譯成Android和iOS的原生App發(fā)行。但是實(shí)際上,要想在移動(dòng)平臺(tái)獲得web的運(yùn)行效果,其中有大量的調(diào)優(yōu)和適配工作。有興趣的同學(xué)可以自己研究一下。
十、總結(jié)
至此,筆者的小游戲就大致介紹完了,更加具體的細(xì)節(jié),需要讀者在學(xué)習(xí)了Cocos2dx-JS之后再參考源碼。本文只是一個(gè)大致思路的說明。
筆者學(xué)習(xí)Cocos2dx-JS純屬偶然,更直接的原因其實(shí)是在亞馬遜上看到了相關(guān)的教程,于是就買了,看了,然后就寫了。這其中自然會(huì)遇到一些坑,但是在學(xué)習(xí)新知識(shí)的過程中,在不斷思考不斷改進(jìn)中收獲成長(zhǎng)卻也是學(xué)習(xí)的最大樂趣。
這篇簡(jiǎn)單的教程是自己的小小的總結(jié),如果能幫到大家或者讓大家看到后能產(chǎn)生一點(diǎn)點(diǎn)學(xué)習(xí)新知識(shí)的興趣,也就足夠了。