前言
上文完成了引擎提炼的第一次迭代,搭建了引擎的整体框架,本文会进行引擎提炼的第二次迭代,进一步提高引擎的通用性,完善引擎框架。
由于第二次迭代内容过多,因此分为上、下两篇博文,本文为上篇。
本文目的
1、提高引擎的通用性,完善引擎框架。
2、对应修改炸弹人游戏。
本文主要内容
第一次迭代后的引擎领域模型
开发策略
本文会对引擎领域模型从左到右一一进行分析, 二次提炼和重构引擎类。
本文迭代步骤
迭代步骤说明
- 确定要重构的引擎类
按照第一次迭代提出的引擎领域模型,从左往右一一分析,判断是否需要重构。
- 发现问题
从是否包含用户逻辑、是否违反引擎设计原则、是否可从炸弹人类中提炼更多的通用模式等方面来审视引擎类,如果存在问题则给出引擎类与问题相关的当前设计。
- 分析问题
分析当前设计,指出其中存在的问题,给出问题的解决方案。
- 具体实施
按照解决方案修改当前设计。
- 通过游戏的运行测试
- 修改并通过引擎的单元测试
通过游戏运行测试和引擎单元测试后,继续分析该引擎类,发现并解决下一个问题。
- 完成本次迭代
解决了引擎类所有的问题后,就可以确定下一个要重构的引擎类,进入新一轮迭代。
不讨论测试
因为测试并不是本系列的主题,所以本系列不会讨论专门测试的过程,“本文源码下载”中也没有单元测试代码。
您可以在最新的引擎版本中找到引擎完整的单元测试代码: YEngine2D
修改Main
改为继承重写
上文对用户使用引擎的方式进行了思考,给出了“引擎Main、Director采用实例重写的方式”的设计。
但是现在重新思考后,发现Main采用实例重写的方式并不合适。
当前设计
领域模型
引擎Main
(function () {
var _instance = null;
namespace("YE").Main = YYC.Class({
Init: function () {
this._imgLoader = new YE.ImgLoader();
},
Private: {
_imgLoader: null,
_prepare: function () {
this.loadResource();
this._imgLoader.onloading = this.onloading;
this._imgLoader.onload = this.onload;
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
director.init();
director.start();
}
}
},
Public: {
init: function () {
this._prepare();
this._imgLoader.done();
},
getImg: function (id) {
return this._imgLoader.get(id);
},
load: function (images) {
this._imgLoader.load(images);
},
//* 钩子
loadResource: function () {
},
onload: function () {
},
onloading: function (currentLoad, imgCount) {
}
},
Static: {
getInstance: function () {
if (_instance === null) {
_instance = new this();
}
return _instance;
}
}
});
}());
炸弹人Main
(function(){
//获得引擎Main实例
var main = YE.Main.getInstance();
var _getImg = function () {
…
};
var _addImg = function (urls, imgs) {
…
};
var _hideBar = function () {
…
};
//重写引擎Main实例的钩子
main.loadResource = function () {
this.load(_getImg());
};
main.onloading = function (currentLoad, imgCount) {
$("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10)); //调用进度条插件
};
main.onload = function () {
_hideBar();
};
}());
其它炸弹人类通过调用引擎Main的getImg方法来获得加载的图片对象。
var img = YE.Main.getInstance().getImg(imgId); //获得id为imgID的图片对象
页面调用引擎Main的init方法进入游戏
<script type="text/javascript">
(function () {
YE.Main.getInstance().init();
})();
</script>
分析问题
因为炸弹人Main与引擎Main都属于“入口”概念,负责资源加载的管理,所以炸弹人Main与引擎Main应该为继承关系,引擎Main需要改造为可被继承的类,炸弹人Main也要改造为继于引擎Main。
具体实施
引擎Main应该为抽象类,不再为单例:
引擎Main
(function () {
namespace("YE").Main = YYC.AClass({
…
});
}());
炸弹人Main改为单例并继承引擎Main,提供getImg方法返回图片对象,供其它用户类调用。
炸弹人Main
(function () {
var Main = YYC.Class(YE.Main, {
Private:{
_getImg: function () {
…
},
_addImg: function (urls, imgs) {
…
},
_hideBar: function () {
…
}
},
Public:{
//返回对应id的图片对象
getImg:function(id){
return this.base(id);
},
loadResource: function () {
this.load(_getImg());
},
onloading: function (currentLoad, imgCount) {
$("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));
},
onload: function () {
this._hideBar();
}
},
Static: {
getInstance: function () {
if (_instance === null) {
_instance = new this();
}
return _instance;
}
}
});
window.Main = Main ;
}());
其它炸弹人类改为调用炸弹人Main的getImg方法来获得图片数据。
var img = Main.getInstance().getImg(imgId);
页面改为调用炸弹人Main的init方法
<script type="text/javascript">
(function () {
Main.getInstance().init();
})();
</script>
引擎Main不应该封装ImgLoader
进行上面的修改后,运行测试时会报错,错误信息为炸弹人Main在重写的onload方法中调用的“this._hideBar”为undefined。
造成这个错误的原因是在第一次迭代的设计中,引擎Main封装了引擎ImgLoader,将它的onload与ImgLoader的onload绑定在了一起,导致执行炸弹人Main的onload时,this指向了引擎ImgLoader实例imgLoader,而不是指向炸弹人Main。
引擎Main
_prepare: function () {
…
//绑定了引擎Main和引擎ImgLoader的钩子
this._imgLoader.onloading = this.onloading;
this._imgLoader.onload = this.onload;
…
}
},
Public: {
init: function () {
this._prepare();
…
},
getImg: function (id) {
return this._imgLoader.get(id);
},
引擎Main提供了getImg方法来获得引擎ImgLoader实例imgLoader保存的图片对象。
引擎Main改为继承重写后,由于其他炸弹人类不能直接访问到引擎Main的getImg方法,所以炸弹人必须增加getImg方法,对其它炸弹人类暴露引擎Main的getImg方法。
这样的设计是不合理的,引擎Main的getImg方法并不是设计为被用户重写的方法,而且炸弹人Main也不需要知道引擎Main的getImg方法的实现,这增加了用户的负担,违反了引擎设计原则“尽量减少用户负担”。
因此,引擎Main不再封装imgLoader,而是将其暴露给炸弹人Main,再由它暴露给其它炸弹人类。
具体来说就是,引擎Main删除getImg、load方法,将imgLoader属性设为公有属性;炸弹人Main将imgLoader设为全局属性,直接重写imgLoader的onload、onloading钩子,并删除getImg方法。。
这样其它炸弹人类可以直接访问引擎Main的imgLoader属性,调用它的get方法来获得图片数据
由于炸弹人没有要插入到引擎Main的用户逻辑,因此引擎Main删除onload、onloading钩子。
修改后相关代码
引擎Main
Private: {
//删除了onload和onloading钩子,不再绑定引擎Main和引擎ImgLoader的钩子了
_prepare: function () {
…
}
},
Public: {
//imgLoader作为公有属性
imgLoader: null,
炸弹人Main
loadResource: function () {
//获得引擎Main的imgloader
var loader = this.imgLoader,
self = this;
//重写imgLoader的钩子
loader.load(this._getImg());
loader.onloading = function (currentLoad, imgCount) {
…
};
loader.onload = function (imgCount) {
…
};
//imgLoader设为全局属性,供其它炸弹人类操作
window.imgLoader = this.imgLoader;
}
其它炸弹人类通过window.imgLoader.get方法获得图片数据
重构后的领域模型
修改Director
炸弹人Game的名字与其职责不符
引擎Director暂时找不出问题,因此来看下与它相关的炸弹人Game。
当前设计
现在炸弹人Game实例重写了引擎Director。
引擎Scene不能被重写,只能提供API供炸弹人Game和引擎Director调用。
重构前领域模型
炸弹人Game
(function () {
var director = YE.Director.getInstance();
var Game = YYC.Class({
…
Public: {
init: function () {
//初始化游戏全局状态
window.gameState = window.bomberConfig.game.state.NORMAL;
window.subject = new YYC.Pattern.Subject();
this.sleep = 1000 / director.getFps();
//初始化游戏场景
this._createScene();
this._addElements();
this._initLayer();
this._initEvent();
window.subject.subscribe(this.scene.getLayer("mapLayer"), this.scene.getLayer("mapLayer").changeSpriteImg);
},
//管理游戏状态
judgeGameState: function () {
…
}
}
});
var game = new Game();
director.init = function () {
game.init();
//设置场景
this.setScene(game.scene);
};
director.onStartLoop = function () {
game.judgeGameState();
};
}());
引擎Scene
//引擎Scene为普通的类,向炸弹人类和引擎类提供API
namespace("YE").Scene = YYC.Class(YE.Hash, {
…
分析问题
炸弹人Game现在只负责初始化游戏场景和管理游戏状态的逻辑,该逻辑属于场景的范围,不属于统一调度的范围,因此Game应该改造为炸弹人场景类,与引擎Scene对应,而不是与引擎Director对应。
考虑到炸弹人场景类与引擎Scene同属一个概念,因此炸弹人场景类应该使用继承重写的方式来使用引擎Scene。
由于引擎Director依赖引擎Scene,而引擎Scene不依赖引擎Director,所以炸弹人场景类也不应该再依赖引擎Director。
因此,应该进行下面的重构:
1、改造引擎Scene类为可被继承重写的类。
2、将炸弹人Game改造为炸弹人场景类Scene,继承重写引擎Scene。
3、引擎Director应该改造为一个封闭的单例类,用户不能重写,向引擎类和用户类提供主循环和场景操作相关的API。将它的钩子方法移到引擎Scene类,炸弹人Game对引擎Director钩子方法的重写变为对引擎Scene钩子方法的重写,对应修改钩子方法的调用机制。
具体实施
按照下面的步骤重构:
1、改造引擎Scene为可被继承的类,将引擎Director的钩子移到其中;
2、将炸弹人Game改造为场景类Scene,继承重写引擎Scene;
3、改造引擎Director,修改钩子方法的调用机制;
4、重构相关的引擎类和炸弹人类。
改造引擎Scene类为可被继承的类
引擎Scene改为抽象类,将引擎Director的init、onStartLoop、onEndLoop钩子方法移到其中。
引擎Scene
(function () {
namespace("YE").Scene = YYC.AClass({
…
Public: {
…
init: function () {
},
onStartLoop: function () {
},
onEndLoop: function () {
}
}
});
}());
引擎Director删除钩子方法
将炸弹人Game改造为场景类Scene,继承重写引擎Scene
Game进行下面的修改:
(1)炸弹人Game重命名为Scene。
(2)继承引擎Scene,重写钩子方法init和onStartLoop。
(3)删除scene属性,将调用scene属性的成员改为调用自身的成员(“self/this.scene.xxx”改为“self/this.xxx”)。
(4)不再创建scene实例了,对应修改_createScene方法,删除其中的“创建scene”逻辑,保留“加入层”逻辑,将其重命名为_addLayer。
炸弹人Scene
var Scene = YYC.Class(YE.Scene, {
Private: {
_sleep: 0,
_addLayer: function () {
this.addLayer("mapLayer", layerFactory.createMap());
this.addLayer("enemyLayer", layerFactory.createEnemy(this._sleep));
this.addLayer("playerLayer", layerFactory.createPlayer(this._sleep));
this.addLayer("bombLayer", layerFactory.createBomb());
this.addLayer("fireLayer", layerFactory.createFire());
},
_addElements: function () {
var mapLayerElements = this._createMapLayerElement(),
playerLayerElements = this._createPlayerLayerElement(),
enemyLayerElements = this._createEnemyLayerElement();
this.addSprites("mapLayer", mapLayerElements);
this.addSprites("playerLayer", playerLayerElements);
this.addSprites("enemyLayer", enemyLayerElements);
},
_createMapLayerElement: function () {
…
},
_getMapImg: function (i, j, mapData) {
…
},
_createPlayerLayerElement: function () {
…
},
_createEnemyLayerElement: function () {
….
},
_initLayer: function () {
this.initLayer();
},
_initEvent: function () {
…
},
_judgeGameState: function () {
…
},
_gameOver: function () {
…
},
_gameWin: function () {
…
}
},
Public: {
//重写引擎Scene的init钩子
init: function(){
window.gameState = window.bomberConfig.game.state.NORMAL;
window.subject = new YYC.Pattern.Subject();
this.sleep = 1000 / director.getFps();
this._addLayer();
this._addElements();
this._initLayer();
this._initEvent();
window.subject.subscribe(this.getLayer("mapLayer"), this.getLayer("mapLayer").changeSpriteImg);
},
//重写引擎Scene的onStartLoop钩子
onStartLoop: function(){
this._judgeGameState();
}
}
});
改造引擎Director类
修改了引擎Director和引擎Scene的钩子方法后,需要对应修改这些钩子方法的调用机制。
当前设计
在修改前先来看下引擎Main、Director、Scene以及炸弹人Game之间关于场景的交互机制:
完成加载图片后会触发引擎ImgLoader的onload_game钩子,该钩子被引擎Main重写,触发引擎Director的init钩子,执行炸弹人Game插入的初始化场景的逻辑,:
引擎Main
_prepare: function () {
…
//加载图片完成后,触发引擎ImgLoader的onload_game钩子
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//触发init钩子
director.init();
director.start();
}
}
…
炸弹人Game
_createScene: function () {
this.scene = new YE.Scene();
…
},
…
init: function () {
…
this. _createScene();
…
}
var director = YE.Director.getInstance();
…
//重写引擎Director的init钩子
director.init = function () {
game.init();
//调用引擎Director的setScene方法,设置当前场景
this.setScene(game.scene);
};
然后onload_game会调用引擎Director的start方法,启动主循环,触发引擎Director的钩子方法onStartLoop和onEndLoop,执行炸弹人Game重写插入的场景逻辑:
引擎Main
_prepare: function () {
…
//加载图片完成后,触发引擎ImgLoader的onload_game钩子
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
director.init();
//调用start方法
director.start();
}
}
炸弹人Game
var director = YE.Director.getInstance();
…
//重写引擎Director的onStartLoop钩子
director.onStartLoop = function () {
game.judgeGameState();
};
引擎Director
start:function(){
…
//启动主循环
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
…
},
…
_run: function (time) {
var self = this;
//主循环逻辑在_loopBody方法中
this._loopBody(time);
if (this._gameState === GameState.STOP) {
return;
}
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
},
_loopBody: function (time) {
…
//触发自己的onStartLoop和onEndLoop钩子
this.onStartLoop();
…
this.onEndLoop();
},
修改后的设计
进行下面四个修改:
(1)onload_game不再调用引擎Director的init方法。
(2)onload_game会传入引擎Main创建的炸弹人Scene实例(这只是临时解决方案,这样的设计导致了引擎Main依赖炸弹人Scene,违反了引擎设计原则!后面会进行重构)到引擎Director的start方法中。
(3)引擎Director的start方法会触发炸弹人Scene实例的init钩子方法,并设置该实例为当前场景。
(4)引擎Director在主循环中改为触发当前场景的onStartLoop和onEndLoop钩子方法。
修改后的场景的交互机制序列图
引擎Main
_prepare: function () {
…
this. _imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//传入创建的炸弹人场景实例
director.start(new Scene());
};
}
引擎Director
start: function (scene) {
var self = this;
//触发场景的init钩子
scene.init();
//设置为当前场景
this.setScene(scene);
…
},
引擎Director
_loopBody: function (time) {
…
this._scene.onStartLoop();
…
this._scene.onEndLoop();
},
重构相关的引擎类和炸弹人类
引擎Director类的start方法重命名为runWithScene
由于start方法传入了炸弹人Scene的实例,所以将该方法重命名为runWithScene更合适:
引擎Director
runWithScene:function(scene){
…
}
引擎Main
_prepare: function () {
…
this._imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//改为调用引擎Director的runWithScene方法
director.runWithScene(new Scene());
};
}
解除引擎Main对炸弹人Scene的依赖
现在引擎Main创建了炸弹人Scene的实例:
引擎Main
_prepare: function () {
…
this. _imgLoader.onload_game = function () {
var director = YE.Director.getInstance();
//创建并注入炸弹人Scene实例
director.runWithScene (new Scene());
};
}
这导致了引擎依赖用户,违反了引擎设计原则。
因为引擎ImgLoader的onload_game与onload钩子执行时间相同,所以可以将onload_game中的逻辑移到炸弹人Main重写ImgLoader的onload钩子中,由炸弹人Main创建炸弹人Scene实例,解除了引擎Main对炸弹人Scene的依赖:
炸弹人Main
loadResource: function () {
…
loader.onload = function (imgCount) {
…
YE.Director.getInstanc.runWithScene(new Scene());
};
…
}
删除引擎ImgLoader的onload_game钩子
ImgLoader的onload_game钩子和onload钩子重复了,这是第一次迭代提出的临时解决方案。
现在onload_game钩子已经没有用了,因此将其删除。
引擎类继承重写的钩子方法都设成虚方法
继承重写的钩子方法是设计为被用户继承重写的,属于多态,应该将其设为虚方法。
对于实例重写的钩子方法,用户只是重写实例的钩子方法,并没有继承引擎类,不属于多态,不设为虚方法。
又由于用户不是必须要重写钩子方法,因此钩子方法不应该设为抽象方法。
引擎Main
Virtual:{
loadResource: function () {
}
}
引擎Scene
Virtual: {
init: function () {
},
onStartLoop: function () {
},
onEndLoop: function () {
}
}
游戏结束时引擎要停止所有定时器
目前引擎Director只有退出主循环的机制:
引擎Director
_run: function (time) {
var self = this;
this._loopBody(time);
//如果游戏状态为STOP,则退出主循环
if (this._gameState === YE.Director.GameState.STOP) {
return;
}
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
},
…
stop: function () {
this._gameStatus = GameStatus.STOP;
}
用户可能会在游戏中调用setTimeout、setInterval方法设置定时器,所以引擎需要在游戏结束时停止这些定时器。
因此,引擎Director的stop方法增加停止所有定时器的逻辑:
引擎Director
stop: function () {
…
YE.Tool.async.clearAllTimer();
}
引擎Tool增加clearAllTimer方法,使用暴力清除法停止所有的定时器:
引擎Tool
namespace("YE.Tool").async = {
/**
* 清空序号在1-500范围中的定时器
*/
clearAllTimer: function () {
var i = 0,
num = 0,
timerNum = 500, //最大定时器个数
firstIndex = 0;
firstIndex = 1;
num = firstIndex + timerNum; //循环次数
for (i = firstIndex; i < num; i++) {
window.clearTimeout(i);
}
for (i = firstIndex; i < num; i++) {
window.clearInterval(i);
}
}
}
兼容IE
clearAllTimer方法在IE浏览器中有问题。虽然定时器序号在所有浏览器中都是每次只加1,但是在IE浏览器中,每次刷新浏览器后定时器起始序号会叠加,导致IE中起始序号可能很大(而在Chrome和Firefox中定时器序号的起始值始终为1),可能超出定时器的清理范围。
因此需要用户使用定时器时要保存任意一个定时器的序号到引擎中,并将clearAllTimer方法改为清空该序号前后一定范围内的定时器。
修改后代码
引擎Tool
/**
* 清空序号在index前后timerNum范围中的定时器
* @param index 定时器序号
*/
clearAllTimer: function (index) {
var i = 0,
num = 0,
timerNum = 250,
firstIndex = 0;
//获得最小的定时器序号
firstIndex = (index - timerNum >= 1) ? (index - timerNum) : 1;
//循环次数
num = firstIndex + timerNum * 2;
for (i = firstIndex; i < num; i++) {
window.clearTimeout(i);
}
for (i = firstIndex; i < num; i++) {
window.clearInterval(i);
}
}
引擎Director增加保存定时器序号的_timeIndex属性,在stop方法中将_timeIndex传入clearAllTimer,并增加设置定时器序号的方法setTimerIndex:
引擎Director
_timerIndex: 0,
…
stop: function () {
…
YE.Tool.async.clearAllTimer(this._timerIndex);
},
setTimerIndex: function (index) {
this._timerIndex = index;
}
对应修改炸弹人源码,调用引擎Director的setTimerIndex方法保存任意一个定时器的序号到引擎中:
炸弹人BombLayer
explode: function (bomb) {
…
index = setTimeout(function () {
…
}, 300);
//保存定时器序号
YE.Director.getInstance().setTimerIndex(index);
},
重构后的领域模型
修改Scene
删除change方法
当前设计
主循环调用了引擎Scene的change方法,它又调用了场景内层的change方法。
引擎Scene
change: function () {
this.__iterator("change");
},
run: function () {
this.__iterator("run");
},
引擎Director
_loopBody: function (time) {
…
this._scene.run();
this._scene.change();
…
},
分析问题
引擎Scene的change方法没有自己的逻辑。因此删除change方法,将其合并到引擎Scene的主循环方法run中。
具体实施
引擎Scene
run: function () {
this.__iterator("run");
this.__iterator("change");
},
引擎Director
_loopBody: function (time) {
…
//不再调用场景的change方法了
this._scene.run();
…
},
不应该关联引擎Sprite
当前设计
现在引擎Scene提供了addSprites方法,负责将精灵加入到层中:
引擎Scene
addSprites: function (name, elements) {
this.getLayer(name).addChilds(elements);
},
炸弹人Scene
_addElements: function () {
var mapLayerElements = this._createMapLayerElement(),
playerLayerElements = this._createPlayerLayerElement(),
enemyLayerElements = this._createEnemyLayerElement();
this.addSprites("mapLayer", mapLayerElements);
this.addSprites("playerLayer", playerLayerElements);
this.addSprites("enemyLayer", enemyLayerElements);
},
分析问题
引擎Director、Scene、Layer、Sprite分别对应不同的层面,上层不应该跨层依赖下层(引擎Director是个特例,因为其它引擎类可能需要调用它提供的操作主循环的API,因此它可被下层跨层依赖):
当前设计造成了引擎Scene关联引擎Sprite,应该去掉两者的关联:
具体实施
引擎Scene删除addSprites方法。
炸弹人Scene改为先获得layer,然后再调用layer的addChilds方法来实现加入精灵到层中:
炸弹人Scene
_addLayer: function () {
this.getLayer("mapLayer").addChilds(this._createMapLayerElement());
this.getLayer("playerLayer").addChilds(this._createPlayerLayerElement());
this.getLayer("enemyLayer").addChilds(this._createEnemyLayerElement()); },
修改Layer
封装画布操作
当前设计
现在画布的操作由用户负责,用户需要实现setCanvas方法,指定层对应的画布,将画布dom保存到引擎Layer的P_canvas属性中,并设置画布的位置。引擎Layer则直接通过用户设置好的P_canvas属性来操作画布:
引擎Layer
Abstract: {
//抽象方法,由用户实现
setCanvas: function () {
},
…
炸弹人BombLayer
var BombLayer = YYC.Class(YE.Layer, {
…
setCanvas: function () {
this.P_canvas = document.getElementById("bombLayerCanvas");
var css = {
"position": "absolute",
"top": bomberConfig.canvas.TOP,
"left": bomberConfig.canvas.LEFT,
"z-index": 1
};
$("#bombLayerCanvas").css(css);
},
引擎Layer还将画布canvas的context属性暴露给了用户:
引擎Layer
__getContext: function () {
//获得画布的context,暴露给用户
this.P_context = this. P_canvas.getContext("2d");
},
炸弹人BombLayer
draw: function () {
//炸弹人可直接访问画布的context
this.iterator("draw", this.P_context);
},
分析问题
画布操作属于底层逻辑,不应该由用户实现,应该由引擎封装,向用户提供操作画布的API。
因此,进行下面的重构:
(1)引擎Layer封装画布,向用户提供操作画布的API。
(2)引擎Layer封装画布的context属性,向用户提供操作context的API。
具体实施
按照下面的步骤重构:
1、封装画布
(1)将P_canvas属性改为私有属性。
(2)引擎Layer增加操作画布的API。
(3)修改用户Layer类的setCanvas方法,用户不再直接操作画布,而是通过引擎Layer提供的API来操作画布。
(4)引擎Layer的构造函数增加设置画布的逻辑,这样用户就可以通过“创建用户Layer实例时传入画布参数”来设置画布。
(5)引擎Layer删除setCanvas方法,不再限定用户在setCanvas方法中设置画布。
2、封装context。
将P_context改为私有属性,并提供getContext方法。
封装canvas
1、将保护属性P_canvas改成私有属性__canvas
引擎Layer
Private:{
__canvas: null,
…
},
2、增加setCanvasByID、setWidth、setHeight、setZIndex、setPosition方法
相关代码
引擎Layer
Public:{
//保存对应id的画布
setCanvasByID: function (canvasID) {
this.__canvas = document.getElementById(canvasID);
},
//设置画布宽度
setWidth: function (width) {
this.__canvas.width = width;
},
//设置画布高度
setHeight: function (height) {
this.__canvas.height = height;
},
//设置画布层级顺序
setZIndex: function (zIndex) {
this.__canvas.style.zIndex = zIndex;
},
//设置画布坐标
setPosition: function (x, y) {
this.__canvas.style.top = x.toString() + "px";
this.__canvas.style.left = y.toString() + "px";
},
引擎Layer的setPosition方法对top和left值加上了“px”字符串,因此需要对应修改炸弹人Config设置的画布坐标:
炸弹人Config
修改前
canvas: {
…
TOP: "0px",
LEFT: "0px"
},
修改后
canvas: {
…
TOP: 0,
LEFT: 0
},
3、修改用户Layer类的setCanvas方法,用户不再直接操作画布,而是通过引擎Layer提供的API来操作画布
相关代码
炸弹人BombLayer
setCanvas: function () {
this.setCanvasByID("bombLayerCanvas");
this.setPosition(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(1);
},
炸弹人EnemyLayer
setCanvas: function () {
this.setCanvasByID("enemyLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(3);
},
炸弹人FireLayer
setCanvas: function () {
this.setCanvasByID("fireLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(2);
},
炸弹人MapLayer
setCanvas: function () {
…
this.setCanvasByID("mapLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(0);
},
炸弹人PlayerLayer
setCanvas: function () {
this.setCanvasByID("playerLayerCanvas");
this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(3);
},
4、引擎Layer的构造函数增加设置画布的逻辑
在构造函数中判断是否传入了画布参数,如果传入则调用操作画布API设置画布:
引擎Layer
Init: function (id, zIndex, position) {
if (arguments.length === 3) {
this.setCanvasByID(id);
this.setZIndex(zIndex);
this.setPosition (position.x, position.y);
}
},
这样用户就有两种方式设置画布了:
(1)创建用户Layer实例时传入画布参数。
(2)在setCanvas方法中调用画布操作API。
5、引擎Layer删除setCanvas方法,不再限定用户必须在setCanvas方法中设置画布
因为用户可以在创建用户Layer实例时设置画布,所以“强迫用户在setCanvas抽象方法中设置画布”的设计就不合适了。
因此,引擎Layer删除setCanvas方法,对应修改引擎Scene,初始化层时不再调用layer的setCanvas方法了:
引擎Scene
initLayer: function () {
//this.__iterator("setCanvas");
…
}
- 用户需要什么时候设置画布?
因为引擎Layer初始化时需要获得画布的context属性,所以用户需要在这之前设置画布:
引擎Layer
init: function () {
this.__getContext();
},
因此,用户除了可在创建用户Layer实例时设置画布,还可以在引擎Layer初始化之前设置画布。
如炸弹人BombLayer可重写引擎Layer的init方法,在执行引擎Layer初始化前设置画布:
___setCanvas: function () {
this.setCanvasByID("bombLayerCanvas");
this.setPosition(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);
this.setZIndex(1);
},
…
init: function (layers) {
this. ___setCanvas(); //在执行引擎初始化逻辑前设置画布
…
this.base(); //执行父类引擎Layer的init方法
},
封装context
将P_context改为私有属性__context,并提供getContext方法
引擎Layer
__context: null,
…
getContext: function () {
return this.__context;
},
对应修改用户Layer类,使用getContext来获得__context
如炸弹人BombLayer
draw: function () {
this.iterator("draw", this.getContext());
},
引擎执行层的初始化
当前设计
引擎Scene提供初始化层的方法initLayer,由炸弹人Scene在场景初始化时调用,执行场景内层的初始化:
引擎Scene
initLayer: function () {
this.__iterator("init", this.__getLayers());
},
炸弹人Scene
init: function () {
…
this._initLayer();
…
},
…
_initLayer: function () {
…
this.initLayer();
},
分析问题
“执行层的初始化”属于底层逻辑,应该由引擎负责,引擎类Scene应该对用户隐藏initLayer方法。
具体实施
将引擎Scene的initLayer设为私有方法,并在引擎Scene的init钩子方法中调用:
引擎Scene
__initLayer: function () {
this.__iterator("init", this.__getLayers());
}
…
init: function () {
this.__initLayer();
},
不过这样修改后,炸弹人Scene在重写init钩子时就需要先执行引擎Scene的初始化逻辑,再执行自己的用户逻辑,违反了引擎设计原则“尽量减少用户负担”,会在后面进行重构。
炸弹人Scene
init: function () {
//执行引擎类初始化逻辑
this.base();
//用户初始化逻辑
…
}
分离引擎的初始化逻辑与用户的初始化逻辑
当前设计
现在引擎Scene、引擎Layer、引擎Sprite提供了init钩子方法,负责引擎类的初始化。该方法为虚方法,用户可重写,加入自己的初始化逻辑。
用户代码示例:
炸弹人Scene
init: function () {
//执行引擎类初始化逻辑
this.base();
//用户初始化逻辑
this._sleep = 1000 / director.getFps();
…
}
炸弹人BombLayer
init: function (layers) {
//执行引擎类初始化逻辑
this.base();
//用户初始化逻辑
this.fireLayer = layers.fireLayer;
…
}
炸弹人MoveSprite
init: function () {
//执行引擎类初始化逻辑
this.base();
//用户初始化逻辑
this.P_context.setPlayerState(this.__getCurrentState());
…
}
分析问题
用户在加入自己的初始化逻辑时,需要先执行引擎类的初始化逻辑,导致用户不仅需要知道引擎类的初始化逻辑,还需要知道用户初始化逻辑和引擎初始化逻辑的调用顺序,违反了引擎设计原则“尽量减少用户负担”。
因此,引擎Scene、Layer、Sprite类的初始化应该由引擎负责并对用户隐藏,将引擎的初始化逻辑与用户的初始化逻辑分离。
具体实施
引擎Sprite、Layer、Sprite增加initData钩子方法,用户可重写它来插入自己的初始化逻辑。而引擎的init方法不再作为钩子方法供用户重写,它负责引擎的初始化和调用initData方法执行用户的初始化。
关于“引擎的init方法中调用initData方法的顺序”的思考
因为用户依赖于引擎,所以照理说应该先进行引擎类的初始化,然后再调用initData方法进行用户的初始化,这样用户初始化时就可获得引擎类初始化后的状态。
然而对于引擎Layer来说,它的初始化逻辑需要操作画布,需要用户先设置好画布。
用户可以在创建用户Layer实例时设置画布,也可以在重写的initData方法中设置画布。对于引擎来说要做最坏的假设,即假设用户在initData方法中设置画布,这样的话引擎Layer就必须在init方法中先调用initData方法,再进行自己的初始化。
同样,引擎Scene也需要用户先加入层到场景中,然后才能执行自己的场景初始化逻辑。
所以Scene和Layer应该先调用initData钩子方法,然后再执行自己的初始化逻辑。
而引擎Sprite的初始化逻辑与用户没有顺序依赖,因而引擎Sprite可以先进行引擎类的初始化,然后再调用initData进行用户的初始化。
相关代码
引擎Scene
init: function () {
//需要用户先加入层到场景中后,才能初始化层
this.initData();
this.__initLayer();
},
//*钩子
Virtual: {
initData: function(){
},
引擎Layer
init: function (layers) {
//需要用户设置画布后,才能初始化画布
//这里将layers传入initData中
this.initData(layers);
this.__getContext();
this.__initCanvas();
},
Virtual: {
initData: function (layers) {
},
引擎Sprite
init: function () {
//引擎可以先执行自己的初始化逻辑,再执行用户的初始化逻辑
this.setAnim(this.defaultAnimId);
this.initData();
},
…
Virtual: {
initData: function () {
},
用户代码示例:
炸弹人Scene
initData: function () {
//执行用户初始化逻辑
…
}
炸弹人BombLayer
initData: function (layers) {
//执行用户初始化逻辑
…
}
炸弹人MoveSprite
initData: function () {
//执行用户初始化逻辑
…
}
clear方法只负责清除画布
当前设计
引擎Layer的clear方法会根据参数个数来判断是清除所有的精灵,还是清除指定的精灵:
引擎Layer
clear: function (sprite) {
if (arguments.length === 0) {
//清除所有层内精灵
this.P_iterator("clear", this.__context);
}
else if (arguments.length === 1) {
//清除指定的精灵
sprite.clear(this.__context);
}
},
用户代码示例:
炸弹人BombLayer
___removeBomb: function (bomb) {
//从画布中清除bomb精灵
this.clear(bomb);
…
},
分析问题
引擎Layer的clear方法的判断逻辑是多余的,因为引擎Sprite的clear方法是供用户调用的,如果用户想要清除某个精灵,可以直接调用该精灵的clear方法。
又因为引擎Layer最清楚层内的所有精灵,所以它的clear方法保留“清除层内所有精灵”的逻辑。
具体实施
引擎Layer的clear方法只负责清除层内所有精灵。
引擎Layer
clear: function () {
this. P_iterator ("clear", this.__context);
}
炸弹人BombLayer
___removeBomb: function (bomb) {
//直接调用bomb精灵的clear方法
bomb.clear(this.getContext());
…
},
继续修改引擎Layer和Sprite的clear方法
当前设计
引擎Layer的clear方法通过调用层内所有精灵的clear方法,达到清空画布的目的:
引擎Layer
clear: function () {
this.iterator("clear", this._context);
},
引擎Sprite的clear方法直接清空画布:
引擎Sprite
clear: function (context) {
//直接清空画布区域
context.clearRect(0, 0, YE.Config.canvas.WIDTH, YE.Config.canvas.HEIGHT);
}
炸弹人MapLayer实现了“清空画布”的逻辑:
炸弹人MapLayer
clear: function () {
this.P_context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
…
},
分析问题
当前设计有下面几个问题:
(1)引擎Sprite的clear方法应该只负责从画布中清除自己,“清空画布”的逻辑应该由引擎Layer的clear方法负责。
(2)引擎Layer的clear方法应该直接清空画布。
(3)“清空画布”属于底层逻辑,不应该由用户类实现。
具体实施
引擎Layer的clear方法负责清空画布:
引擎Layer
clear: function () {
this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
},
引擎Sprite的clear方法负责从画布中清除自己:
引擎Sprite
clear: function (context) {
context.clearRect(this.x, this.y, this.bitmap.width, this.bitmap.height);
}
因为用户类可能需要知道画布大小,因此引擎Layer增加getCanvasWidth、getCanvasHeight方法:
引擎Layer
getCanvasWidth: function () {
return this._canvas.width;
},
getCanvasHeight: function () {
return this._canvas.height;
},
…
//对应修改clear方法
clear: function () {
this._context.clearRect(0, 0, this. getCanvasWidth(), this. getCanvasHeight());
},
修改炸弹人MapLayer,直接调用引擎Layer的clear方法清除画布:
炸弹人MapLayer
clear: function () {
this.base();
…
},
封装run方法
当前设计
引擎类的run方法封装了引擎类在主循环中的逻辑,该方法由上层引擎类在主循环中调用。
(关于引擎run方法的作用,可参考《炸弹人游戏开发系列(4)》的“增加run方法”一节])
引擎Director
_loopBody: function (time) {
…
//调用场景的run方法
this._scene.run();
…
},
…
_run: function (time) {
var self = this;
this._loopBody(time);
…
window.requestNextAnimationFrame(function (time) {
self._run(time);
});
},
引擎Scene
run: function () {
//调用场景内层的run方法
this.__iterator("run");
…
},
现在引擎Layer向用户提供了P_render方法,而它的run方法为抽象方法,由用户实现:
引擎Layer
P_render: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
this.setStateNormal();
}
}
…
Abstract: {
…
run: function () {
}
我们来看下炸弹人Layer类实现的run方法:
炸弹人BombLayer
run: function () {
this.P_render();
}
炸弹人FireLayer
run: function () {
this.P_render();
}
炸弹人MapLayer
run: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
}
}
炸弹人CharacterLayer
run: function () {
this.___setDir();
this.___move();
this.___render();
}
…
___render: function () {
if (this.P_isChange()) {
this.clear();
this.___update(this.___deltaTime);
this.draw();
this.setStateNormal();
}
}
炸弹人EnemyLayer
run: function () {
if (this.collideWithPlayer()) {
window.gameState = window.bomberConfig.game.state.OVER;
return;
}
this.__getPath();
//调用Character->run
this.base();
}
炸弹人PlayerLayer
run: function () {
if (keyState[YE.Event.KeyCodeMap.SPACE]) {
this.createAndAddBomb();
keyState[YE.Event.KeyCodeMap.SPACE] = false;
}
//调用Character->run
this.base();
}
分析问题
引擎Layer应该实现主循环逻辑,并且由于这属于底层逻辑,应该对用户隐藏。
因此,引擎Layer应该实现并对用户隐藏run方法。
下面从三个步骤进行分析:
1、识别出炸弹人Layer类的run方法的通用模式。
2、将其提到引擎Layer的run方法中。
3、在引擎Layer的run方法中调用增加的钩子方法,执行炸弹人Layer类插入的逻辑。
识别出炸弹人Layer的run方法的通用模式
分析炸弹人Layer类的相关代码,可以看到炸弹人BombLayer、FireLayer的run方法直接调用了引擎Layer的P_render方法;
炸弹人MapLayer的run方法与P_render方法相比,虽然少调用了引擎Layer的setStateNormal方法,但因为引擎Scene的run方法会调用MapLayer的change方法,而它又会调用引擎Layer的setStateNormal方法,所以MapLayer的run方法也等效于调用了P_render方法。
引擎Scene
run: function () {
this.__iterator("run");
this.__iterator("change");
},
炸弹人MapLayer
change: function () {
this.setStateNormal();
},
再来看下CharaterLayer的run方法,它调用了___render方法,该方法与P_render方法相比,多调用了“___update”方法。
而EnemyLayer、PlayerLayer继承CharacterLayer,它们的run方法都调用CharacterLayer的run方法,也就是说都调用了___render方法。
由此可见,炸弹人Layer类的run方法的通用模式是都调用了引擎Layer的P_render方法,只是有些炸弹人Layer类还有自己要插入的逻辑。
提取通用模式到引擎Layer的run方法中
再来看下引擎Layer的P_render方法是否需要重构:
P_render: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
this.setStateNormal();
}
}
(1)判断是否包含用户逻辑
它调用的都是引擎类Layer的方法,没有包含用户逻辑。
(2)判断是否具有通用性
所有用户Layer类在每次主循环中都要先判断画布的状态,如果状态为CHANGE,表明画布更改过,则先清除画布,然后绘制画布,最后设置画布状态为NORMAL,因此该方法具有通用性。
综上所述,可以将P_render方法直接合并到引擎Layer的run方法中。
增加onAfterDraw钩子方法
炸弹人CharacterPlayer的run方法调用了自己的“___update”方法,该方法需要在引擎Layer的run方法中执行。
为了能让CharacterPlayer及其子类直接使用引擎Layer的run方法,引擎Layer需要增加onAfterDraw钩子方法,并在run方法中调用该钩子。
具体实施
将引擎Layer的P_render方法合并到run方法中,增加onAfterDraw钩子方法:
引擎Layer
run: function () {
if (this.P_isChange()) {
this.clear();
this.draw();
//触发onAfterDraw钩子
this.onAfterDraw();
this.setStateNormal();
}
},
Virtual: {
…
onAfterDraw: function () {
}
},
对应修改炸弹人BombLayer、FireLayer、MapLayer,删除run方法
炸弹人CharacterLayer、EnemyLayer、PlayerLayer由于还有其它的用户逻辑需要在引擎Layer的run方法之前执行,所以暂时保留run方法(后面会重构):
炸弹人CharacterLayer
run: function () {
this.___setDir();
this.___move();
//调用引擎Layer的run方法
this.base();
},
onAfterDraw: function () {
this.___update(this.___deltaTime);
}
炸弹人EnemyLayer
run: function () {
if (this.collideWithPlayer()) {
window.gameState = window.bomberConfig.game.state.OVER;
return;
}
this.__getPath();
//调用Character的run方法
this.base();
}
炸弹人PlayerLayer
run: function () {
if (keyState[YE.Event.KeyCodeMap.SPACE]) {
this.createAndAddBomb();
keyState[YE.Event.KeyCodeMap.SPACE] = false;
}
//调用Character的run方法
this.base();
}
增加onStartLoop、onEndLoop钩子
当前设计
经过上一步的修改后,炸弹人CharacterLayer、EnemyLayer、PlayerLayer仍然要重写引擎Layer的run方法,没有达到“引擎Layer对用户隐藏run方法”的设计目的。
分析问题
引擎和用户的run方法的逻辑现在混杂到了一起。
在前面的“应该将引擎的初始化逻辑与用户的初始化逻辑分离”重构中,我们已经看到这种设计很不好,应该将引擎逻辑和用户的逻辑分离。
具体实施
参考引擎Scene,引擎Layer也提出onStartLoop、onEndLoop钩子方法,这两个钩子分别在引擎Layer的run方法执行前、后触发。
引擎Layer
Virtual:{
…
onStartLoop: function () {
},
onEndLoop: function () {
}
在引擎Scene的run方法中触发引擎Layer的钩子:
引擎Scene
run: function () {
this.__iterator("onStartLoop");
this.__iterator("run");
this.__iterator("change");
this.__iterator("onEndLoop");
},
对应修改炸弹人CharacterLayer、EnemyLayer、PlayerLayer,将自己的逻辑放到钩子中,不再重写引擎Layer的run方法:
炸弹人CharacterLayer
onStartLoop: function () {
this.___setDir();
this.___move();
},
炸弹人EnemyLayer
onStartLoop: function () {
if (this.collideWithPlayer()) {
window.gameState = window.bomberConfig.game.state.OVER;
return;
}
this.__getPath();
//调用CharacterLayer的onStartLoop
this.base();
}
炸弹人PlayerLayer
onStartLoop: function () {
if (keyState[YE.Event.KeyCodeMap.SPACE]) {
this.createAndAddBomb();
keyState[YE.Event.KeyCodeMap.SPACE] = false;
}
//调用CharacterLayer的onStartLoop
this.base();
}
将P_isNorml、P_isChange改成私有方法
分析问题
经过“封装run方法”的修改后,用户Layer类不会再用到引擎Layer的P_isChange、P_isNorml方法了,因此将其设为私有方法。
具体实施
引擎Layer
__isChange: function () {
return this.__state === State.CHANGE;
},
__isNormal: function () {
return this.__state === State.NORMAL;
}
提取炸弹人draw方法的通用模式
继续从炸弹人Layer类中提取通用模式。
当前设计
现在引擎Layer的draw方法为抽象方法,由用户实现:
引擎Layer
Abstract: {
…
draw: function () {
},
炸弹人BombLayer、CharacterLayer、FireLayer的draw方法具有共同的模式,都是绘制所有精灵:
draw: function () {
this.iterator("draw", this.getContext());
},
分析问题
可将通用模式提到引擎Layer的draw方法中。
又由于不是所有炸弹人Layer类的绘制逻辑都是“绘制所有精灵”,所以将draw方法设为虚方法,用户可重写该方法实现不同的逻辑。
具体实施
实现引擎Layer的draw方法,对应删除炸弹人BombLayer、CharacterLayer、FireLayer的draw方法。
引擎Layer
Virtual:{
…
draw: function () {
this.iterator("draw", this.getContext());
},
增加钩子方法isChange,change方法不再为抽象方法
当前设计
现在引擎Layer的change方法为抽象方法,由用户实现,通过调用引擎Layer提供的setStateChange和setStateNormal方法来设置画布状态。
画布状态的作用
引擎Layer在主循环中会判断画布状态,如果为CHANGE,则重绘画布,否则不重绘。
引擎Layer
Abstract: {
change: function () {
}
}
用户代码示例:
如炸弹人BombLayer
change: function () {
//如果炸弹人放置了炸弹,则设置画布状态为CHANGE,从而在下次主循环时重绘画布,显示炸弹
if (this.___hasBomb()) {
this.setStateChange();
}
}
分析问题
其实用户只需要决定下次主循环时是否重绘画布,而不需要知道画布状态。根据引擎设计原则“尽量减少用户负担”,引擎Layer应该对用户隐藏“画布状态”。
具体实施
引擎Layer增加虚方法isChange,用户可以重写该方法,如果需要重绘则返回true,否则返回false。
引擎Layer的change方法会调用isChange方法,根据返回值判断是调用setStateChange方法,还是调用setStateNormal方法。
因为用户可能需要在isChange方法之外设置画布状态,所以引擎Layer保留setStateNormal、setStateChange方法供用户调用。
引擎Layer
change: function () {
if(this.isChange() === true){
this.setStateChange();
}
else{
this.setStateNormal();
}
},
Virtual: {
…
isChange: function(){
return true;
},
炸弹人只需要重写isChange方法
如炸弹人BombLayer
isChange: function () {
if (this.___hasBomb()) {
return true;
}
}
思考
- 引擎Layer现在没有抽象方法了,但仍然应该为抽象类
如果引擎Layer为类,则用户就不能有继承引擎Layer的抽象子类。
例如:用户可能有多个Layer类,对应多个画布,可能需要从中提出抽象基类,抽象基类也需要继承引擎Layer。如果引擎Layer为类,则提出抽象基类不能继承它。
修改Sprite
引擎执行精灵的初始化
当前设计
目前由用户负责执行精灵的初始化:
炸弹人Scene
_createPlayerLayerElement: function () {
var element = [],
player = spriteFactory.createPlayer();
//执行玩家精灵的初始化
player.init();
…
},
_createEnemyLayerElement: function () {
var element = [],
enemy = spriteFactory.createEnemy(),
enemy2 = spriteFactory.createEnemy2();
//执行敌人精灵的初始化
enemy.init();
enemy2.init();
…
},
分析问题
“执行精灵的初始化”属于底层逻辑,应该由引擎负责执行。
由哪个引擎类负责
因为引擎Layer负责管理层内精灵,所以应该由它负责。
在哪里执行精灵的初始化
有两个选择:
1、在初始化层时执行层中的所有精灵的初始化。
2、在加入精灵到层中时执行精灵的初始化。
因为在初始化层时,不一定加入了精灵到层中,所以应该选择在加入精灵到层中时执行精灵的初始化。
具体实施
引擎Layer重写引擎Collection的addChilds方法,加入精灵到层中时执行精灵的初始化:
引擎Layer
namespace("YE").Layer = YYC.AClass(YE.Collection, {
…
addChilds: function (elements) {
this.base(elements);
elements.forEach(function(e){
//执行精灵的初始化
e.init();
});
},
炸弹人Scene不再负责执行精灵的初始化了。
修改后,游戏运行测试会报错。因为在加入地图精灵到层中时,会执行地图精灵的初始化,设置地图精灵的默认动画。然而地图精灵没有动画,其defaultAnimId为undefined,所以执行setAnim方法时会报错。
引擎Sprite
init: function () {
//显示默认动画
this.setAnim(this.defaultAnimId);
…
},
为了让游戏运行通过,暂时在引擎Sprite的init方法中加入defaultAnimId的判断:
引擎Sprite
init: function () {
//如果有默认动画Id,则显示默认动画
if (this.defaultAnimId) {
this.setAnim(this.defaultAnimId);
}
…
},
其实可以看到,引擎Sprite的defaultAnimId属性是默认动画的id,属于用户逻辑,后面会进行重构,去除该用户逻辑。
提取炸弹人中每次主循环持续时间的计算逻辑到引擎Sprite的update方法中
当前设计
游戏需要计算每次主循环持续时间deltaTime,用于在动画管理中计算当前帧播放的时间,确定是否对当前帧进行切换等操作。
目前由炸弹人实现deltaTime的计算。炸弹人Scene计算deltaTime,然后传入炸弹人Layer,然后再传入炸弹人精灵的update方法(引擎Sprite实现),最后传入引擎Animation的update方法。
炸弹人Scene
initData: function(){
…
this._sleep = 1000 / director.getFps(); //计算本次主循环持续时间,保存到_sleep属性中
…
},
…
_addLayer: function () {
…
//deltaTime传入layer
this.addLayer("enemyLayer", layerFactory.createEnemy(this._sleep));
this.addLayer("playerLayer", layerFactory.createPlayer(this._sleep));
…
},
炸弹人CharacterLayer
Init: function (deltaTime) {
this.___deltaTime = deltaTime;
},
…
___update: function (deltaTime) {
//deltaTime传入炸弹人精灵的update方法
this.iterator("update", deltaTime);
},
…
onAfterDraw: function () {
this.___update(this.___deltaTime);
}
引擎Sprite
update: function (deltaTime) {
this._updateFrame(deltaTime);
},
…
_updateFrame: function (deltaTime) {
if (this.currentAnim) {
//deltaTime传入引擎Animation的update方法
this.currentAnim.update(deltaTime);
}
}
引擎Animation
update: function (deltaTime) {
…
//根据deltaTime,计算当前帧的已播放时间
this._currentFramePlayed += deltaTime;
…
},
分析问题
1、引擎负责计算帧率fps,所以它知道如何计算deltaTime。
2、deltaTime与主循环密切相关,而主循环是由引擎来负责的。
因此,应该由引擎计算deltaTime。
由哪个引擎类负责?
(1)只有引擎Animation需要用到deltaTime,而它又是由引擎Sprite的update方法传入的,引擎Sprite是直接关联方。
(2)引擎Scene和引擎Layer都只是传递deltaTime值,没有自己的逻辑。
(3)计算deltaTime需要获得引擎Director的帧率,引擎Sprite能够访问引擎Director,从而能够计算deltaTime。
因此应该由引擎Sprite负责。
具体实施
引擎Sprite的update方法负责计算deltaTime:
引擎Sprite
update: function () {
this._updateFrame(1000 / YE.Director.getInstance().getFps());
},
对应修改炸弹人Scene和炸弹人CharacterLayer,不再负责计算和传递deltaTime了。