提炼游戏引擎系列:第一次迭代
前言
上文我们完成了引擎的初步设计,本文进行引擎提炼的第一次迭代,从炸弹人游戏中提炼引擎类,搭建引擎的整体框架。
名词解释
- 用户
“用户”是个相对的概念,指使用的一方法,默认指游戏开发者。
当相对于某个引擎类时,“用户”就是指引擎类的使用方;当相对于整个引擎时,“用户”就是指引擎的使用方。
- 用户逻辑
引擎使用方的逻辑,默认指与具体游戏相关的业务逻辑。
本文目的
1、参考引擎初步领域模型,从炸弹人参考模型中提炼出对应的通用类,搭建引擎框架。
2、将炸弹人游戏改造为基于引擎实现。
本文主要内容
- 修改namespace方法,提出Tool
- 提出ImgLoader
- 提出Main
- 提出Director
- 提出Scene和Hash
- 提出Layer和Collection
- 提出Sprite、Config和collision
- 提出Factory
- 提出Animation
- 提出AI
- 提出EventManager和Event
- 提出DataOperator
- 提出Data
- 本文最终领域模型
- 高层划分
- 总结
- 本文源码下载
- 参考资料
炸弹人的参考模型
引擎初步领域模型
开发策略
按照引擎初步领域模型从左往右的顺序,确定要提炼的引擎类,从炸弹人参考模型对应的炸弹人类中提炼出通用的引擎类。
本文迭代步骤
迭代步骤说明
- 确定要提炼的引擎类
按照“引擎初步领域模型”从左往右的顺序依次确定要提炼的引擎类。
每次迭代提炼一个引擎类,如果有强关联的引擎类,也一并提出。
- 提炼引擎类
从炸弹人参考模型中确定对应的炸弹人类,从中提炼出可复用的、通用的引擎类。
- 重构提炼的引擎类
如果提炼的引擎类有坏味道,或者包含了用户逻辑,就需要进行重构。
- 炸弹人改造为基于引擎实现
对应修改炸弹人类,使用提炼的引擎类。
这样能够站在用户角度发现引擎的改进点,得到及时反馈,从而马上重构。
- 通过游戏的运行测试
通过运行测试,修复由于修改炸弹人代码带来的bug。
- 重构引擎
如果有必要的话,对引擎进行相应的重构。
下面的几个原因会导致重构引擎:
1、提炼出新的引擎类后,与之关联的引擎类需要对应修改。
2、得到了新的反馈,需要改进引擎。
3、违反了引擎设计原则。
4、处理前面遗留的问题
- 通过游戏的运行测试
通过运行测试,修复炸弹人和引擎的bug。
- 编写并通过引擎的单元测试
因为引擎是从炸弹人代码中提炼出来的,所以引擎的单元测试代码可以参考或者直接复用炸弹人的单元测试代码。
炸弹人代码只进行运行测试,不进行单元测试。因为本系列的重点是提炼引擎,不是二次开发炸弹人,这样做可以节省精力,专注于引擎的提炼。
- 完成本次迭代
进入新一轮迭代,确定下一个要提炼的引擎类。
思考
1、为什么先提炼引擎,后进行引擎的单元测试?
在炸弹人开发中,我采用TDD的方式,即先写测试,再进行开发,然而这里不应该采用这种方式,这是因为:
(1)我已经有了一定的游戏开发经验了,可以先进行一大步的开发,然后再写对应的测试来覆盖开发。
(2)在提炼引擎类前我只知道引擎类的大概的职责,不能确定引擎类的详细设计。在提炼的过程中,引擎类会不停的变化,如果先写了引擎类的单元测试代码,则需要不停地修改,浪费很多时间。
2、为什么要先进行游戏的运行测试,再进行引擎的单元测试?
因为:
(1)炸弹人游戏并不复杂,如果运行测试失败,也能比较容易地定位错误
(2)先进行游戏的运行测试,不用修改单元测试代码就能直接修复发现的引擎bug,这样之后进行的引擎单元测试就能比较顺利的通过,节省时间。
不讨论测试
因为测试并不是本系列的主题,所以本系列不会讨论专门测试的过程,“本文源码下载”中也没有单元测试代码!
您可以在最新的引擎版本中找到引擎完整的单元测试代码: YEngine2D
引擎使用方式的初步研究
在开篇介绍中,给出了引擎的三种使用方式:直接使用引擎类提供的API、继承重写、实例重写,现在来研究下后两种使用方式。
继承重写应用了模板模式,由引擎类搭建框架,将变化点以钩子方法、虚方法和抽象成员的形式提供给用户子类实现。
实例重写也是应用了模板模式的思想,引擎类也提供钩子方法供用户类重写,不过用户类并不是继承复用引擎类,而是委托复用引擎类。
继承重写与实例重写的区别,实际上就是继承与委托的区别。
继承重写和实例重写的比较
共同点
(1)都是单向关联,即用户类依赖引擎类,引擎类不依赖用户类。
(2)用户都可以插入自己的逻辑到引擎中。
不同点
(1)继承重写通过继承的方式实现引擎类的使用,实例重写通过委托的方式实现引擎类的使用
(2)继承重写不仅提供了钩子方法,还提供了虚方法、抽象成员供用户重写,实例重写则只提供了钩子方法。
实例重写的优势主要在于用户类与引擎类的关联性较弱,用户类只与引擎类实例的钩子方法耦合,不会与整个引擎类耦合。
继承重写的优势主要在于父类和子类代码共享,提高代码的重用性。
什么时候用继承重写
当用户类与引擎类同属于一个概念,引擎类是精心设计用于被继承的类时,应该用继承重写。
什么时候用实例重写
当用户类需要插入自己的逻辑到引擎类中而又不想与引擎类紧密耦合时,应该用实例重写。
本文选用的方式
因为引擎Main和Director是从炸弹人Main、Game中提出来的,不是设计为可被继承的类,所以引擎Main、Director采用实例重写的方式,
(它们的使用方式会在第二次迭代中修改)
引擎Layer和Sprite是从炸弹人Layer、Sprite中提出来的,都是抽象基类,本身就是设计为被继承的类,所以引擎Layer和Sprite采用继承重写的方式。
其它引擎类不能被重写,而是提供API,供引擎类或用户类调用。
(第二次迭代会将引擎Scene改为继承重写的方式)
修改namespace方法,提出Tool
修改namespace方法
引擎使用命名空间来组织,引擎的顶级命名空间为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);
}());
}());
提出Tool类
不应该直接修改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类。
领域模型
提出ImgLoader
按照从左到右的提炼顺序,首先要提炼引擎初步领域模型中的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(...);
},
领域模型
提出Main
接着就是提炼依赖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
炸弹人应该只负责定义要加载的图片和在加载图片过程中要插入的用户逻辑,不需要知道如何加载图片。这个工作应该交给引擎Main,由它封装引擎ImgLoader,向用户提供操作图片的方法和在加载图片的过程中插入用户逻辑的钩子方法。
1、重构引擎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) {
}
}
});
2、重构引擎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) {
}
},
…
});
}());
3、修改炸弹人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();
};
}());
将依赖的YTool方法移到Tool
修改后的引擎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。
提炼引擎类
领域类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
使用方式为实例重写
引擎Director提供了init、onStartLoop、onEndLoop钩子方法供用户重写。
引擎会在加载完图片后调用钩子方法init,用户可以通过重写该钩子,插入初始化游戏的用户逻辑。
onStartLoop、onEndLoop钩子分别在每次主循环开始和结束时调用,插入用户逻辑:
引擎Director
_loopBody: function (time) {
this._tick(time);
this.onStartLoop();
…
this.onEndLoop();
},
单例
因为全局只有一个Director,因此为单例。
主循环
使用requestAnimationFrame实现主循环
炸弹人Game中使用setInterval方法,而引擎Director使用requestAnimationFrame方法实现主循环。这是因为可以通过setTimeout和setInterval方法在脚本中实现动画,但是这样效果可能不够流畅,且会占用额外的资源。
参考《HTML5 Canvas核心技术:图形、动画与游戏开发》中的论述:
它们有如下的特征:
1、即使向其传递毫秒为单位的参数,它们也不能达到ms的准确性。这是因为javascript是单线程的,可能会发生阻塞。
2、没有对调用动画的循环机制进行优化。
3、没有考虑到绘制动画的最佳时机,只是一味地以某个大致的事件间隔来调用循环。
其实,使用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
目前LayerManager为炸弹人类,用户通过调用引擎Director的setLayerManager方法将其注入到引擎Director中。
领域模型
引擎Director在主循环中调用layerManager实例的run和change方法,执行炸弹人LayerManager的主循环逻辑。
- 为什么要由用户注入LayerManager实例,而不是直接在引擎Director中创建LayerManager实例?
(1)根据引擎设计原则“引擎不应该依赖用户,用户应该依赖引擎”,LayerManager为用户类,引擎不应该依赖用户。
(2)这样会降低引擎Director的通用性
引擎Director应该操作抽象角色,而不应该直接操作具体的“层管理”类,这样会导致具体的“层管理”类变化时,引擎Director也会受到影响。
因此,此处采用“由用户注入”的设计更加合理。
- 为什么由引擎Director调用炸弹人LayerManager的change方法?
LayerManager的change方法负责调用每个层的change方法,设置画布的状态(主循环中会判断画布状态,决定是否更新画布):
炸弹人LayerManager
change: function () {
this.__iterator("change");
}
change方法的调用有两个选择:
(1)由用户调用
用户可在重写引擎Director提供的钩子方法中(如onEndLoop),调用炸弹人LayerManager的change方法
(2)由引擎调用
引擎Director主循环在调用layerManager的run方法后调用layerManager的change方法。
因为:
(1)设置画布状态的逻辑属于通用逻辑
(2)引擎对什么时候设置画布状态有最多的知识
所以应该由引擎Director调用。
计算帧率fps
引擎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改为只负责初始化场景和管理游戏状态,其它逻辑委托引擎实现。
炸弹人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方法中。
- 将gameOver、gameWin设为私有方法,judgeGameState设为公有方法
因为只有Game调用这两个方法,因此将其设为私有方法。
而judgeGameState方法被director的钩子方法调用,因此将其设为公有方法。
炸弹人Game实例重写引擎Director
- 重写引擎Director的init钩子
在init钩子中,炸弹人插入了Game的初始化场景的逻辑,注入了Game创建的layerManager实例。
- 删除start和run方法
这部分职责已经移到引擎Director中了,所以Game删除start和run方法,由引擎负责控制主循环。
- 重写了Director的onStartLoop钩子,实现了炸弹人游戏的结束机制
修改了Game的gameOver、gameWin方法,改为调用director.stop方法来结束主循环。
将Game的run方法的“关于全局游戏状态判断”的逻辑移到Director的onStartLoop钩子中,引擎会在每次主循环开始时判断一次全局游戏状态,决定是否调用Game的gameOver或gameWin方法结束游戏。
修改炸弹人Main
为了能通过游戏的运行测试,先修改炸弹人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();
};
重构引擎
- 将炸弹人Main的“执行游戏初始化并启动主循环”的逻辑移到引擎Main中
因为:
(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和Hash
现在应该提出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");
}
}
});
}());
炸弹人使用提炼的引擎类
重构炸弹人Game,改为依赖引擎Scene
因为炸弹人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和Collection
现在应该提出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
引擎Collection重命名appendChild、appendChilds为addChild、addChilds:
引擎Collection
addChild: function (child) {
…
},
addChilds: function (childs) {
…
},
重构Layer
现在引擎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
由于引擎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、Config和collision
现在应该提出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);
}
}
}
});
}());
重构提炼的引擎类
提出Config
现在引擎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);
}
有下面几个问题:
1、引擎Sprite依赖了炸弹人Config,违背了引擎设计原则“不应该依赖用户”。
2、“方格大小”和“画布大小”与精灵无关,因此不应该像引擎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类,而引擎类应该只访问引擎Config类。
提出collision
引擎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
由于引擎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提取到引擎中了,因此炸弹人改为依赖引擎的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
现在提炼Factory类。
思考
有两个问题需要思考:
1、哪些引擎类需要工厂。
2、用哪种方式实现工厂。
对于第1个问题,目前我认为抽象类不需要工厂(第二次迭代中抽象类Scene、Layer、Sprite也会加上工厂方法create,使得用户可直接使用这些引擎类),其它非单例的类都统一用工厂创建实例。
对于第2个问题,有以下两个选择:
1、与炸弹人代码一样,提出工厂类LayerFactory、SpriteFactory,分别负责创建引擎Layer、Sprite的实例
2、直接在类中提出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
提炼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);
}
}
});
}());
炸弹人使用提炼的引擎类
炸弹人改为创建引擎Animation实例
修改炸弹人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类。
提炼引擎类
领域类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);
}
};
}());
重构提炼的引擎类
用户能够设置寻路方向数为4或者为8
现在引擎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和Event
现在提炼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
KeyCodeMap是键盘按键的枚举值,因为所有浏览器中的键盘按键值都一样,因此具有通用性,可以将其提取到引擎中。
不提出KeyState
炸弹人KeyState是存储当前按键状态的容器类,与用户逻辑相关,因此不提取到引擎中。
从KeyEventManager中提出EventManager
炸弹人KeyEventManager负责键盘事件的监听和移除,可以从中提出一个通用的、负责所有事件的监听和移除的引擎类EventManager。
另外,将事件类型(如"keydown"、"keyup")提取为枚举值EventType,从而对用户隔离具体的事件类型的变化。
提出事件枚举类Event
引擎增加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方法移到Tool
引擎类依赖了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
现在引擎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的初始化事件逻辑
修改前
炸弹人实现了监听事件的逻辑:
炸弹人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
因为炸弹人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;
}());
炸弹人改为使用引擎Event的KeyCodeMap
如对应修改炸弹人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类。
提炼引擎类
领域类DataOperator负责对数据进行读、写操作,对应炸弹人数据操作层的类,具体为MapDataOperate、GetPath、TerrainDataOperate、GetSpriteData、GetFrames。
这些数据操作类都与具体的业务逻辑相关,没有可提炼的。
提出Data
现在提炼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;
}());
领域模型
本文最终领域模型
此处炸弹人省略了与引擎类无关的类。
高层划分
包图
对应领域模型
- 核心包
放置引擎的核心类。- Main
- Director
- Scene
- Layer
- Sprite
- 算法包
放置通用的算法类。- AStar
- collision
- 动画包
放置游戏动画的相关类。- Animation
- 加载包
负责游戏资源的加载和管理。- ImgLoader
- 数据结构包
放置引擎的基础结构类。- Bitmap
- 集合包
放置引擎集合类。- Collection
- Hash
- 通用工具包
放置引擎通用的方法类。- Tool
- 配置包
放置引擎配置类。- Config
- 事件管理包
负责事件的管理。- Event
- EventManager
引擎集合类也属于数据结构,为什么不放在数据结构包中,而是放在单独的集合包中?
因为引擎集合类的使用方式为继承,而数据结构包中的引擎Bitmap的使用方式为委托,两者使用方式不同,因此不能放到一个包中。
总结
本文将炸弹人通用的类提炼到了引擎中,搭建了引擎的整体框架。但是现在引擎还很粗糙,包含了很多炸弹人逻辑,不具备通用性。因此,在下文中,我会进行第二次迭代,对引擎进行进一步的抽象和提炼。