上文咱們完成了引擎的初步設計,本文進行引擎提煉的第一次迭代,從炸彈人遊戲中提煉引擎類,搭建引擎的總體框架。javascript
「用戶」是個相對的概念,指使用的一方法,默認指遊戲開發者。
當相對於某個引擎類時,「用戶」就是指引擎類的使用方;當相對於整個引擎時,「用戶」就是指引擎的使用方。css
引擎使用方的邏輯,默認指與具體遊戲相關的業務邏輯。html
一、參考引擎初步領域模型,從炸彈人蔘考模型中提煉出對應的通用類,搭建引擎框架。
二、將炸彈人遊戲改造爲基於引擎實現。java
按照引擎初步領域模型從左往右的順序,肯定要提煉的引擎類,從炸彈人蔘考模型對應的炸彈人類中提煉出通用的引擎類。
本文迭代步驟
迭代步驟說明jquery
按照「引擎初步領域模型」從左往右的順序依次肯定要提煉的引擎類。
每次迭代提煉一個引擎類,若是有強關聯的引擎類,也一併提出。git
從炸彈人蔘考模型中肯定對應的炸彈人類,從中提煉出可複用的、通用的引擎類。github
若是提煉的引擎類有壞味道,或者包含了用戶邏輯,就須要進行重構。web
對應修改炸彈人類,使用提煉的引擎類。
這樣可以站在用戶角度發現引擎的改進點,獲得及時反饋,從而立刻重構。算法
經過運行測試,修復因爲修改炸彈人代碼帶來的bug。canvas
若是有必要的話,對引擎進行相應的重構。
下面的幾個緣由會致使重構引擎:
一、提煉出新的引擎類後,與之關聯的引擎類須要對應修改。
二、獲得了新的反饋,須要改進引擎。
三、違反了引擎設計原則。
四、處理前面遺留的問題
經過運行測試,修復炸彈人和引擎的bug。
由於引擎是從炸彈人代碼中提煉出來的,因此引擎的單元測試代碼能夠參考或者直接複用炸彈人的單元測試代碼。
炸彈人代碼只進行運行測試,不進行單元測試。由於本系列的重點是提煉引擎,不是二次開發炸彈人,這樣作能夠節省精力,專一於引擎的提煉。
進入新一輪迭代,肯定下一個要提煉的引擎類。
思考
一、爲何先提煉引擎,後進行引擎的單元測試?
在炸彈人開發中,我採用TDD的方式,即先寫測試,再進行開發,然而這裏不該該採用這種方式,這是由於:
(1)我已經有了必定的遊戲開發經驗了,能夠先進行一大步的開發,而後再寫對應的測試來覆蓋開發。
(2)在提煉引擎類前我只知道引擎類的大概的職責,不能肯定引擎類的詳細設計。在提煉的過程當中,引擎類會不停的變化,若是先寫了引擎類的單元測試代碼,則須要不停地修改,浪費不少時間。
二、爲何要先進行遊戲的運行測試,再進行引擎的單元測試?
由於:
(1)炸彈人遊戲並不複雜,若是運行測試失敗,也能比較容易地定位錯誤
(2)先進行遊戲的運行測試,不用修改單元測試代碼就能直接修復發現的引擎bug,這樣以後進行的引擎單元測試就能比較順利的經過,節省時間。
由於測試並非本系列的主題,因此本系列不會討論專門測試的過程,「本文源碼下載」中也沒有單元測試代碼!
您能夠在最新的引擎版本中找到引擎完整的單元測試代碼: YEngine2D
在開篇介紹中,給出了引擎的三種使用方式:直接使用引擎類提供的API、繼承重寫、實例重寫,如今來研究下後兩種使用方式。
繼承重寫應用了模板模式,由引擎類搭建框架,將變化點以鉤子方法、虛方法和抽象成員的形式提供給用戶子類實現。
實例重寫也是應用了模板模式的思想,引擎類也提供鉤子方法供用戶類重寫,不過用戶類並非繼承複用引擎類,而是委託複用引擎類。
繼承重寫與實例重寫的區別,實際上就是繼承與委託的區別。
繼承重寫和實例重寫的比較
共同點
(1)都是單向關聯,即用戶類依賴引擎類,引擎類不依賴用戶類。
(2)用戶均可以插入本身的邏輯到引擎中。
不一樣點
(1)繼承重寫經過繼承的方式實現引擎類的使用,實例重寫經過委託的方式實現引擎類的使用
(2)繼承重寫不只提供了鉤子方法,還提供了虛方法、抽象成員供用戶重寫,實例重寫則只提供了鉤子方法。
實例重寫的優點主要在於用戶類與引擎類的關聯性較弱,用戶類只與引擎類實例的鉤子方法耦合,不會與整個引擎類耦合。
繼承重寫的優點主要在於父類和子類代碼共享,提升代碼的重用性。
何時用繼承重寫
當用戶類與引擎類同屬於一個概念,引擎類是精心設計用於被繼承的類時,應該用繼承重寫。
何時用實例重寫
當用戶類須要插入本身的邏輯到引擎類中而又不想與引擎類緊密耦合時,應該用實例重寫。
本文選用的方式
由於引擎Main和Director是從炸彈人Main、Game中提出來的,不是設計爲可被繼承的類,因此引擎Main、Director採用實例重寫的方式,
(它們的使用方式會在第二次迭代中修改)
引擎Layer和Sprite是從炸彈人Layer、Sprite中提出來的,都是抽象基類,自己就是設計爲被繼承的類,因此引擎Layer和Sprite採用繼承重寫的方式。
其它引擎類不能被重寫,而是提供API,供引擎類或用戶類調用。
(第二次迭代會將引擎Scene改成繼承重寫的方式)
引擎使用命名空間來組織,引擎的頂級命名空間爲YE。
在炸彈人開發中,我使用工具庫YTool的namespace方法來定義命名空間。
分析YTool的namespace方法:
var YToolConfig = { topNamespace: "YYC", //指定了頂級命名空間爲YYC toolNamespace: "Tool" }; ... namespace: function (str) { var parent = window[YToolConfig.topNamespace], parts = str.split('.'), i = 0, len = 0; if (str.length == 0) { throw new Error("命名空間不能爲空"); } if (parts[0] === YToolConfig.topNamespace) { parts = parts.slice(1); } for (i = 0, len = parts.length; i < len; i++) { if (typeof parent[parts[i]] === "undefined") { parent[parts[i]] = {}; } parent = parent[parts[i]]; } return parent; },
該方法指定了頂級命名空間爲YYC,不能修改,這顯然不符合引擎的「頂級命名空間爲YE」的需求。
所以將其修改成不指定頂級命名空間,並設爲全局方法:
(function(){ var extend = function (destination, source) { var property = ""; for (property in source) { destination[property] = source[property]; } return destination; }; (function () { /** * 建立命名空間。 示例: namespace("YE.Collection"); */ var global = { namespace: function (str) { var parent = window, parts = str.split('.'), i = 0, len = 0; if (str.length == 0) { throw new Error("命名空間不能爲空"); } for (i = 0, len = parts.length; i < len; i++) { if (typeof parent[parts[i]] === "undefined") { parent[parts[i]] = {}; } parent = parent[parts[i]]; //遞歸增長命名空間 } return parent; } }; extend(window, global); }()); }());
不該該直接修改YTool的namespace方法,而應該將修改後的方法提取到引擎中,由於:
(1)致使引擎依賴工具庫YTool
YTool中的不少方法引擎都使用不到,若是將修改後的namespace方法放到YTool中,在使用引擎時就必須引入YTool。
這樣作會增長引擎的不穩定性,增長整個引擎文件的大小,違反引擎設計原則「儘可能減小引擎依賴的外部文件」。
(2)致使大量關聯代碼修改
個人不少代碼都使用了YTool,若是修改了YTool的namespace方法,那麼使用了YTool的namespace方法的相關代碼可能都須要進行修改。
因此,引擎增長Tool類,負責放置引擎內部使用的通用方法,將修改後的namespace方法放在Tool類中,從而將引擎的依賴YTool改成依賴本身的Tool。
同理,在後面的提煉引擎類時,將引擎類依賴的YTool的方法也所有轉移到Tool類中。
引擎Tool的命名空間爲YE.Tool。
由於引擎Tool類僅供引擎內部使用,因此炸彈人仍然依賴YTool,而不依賴引擎Tool類。
按照從左到右的提煉順序,首先要提煉引擎初步領域模型中的LoaderResource。
領域類LoaderResource負責加載各類資源,對應炸彈人PreLoadImg類,該類自己就是一個獨立的圖片預加載組件(參考發佈個人圖片預加載控件YPreLoadImg v1.0),可直接提煉到引擎中。
我將其重命名爲ImgLoader,加入到命名空間YE中,代碼以下:
引擎ImgLoader
namespace("YE").ImgLoader = YYC.Class({ Init: function (images, onstep, onload) { this._checkImages(images); this.config = { images: images || [], onstep: onstep || function () { }, onload: onload || function () { } }; this._imgs = {}; this.imgCount = this.config.images.length; this.currentLoad = 0; this.timerID = 0; this.loadImg(); }, Private: { _imgs: {}, _checkImages: function (images) { var i = null; for (var i in images) { if (images.hasOwnProperty(i)) { if (images[i].id === undefined || images[i].url === undefined) { throw new Error("應該包含id和url屬性"); } } } } }, Public: { imgCount: 0, currentLoad: 0, timerID: 0, get: function (id) { return this._imgs[id]; }, loadImg: function () { var c = this.config, img = null, i, self = this, image = null; for (i = 0; i < c.images.length; i++) { img = c.images[i]; image = this._imgs[img.id] = new Image(); image.onload = function () { this.onload = null; YYC.Tool.func.bind(self, self.onload)(); }; image.src = img.url; this.timerID = (function (i) { return setTimeout(function () { if (i == self.currentLoad) { image.src = img.url; } }, 500); })(i); } }, onload: function (i) { clearTimeout(this.timerID); this.currentLoad++; this.config.onstep(this.currentLoad, this.imgCount); if (this.currentLoad === this.imgCount) { this.config.onload(this.currentLoad); } }, dispose: function () { var i, _imgs = this._imgs; for (i in _imgs) { _imgs[i].onload = null; _imgs[i] = null; } this.config = null; } } });
對應修改炸彈人Main,改成使用引擎ImgLoader加載圖片:
炸彈人Main修改前
init: function () { window.imgLoader = new YYC.Control.PreLoadImg(…); },
炸彈人Main修改後
init: function () { window.imgLoader = new YE.ImgLoader(...); },
接着就是提煉依賴LoadResource的Main。
領域類Main負責啓動遊戲,對應炸彈人Main。
先來看下相關代碼:
炸彈人Main
var main = (function () { var _getImg = function () { var urls = []; var i = 0, len = 0; var map = [ { id: "ground", url: getImages("ground") }, { id: "wall", url: getImages("wall") } ]; var player = [ { id: "player", url: getImages("player") } ]; var enemy = [ { id: "enemy", url: getImages("enemy") } ]; var bomb = [ { id: "bomb", url: getImages("bomb") }, { id: "explode", url: getImages("explode") }, { id: "fire", url: getImages("fire") } ]; _addImg(urls, map, player, enemy, bomb); return urls; }; var _addImg = function (urls, imgs) { var args = Array.prototype.slice.call(arguments, 1), i = 0, j = 0, len1 = 0, len2 = 0; for (i = 0, len1 = args.length; i < len1; i++) { for (j = 0, len2 = args[i].length; j < len2; j++) { urls.push({ id: args[i][j].id, url: args[i][j].url }); } } }; var _hideBar = function () { $("#progressBar").css("display", "none"); }; return { init: function () { //使用引擎ImgLoader加載圖片 window.imgLoader = new YE.ImgLoader(_getImg(), function (currentLoad, imgCount) { $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10)); //調用進度條插件 }, YYC.Tool.func.bind(this, this.onload)); }, onload: function () { _hideBar(); var game = new Game(); game.init(); game.start(); }, }; window.main = main; }());
炸彈人Main負責如下的邏輯:
(1)定義要加載的圖片數據
(2)建立ImgLoader實例,加載圖片
(3)完成圖片加載後,啓動遊戲
(4)提供入口方法,由頁面調用
能夠將第4個邏輯提到引擎Main中,由引擎搭建一個框架,炸彈人Main負責填充具體的業務邏輯。
引擎Main:
(function () { var _instance = null; namespace("YE").Main = YYC.Class({ Init: function () { }, Public: { init: function () { this. loadResource (); }, //* 鉤子 loadResource: function () { } }, Static: { getInstance: function () { if (instance === null) { _instance = new this(); } return _instance; } } }); }());
分析引擎Main
提供了loadResource鉤子方法供用戶重寫。
由於遊戲只有一個入口類,所以引擎Main爲單例類。
頁面調用引擎Main的init方法進入遊戲,init方法調用鉤子方法loadResource,該鉤子方法由炸彈人Main重寫,從而實如今引擎框架中插入用戶邏輯。
炸彈人Main經過重寫引擎Main的loadResource鉤子方法來插入用戶邏輯。
炸彈人Main
(function () { var main = YE.Main.getInstance(); var _getImg = function () { ... }; var _addImg = function (urls, imgs) { ... }; var _hideBar = function () { ... }; var _onload = function(){ … }; //重寫引擎Main的loadResource鉤子 main.loadResource =function () { window.imgLoader = new YE.ImgLoader(_getImg(), function (currentLoad, imgCount) { $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10)); }, YYC.Tool.func.bind(this,_onload)); } }());
修改頁面,調用引擎Main的init方法進入遊戲:
頁面修改前
<script type="text/javascript"> (function () { //調用炸彈人Main的init方法 main.init(); })(); </script>
頁面修改後
<script type="text/javascript"> (function () { YE.Main.getInstance().init(); })(); </script>
炸彈人應該只負責定義要加載的圖片和在加載圖片過程當中要插入的用戶邏輯,不須要知道如何加載圖片。這個工做應該交給引擎Main,由它封裝引擎ImgLoader,向用戶提供操做圖片的方法和在加載圖片的過程當中插入用戶邏輯的鉤子方法。
一、重構引擎ImgLoader
因爲引擎ImgLoader設計僵化,須要先進行重構。
如今來看下引擎ImgLoader的構造函數
Init: function (images, onstep, onload) { this._checkImages(images); this.config = { images: images || [], onstep: onstep || function () { }, onload: onload || function () { } }; this._imgs = {}; this.imgCount = this.config.images.length; this.currentLoad = 0; this.timerID = 0; this.loadImg(); },
「設置加載圖片的回調函數」和「加載圖片」的邏輯和ImgLoader構造函數綁定在了一塊兒,建立ImgLoader實例時會執行這兩項任務。
須要將其從構造函數中分離出來,由用戶本身決定什麼時候執行這兩個任務。
所以進行下面的重構:
(1)將回調函數onstep重命名爲onloading,將onload、onloading從構造函數中提出,做爲鉤子方法。
(2)將圖片數據images的設置和檢查提取到新增的load方法中。
(3)提出done方法,負責調用_loadImg方法加載圖片。
引擎ImgLoader修改後
namespace("YE").ImgLoader = YYC.Class({ Init: function () { }, Private: { _images: [], _imgs: {}, //修改了原來的_checkImages方法,如今傳入的圖片數據能夠爲單個數據,也可爲數組形式的多個數據 _checkImages: function (images) { var i = 0, len = 0; if (YYC.Tool.judge.isArray(images)) { for (len = images.length; i < len; i++) { if (images[i].id === undefined || images[i].url === undefined) { throw new Error("應該包含id和url屬性"); } } } else { if (images.id === undefined || images.url === undefined) { throw new Error("應該包含id和url屬性"); } } }, //將onload改成私有方法 _onload: function (i) { … //調用鉤子 this.onloading(this.currentLoad, this.imgCount); if (this.currentLoad === this.imgCount) { this.onload(this.imgCount); } }, //改成私有方法 _loadImg: function () { … } } }, Public: { … done: function () { this._loadImg(); }, //負責檢查和保存圖片數據 load: function (images) { this._checkImages(images); if (YYC.Tool.judge.isArray(images)) { this._images = this._images.concat(images); } else { this._images.push(images); } this.imgCount = this._images.length; }, … //*鉤子 onloading: function (currentLoad, imgCount) { }, onload: function (imgCount) { } } });
二、重構引擎Main
如今回到引擎Main的重構,經過下面的重構來實現封裝引擎ImgLoader,向用戶提供鉤子方法和操做圖片的方法:
(1)構造函數中建立ImgLoader實例
(2)init方法中調用ImgLoader的done方法加載圖片
(3)提供getImg和load方法來操做圖片數據
(4)增長onload、onloading鉤子,將其與ImgLoader的onload、onloading鉤子綁定到一塊兒。
綁定鉤子的目的是爲了讓炸彈人Main只須要知道引擎Main的鉤子,從而達到引擎Main封裝引擎ImgLoader的目的。
這個方案並非很好,在第二次迭代中會修改。
引擎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; } }, 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) { } }, … }); }());
三、修改炸彈人Main
炸彈人Main在重寫的引擎Main的loadResource方法中重寫引擎Main的onload、onloading鉤子方法,這至關於重寫了imgLoader的onload、onloading鉤子方法,從而在加載圖片的過程當中插入用戶邏輯。
炸彈人Main
(function () { … main.loadResource = function () { this.load(_getImg()); }; main.onloading = function (currentLoad, imgCount) { $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10)); }; main.onload = function () { _hideBar(); var game = new Game(); game.init(); game.start(); }; }());
修改後的引擎ImgLoader須要調用YTool的isArray方法,將其移到引擎Tool中。
引擎Tool
namespace("YE.Tool").judge = { isArray: function (val) { return Object.prototype.toString.call(val) === "[object Array]"; } };
對應修改ImgLoader,將YYC.Tool調用改成YE.Tool
...
if (YE.Tool.judge.isArray(images)) {
…
}
繼續往右提煉Director。
領域類Director負責遊戲的統一調度,對應炸彈人的Game類
炸彈人Game
(function () { //初始化遊戲全局狀態 window.gameState = window.bomberConfig.game.state.NORMAL; var Game = YYC.Class({ Init: function () { window.subject = new YYC.Pattern.Subject(); }, Private: { _createLayerManager: function () { this.layerManager = new LayerManager(); this.layerManager.addLayer("mapLayer", layerFactory.createMap()); this.layerManager.addLayer("enemyLayer", layerFactory.createEnemy(this.sleep)); this.layerManager.addLayer("playerLayer", layerFactory.createPlayer(this.sleep)); this.layerManager.addLayer("bombLayer", layerFactory.createBomb()); this.layerManager.addLayer("fireLayer", layerFactory.createFire()); }, _addElements: function () { var mapLayerElements = this._createMapLayerElement(), playerLayerElements = this._createPlayerLayerElement(), enemyLayerElements = this._createEnemyLayerElement(); this.layerManager.addSprites("mapLayer", mapLayerElements); this.layerManager.addSprites("playerLayer", playerLayerElements); this.layerManager.addSprites("enemyLayer", enemyLayerElements); }, _createMapLayerElement: function () { var i = 0, j = 0, x = 0, y = 0, row = bomberConfig.map.ROW, col = bomberConfig.map.COL, element = [], mapData = mapDataOperate.getMapData(), img = null; for (i = 0; i < row; i++) { y = i * bomberConfig.HEIGHT; for (j = 0; j < col; j++) { x = j * bomberConfig.WIDTH; img = this._getMapImg(i, j, mapData); element.push(spriteFactory.createMapElement({ x: x, y: y }, bitmapFactory.createBitmap({ img: img, width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT }))); } } return element; }, _getMapImg: function (i, j, mapData) { var img = null; switch (mapData[i][j]) { case 1: img = YE.Main.getInstance().getImg("ground"); break; case 2: img = YE.Main.getInstance().getImg("wall"); break; default: break } return img; }, _createPlayerLayerElement: function () { var element = [], player = spriteFactory.createPlayer(); player.init(); element.push(player); return element; }, _createEnemyLayerElement: function () { var element = [], enemy = spriteFactory.createEnemy(), enemy2 = spriteFactory.createEnemy2(); enemy.init(); enemy2.init(); element.push(enemy); element.push(enemy2); return element; }, _initLayer: function () { this.layerManager.initLayer(); }, _initEvent: function () { //監聽整個document的keydown,keyup事件 keyEventManager.addKeyDown(); keyEventManager.addKeyUp(); }, _judgeGameState: function () { switch (window.gameState) { case window.bomberConfig.game.state.NORMAL: break; case window.bomberConfig.game.state.OVER: this.gameOver(); return "over"; break; case window.bomberConfig.game.state.WIN: this.gameWin(); return "over"; break; default: throw new Error("未知的遊戲狀態"); } return false; } }, Public: { sleep: 0, layerManager: null, mainLoop: null, init: function () { this.sleep = Math.floor(1000 / bomberConfig.FPS); this._createLayerManager(); this._addElements(); this._initLayer(); this._initEvent(); window.subject.subscribe(this.layerManager.getLayer("mapLayer"), this.layerManager.getLayer("mapLayer").changeSpriteImg); }, start: function () { var self = this; this.mainLoop = window.setInterval(function () { self.run(); }, this.sleep); }, run: function () { if (this._judgeGameState() === "over") { return; } this.layerManager.run(); this.layerManager.change(); }, gameOver: function () { YYC.Tool.asyn.clearAllTimer(this.mainLoop); alert("Game Over!"); }, gameWin: function () { YYC.Tool.asyn.clearAllTimer(this.mainLoop); alert("You Win!"); } } }); window.Game = Game; }());
炸彈人Game負責遊戲的統一調度,包括如下的邏輯:
(1)初始化場景
(2)調度layerManager
(3)控制主循環
(4)計算幀率fps
(5)管理遊戲狀態
其中控制主循環、調度layerManager、計算fps的邏輯能夠提取到引擎Director中:
引擎Director
(function () { var _instance = null; var GameStatus = { NORMAL: 0, STOP: 1 }; var STARTING_FPS = 60; namespace("YE").Director = YYC.Class({ Private: { _startTime: 0, _lastTime: 0, _fps: 0, _layerManager: null, //內部遊戲狀態 _gameState: null, _getTimeNow: function () { return +new Date(); }, _run: function (time) { var self = this; this._loopBody(time); if (this._gameState === GameStatus.STOP) { return; } window.requestNextAnimationFrame(function (time) { self._run(time); }); }, _loopBody: function (time) { this._tick(time); this.onStartLoop(); this._layerManager.run(); this._layerManager.change(); this.onEndLoop(); }, _tick: function (time) { this._updateFps(time); this.gameTime = this._getTimeNow() - this._startTime; this._lastTime = time; }, _updateFps: function (time) { if (this._lastTime === 0) { this._fps =STARTING_FPS; } else { this._fps = 1000 / (time - this._lastTime); } } }, Public: { gameTime: null, start: function () { var self = this; this._startTime = this._getTimeNow(); window.requestNextAnimationFrame(function (time) { self._run(time); }); }, setLayerManager: function (layerManager) { this._layerManager = layerManager; }, getFps: function () { return this._fps; }, stop: function () { this._gameState = GameStatus.STOP; }, //*鉤子 init: function () { }, onStartLoop: function () { }, onEndLoop: function () { } }, Static: { getInstance: function () { if (_instance === null) { _instance = new this(); } return _instance; } } }); }());
引擎Director提供了init、onStartLoop、onEndLoop鉤子方法供用戶重寫。
引擎會在加載完圖片後調用鉤子方法init,用戶能夠經過重寫該鉤子,插入初始化遊戲的用戶邏輯。
onStartLoop、onEndLoop鉤子分別在每次主循環開始和結束時調用,插入用戶邏輯:
引擎Director
_loopBody: function (time) { this._tick(time); this.onStartLoop(); … this.onEndLoop(); },
由於全局只有一個Director,所以爲單例。
炸彈人Game中使用setInterval方法,而引擎Director使用requestAnimationFrame方法實現主循環。這是由於能夠經過setTimeout和setInterval方法在腳本中實現動畫,可是這樣效果可能不夠流暢,且會佔用額外的資源。
參考《HTML5 Canvas核心技術:圖形、動畫與遊戲開發》中的論述:
它們有以下的特徵:
一、即便向其傳遞毫秒爲單位的參數,它們也不能達到ms的準確性。這是由於javascript是單線程的,可能會發生阻塞。
二、沒有對調用動畫的循環機制進行優化。
三、沒有考慮到繪製動畫的最佳時機,只是一味地以某個大體的事件間隔來調用循環。
其實,使用setInterval或setTimeout來實現主循環,根本錯誤就在於它們抽象等級不符合要求。咱們想讓瀏覽器執行的是一套能夠控制各類細節的api,實現如「最優幀速率」、「選擇繪製下一幀的最佳時機」等功能。可是若是使用它們的話,這些具體的細節就必須由開發者本身來完成。
requestAnimationFrame不須要使用者指定循環間隔時間,瀏覽器會基於當前頁面是否可見、CPU的負荷狀況等來自行決定最佳的幀速率,從而更合理地使用CPU。
須要注意的時,不一樣的瀏覽器對於requestAnimationFrame、cancelNextRequestAnimationFrame的實現不同,所以須要定義通用的方法,放到引擎Tool類中。
引擎Tool
/** * 來自《HTML5 Canvas核心技術:圖形、動畫與遊戲開發》 */ window.requestNextAnimationFrame = (function () { var originalWebkitRequestAnimationFrame = undefined, wrapper = undefined, callback = undefined, geckoVersion = 0, userAgent = navigator.userAgent, index = 0, self = this; // Workaround for Chrome 10 bug where Chrome // does not pass the time to the animation function if (window.webkitRequestAnimationFrame) { // Define the wrapper wrapper = function (time) { if (time === undefined) { time = +new Date(); } self.callback(time); }; // Make the switch originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame; window.webkitRequestAnimationFrame = function (callback, element) { self.callback = callback; // Browser calls the wrapper and wrapper calls the callback originalWebkitRequestAnimationFrame(wrapper, element); } } // Workaround for Gecko 2.0, which has a bug in // mozRequestAnimationFrame() that restricts animations // to 30-40 fps. if (window.mozRequestAnimationFrame) { // Check the Gecko version. Gecko is used by browsers // other than Firefox. Gecko 2.0 corresponds to // Firefox 4.0. index = userAgent.indexOf('rv:'); if (userAgent.indexOf('Gecko') != -1) { geckoVersion = userAgent.substr(index + 3, 3); if (geckoVersion === '2.0') { // Forces the return statement to fall through // to the setTimeout() function. window.mozRequestAnimationFrame = undefined; } } } return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback, element) { var start, finish; window.setTimeout(function () { start = +new Date(); callback(start); finish = +new Date(); self.timeout = 1000 / 60 - (finish - start); }, self.timeout); }; }()); window.cancelNextRequestAnimationFrame = window.cancelRequestAnimationFrame || window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame || window.mozCancelRequestAnimationFrame || window.oCancelRequestAnimationFrame || window.msCancelRequestAnimationFrame || clearTimeout;
主循環的邏輯封裝在_run方法中。
start方法負責啓動主循環。
退出主循環的機制
爲了可以退出主循環,增長內部遊戲狀態_gameState。用戶可調用引擎Director的stop方法來設置內部遊戲狀態爲STOP,而後Director會在主循環中的_run方法中判斷內部遊戲狀態,若是爲STOP狀態,則退出主循環。
引擎Director
_run: function (time) { var self = this; this._loopBody(time); if (this._gameState === GameStatus.STOP) { //退出主循環 return; } window.requestNextAnimationFrame(function (time) { self._run(time); }); }, … stop: function () { this._gameState = GameStatus.STOP; },
這裏有同窗可能會問爲何stop方法不直接調用cancelNextRequestAnimationFrame方法來結束主循環?
參考代碼以下所示:
引擎Director _run: function (time) { var self = this; this._loopBody(time); //刪除遊戲狀態的判斷 this._loopId = window.requestNextAnimationFrame(function (time) { self._run(time); }); }, … stop: function () { //直接在stop方法中結束主循環 window.cancelNextRequestAnimationFrame(this._loopId); }
這是由於:
若是用戶是在引擎的鉤子中調用stop方法,因爲引擎的鉤子方法都是在主循環中調用的(_loopBody方法中調用),因此不能結束主循環!
//該方法包含了主循環邏輯,全部的鉤子方法都是在該方法中調用 _loopBody: function (time) { this._tick(time); this._scene.onStartLoop(); this._scene.run(); this._scene.onEndLoop(); },
只有當用戶在引擎主循環外部調用stop方法時,才能夠結束主循環。
詳見《深刻理解requestAnimationFrame》中的「爲何在callback內部執行cancelAnimationFrame不能取消動畫」
目前LayerManager爲炸彈人類,用戶經過調用引擎Director的setLayerManager方法將其注入到引擎Director中。
領域模型
引擎Director在主循環中調用layerManager實例的run和change方法,執行炸彈人LayerManager的主循環邏輯。
(1)根據引擎設計原則「引擎不該該依賴用戶,用戶應該依賴引擎」,LayerManager爲用戶類,引擎不該該依賴用戶。
(2)這樣會下降引擎Director的通用性
引擎Director應該操做抽象角色,而不該該直接操做具體的「層管理」類,這樣會致使具體的「層管理」類變化時,引擎Director也會受到影響。
所以,此處採用「由用戶注入」的設計更加合理。
LayerManager的change方法負責調用每一個層的change方法,設置畫布的狀態(主循環中會判斷畫布狀態,決定是否更新畫布):
炸彈人LayerManager
change: function () { this.__iterator("change"); }
change方法的調用有兩個選擇:
(1)由用戶調用
用戶可在重寫引擎Director提供的鉤子方法中(如onEndLoop),調用炸彈人LayerManager的change方法
(2)由引擎調用
引擎Director主循環在調用layerManager的run方法後調用layerManager的change方法。
由於:
(1)設置畫布狀態的邏輯屬於通用邏輯
(2)引擎對何時設置畫布狀態有最多的知識
因此應該由引擎Director調用。
引擎Director的_updateFps方法負責根據上一次主循環執行時間計算fps:
//time爲當前主循環的開始時間(從1970年1月1日到當前所通過的毫秒數) //lastTime爲上一次主循環的開始時間 _updateFps: function (time) { if (this._lastTime === 0) { this._fps = STARTING_FPS; } else { this._fps = 1000 / (time - this._lastTime); } }
其中引擎Director的STARTING_FPS定義了初始的fps,「time-this._lastTime」計算的是上次主循環的執行時間。
若是爲第一次主循環,lastTime爲0,fps爲初始值;
不然,fps爲上次主循環執行時間的倒數。
炸彈人Game改成只負責初始化場景和管理遊戲狀態,其它邏輯委託引擎實現。
炸彈人Game
(function () { //得到引擎Director實例,從而可實例重寫。 var director = YE.Director.getInstance(); var Game = YYC.Class({ Init: function () { }, Private: { ... _gameOver: function () { director.stop(); //結束主循環 alert("Game Over!"); }, _gameWin: function () { director.stop(); //結束主循環 alert("You Win!"); } }, Public: { … init: function () { //初始化遊戲全局狀態 window.gameState = window.bomberConfig.game.state.NORMAL; window.subject = new YYC.Pattern.Subject(); //調用引擎Director的getFps方法得到fps this.sleep = 1000 / director.getFps(); … }, judgeGameState: function () { … } } }); var game = new Game(); //重寫引擎Director的init鉤子 director.init = function () { game.init(); //設置場景 this.setLayerManager(game.layerManager); }; //重寫引擎Director的onStartLoop鉤子 director.onStartLoop = function () { game.judgeGameState(); }; }());
重構炸彈人Game
將Game中屬於「初始化場景」職責的「初始化遊戲全局狀態」和「建立Subject實例」邏輯提到Game的 init方法中。
由於只有Game調用這兩個方法,所以將其設爲私有方法。
而judgeGameState方法被director的鉤子方法調用,所以將其設爲公有方法。
炸彈人Game實例重寫引擎Director
在init鉤子中,炸彈人插入了Game的初始化場景的邏輯,注入了Game建立的layerManager實例。
這部分職責已經移到引擎Director中了,因此Game刪除start和run方法,由引擎負責控制主循環。
修改了Game的gameOver、gameWin方法,改成調用director.stop方法來結束主循環。
將Game的run方法的「關於全局遊戲狀態判斷」的邏輯移到Director的onStartLoop鉤子中,引擎會在每次主循環開始時判斷一次全局遊戲狀態,決定是否調用Game的gameOver或gameWin方法結束遊戲。
爲了能經過遊戲的運行測試,先修改炸彈人Main重寫引擎Main的onload鉤子,改成調用引擎Director的init和start方法來執行遊戲初始化並啓動主循環。
炸彈人Main修改前
main.onload = function () { … var game = new Game(); game.init(); game.start(); };
炸彈人Main修改後
main.onload = function () { … var director = YE.Director.getInstance(); director.init(); director.start(); };
由於:
(1)「執行遊戲初始化」的邏輯具體是調用Director的鉤子方法init,而鉤子方法應該由引擎調用。
(2)「執行遊戲初始化」和「啓動主循環」的邏輯應該由入口類負責,也就是說能夠由引擎Main或炸彈人Main負責。由於該邏輯與引擎更相關,而且考慮到引擎設計原則「儘可能減小用戶負擔」,因此應該由引擎Main負責。
因此應該由引擎Main負責該邏輯。
所以修改引擎ImgLoader,增長onload_game鉤子;而後在引擎Main中重寫ImgLoader的onload_game鉤子,實現「執行遊戲初始化並啓動主循環」的邏輯;最後修改炸彈人Main重寫引擎Main的onload鉤子,再也不調用引擎Director的init和start方法。
爲何引擎ImgLoader要增長onload_game鉤子?
由於如今引擎ImgLoader的鉤子是供炸彈人Main重寫的,引擎Main沒法重寫引擎ImgLoader的鉤子來執行「執行遊戲初始化並啓動主循環」邏輯,因此引擎ImgLoader增長內部鉤子onload_game,供引擎Main重寫,而炸彈人Main則負責在重寫的引擎ImgLoader的onload鉤子中實現「加載圖片完成到執行遊戲初始化並啓動主循環」之間的用戶邏輯。
相關代碼
引擎ImgLoader
_onload: function (i) { ... if (this.currentLoad === this.imgCount) { //圖片加載完成後調用onload和onload_game鉤子 this.onload(this.imgCount); this.onload_game(); } }, ... //*內部鉤子 onload_game: function () { }, ... }
引擎Main
_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(); } }
炸彈人Main
main.onload = function () { //隱藏資源加載進度條 _hideBar(); };
引擎ImgLoader的onload鉤子和onload_game鉤子重複了,二者都是在加載圖片完成後調用。
提出onload_game鉤子只是一個臨時的解決方案,在第二次迭代中會刪除它。
如今應該提出Scene領域類,使引擎Director依賴引擎Scene,而不是依賴炸彈人LayerManager。
因爲Scene繼承於Hash,所以將Hash也一塊兒提出。
領域類Scene負責管理場景,對應炸彈人LayerManager;領域類Hash爲哈希結構的集合類,對應炸彈人Hash。
炸彈人LayerManager是一個容器類,負責層的管理,屬於通用類,可直接提取到引擎中,重命名爲Scene。
炸彈人Hash是一個獨立的抽象類,可直接提取到引擎中
引擎Hash
(function () { namespace("YE").Hash = YYC.AClass({ Private: { //容器 _childs: {} }, Public: { getChilds: function () { return this._childs; }, getValue: function (key) { return this._childs[key]; }, add: function (key, value) { this._childs[key] = value; return this; } } }); }());
引擎Scene
(function () { namespace("YE").Scene = YYC.Class(YE.Hash, { Private: { __iterator: function (handler, args) { var args = Array.prototype.slice.call(arguments, 1), i = null, layers = this.getChilds(); for (i in layers) { if (layers.hasOwnProperty(i)) { layers[i][handler].apply(layers[i], args); } } }, __getLayers: function () { return this.getChilds(); } }, Public: { addLayer: function (name, layer) { this.add(name, layer); return this; }, getLayer: function (name) { return this.getValue(name); }, addSprites: function (name, elements) { this.getLayer(name).appendChilds(elements); }, initLayer: function () { this.__iterator("setCanvas"); this.__iterator("init", this.__getLayers()); }, run: function () { this.__iterator("run"); }, change: function () { this.__iterator("change"); } } }); }());
由於炸彈人LayerManager重構爲引擎Scene了,所以炸彈人Game也要對應修改成依賴引擎Scene。
領域模型
將Game的layerMangaer屬性重命名爲scene,並重命名_createLayerManager方法爲_createScene,改成建立引擎Scene實例。
炸彈人Game
_createScene: function () { this.scene = new YE.Scene(); this.scene.addLayer("mapLayer", layerFactory.createMap()); this.scene.addLayer("enemyLayer", layerFactory.createEnemy(this.sleep)); this.scene.addLayer("playerLayer", layerFactory.createPlayer(this.sleep)); this.scene.addLayer("bombLayer", layerFactory.createBomb()); this.scene.addLayer("fireLayer", layerFactory.createFire()); }, _addElements: function () { … this.scene.addSprites("mapLayer", mapLayerElements); this.scene.addSprites("playerLayer", playerLayerElements); this.scene.addSprites("enemyLayer", enemyLayerElements); }, … _initLayer: function () { this.scene.initLayer(); }, … init: function () { … this._createScene(); … }
由於引擎Director依賴引擎Scene了,因此應該將_layerManager屬性重命名爲scene,將setLayerManager方法重命名爲setScene。
引擎Director
_scene: null, … _loopBody: function (time) { … this._scene.run(); this._scene.change(); … }, … setScene: function (scene) { this._scene = scene; },
對應修改Game,改成調用setScene方法:
炸彈人Game
director.init = function () { … //設置場景 this.setScene(game.scene); };
如今應該提出Layer領域類,使引擎Scene依賴引擎Layer。
因爲Layer繼承於Collection類,所以將Collection也一塊兒提出。
領域類Layer負責層內精靈的統一管理,對應炸彈人的Layer。
領域類Collection爲線性結構的集合類,對應炸彈人Collection.
炸彈人Layer是一個抽象類,負責精靈的管理,具備通用性,直接提取到引擎中。
炸彈人Collection是一個獨立的類,可直接提取到引擎中
引擎Layer
(function () { namespace("YE").Layer = YYC.AClass(Collection, { Init: function () { }, Private: { __state: bomberConfig.layer.state.CHANGE, __getContext: function () { this.P_context = this.P_canvas.getContext("2d"); } }, Protected: { P_canvas: null, P_context: null, P_isChange: function () { return this.__state === bomberConfig.layer.state.CHANGE; }, P_isNormal: function () { return this.__state === bomberConfig.layer.state.NORMAL; }, P_iterator: function (handler) { var args = Array.prototype.slice.call(arguments, 1), nextElement = null; while (this.hasNext()) { nextElement = this.next(); nextElement[handler].apply(nextElement, args); //要指向nextElement } this.resetCursor(); }, P_render: function () { if (this.P_isChange()) { this.clear(); this.draw(); this.setStateNormal(); } } }, Public: { remove: function (sprite) { this.base(function (e, obj) { if (e.x === obj.x && e.y === obj.y) { return true; } return false; }, sprite); }, setStateNormal: function () { this.__state = bomberConfig.layer.state.NORMAL; }, setStateChange: function () { this.__state = bomberConfig.layer.state.CHANGE; }, Virtual: { init: function () { this.__getContext(); }, clear: function (sprite) { if (arguments.length === 0) { this.P_iterator("clear", this.P_context); } else if (arguments.length === 1) { sprite.clear(this.P_context); } } } }, Abstract: { setCanvas: function () { }, change: function () { }, draw: function () { }, //遊戲主循環調用的方法 run: function () { } } }); }());
引擎Collecton
(function () { //*使用迭代器模式 var IIterator = YYC.Interface("hasNext", "next", "resetCursor"); namespace("YE").Collection = YYC.AClass({Interface: IIterator}, { Private: { //當前遊標 _cursor: 0, //容器 _childs: [] }, Public: { getChilds: function () { return YYC.Tool.array.clone(this._childs); }, getChildAt: function (index) { return this._childs[index]; }, appendChild: function (child) { this._childs.push(child); return this; }, appendChilds: function (childs) { var i = 0, len = 0; for (i = 0, len = childs.length; i < len; i++) { this.addChild(childs[i]); } }, removeAll: function () { this._childs = []; }, hasNext: function () { if (this._cursor === this._childs.length) { return false; } else { return true; } }, next: function () { var result = null; if (this.hasNext()) { result = this._childs[this._cursor]; this._cursor += 1; } else { result = null; } return result; }, resetCursor: function () { this._cursor = 0; }, Virtual: { remove: function (func, child) { this._childs.remove(func, child); } } } }); }());
將引擎Collection依賴YTool的clone方法提到引擎Tool中。
引擎Tool
namespace("YE.Tool").array = { /*返回一個新的數組,元素與array相同(地址不一樣)*/ clone: function (array) { var new_array = new Array(array.length); for (var i = 0, _length = array.length; i < _length; i++) { new_array[i] = array[i]; } return new_array; } };
對應修改引擎Collection
getChilds: function () { return YE.Tool.array.clone(this._childs); },
引擎Collection重命名appendChild、appendChilds爲addChild、addChilds:
引擎Collection
addChild: function (child) { … }, addChilds: function (childs) { … },
如今引擎Layer依賴炸彈人Config定義的枚舉值State:
引擎Layer
Private: { __state: bomberConfig.layer.state.CHANGE, … Protected: { … P_isChange: function () { return this.__state === bomberConfig.layer.state.CHANGE; }, P_isNormal: function () { return this.__state === bomberConfig.layer.state.NORMAL; }, … Public: { … setStateNormal: function () { this.__state = bomberConfig.layer.state.NORMAL; }, setStateChange: function () { this.__state = bomberConfig.layer.state.CHANGE; },
由於引擎Layer不該該依賴用戶類,所以應該將枚舉值State移到引擎類中。又由於State爲畫布狀態,與引擎Layer相關,所以將其提出來直接放到引擎Layer中,解除引擎Layer對炸彈人Config的依賴。
引擎Layer
//定義State枚舉值 var State = { NORMAL: 0, CHANGE: 1 }; namespace("YE").Layer = YYC.AClass(YE.Collection, { Init: function () { }, Private: { __state: State.CHANGE, … Protected: { … P_isChange: function () { return this.__state === State.CHANGE; }, P_isNormal: function () { return this.__state === State.NORMAL; }, … Public: { … setStateNormal: function () { this.__state = State.NORMAL; }, setStateChange: function () { this.__state = State.CHANGE; },
因爲引擎Layer的使用方式爲繼承重寫,因此修改炸彈人BombLayer、CharacterLayer、FireLayer、MapLayer、PlayerLayer,繼承引擎Layer:
var BombLayer = YYC.Class(YE.Layer, { … var CharacterLayer = YYC.Class(YE.Layer, { … var FireLayer = YYC.Class(YE.Layer, { … var MapLayer = YYC.Class(YE.Layer, { … var PlayerLayer = YYC.Class(YE.Layer, {
如今應該提出Sprite類,使引擎Layer依賴引擎Sprite。
領域類Sprite爲精靈類,對應炸彈人的Sprite。
炸彈人Sprite做爲抽象類,提煉了炸彈人精靈類的共性,具備通用性,所以將其直接提取到引擎中。
引擎Sprite
(function () { namespace("YE").Sprite = YYC.AClass({ Init: function (data, bitmap) { this.bitmap = bitmap; if (data) { //初始座標 this.x = data.x; this.y = data.y; this.defaultAnimId = data.defaultAnimId; this.anims = data.anims; } }, Private: { //更新幀動畫 _updateFrame: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } } }, Public: { //bitmap實例 bitmap: null, //精靈的座標 x: 0, y: 0, //精靈動畫集合 anims: null, //默認的動畫id defaultAnimId: null, //當前的Animation. currentAnim: null, //設置當前動畫 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, //重置當前幀 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, //取得精靈的碰撞區域, getCollideRect: function () { var obj = { x: this.x, y: this.y, width: this.bitmap.width, height: this.bitmap.height }; return YE.collision.getCollideRect(obj); }, Virtual: { init: function () { //初始化時顯示默認動畫 this.setAnim(this.defaultAnimId); }, // 更新精靈當前狀態. update: function (deltaTime) { this._updateFrame(deltaTime); }, //得到座標對應的方格座標(向下取值) getCellPosition: function (x, y) { return { x: Math.floor(x / YE.Config.WIDTH), y: Math.floor(y / YE.Config.HEIGHT) } }, draw: function (context) { context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height); }, clear: function (context) { //直接清空畫布區域 context.clearRect(0, 0, YE.Config.canvas.WIDTH, YE.Config.canvas.HEIGHT); } } } }); }());
如今引擎Sprite引用了炸彈人Config類定義的「方格大小」和「畫布大小」:
引擎Sprite
getCellPosition: function (x, y) { return { x: Math.floor(x / bomberConfig.Config.WIDTH), y: Math.floor(y / bomberConfig.Config.HEIGHT) } }, … clear: function (context) { context.clearRect(0, 0, bomberConfig.Config.canvas.WIDTH, bomberConfig.Config.canvas.HEIGHT); }
有下面幾個問題:
一、引擎Sprite依賴了炸彈人Config,違背了引擎設計原則「不該該依賴用戶」。
二、「方格大小」和「畫布大小」與精靈無關,所以不該該像引擎Layer的枚舉值State同樣放在Sprite中
所以,引擎提出Config配置類,將「方格大小」和「畫布大小」放在其中,使引擎Sprite依賴引擎Config。
引擎Config
namespace("YE").Config = { //方格寬度 WIDTH: 30, //方格高度 HEIGHT: 30, //畫布 canvas: { //畫布寬度 WIDTH: 600, //畫布高度 HEIGHT: 600 }
對應修改引擎Sprite,依賴引擎Config
引擎Sprite
getCellPosition: function (x, y) { return { x: Math.floor(x / YE.Config.WIDTH), y: Math.floor(y / YE.Config.HEIGHT) } }, … clear: function (context) { context.clearRect(0, 0, YE.Config.canvas.WIDTH, YE.Config.canvas.HEIGHT); }
引擎Config應該放置與引擎相關的、與用戶邏輯無關的配置屬性,而「方格大小」和「畫布大小」與具體的遊戲邏輯相關,屬於用戶邏輯,不該該放在引擎Config中。
另外,引擎Sprite訪問了「方格大小」和「畫布大小」,混入了用戶邏輯。所以引擎Sprite還須要進一步提煉和抽象。
這個重構放到第二次迭代中進行。
炸彈人Config放置與用戶邏輯相關的配置屬性,引擎Config放置與引擎相關的配置屬性,炸彈人類應該只訪問炸彈人的Config類,而引擎類應該只訪問引擎Config類。
引擎Sprite使用了炸彈人collision的getCollideRect方法來得到碰撞區域數據:
引擎Sprite
getCollideRect: function () { … return YYC.Tool.collision.getCollideRect(obj); },
考慮到炸彈人collision是一個碰撞算法類,具備通用性,所以將其提取到引擎中。
引擎collision
namespace("YE").collision = (function () { return { //得到精靈的碰撞區域, getCollideRect: function (obj) { return { x1: obj.x, y1: obj.y, x2: obj.x + obj.width, y2: obj.y + obj.height } }, //矩形和矩形間的碰撞 col_Between_Rects: function (obj1, obj2) { var rect1 = this.getCollideRect(obj1); var rect2 = this.getCollideRect(obj2); if (rect1 && rect2 && !(rect1.x1 >= rect2.x2 || rect1.y1 >= rect2.y2 || rect1.x2 <= rect2.x1 || rect1.y2 <= rect2.y1)) { return true; } return false; } }; }());
對應修改引擎Sprite,依賴引擎collision
getCollideRect: function () { … return YE.collision.getCollideRect(obj); },
因爲引擎Sprite的使用方式爲繼承重寫,因此修改炸彈人的具體精靈類BombSprite、FireSprite、MapElementSprite、MoveSprite,繼承引擎Sprite類
var BombSprite= YYC.Class(YE.Sprite, { … var FireSprite = YYC.Class(YE.Sprite, { … var MapElementSprite = YYC.Class(YE.Sprite, { … var MoveSprite = YYC.Class(YE.Sprite, { …
由於炸彈人collision提取到引擎中了,所以炸彈人改成依賴引擎的collision。
炸彈人BombSprite
collideFireWithCharacter: function (sprite) { … if (YE.collision.col_Between_Rects(fire, obj2)) { return true; }
炸彈人EnemySprite
collideWithPlayer: function (sprite2) { … if (YE.collision.col_Between_Rects(obj1, obj2)) { throw new Error(); }
如今提煉Factory類。
有兩個問題須要思考:
一、哪些引擎類須要工廠。
二、用哪一種方式實現工廠。
對於第1個問題,目前我認爲抽象類不須要工廠(第二次迭代中抽象類Scene、Layer、Sprite也會加上工廠方法create,使得用戶可直接使用這些引擎類),其它非單例的類都統一用工廠建立實例。
對於第2個問題,有如下兩個選擇:
一、與炸彈人代碼同樣,提出工廠類LayerFactory、SpriteFactory,分別負責建立引擎Layer、Sprite的實例
二、直接在類中提出create靜態方法,負責建立自身的實例
考慮到工廠只須要負責建立實例,沒有複雜的邏輯,所以採用第二個選擇,引擎全部的非單例類都提出create靜態方法。
目前只有引擎ImgLoader須要增長create方法
引擎ImgLoader
Static: { create: function(){ return new this(); } }
對應修改引擎Main,使用引擎ImgLoader的create方法建立它的實例
getInstance: function () { if (_instance === null) { _instance = new this(); _instance.imgLoader = YE.ImgLoader.create(); } return _instance; },
提煉Animation類,使引擎Sprite依賴引擎Animation。
領域類Animation負責控制幀動畫的播放,對應炸彈人Animation類。
該類負責幀動畫的控制,具備通用性,所以將其提取到引擎中
引擎Animation
(function () { namespace("YE").Animation = YYC.Class({ Init: function (config) { this._frames = YE.Tool.array.clone(config); this._init(); }, Private: { //幀數據 _frames: null, _frameCount: -1, _img: null, _currentFrame: null, _currentFrameIndex: -1, _currentFramePlayed: -1, _init: function () { this._frameCount = this._frames.length; this.setCurrentFrame(0); } }, Public: { setCurrentFrame: function (index) { this._currentFrameIndex = index; this._currentFrame = this._frames[index]; this._currentFramePlayed = 0; }, /** * 更新當前幀 * @param deltaTime 主循環的持續時間 */ update: function (deltaTime) { //若是沒有duration屬性(表示動畫只有一幀),則返回(由於不須要更新當前幀) if (this._currentFrame.duration === undefined) { return; } //判斷當前幀是否播放完成 if (this._currentFramePlayed >= this._currentFrame.duration) { //播放下一幀 if (this._currentFrameIndex >= this._frameCount - 1) { //當前是最後一幀,則播放第0幀 this._currentFrameIndex = 0; } else { //播放下一幀 this._currentFrameIndex++; } //設置當前幀 this.setCurrentFrame(this._currentFrameIndex); } else { //增長當前幀的已播放時間. this._currentFramePlayed += deltaTime; } }, getCurrentFrame: function () { return this._currentFrame; } }, Static: { create: function(config){ return new this(config); } } }); }());
修改炸彈人SpriteData,改成建立引擎Animation實例
炸彈人SpriteData
anims: { "stand_right": YE.Animation.create(getFrames("player", "stand_right")), …
引擎Animation改成依賴引擎Tool的clone方法
引擎Animation
Init: function (config) { this._frames = YE.Tool.array.clone(config); … },
如今提煉AI類。
領域類AI負責實現人工智能算法,對應炸彈人使用的碰撞算法和尋路算法。碰撞算法已經提煉到引擎中了(提煉爲引擎collision),尋路算法對應炸彈人FindPath類,它實現了A*尋路算法,屬於通用的算法,應該將其提取到引擎中。
然而「FindPath」這個名字範圍太大了,應該重命名爲實際採用的尋路算法的名字,所以將其重命名爲AStar。
引擎AStar
(function () { … function aCompute(mapData, begin, end) { … //8方向尋路 if (bomberConfig.algorithm.DIRECTION == 8) { … //4方向尋路 if (bomberConfig.algorithm.DIRECTION == 4) { … } … namespace("YE").AStar = { aCompute: function (terrainData, begin, end) { … return aCompute(terrainData, begin, end); } }; }());
如今引擎AStar直接讀取炸彈人Config中配置的尋路方向數algorithm.Director,致使引擎AStar依賴用戶類,違反了引擎設計原則。
所以,引擎AStar增長setDirection方法,由用戶調用該方法來設置尋路方向數,並刪除炸彈人Config的algorithm屬性。
引擎AStar
… DIRECTION = 4; //默認爲4方向尋路 … if (DIRECTION == 8) { … if (DIRECTION == 4) { … namespace("YE").AStar = { … /** * 設置尋路方向 * @param direction 4或者8 */ setDirection: function (direction) { DIRECTION = direction; } }
修改炸彈人EnemySprite,在構造函數中設置尋路的方向數爲4,並改成調用引擎AStar的aCompute方法來尋路。
炸彈人EnemySprite
Init: function (data, bitmap) { … YE.AStar.setDirection(4); … }, Private: { ___findPath: function () { return YE.AStar.aCompute(window.terrainData, this.___computeCurrentCoordinate(), this.___computePlayerCoordinate()).path },
如今提煉EventManager類。
領域類EventManager負責事件的監聽和移除,與炸彈人KeyCodeMap、KeyState以及KeyEventManager對應。
炸彈人KeyCodeMap、KeyState以及KeyEventManager都在KeyEventManager.js文件中,先來看下這個文件:
KeyEventManager.js
(function () { //枚舉值 var keyCodeMap = { LEFT: 65, // A鍵 RIGHT: 68, // D鍵 DOWN: 83, // S鍵 UP: 87, // W鍵 SPACE: 32 //空格鍵 }; //按鍵狀態 var keyState = { }; keyState[keyCodeMap.LEFT] = false; keyState[keyCodeMap.RIGHT] = false; keyState[keyCodeMap.UP] = false; keyState[keyCodeMap.DOWN] = false; keyState[keyCodeMap.SPACE] = false; //鍵盤事件管理類 var KeyEventManager = YYC.Class({ Private: { _keyDown: function () { }, _keyUp: function () { } }, Public: { addKeyDown: function () { this._keyDown = YYC.Tool.event.bindEvent(this, function (e) { keyState[e.keyCode] = true; e.preventDefault(); }); YYC.Tool.event.addEvent(document, "keydown", this._keyDown); }, removeKeyDown: function () { YYC.Tool.event.removeEvent(document, "keydown", this._keyDown); }, addKeyUp: function () { this._keyUp = YYC.Tool.event.bindEvent(this, function (e) { keyState[e.keyCode] = false; }); YYC.Tool.event.addEvent(document, "keyup", this._keyUp); }, removeKeyUp: function () { YYC.Tool.event.removeEvent(document, "keyup", this._keyUp); } } }); window.keyCodeMap = keyCodeMap; window.keyState = keyState; window.keyEventManager = new KeyEventManager(); }());
KeyCodeMap是鍵盤按鍵的枚舉值,由於全部瀏覽器中的鍵盤按鍵值都同樣,所以具備通用性,能夠將其提取到引擎中。
炸彈人KeyState是存儲當前按鍵狀態的容器類,與用戶邏輯相關,所以不提取到引擎中。
炸彈人KeyEventManager負責鍵盤事件的監聽和移除,能夠從中提出一個通用的、負責全部事件的監聽和移除的引擎類EventManager。
另外,將事件類型(如"keydown"、"keyup")提取爲枚舉值EventType,從而對用戶隔離具體的事件類型的變化。
引擎增長Event類,放置KeyCodeMap和EventType枚舉值。
引擎EventManager
(function () { var _keyListeners = {}; namespace("YE").EventManager = { _getEventType: function (event) { var eventType = "", e = YE.Event; switch (event) { case e.KEY_DOWN: eventType = "keydown"; break; case e.KEY_UP: eventType = "keyup"; break; case e.KEY_PRESS: eventType = "keypress"; break; default: throw new Error("事件類型錯誤"); } return eventType; }, addListener: function (event, handler) { var eventType = ""; eventType = this._getEventType(event); YYC.Tool.event.addEvent(window, eventType, handler); this._registerEvent(eventType, handler); }, _registerEvent: function (eventType, handler) { if (_keyListeners[eventType] === undefined) { _keyListeners[eventType] = [handler]; } else { _keyListeners[eventType].push(handler); } }, removeListener: function (event) { var eventType = ""; eventType = this._getEventType(event); if (_keyListeners[eventType]) { _keyListeners[eventType].forEach(function (e, i) { YYC.Tool.event.removeEvent(window, eventType, e); }) } } }; }());
引擎Event
namespace("YE").Event = { //事件枚舉值 KEY_DOWN: 0, KEY_UP: 1, KEY_PRESS: 2, //按鍵枚舉值 KeyCodeMap: { LEFT: 65, // A鍵 RIGHT: 68, // D鍵 DOWN: 83, // S鍵 UP: 87, // W鍵 SPACE: 32 //空格鍵 } };
目前引擎只支持鍵盤事件,之後能夠經過「增長Event事件枚舉值,並對應修改EventManager的_getEventType方法」的方式來增長更多的事件支持。
引擎類依賴了YTool事件操做方法addEvent和removeEvent,考慮到YTool的event中的事件操做方法都具備通用性,所以將其提取到引擎Tool類中
又由於YTool的event對象依賴YTool的judge對象的方法,因此將judge對象的相關的方法提取到引擎Tool中。
引擎Tool
namespace("YE.Tool").judge = { … /** * 判斷是否爲jQuery對象 */ isjQuery: function (ob) { … }, /** * 檢查宿主對象是否可調用 * * 任何對象,若是其語義在ECMAScript規範中被定義過,那麼它被稱爲原生對象; 環境所提供的,而在ECMAScript規範中沒有被描述的對象,咱們稱之爲宿主對象。 該方法用於特性檢測,判斷對象是否可用。用法以下: MyEngine addEvent(): if (Tool.judge.isHostMethod(dom, "addEventListener")) { //判斷dom是否具備addEventListener方法 dom.addEventListener(sEventType, fnHandler, false); } */ isHostMethod: (function () { … }()) }; namespace("YE.Tool").event = (function () { return { bindEvent: function (object, fun) { … }, /* oTarget既能夠是單個dom元素,也能夠是jquery集合。 如: Tool.event.addEvent(document.getElementById("test_div"), "mousedown", _Handle); Tool.event.addEvent($("div"), "mousedown", _Handle); */ addEvent: function (oTarget, sEventType, fnHandler) { … }, removeEvent: function (oTarget, sEventType, fnHandler) { … }, wrapEvent: function (oEvent) { … }, getEvent: function () { … } } }());
如今引擎KeyCodeMap的枚舉變量與用戶邏輯有關,定死了上下左右移動對應的按鍵keyCode值(如左對應A鍵,右對應D鍵):
引擎Event
KeyCodeMap: { LEFT: 65, // A鍵 RIGHT: 68, // D鍵 DOWN: 83, // S鍵 UP: 87, // W鍵 SPACE: 32 //空格鍵 }
然而對於不一樣的遊戲,它的上下左右對應的按鍵可能不一樣。
所以KeyCodeMap應該只定義按鍵對應的keyCode值,由用戶來決定上下左右移動對應的按鍵。
引擎Event
KeyCodeMap: { A: 65, D: 68, S: 83, W: 87, SPACE: 32 }
修改前
炸彈人實現了監聽事件的邏輯:
炸彈人Game
_initEvent: function () { //監聽整個document的keydown,keyup事件 keyEventManager.addKeyDown(); keyEventManager.addKeyUp(); },
炸彈人KeyEventManager
addKeyDown: function () { this._keyDown = YYC.Tool.event.bindEvent(this, function (e) { keyState[e.keyCode] = true; e.preventDefault(); }); YYC.Tool.event.addEvent(document, "keydown", this._keyDown); }, addKeyUp: function () { this._keyUp = YYC.Tool.event.bindEvent(this, function (e) { keyState[e.keyCode] = false; }); YYC.Tool.event.addEvent(document, "keyup", this._keyUp); },
修改後
炸彈人調用引擎EventManager API和傳入鍵盤事件的枚舉值來監聽鍵盤事件:
炸彈人Game
_initEvent: function () { //調用引擎EventManager的addListener綁定事件,傳入引擎Event定義的事件類型枚舉值,並定義事件處理方法 YE.EventManager.addListener(YE.Event.KEY_DOWN, function (e) { window.keyState[e.keyCode] = true; e.preventDefault(); }); YE.EventManager.addListener(YE.Event.KEY_UP, function (e) { window.keyState[e.keyCode] = false; }); }
由於炸彈人KeyEventManager.js中的KeyCodeMap和KeyEventManager已經移到引擎中了,因此刪除它們,只保留keyState,並重命名文件爲KeyState.js。
炸彈人KeyState
(function () { //按鍵狀態 var keyState = { }; keyState[keyCodeMap.LEFT] = false; keyState[keyCodeMap.RIGHT] = false; keyState[keyCodeMap.UP] = false; keyState[keyCodeMap.DOWN] = false; keyState[keyCodeMap.SPACE] = false; window.keyState = keyState; }());
如對應修改炸彈人KeyState和PlayerLayer
炸彈人KeyState
keyState[YE.Event.KeyCodeMap.A] = false; keyState[YE.Event.KeyCodeMap.D] = false; keyState[YE.Event.KeyCodeMap.W] = false; keyState[YE.Event.KeyCodeMap.S] = false; keyState[YE.Event.KeyCodeMap.SPACE] = false;
炸彈人PlayerLayer
___keyDown: function () { if (keyState[YE.Event.KeyCodeMap.A] === true || keyState[YE.Event.KeyCodeMap.D] === true || keyState[YE.Event.KeyCodeMap.W] === true || keyState[YE.Event.KeyCodeMap.S] === true) { return true; } else { return false; } },
如今提煉DataOperator類。
領域類DataOperator負責對數據進行讀、寫操做,對應炸彈人數據操做層的類,具體爲MapDataOperate、GetPath、TerrainDataOperate、GetSpriteData、GetFrames。
這些數據操做類都與具體的業務邏輯相關,沒有可提煉的。
如今提煉Data類。
領域類Data負責保存遊戲數據,對應炸彈人的數據層的類,具體爲MapData、Bitmap、ImgPathData、TerrainData、SpriteData、FrameData。
其中Bitmap是圖片的包裝類,包含與圖片自己密切相關的屬性和方法,但不包含與遊戲相關的具體圖片,所以具備通用性,可提取到引擎中。
引擎Bitmap
(function () { namespace("YE").Bitmap = YYC.Class({ Init: function (data) { this.img = data.img; this.width = data.width; this.height = data.height; }, Private: { }, Public: { img: null, width: 0, height: 0 } }); }());
修改炸彈人BitmapFactory,改成建立引擎的Bitmap實例
炸彈人BitmapFactory
(function () { var bitmapFactory = { createBitmap: function (data) { … return new YE.Bitmap(bitmapData); } } window.bitmapFactory = bitmapFactory; }());
此處炸彈人省略了與引擎類無關的類。
引擎集合類也屬於數據結構,爲何不放在數據結構包中,而是放在單獨的集合包中?
由於引擎集合類的使用方式爲繼承,而數據結構包中的引擎Bitmap的使用方式爲委託,二者使用方式不一樣,所以不能放到一個包中。
本文將炸彈人通用的類提煉到了引擎中,搭建了引擎的總體框架。可是如今引擎還很粗糙,包含了不少炸彈人邏輯,不具有通用性。所以,在下文中,我會進行第二次迭代,對引擎進行進一步的抽象和提煉。