提煉游戲引擎系列:第二次迭代(上)
前言
上文完成了引擎提煉的第一次迭代,搭建了引擎的整體框架,本文會進行引擎提煉的第二次迭代,進一步提高引擎的通用性,完善引擎框架。
由于第二次迭代內容過多,因此分為上、下兩篇博文,本文為上篇。
本文目的
1、提高引擎的通用性,完善引擎框架。
2、對應修改炸彈人游戲。
本文主要內容
第一次迭代后的引擎領域模型

開發策略
本文會對引擎領域模型從左到右一一進行分析, 二次提煉和重構引擎類。
本文迭代步驟

迭代步驟說明
- 確定要重構的引擎類
按照第一次迭代提出的引擎領域模型,從左往右一一分析,判斷是否需要重構。
- 發現問題
從是否包含用戶邏輯、是否違反引擎設計原則、是否可從炸彈人類中提煉更多的通用模式等方面來審視引擎類,如果存在問題則給出引擎類與問題相關的當前設計。
- 分析問題
分析當前設計,指出其中存在的問題,給出問題的解決方案。
- 具體實施
按照解決方案修改當前設計。
- 通過游戲的運行測試
- 修改并通過引擎的單元測試
通過游戲運行測試和引擎單元測試后,繼續分析該引擎類,發現并解決下一個問題。
- 完成本次迭代
解決了引擎類所有的問題后,就可以確定下一個要重構的引擎類,進入新一輪迭代。
不討論測試
因為測試并不是本系列的主題,所以本系列不會討論專門測試的過程,“本文源碼下載”中也沒有單元測試代碼。
您可以在最新的引擎版本中找到引擎完整的單元測試代碼: YEngine2D
修改Main
改為繼承重寫
上文對用戶使用引擎的方式進行了思考,給出了“引擎Main、Director采用實例重寫的方式”的設計。
但是現在重新思考后,發現Main采用實例重寫的方式并不合適。
當前設計
領域模型

引擎Main
(function () {
var _instance = null;
namespace("YE").Main = YYC.Class({
Init: function () {
this._imgLoader = new YE.ImgLoader();
},
Private: {
_imgLoader: null,
_prepare: function () {
this.loadResource();
this._imgLoader.onloading = this.onloading;
this._imgLoader.onload = this.onload;
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
director.init();
director.start();
}
}
},
Public: {
init: function () {
this._prepare();
this._imgLoader.done();
},
getImg: function (id) {
return this._imgLoader.get(id);
},
load: function (images) {
this._imgLoader.load(images);
},
//* 鉤子
loadResource: function () {
},
onload: function () {
},
onloading: function (currentLoad, imgCount) {
}
},
Static: {
getInstance: function () {
if (_instance === null) {
_instance = new this();
}
return _instance;
}
}
});
}());
炸彈人Main
(function(){
//獲得引擎Main實例
var main = YE.Main.getInstance();
var _getImg = function () {
…
};
var _addImg = function (urls, imgs) {
…
};
var _hideBar = function () {
…
};
//重寫引擎Main實例的鉤子
main.loadResource = function () {
this.load(_getImg());
};
main.onloading = function (currentLoad, imgCount) {
$("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10)); //調用進度條插件
};
main.onload = function () {
_hideBar();
};
}());
其它炸彈人類通過調用引擎Main的getImg方法來獲得加載的圖片對象。
var img = YE.Main.getInstance().getImg(imgId); //獲得id為imgID的圖片對象
頁面調用引擎Main的init方法進入游戲
<script type="text/javascript">
(function () {
YE.Main.getInstance().init();
})();
</script>
分析問題
因為炸彈人Main與引擎Main都屬于“入口”概念,負責資源加載的管理,所以炸彈人Main與引擎Main應該為繼承關系,引擎Main需要改造為可被繼承的類,炸彈人Main也要改造為繼于引擎Main。
具體實施
引擎Main應該為抽象類,不再為單例:
引擎Main
(function () {
namespace("YE").Main = YYC.AClass({
…
});
}());
炸彈人Main改為單例并繼承引擎Main,提供getImg方法返回圖片對象,供其它用戶類調用。
炸彈人Main
(function () {
var Main = YYC.Class(YE.Main, {
Private:{
_getImg: function () {
…
},
_addImg: function (urls, imgs) {
…
},
_hideBar: function () {
…
}
},
Public:{
//返回對應id的圖片對象
getImg:function(id){
return this.base(id);
},
loadResource: function () {
this.load(_getImg());
},
onloading: function (currentLoad, imgCount) {
$("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));
},
onload: function () {
this._hideBar();
}
},
Static: {
getInstance: function () {
if (_instance === null) {
_instance = new this();
}
return _instance;
}
}
});
window.Main = Main ;
}());
其它炸彈人類改為調用炸彈人Main的getImg方法來獲得圖片數據。
var img = Main.getInstance().getImg(imgId);
頁面改為調用炸彈人Main的init方法
<script type="text/javascript">
(function () {
Main.getInstance().init();
})();
</script>
引擎Main不應該封裝ImgLoader
進行上面的修改后,運行測試時會報錯,錯誤信息為炸彈人Main在重寫的onload方法中調用的“this._hideBar”為undefined。
造成這個錯誤的原因是在第一次迭代的設計中,引擎Main封裝了引擎ImgLoader,將它的onload與ImgLoader的onload綁定在了一起,導致執行炸彈人Main的onload時,this指向了引擎ImgLoader實例imgLoader,而不是指向炸彈人Main。
引擎Main
_prepare: function () {
…
//綁定了引擎Main和引擎ImgLoader的鉤子
this._imgLoader.onloading = this.onloading;
this._imgLoader.onload = this.onload;
…
}
},
Public: {
init: function () {
this._prepare();
…
},
getImg: function (id) {
return this._imgLoader.get(id);
},
引擎Main提供了getImg方法來獲得引擎ImgLoader實例imgLoader保存的圖片對象。
引擎Main改為繼承重寫后,由于其他炸彈人類不能直接訪問到引擎Main的getImg方法,所以炸彈人必須增加getImg方法,對其它炸彈人類暴露引擎Main的getImg方法。
這樣的設計是不合理的,引擎Main的getImg方法并不是設計為被用戶重寫的方法,而且炸彈人Main也不需要知道引擎Main的getImg方法的實現,這增加了用戶的負擔,違反了引擎設計原則“盡量減少用戶負擔”。
因此,引擎Main不再封裝imgLoader,而是將其暴露給炸彈人Main,再由它暴露給其它炸彈人類。
具體來說就是,引擎Main刪除getImg、load方法,將imgLoader屬性設為公有屬性;炸彈人Main將imgLoader設為全局屬性,直接重寫imgLoader的onload、onloading鉤子,并刪除getImg方法。。
這樣其它炸彈人類可以直接訪問引擎Main的imgLoader屬性,調用它的get方法來獲得圖片數據
由于炸彈人沒有要插入到引擎Main的用戶邏輯,因此引擎Main刪除onload、onloading鉤子。
修改后相關代碼
引擎Main
Private: {
//刪除了onload和onloading鉤子,不再綁定引擎Main和引擎ImgLoader的鉤子了
_prepare: function () {
…
}
},
Public: {
//imgLoader作為公有屬性
imgLoader: null,
炸彈人Main
loadResource: function () {
//獲得引擎Main的imgloader
var loader = this.imgLoader,
self = this;
//重寫imgLoader的鉤子
loader.load(this._getImg());
loader.onloading = function (currentLoad, imgCount) {
…
};
loader.onload = function (imgCount) {
…
};
//imgLoader設為全局屬性,供其它炸彈人類操作
window.imgLoader = this.imgLoader;
}
其它炸彈人類通過window.imgLoader.get方法獲得圖片數據
重構后的領域模型

修改Director
炸彈人Game的名字與其職責不符
引擎Director暫時找不出問題,因此來看下與它相關的炸彈人Game。
當前設計
現在炸彈人Game實例重寫了引擎Director。
引擎Scene不能被重寫,只能提供API供炸彈人Game和引擎Director調用。
重構前領域模型

炸彈人Game
(function () {
var director = YE.Director.getInstance();
var Game = YYC.Class({
…
Public: {
init: function () {
//初始化游戲全局狀態
window.gameState = window.bomberConfig.game.state.NORMAL;
window.subject = new YYC.Pattern.Subject();
this.sleep = 1000 / director.getFps();
//初始化游戲場景
this._createScene();
this._addElements();
this._initLayer();
this._initEvent();
window.subject.subscribe(this.scene.getLayer("mapLayer"), this.scene.getLayer("mapLayer").changeSpriteImg);
},
//管理游戲狀態
judgeGameState: function () {
…
}
}
});
var game = new Game();
director.init = function () {
game.init();
//設置場景
this.setScene(game.scene);
};
director.onStartLoop = function () {
game.judgeGameState();
};
}());
引擎Scene
//引擎Scene為普通的類,向炸彈人類和引擎類提供API
namespace("YE").Scene = YYC.Class(YE.Hash, {
…
分析問題
炸彈人Game現在只負責初始化游戲場景和管理游戲狀態的邏輯,該邏輯屬于場景的范圍,不屬于統一調度的范圍,因此Game應該改造為炸彈人場景類,與引擎Scene對應,而不是與引擎Director對應。
考慮到炸彈人場景類與引擎Scene同屬一個概念,因此炸彈人場景類應該使用繼承重寫的方式來使用引擎Scene。
由于引擎Director依賴引擎Scene,而引擎Scene不依賴引擎Director,所以炸彈人場景類也不應該再依賴引擎Director。
因此,應該進行下面的重構:
1、改造引擎Scene類為可被繼承重寫的類。
2、將炸彈人Game改造為炸彈人場景類Scene,繼承重寫引擎Scene。
3、引擎Director應該改造為一個封閉的單例類,用戶不能重寫,向引擎類和用戶類提供主循環和場景操作相關的API。將它的鉤子方法移到引擎Scene類,炸彈人Game對引擎Director鉤子方法的重寫變為對引擎Scene鉤子方法的重寫,對應修改鉤子方法的調用機制。
具體實施
按照下面的步驟重構:
1、改造引擎Scene為可被繼承的類,將引擎Director的鉤子移到其中;
2、將炸彈人Game改造為場景類Scene,繼承重寫引擎Scene;
3、改造引擎Director,修改鉤子方法的調用機制;
4、重構相關的引擎類和炸彈人類。
改造引擎Scene類為可被繼承的類
引擎Scene改為抽象類,將引擎Director的init、onStartLoop、onEndLoop鉤子方法移到其中。
引擎Scene
(function () {
namespace("YE").Scene = YYC.AClass({
…
Public: {
…
init: function () {
},
onStartLoop: function () {
},
onEndLoop: function () {
}
}
});
}());
引擎Director刪除鉤子方法
將炸彈人Game改造為場景類Scene,繼承重寫引擎Scene
Game進行下面的修改:
(1)炸彈人Game重命名為Scene。
(2)繼承引擎Scene,重寫鉤子方法init和onStartLoop。
(3)刪除scene屬性,將調用scene屬性的成員改為調用自身的成員(“self/this.scene.xxx”改為“self/this.xxx”)。
(4)不再創建scene實例了,對應修改_createScene方法,刪除其中的“創建scene”邏輯,保留“加入層”邏輯,將其重命名為_addLayer。
炸彈人Scene
var Scene = YYC.Class(YE.Scene, {
Private: {
_sleep: 0,
_addLayer: function () {
this.addLayer("mapLayer", layerFactory.createMap());
this.addLayer("enemyLayer", layerFactory.createEnemy(this._sleep));
this.addLayer("playerLayer", layerFactory.createPlayer(this._sleep));
this.addLayer("bombLayer", layerFactory.createBomb());
this.addLayer("fireLayer", layerFactory.createFire());
},
_addElements: function () {
var mapLayerElements = this._createMapLayerElement(),
playerLayerElements = this._createPlayerLayerElement(),
enemyLayerElements = this._createEnemyLayerElement();
this.addSprites("mapLayer", mapLayerElements);
this.addSprites("playerLayer", playerLayerElements);
this.addSprites("enemyLayer", enemyLayerElements);
},
_createMapLayerElement: function () {
…
},
_getMapImg: function (i, j, mapData) {
…
},
_createPlayerLayerElement: function () {
…
},
_createEnemyLayerElement: function () {
….
},
_initLayer: function () {
this.initLayer();
},
_initEvent: function () {
…
},
_judgeGameState: function () {
…
},
_gameOver: function () {
…
},
_gameWin: function () {
…
}
},
Public: {
//重寫引擎Scene的init鉤子
init: function(){
window.gameState = window.bomberConfig.game.state.NORMAL;
window.subject = new YYC.Pattern.Subject();
this.sleep = 1000 / director.getFps();
this._addLayer();
this._addElements();
this._initLayer();
this._initEvent();
window.subject.subscribe(this.getLayer("mapLayer"), this.getLayer("mapLayer").changeSpriteImg);
},
//重寫引擎Scene的onStartLoop鉤子
onStartLoop: function(){
this._judgeGameState();
}
}
});
改造引擎Director類
修改了引擎Director和引擎Scene的鉤子方法后,需要對應修改這些鉤子方法的調用機制。
當前設計
在修改前先來看下引擎Main、Director、Scene以及炸彈人Game之間關于場景的交互機制:

完成加載圖片后會觸發引擎ImgLoader的onload_game鉤子,該鉤子被引擎Main重寫,觸發引擎Director的init鉤子,執行炸彈人Game插入的初始化場景的邏輯,:
引擎Main
_prepare: function () {
…
//加載圖片完成后,觸發引擎ImgLoader的onload_game鉤子
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//觸發init鉤子
director.init();
director.start();
}
}
…
炸彈人Game
_createScene: function () {
this.scene = new YE.Scene();
…
},
…
init: function () {
…
this. _createScene();
…
}
var director = YE.Director.getInstance();
…
//重寫引擎Director的init鉤子
director.init = function () {
game.init();
//調用引擎Director的setScene方法,設置當前場景
this.setScene(game.scene);
};
然后onload_game會調用引擎Director的start方法,啟動主循環,觸發引擎Director的鉤子方法onStartLoop和onEndLoop,執行炸彈人Game重寫插入的場景邏輯:
引擎Main
_prepare: function () {
…
//加載圖片完成后,觸發引擎ImgLoader的onload_game鉤子
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
director.init();
//調用start方法
director.start();
}
}
炸彈人Game
var director = YE.Director.getInstance();
…
//重寫引擎Director的onStartLoop鉤子
director.onStartLoop = function () {
game.judgeGameState();
};
引擎Director
start:function(){
…
//啟動主循環
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
…
},
…
_run: function (time) {
var self = this;
//主循環邏輯在_loopBody方法中
this._loopBody(time);
if (this._gameState === GameState.STOP) {
return;
}
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
},
_loopBody: function (time) {
…
//觸發自己的onStartLoop和onEndLoop鉤子
this.onStartLoop();
…
this.onEndLoop();
},
修改后的設計
進行下面四個修改:
(1)onload_game不再調用引擎Director的init方法。
(2)onload_game會傳入引擎Main創建的炸彈人Scene實例(這只是臨時解決方案,這樣的設計導致了引擎Main依賴炸彈人Scene,違反了引擎設計原則!后面會進行重構)到引擎Director的start方法中。
(3)引擎Director的start方法會觸發炸彈人Scene實例的init鉤子方法,并設置該實例為當前場景。
(4)引擎Director在主循環中改為觸發當前場景的onStartLoop和onEndLoop鉤子方法。
修改后的場景的交互機制序列圖

引擎Main
_prepare: function () {
…
this. _imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//傳入創建的炸彈人場景實例
director.start(new Scene());
};
}
引擎Director
start: function (scene) {
var self = this;
//觸發場景的init鉤子
scene.init();
//設置為當前場景
this.setScene(scene);
…
},
引擎Director
_loopBody: function (time) {
…
this._scene.onStartLoop();
…
this._scene.onEndLoop();
},
重構相關的引擎類和炸彈人類
引擎Director類的start方法重命名為runWithScene
由于start方法傳入了炸彈人Scene的實例,所以將該方法重命名為runWithScene更合適:
引擎Director
runWithScene:function(scene){
…
}
引擎Main
_prepare: function () {
…
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//改為調用引擎Director的runWithScene方法
director.runWithScene(new Scene());
};
}
解除引擎Main對炸彈人Scene的依賴
現在引擎Main創建了炸彈人Scene的實例:
引擎Main
_prepare: function () {
…
this. _imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//創建并注入炸彈人Scene實例
director.runWithScene (new Scene());
};
}
這導致了引擎依賴用戶,違反了引擎設計原則。
因為引擎ImgLoader的onload_game與onload鉤子執行時間相同,所以可以將onload_game中的邏輯移到炸彈人Main重寫ImgLoader的onload鉤子中,由炸彈人Main創建炸彈人Scene實例,解除了引擎Main對炸彈人Scene的依賴:
炸彈人Main
loadResource: function () {
…
loader.onload = function (imgCount) {
…
YE.Director.getInstanc.runWithScene(new Scene());
};
…
}
刪除引擎ImgLoader的onload_game鉤子
ImgLoader的onload_game鉤子和onload鉤子重復了,這是第一次迭代提出的臨時解決方案。
現在onload_game鉤子已經沒有用了,因此將其刪除。
引擎類繼承重寫的鉤子方法都設成虛方法
繼承重寫的鉤子方法是設計為被用戶繼承重寫的,屬于多態,應該將其設為虛方法。
對于實例重寫的鉤子方法,用戶只是重寫實例的鉤子方法,并沒有繼承引擎類,不屬于多態,不設為虛方法。
又由于用戶不是必須要重寫鉤子方法,因此鉤子方法不應該設為抽象方法。
引擎Main
Virtual:{
loadResource: function () {
}
}
引擎Scene
Virtual: {
init: function () {
},
onStartLoop: function () {
},
onEndLoop: function () {
}
}
游戲結束時引擎要停止所有定時器
目前引擎Director只有退出主循環的機制:
引擎Director
_run: function (time) {
var self = this;
this._loopBody(time);
//如果游戲狀態為STOP,則退出主循環
if (this._gameState === YE.Director.GameState.STOP) {
return;
}
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
},
…
stop: function () {
this._gameStatus = GameStatus.STOP;
}
用戶可能會在游戲中調用setTimeout、setInterval方法設置定時器,所以引擎需要在游戲結束時停止這些定時器。
因此,引擎Director的stop方法增加停止所有定時器的邏輯:
引擎Director
stop: function () {
…
YE.Tool.async.clearAllTimer();
}
引擎Tool增加clearAllTimer方法,使用暴力清除法停止所有的定時器:
引擎Tool
namespace("YE.Tool").async = {
/**
* 清空序號在1-500范圍中的定時器
*/
clearAllTimer: function () {
var i = 0,
num = 0,
timerNum = 500, //最大定時器個數
firstIndex = 0;
firstIndex = 1;
num = firstIndex + timerNum; //循環次數
for (i = firstIndex; i < num; i++) {
window.clearTimeout(i);
}
for (i = firstIndex; i < num; i++) {
window.clearInterval(i);
}
}
}
兼容IE
clearAllTimer方法在IE瀏覽器中有問題。雖然定時器序號在所有瀏覽器中都是每次只加1,但是在IE瀏覽器中,每次刷新瀏覽器后定時器起始序號會疊加,導致IE中起始序號可能很大(而在Chrome和Firefox中定時器序號的起始值始終為1),可能超出定時器的清理范圍。
因此需要用戶使用定時器時要保存任意一個定時器的序號到引擎中,并將clearAllTimer方法改為清空該序號前后一定范圍內的定時器。
修改后代碼
引擎Tool
/**
* 清空序號在index前后timerNum范圍中的定時器
* @param index 定時器序號
*/
clearAllTimer: function (index) {
var i = 0,
num = 0,
timerNum = 250,
firstIndex = 0;
//獲得最小的定時器序號
firstIndex = (index - timerNum >= 1) ? (index - timerNum) : 1;
//循環次數
num = firstIndex + timerNum * 2;
for (i = firstIndex; i < num; i++) {
window.clearTimeout(i);
}
for (i = firstIndex; i < num; i++) {
window.clearInterval(i);
}
}
引擎Director增加保存定時器序號的_timeIndex屬性,在stop方法中將_timeIndex傳入clearAllTimer,并增加設置定時器序號的方法setTimerIndex:
引擎Director
_timerIndex: 0,
…
stop: function () {
…
YE.Tool.async.clearAllTimer(this._timerIndex);
},
setTimerIndex: function (index) {
this._timerIndex = index;
}
對應修改炸彈人源碼,調用引擎Director的setTimerIndex方法保存任意一個定時器的序號到引擎中:
炸彈人BombLayer
explode: function (bomb) {
…
index = setTimeout(function () {
…
}, 300);
//保存定時器序號
YE.Director.getInstance().setTimerIndex(index);
},
重構后的領域模型

修改Scene
刪除change方法
當前設計
主循環調用了引擎Scene的change方法,它又調用了場景內層的change方法。
引擎Scene
change: function () {
this.__iterator("change");
},
run: function () {
this.__iterator("run");
},
引擎Director
_loopBody: function (time) {
…
this._scene.run();
this._scene.change();
…
},
分析問題
引擎Scene的change方法沒有自己的邏輯。因此刪除change方法,將其合并到引擎Scene的主循環方法run中。
具體實施
引擎Scene
run: function () {
this.__iterator("run");
this.__iterator("change");
},
引擎Director
_loopBody: function (time) {
…
//不再調用場景的change方法了
this._scene.run();
…
},
不應該關聯引擎Sprite
當前設計
現在引擎Scene提供了addSprites方法,負責將精靈加入到層中:
引擎Scene
addSprites: function (name, elements) {
this.getLayer(name).addChilds(elements);
},
炸彈人Scene
_addElements: function () {
var mapLayerElements = this._createMapLayerElement(),
playerLayerElements = this._createPlayerLayerElement(),
enemyLayerElements = this._createEnemyLayerElement();
this.addSprites("mapLayer", mapLayerElements);
this.addSprites("playerLayer", playerLayerElements);
this.addSprites("enemyLayer", enemyLayerElements);
},
分析問題
引擎Director、Scene、Layer、Sprite分別對應不同的層面,上層不應該跨層依賴下層(引擎Director是個特例,因為其它引擎類可能需要調用它提供的操作主循環的API,因此它可被下層跨層依賴):

當前設計造成了引擎Scene關聯引擎Sprite,應該去掉兩者的關聯:

具體實施
引擎Scene刪除addSprites方法。
炸彈人Scene改為先獲得layer,然后再調用layer的addChilds方法來實現加入精靈到層中:
炸彈人Scene
_addLayer: function () {
this.getLayer("mapLayer").addChilds(this._createMapLayerElement());
this.getLayer("playerLayer").addChilds(this._createPlayerLayerElement());
this.getLayer("enemyLayer").addChilds(this._createEnemyLayerElement()); },
修改Layer
封裝畫布操作
當前設計
現在畫布的操作由用戶負責,用戶需要實現setCanvas方法,指定層對應的畫布,將畫布dom保存到引擎Layer的P_canvas屬性中,并設置畫布的位置。引擎Layer則直接通過用戶設置好的P_canvas屬性來操作畫布:
引擎Layer
Abstract: {
//抽象方法,由用戶實現
setCanvas: function () {
},
…
炸彈人BombLayer
var BombLayer = YYC.Class(YE.Layer, {
…
setCanvas: function () {
this.P_canvas = document.getElementById("bombLayerCanvas");
var css = {
"position": "absolute",
"top": bomberConfig.canvas.TOP,
"left": bomberConfig.canvas.LEFT,
"z-index": 1
};
$("#bombLayerCanvas").css(css);
},
引擎Layer還將畫布canvas的context屬性暴露給了用戶:
引擎Layer
__getContext: function () {
//獲得畫布的context,暴露給用戶
this.P_context = this. P_canvas.getContext("2d");
},
炸彈人BombLayer
draw: function () {
//炸彈人可直接訪問畫布的context
this.iterator("draw", this.P_context);
},
分析問題
畫布操作屬于底層邏輯,不應該由用戶實現,應該由引擎封裝,向用戶提供操作畫布的API。
因此,進行下面的重構:
(1)引擎Layer封裝畫布,向用戶提供操作畫布的API。
(2)引擎Layer封裝畫布的context屬性,向用戶提供操作context的API。
具體實施
按照下面的步驟重構:
1、封裝畫布
(1)將P_canvas屬性改為私有屬性。
(2)引擎Layer增加操作畫布的API。
(3)修改用戶Layer類的setCanvas方法,用戶不再直接操作畫布,而是通過引擎Layer提供的API來操作畫布。
(4)引擎Layer的構造函數增加設置畫布的邏輯,這樣用戶就可以通過“創建用戶Layer實例時傳入畫布參數”來設置畫布。
(5)引擎Layer刪除setCanvas方法,不再限定用戶在setCanvas方法中設置畫布。
2、封裝context。
將P_context改為私有屬性,并提供getContext方法。
封裝canvas
1、將保護屬性P_canvas改成私有屬性__canvas
引擎Layer
Private:{
__canvas: null,
…
},
2、增加setCanvasByID、setWidth、setHeight、setZIndex、setPosition方法
相關代碼
引擎Layer
Public:{
//保存對應id的畫布
setCanvasByID: function (canvasID) {
this.__canvas = document.getElementById(canvasID);
},
//設置畫布寬度
setWidth: function (width) {
this.__canvas.width = width;
},
//設置畫布高度
setHeight: function (height) {
this.__canvas.height = height;
},
//設置畫布層級順序
setZIndex: function (zIndex) {
this.__canvas.style.zIndex = zIndex;
},
//設置畫布坐標
setPosition: function (x, y) {
this.__canvas.style.top = x.toString() + "px";
this.__canvas.style.left = y.toString() + "px";
},
引擎Layer的setPosition方法對top和left值加上了“px”字符串,因此需要對應修改炸彈人Config設置的畫布坐標:
炸彈人Config
修改前
canvas: {
…
TOP: "0px",
LEFT: "0px"
},
修改后
canvas: {
…
TOP: 0,
LEFT: 0
},
3、修改用戶Layer類的setCanvas方法,用戶不再直接操作畫布,而是通過引擎Layer提供的API來操作畫布
相關代碼
炸彈人BombLayer
setCanvas: function () {
this.setCanvasByID("bombLayerCanvas");
this.setPosition(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(1);
},
炸彈人EnemyLayer
setCanvas: function () {
this.setCanvasByID("enemyLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(3);
},
炸彈人FireLayer
setCanvas: function () {
this.setCanvasByID("fireLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(2);
},
炸彈人MapLayer
setCanvas: function () {
…
this.setCanvasByID("mapLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(0);
},
炸彈人PlayerLayer
setCanvas: function () {
this.setCanvasByID("playerLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(3);
},
4、引擎Layer的構造函數增加設置畫布的邏輯
在構造函數中判斷是否傳入了畫布參數,如果傳入則調用操作畫布API設置畫布:
引擎Layer
Init: function (id, zIndex, position) {
if (arguments.length === 3) {
this.setCanvasByID(id);
this.setZIndex(zIndex);
this.setPosition (position.x, position.y);
}
},
這樣用戶就有兩種方式設置畫布了:
(1)創建用戶Layer實例時傳入畫布參數。
(2)在setCanvas方法中調用畫布操作API。
5、引擎Layer刪除setCanvas方法,不再限定用戶必須在setCanvas方法中設置畫布
因為用戶可以在創建用戶Layer實例時設置畫布,所以“強迫用戶在setCanvas抽象方法中設置畫布”的設計就不合適了。
因此,引擎Layer刪除setCanvas方法,對應修改引擎Scene,初始化層時不再調用layer的setCanvas方法了:
引擎Scene
initLayer: function () {
//this.__iterator("setCanvas");
…
}
- 用戶需要什么時候設置畫布?
因為引擎Layer初始化時需要獲得畫布的context屬性,所以用戶需要在這之前設置畫布:
引擎Layer
init: function () {
this.__getContext();
},
因此,用戶除了可在創建用戶Layer實例時設置畫布,還可以在引擎Layer初始化之前設置畫布。
如炸彈人BombLayer可重寫引擎Layer的init方法,在執行引擎Layer初始化前設置畫布:
___setCanvas: function () {
this.setCanvasByID("bombLayerCanvas");
this.setPosition(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(1);
},
…
init: function (layers) {
this. ___setCanvas(); //在執行引擎初始化邏輯前設置畫布
…
this.base(); //執行父類引擎Layer的init方法
},
封裝context
將P_context改為私有屬性__context,并提供getContext方法
引擎Layer
__context: null,
…
getContext: function () {
return this.__context;
},
對應修改用戶Layer類,使用getContext來獲得__context
如炸彈人BombLayer
draw: function () {
this.iterator("draw", this.getContext());
},
引擎執行層的初始化
當前設計
引擎Scene提供初始化層的方法initLayer,由炸彈人Scene在場景初始化時調用,執行場景內層的初始化:
引擎Scene
initLayer: function () {
this.__iterator("init", this.__getLayers());
},
炸彈人Scene
init: function () {
…
this._initLayer();
…
},
…
_initLayer: function () {
…
this.initLayer();
},
分析問題
“執行層的初始化”屬于底層邏輯,應該由引擎負責,引擎類Scene應該對用戶隱藏initLayer方法。
具體實施
將引擎Scene的initLayer設為私有方法,并在引擎Scene的init鉤子方法中調用:
引擎Scene
__initLayer: function () {
this.__iterator("init", this.__getLayers());
}
…
init: function () {
this.__initLayer();
},
不過這樣修改后,炸彈人Scene在重寫init鉤子時就需要先執行引擎Scene的初始化邏輯,再執行自己的用戶邏輯,違反了引擎設計原則“盡量減少用戶負擔”,會在后面進行重構。
炸彈人Scene
init: function () {
//執行引擎類初始化邏輯
this.base();
//用戶初始化邏輯
…
}
分離引擎的初始化邏輯與用戶的初始化邏輯
當前設計
現在引擎Scene、引擎Layer、引擎Sprite提供了init鉤子方法,負責引擎類的初始化。該方法為虛方法,用戶可重寫,加入自己的初始化邏輯。
用戶代碼示例:
炸彈人Scene
init: function () {
//執行引擎類初始化邏輯
this.base();
//用戶初始化邏輯
this._sleep = 1000 / director.getFps();
…
}
炸彈人BombLayer
init: function (layers) {
//執行引擎類初始化邏輯
this.base();
//用戶初始化邏輯
this.fireLayer = layers.fireLayer;
…
}
炸彈人MoveSprite
init: function () {
//執行引擎類初始化邏輯
this.base();
//用戶初始化邏輯
this.P_context.setPlayerState(this.__getCurrentState());
…
}
分析問題
用戶在加入自己的初始化邏輯時,需要先執行引擎類的初始化邏輯,導致用戶不僅需要知道引擎類的初始化邏輯,還需要知道用戶初始化邏輯和引擎初始化邏輯的調用順序,違反了引擎設計原則“盡量減少用戶負擔”。
因此,引擎Scene、Layer、Sprite類的初始化應該由引擎負責并對用戶隱藏,將引擎的初始化邏輯與用戶的初始化邏輯分離。
具體實施
引擎Sprite、Layer、Sprite增加initData鉤子方法,用戶可重寫它來插入自己的初始化邏輯。而引擎的init方法不再作為鉤子方法供用戶重寫,它負責引擎的初始化和調用initData方法執行用戶的初始化。
關于“引擎的init方法中調用initData方法的順序”的思考
因為用戶依賴于引擎,所以照理說應該先進行引擎類的初始化,然后再調用initData方法進行用戶的初始化,這樣用戶初始化時就可獲得引擎類初始化后的狀態。
然而對于引擎Layer來說,它的初始化邏輯需要操作畫布,需要用戶先設置好畫布。
用戶可以在創建用戶Layer實例時設置畫布,也可以在重寫的initData方法中設置畫布。對于引擎來說要做最壞的假設,即假設用戶在initData方法中設置畫布,這樣的話引擎Layer就必須在init方法中先調用initData方法,再進行自己的初始化。
同樣,引擎Scene也需要用戶先加入層到場景中,然后才能執行自己的場景初始化邏輯。
所以Scene和Layer應該先調用initData鉤子方法,然后再執行自己的初始化邏輯。
而引擎Sprite的初始化邏輯與用戶沒有順序依賴,因而引擎Sprite可以先進行引擎類的初始化,然后再調用initData進行用戶的初始化。
相關代碼
引擎Scene
init: function () {
//需要用戶先加入層到場景中后,才能初始化層
this.initData();
this.__initLayer();
},
//*鉤子
Virtual: {
initData: function(){
},
引擎Layer
init: function (layers) {
//需要用戶設置畫布后,才能初始化畫布
//這里將layers傳入initData中
this.initData(layers);
this.__getContext();
this.__initCanvas();
},
Virtual: {
initData: function (layers) {
},
引擎Sprite
init: function () {
//引擎可以先執行自己的初始化邏輯,再執行用戶的初始化邏輯
this.setAnim(this.defaultAnimId);
this.initData();
},
…
Virtual: {
initData: function () {
},
用戶代碼示例:
炸彈人Scene
initData: function () {
//執行用戶初始化邏輯
…
}
炸彈人BombLayer
initData: function (layers) {
//執行用戶初始化邏輯
…
}
炸彈人MoveSprite
initData: function () {
//執行用戶初始化邏輯
…
}
clear方法只負責清除畫布
當前設計
引擎Layer的clear方法會根據參數個數來判斷是清除所有的精靈,還是清除指定的精靈:
引擎Layer
clear: function (sprite) {
if (arguments.length === 0) {
//清除所有層內精靈
this.P_iterator("clear", this.__context);
}
else if (arguments.length === 1) {
//清除指定的精靈
sprite.clear(this.__context);
}
},
用戶代碼示例:
炸彈人BombLayer
___removeBomb: function (bomb) {
//從畫布中清除bomb精靈
this.clear(bomb);
…
},
分析問題
引擎Layer的clear方法的判斷邏輯是多余的,因為引擎Sprite的clear方法是供用戶調用的,如果用戶想要清除某個精靈,可以直接調用該精靈的clear方法。
又因為引擎Layer最清楚層內的所有精靈,所以它的clear方法保留“清除層內所有精靈”的邏輯。
具體實施
引擎Layer的clear方法只負責清除層內所有精靈。
引擎Layer
clear: function () {
this. P_iterator ("clear", this.__context);
}
炸彈人BombLayer
___removeBomb: function (bomb) {
//直接調用bomb精靈的clear方法
bomb.clear(this.getContext());
…
},
繼續修改引擎Layer和Sprite的clear方法
當前設計
引擎Layer的clear方法通過調用層內所有精靈的clear方法,達到清空畫布的目的:
引擎Layer
clear: function () {
this.iterator("clear", this._context);
},
引擎Sprite的clear方法直接清空畫布:
引擎Sprite
clear: function (context) {
//直接清空畫布區域
context.clearRect(0, 0, YE.Config.canvas.WIDTH, YE.Config.canvas.HEIGHT);
}
炸彈人MapLayer實現了“清空畫布”的邏輯:
炸彈人MapLayer
clear: function () {
this.P_context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
…
},
分析問題
當前設計有下面幾個問題:
(1)引擎Sprite的clear方法應該只負責從畫布中清除自己,“清空畫布”的邏輯應該由引擎Layer的clear方法負責。
(2)引擎Layer的clear方法應該直接清空畫布。
(3)“清空畫布”屬于底層邏輯,不應該由用戶類實現。
具體實施
引擎Layer的clear方法負責清空畫布:
引擎Layer
clear: function () {
this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
},
引擎Sprite的clear方法負責從畫布中清除自己:
引擎Sprite
clear: function (context) {
context.clearRect(this.x, this.y, this.bitmap.width, this.bitmap.height);
}
因為用戶類可能需要知道畫布大小,因此引擎Layer增加getCanvasWidth、getCanvasHeight方法:
引擎Layer
getCanvasWidth: function () {
return this._canvas.width;
},
getCanvasHeight: function () {
return this._canvas.height;
},
…
//對應修改clear方法
clear: function () {
this._context.clearRect(0, 0, this. getCanvasWidth(), this. getCanvasHeight());
},
修改炸彈人MapLayer,直接調用引擎Layer的clear方法清除畫布:
炸彈人MapLayer
clear: function () {
this.base();
…
},
封裝run方法
當前設計
引擎類的run方法封裝了引擎類在主循環中的邏輯,該方法由上層引擎類在主循環中調用。
(關于引擎run方法的作用,可參考《炸彈人游戲開發系列(4)》的“增加run方法”一節])
引擎Director
_loopBody: function (time) {
…
//調用場景的run方法
this._scene.run();
…
},
…
_run: function (time) {
var self = this;
this._loopBody(time);
…
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
},
引擎Scene
run: function () {
//調用場景內層的run方法
this.__iterator("run");
…
},
現在引擎Layer向用戶提供了P_render方法,而它的run方法為抽象方法,由用戶實現:
引擎Layer
P_render: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
this.setStateNormal();
}
}
…
Abstract: {
…
run: function () {
}
我們來看下炸彈人Layer類實現的run方法:
炸彈人BombLayer
run: function () {
this.P_render();
}
炸彈人FireLayer
run: function () {
this.P_render();
}
炸彈人MapLayer
run: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
}
}
炸彈人CharacterLayer
run: function () {
this.___setDir();
this.___move();
this.___render();
}
…
___render: function () {
if (this.P_isChange()) {
this.clear();
this.___update(this.___deltaTime);
this.draw();
this.setStateNormal();
}
}
炸彈人EnemyLayer
run: function () {
if (this.collideWithPlayer()) {
window.gameState = window.bomberConfig.game.state.OVER;
return;
}
this.__getPath();
//調用Character->run
this.base();
}
炸彈人PlayerLayer
run: function () {
if (keyState[YE.Event.KeyCodeMap.SPACE]) {
this.createAndAddBomb();
keyState[YE.Event.KeyCodeMap.SPACE] = false;
}
//調用Character->run
this.base();
}
分析問題
引擎Layer應該實現主循環邏輯,并且由于這屬于底層邏輯,應該對用戶隱藏。
因此,引擎Layer應該實現并對用戶隱藏run方法。
下面從三個步驟進行分析:
1、識別出炸彈人Layer類的run方法的通用模式。
2、將其提到引擎Layer的run方法中。
3、在引擎Layer的run方法中調用增加的鉤子方法,執行炸彈人Layer類插入的邏輯。
識別出炸彈人Layer的run方法的通用模式
分析炸彈人Layer類的相關代碼,可以看到炸彈人BombLayer、FireLayer的run方法直接調用了引擎Layer的P_render方法;
炸彈人MapLayer的run方法與P_render方法相比,雖然少調用了引擎Layer的setStateNormal方法,但因為引擎Scene的run方法會調用MapLayer的change方法,而它又會調用引擎Layer的setStateNormal方法,所以MapLayer的run方法也等效于調用了P_render方法。
引擎Scene
run: function () {
this.__iterator("run");
this.__iterator("change");
},
炸彈人MapLayer
change: function () {
this.setStateNormal();
},
再來看下CharaterLayer的run方法,它調用了___render方法,該方法與P_render方法相比,多調用了“___update”方法。
而EnemyLayer、PlayerLayer繼承CharacterLayer,它們的run方法都調用CharacterLayer的run方法,也就是說都調用了___render方法。
由此可見,炸彈人Layer類的run方法的通用模式是都調用了引擎Layer的P_render方法,只是有些炸彈人Layer類還有自己要插入的邏輯。
提取通用模式到引擎Layer的run方法中
再來看下引擎Layer的P_render方法是否需要重構:
P_render: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
this.setStateNormal();
}
}
(1)判斷是否包含用戶邏輯
它調用的都是引擎類Layer的方法,沒有包含用戶邏輯。
(2)判斷是否具有通用性
所有用戶Layer類在每次主循環中都要先判斷畫布的狀態,如果狀態為CHANGE,表明畫布更改過,則先清除畫布,然后繪制畫布,最后設置畫布狀態為NORMAL,因此該方法具有通用性。
綜上所述,可以將P_render方法直接合并到引擎Layer的run方法中。
增加onAfterDraw鉤子方法
炸彈人CharacterPlayer的run方法調用了自己的“___update”方法,該方法需要在引擎Layer的run方法中執行。
為了能讓CharacterPlayer及其子類直接使用引擎Layer的run方法,引擎Layer需要增加onAfterDraw鉤子方法,并在run方法中調用該鉤子。
具體實施
將引擎Layer的P_render方法合并到run方法中,增加onAfterDraw鉤子方法:
引擎Layer
run: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
//觸發onAfterDraw鉤子
this.onAfterDraw();
this.setStateNormal();
}
},
Virtual: {
…
onAfterDraw: function () {
}
},
對應修改炸彈人BombLayer、FireLayer、MapLayer,刪除run方法
炸彈人CharacterLayer、EnemyLayer、PlayerLayer由于還有其它的用戶邏輯需要在引擎Layer的run方法之前執行,所以暫時保留run方法(后面會重構):
炸彈人CharacterLayer
run: function () {
this.___setDir();
this.___move();
//調用引擎Layer的run方法
this.base();
},
onAfterDraw: function () {
this.___update(this.___deltaTime);
}
炸彈人EnemyLayer
run: function () {
if (this.collideWithPlayer()) {
window.gameState = window.bomberConfig.game.state.OVER;
return;
}
this.__getPath();
//調用Character的run方法
this.base();
}
炸彈人PlayerLayer
run: function () {
if (keyState[YE.Event.KeyCodeMap.SPACE]) {
this.createAndAddBomb();
keyState[YE.Event.KeyCodeMap.SPACE] = false;
}
//調用Character的run方法
this.base();
}
增加onStartLoop、onEndLoop鉤子
當前設計
經過上一步的修改后,炸彈人CharacterLayer、EnemyLayer、PlayerLayer仍然要重寫引擎Layer的run方法,沒有達到“引擎Layer對用戶隱藏run方法”的設計目的。
分析問題
引擎和用戶的run方法的邏輯現在混雜到了一起。
在前面的“應該將引擎的初始化邏輯與用戶的初始化邏輯分離”重構中,我們已經看到這種設計很不好,應該將引擎邏輯和用戶的邏輯分離。
具體實施
參考引擎Scene,引擎Layer也提出onStartLoop、onEndLoop鉤子方法,這兩個鉤子分別在引擎Layer的run方法執行前、后觸發。
引擎Layer
Virtual:{
…
onStartLoop: function () {
},
onEndLoop: function () {
}
在引擎Scene的run方法中觸發引擎Layer的鉤子:
引擎Scene
run: function () {
this.__iterator("onStartLoop");
this.__iterator("run");
this.__iterator("change");
this.__iterator("onEndLoop");
},
對應修改炸彈人CharacterLayer、EnemyLayer、PlayerLayer,將自己的邏輯放到鉤子中,不再重寫引擎Layer的run方法:
炸彈人CharacterLayer
onStartLoop: function () {
this.___setDir();
this.___move();
},
炸彈人EnemyLayer
onStartLoop: function () {
if (this.collideWithPlayer()) {
window.gameState = window.bomberConfig.game.state.OVER;
return;
}
this.__getPath();
//調用CharacterLayer的onStartLoop
this.base();
}
炸彈人PlayerLayer
onStartLoop: function () {
if (keyState[YE.Event.KeyCodeMap.SPACE]) {
this.createAndAddBomb();
keyState[YE.Event.KeyCodeMap.SPACE] = false;
}
//調用CharacterLayer的onStartLoop
this.base();
}
將P_isNorml、P_isChange改成私有方法
分析問題
經過“封裝run方法”的修改后,用戶Layer類不會再用到引擎Layer的P_isChange、P_isNorml方法了,因此將其設為私有方法。
具體實施
引擎Layer
__isChange: function () {
return this.__state === State.CHANGE;
},
__isNormal: function () {
return this.__state === State.NORMAL;
}
提取炸彈人draw方法的通用模式
繼續從炸彈人Layer類中提取通用模式。
當前設計
現在引擎Layer的draw方法為抽象方法,由用戶實現:
引擎Layer
Abstract: {
…
draw: function () {
},
炸彈人BombLayer、CharacterLayer、FireLayer的draw方法具有共同的模式,都是繪制所有精靈:
draw: function () {
this.iterator("draw", this.getContext());
},
分析問題
可將通用模式提到引擎Layer的draw方法中。
又由于不是所有炸彈人Layer類的繪制邏輯都是“繪制所有精靈”,所以將draw方法設為虛方法,用戶可重寫該方法實現不同的邏輯。
具體實施
實現引擎Layer的draw方法,對應刪除炸彈人BombLayer、CharacterLayer、FireLayer的draw方法。
引擎Layer
Virtual:{
…
draw: function () {
this.iterator("draw", this.getContext());
},
增加鉤子方法isChange,change方法不再為抽象方法
當前設計
現在引擎Layer的change方法為抽象方法,由用戶實現,通過調用引擎Layer提供的setStateChange和setStateNormal方法來設置畫布狀態。
畫布狀態的作用
引擎Layer在主循環中會判斷畫布狀態,如果為CHANGE,則重繪畫布,否則不重繪。
引擎Layer
Abstract: {
change: function () {
}
}
用戶代碼示例:
如炸彈人BombLayer
change: function () {
//如果炸彈人放置了炸彈,則設置畫布狀態為CHANGE,從而在下次主循環時重繪畫布,顯示炸彈
if (this.___hasBomb()) {
this.setStateChange();
}
}
分析問題
其實用戶只需要決定下次主循環時是否重繪畫布,而不需要知道畫布狀態。根據引擎設計原則“盡量減少用戶負擔”,引擎Layer應該對用戶隱藏“畫布狀態”。
具體實施
引擎Layer增加虛方法isChange,用戶可以重寫該方法,如果需要重繪則返回true,否則返回false。
引擎Layer的change方法會調用isChange方法,根據返回值判斷是調用setStateChange方法,還是調用setStateNormal方法。
因為用戶可能需要在isChange方法之外設置畫布狀態,所以引擎Layer保留setStateNormal、setStateChange方法供用戶調用。
引擎Layer
change: function () {
if(this.isChange() === true){
this.setStateChange();
}
else{
this.setStateNormal();
}
},
Virtual: {
…
isChange: function(){
return true;
},
炸彈人只需要重寫isChange方法
如炸彈人BombLayer
isChange: function () {
if (this.___hasBomb()) {
return true;
}
}
思考
- 引擎Layer現在沒有抽象方法了,但仍然應該為抽象類
如果引擎Layer為類,則用戶就不能有繼承引擎Layer的抽象子類。
例如:用戶可能有多個Layer類,對應多個畫布,可能需要從中提出抽象基類,抽象基類也需要繼承引擎Layer。如果引擎Layer為類,則提出抽象基類不能繼承它。
修改Sprite
引擎執行精靈的初始化
當前設計
目前由用戶負責執行精靈的初始化:
炸彈人Scene
_createPlayerLayerElement: function () {
var element = [],
player = spriteFactory.createPlayer();
//執行玩家精靈的初始化
player.init();
…
},
_createEnemyLayerElement: function () {
var element = [],
enemy = spriteFactory.createEnemy(),
enemy2 = spriteFactory.createEnemy2();
//執行敵人精靈的初始化
enemy.init();
enemy2.init();
…
},
分析問題
“執行精靈的初始化”屬于底層邏輯,應該由引擎負責執行。
由哪個引擎類負責
因為引擎Layer負責管理層內精靈,所以應該由它負責。
在哪里執行精靈的初始化
有兩個選擇:
1、在初始化層時執行層中的所有精靈的初始化。
2、在加入精靈到層中時執行精靈的初始化。
因為在初始化層時,不一定加入了精靈到層中,所以應該選擇在加入精靈到層中時執行精靈的初始化。
具體實施
引擎Layer重寫引擎Collection的addChilds方法,加入精靈到層中時執行精靈的初始化:
引擎Layer
namespace("YE").Layer = YYC.AClass(YE.Collection, {
…
addChilds: function (elements) {
this.base(elements);
elements.forEach(function(e){
//執行精靈的初始化
e.init();
});
},
炸彈人Scene不再負責執行精靈的初始化了。
修改后,游戲運行測試會報錯。因為在加入地圖精靈到層中時,會執行地圖精靈的初始化,設置地圖精靈的默認動畫。然而地圖精靈沒有動畫,其defaultAnimId為undefined,所以執行setAnim方法時會報錯。
引擎Sprite
init: function () {
//顯示默認動畫
this.setAnim(this.defaultAnimId);
…
},
為了讓游戲運行通過,暫時在引擎Sprite的init方法中加入defaultAnimId的判斷:
引擎Sprite
init: function () {
//如果有默認動畫Id,則顯示默認動畫
if (this.defaultAnimId) {
this.setAnim(this.defaultAnimId);
}
…
},
其實可以看到,引擎Sprite的defaultAnimId屬性是默認動畫的id,屬于用戶邏輯,后面會進行重構,去除該用戶邏輯。
提取炸彈人中每次主循環持續時間的計算邏輯到引擎Sprite的update方法中
當前設計
游戲需要計算每次主循環持續時間deltaTime,用于在動畫管理中計算當前幀播放的時間,確定是否對當前幀進行切換等操作。
目前由炸彈人實現deltaTime的計算。炸彈人Scene計算deltaTime,然后傳入炸彈人Layer,然后再傳入炸彈人精靈的update方法(引擎Sprite實現),最后傳入引擎Animation的update方法。
炸彈人Scene
initData: function(){
…
this._sleep = 1000 / director.getFps(); //計算本次主循環持續時間,保存到_sleep屬性中
…
},
…
_addLayer: function () {
…
//deltaTime傳入layer
this.addLayer("enemyLayer", layerFactory.createEnemy(this._sleep));
this.addLayer("playerLayer", layerFactory.createPlayer(this._sleep));
…
},
炸彈人CharacterLayer
Init: function (deltaTime) {
this.___deltaTime = deltaTime;
},
…
___update: function (deltaTime) {
//deltaTime傳入炸彈人精靈的update方法
this.iterator("update", deltaTime);
},
…
onAfterDraw: function () {
this.___update(this.___deltaTime);
}
引擎Sprite
update: function (deltaTime) {
this._updateFrame(deltaTime);
},
…
_updateFrame: function (deltaTime) {
if (this.currentAnim) {
//deltaTime傳入引擎Animation的update方法
this.currentAnim.update(deltaTime);
}
}
引擎Animation
update: function (deltaTime) {
…
//根據deltaTime,計算當前幀的已播放時間
this._currentFramePlayed += deltaTime;
…
},
分析問題
1、引擎負責計算幀率fps,所以它知道如何計算deltaTime。
2、deltaTime與主循環密切相關,而主循環是由引擎來負責的。
因此,應該由引擎計算deltaTime。
由哪個引擎類負責?
(1)只有引擎Animation需要用到deltaTime,而它又是由引擎Sprite的update方法傳入的,引擎Sprite是直接關聯方。
(2)引擎Scene和引擎Layer都只是傳遞deltaTime值,沒有自己的邏輯。
(3)計算deltaTime需要獲得引擎Director的幀率,引擎Sprite能夠訪問引擎Director,從而能夠計算deltaTime。
因此應該由引擎Sprite負責。
具體實施
引擎Sprite的update方法負責計算deltaTime:
引擎Sprite
update: function () {
this._updateFrame(1000 / YE.Director.getInstance().getFps());
},
對應修改炸彈人Scene和炸彈人CharacterLayer,不再負責計算和傳遞deltaTime了。
浙公網安備 33010602011771號