上文咱们完成了引擎的初步设计,本文进行引擎提炼的第一次迭代,从炸弹人游戏中提炼引擎类,搭建引擎的总体框架。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的使用方式为委托,二者使用方式不一样,所以不能放到一个包中。
本文将炸弹人通用的类提炼到了引擎中,搭建了引擎的总体框架。可是如今引擎还很粗糙,包含了不少炸弹人逻辑,不具有通用性。所以,在下文中,我会进行第二次迭代,对引擎进行进一步的抽象和提炼。