前言
上文完成了引擎提炼的第一次迭代,搭建了引擎的整体框架,本文会进行引擎提炼的第二次迭代,进一步提高引擎的通用性,完善引擎框架。
由于第二次迭代内容过多,因此分为上、下两篇博文,本文为上篇。
本文目的
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为普通的类,向炸弹人类和引擎类提供APInamespace("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了。
本文源码下载
参考资料
上一篇博文
下一篇博文
提炼游戏引擎系列:第二次迭代(下)