提煉游戲引擎系列:第二次迭代(下)
前言
本文為引擎提煉第二次迭代的下篇,將會完成引擎中動畫、集合和事件管理相關類的重構。
本文目的
1、提高引擎的通用性,完善引擎框架。
2、對應修改炸彈人游戲。
本文主要內容
修改動畫
2D動畫介紹
實現原理
一組圖片(或一個圖的不同位置)在同一位置以一定的時間間隔顯示,就形成了動畫。
精靈圖片
我們可以將精靈的動畫序列圖合為一張大的圖片,稱之為精靈圖片。
如炸彈人的精靈圖片如下圖所示:

幀
動畫的一“幀”指動畫序列圖片中的一張圖片,如下圖紅框就是往左移動動畫的一幀:

幀數據
幀數據指幀圖片左上頂點在精靈圖片中的橫軸、縱軸坐標x、y以及幀圖片的寬width和高height。
提出Frame、Animate、Animation、AnimationFrame、Geometry
當前設計
當前動畫的設計可參考炸彈人游戲開發系列(4):炸彈人顯示與移動->實現動畫
領域模型

精靈的動畫數據定義在炸彈人精靈數據SpriteData中,動畫的幀數據定義在炸彈人FrameData中。
下面介紹當前的創建動畫和播放動畫的機制。
創建動畫
創建精靈類實例時會注入炸彈人精靈數據SpriteData,從而獲得動畫數據,序列圖如下所示:

在initData方法中,炸彈人Scene會創建精靈類實例,將封裝了精靈圖片的引擎Bitmap實例和精靈類數據SpriteData注入到實例中。
相關代碼
引擎Director
runWithScene: function (scene) {
…
this.setScene(scene);
this._scene.init();
…
},
引擎Scene
init: function () {
//調用initData鉤子方法
this.initData();
…
},
炸彈人Scene
initData: function () {
…
this._addElements();
…
},
…
_addElements: function () {
…
this.getLayer("playerLayer").addChilds(this._createPlayerLayerElement());
…
},
…
_createPlayerLayerElement: function () {
…
player = spriteFactory.createPlayer();
…
},
炸彈人SpriteFactory
createPlayer: function () {
return new PlayerSprite(
getSpriteData("player"),
bitmapFactory.createBitmap({ img: window.imgLoader.get("player"), width: bomberConfig.player.IMGWIDTH, height: bomberConfig.player.IMGHEIGHT})
);
},
炸彈人BitmapFactory
createBitmap: function (data) {
…
return new YE.Bitmap(bitmapData);
}
引擎Sprite
Init: function (data, bitmap) {
//獲得包含精靈圖片對象的bitmap實例
this.bitmap = bitmap;
…
//獲得初始動畫id
this.defaultAnimId = data.defaultAnimId;
//獲得動畫數據
this.anims = data.anims;
…
}
SpriteData定義了精靈的動畫數據,其中一個動畫對應一個引擎Animation實例,并注入了使用getFrames方法獲得的幀數據FrameData。
炸彈GetSpriteData和SpriteData都在同一個文件中。
炸彈人GetSpriteData和SpriteData
var getSpriteData = (function () {
var data = function () {
return {
…
//初始播放動畫id
defaultAnimId: "stand_right",
//動畫數據
anims: {
"stand_right": YE.Animation.create(getFrames("player", "stand_right")),
"stand_left": YE.Animation.create(getFrames("player", "stand_left")),
"stand_down": YE.Animation.create(getFrames("player", "stand_down")),
…
}
return function (spriteName) {
return data()[spriteName];
};
}());
炸彈GetFrameData和FrameData都在同一個文件中
炸彈人GetFrameData和FrameData
var getFrames = (function () {
…
var frames = function () {
return {
…
//只有一幀的動畫幀數據沒有duration屬性
stand_right: [
{
x: offset.x, y: offset.y + 2 * height, width: sw, height: sh
}
],
…
walk_up: [
{ x: offset.x, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 },
{ x: offset.x + width, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 },
{ x: offset.x + 2 * width, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 },
{ x: offset.x + 3 * width, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 }
],
…
return function (who, animName) {
return frames()[who][animName];
};
}());
播放動畫
當玩家按下W鍵時,炸彈人會播放向上走動畫,序列圖如下所示:

序列圖分為兩個階段:
1、玩家按下移動方向鍵后,游戲會設置要播放的動畫
當玩家按下W鍵時,炸彈人PlayerSprite會執行炸彈人WalkUpState的walkUp方法,調用炸彈人PlayerSprite的setAnim方法,設置當前要播放的動畫walk_up:
炸彈人PlayerSprite
__judgeAndSetDir: function () {
…
//判斷玩家是否按下了W鍵
else if (window.keyState[YE.Event.KeyCodeMap.W] === true) {
this.P_context.walkUp();
}
…
},
炸彈人WalkUpState
P_setDir: function () {
var sprite = this.P_context.sprite;
//設置精靈當前動畫為walk_up
sprite.setAnim("walk_up");
…
},
…
walkUp: function () {
//調用父類WalkState的方法
this.P_checkMapAndSetDir();
}
炸彈人WalkState
P_checkMapAndSetDir: function () {
…
//調用子類WalkUpState的方法
this.P_setDir();
…
},
引擎Sprite(炸彈人PlayerSprite的setAnim方法由引擎Sprite實現)
//設置當前動畫
setAnim: function (animId) {
this.currentAnim = this.anims[animId];
},
2、主循環更新動畫幀,播放動畫的當前幀
主循環會調用引擎Layer的run方法,執行炸彈人CharacterLayer在重寫的onAfterDraw鉤子方法中插入的__update方法和炸彈人PlayerSprite的draw方法。
__update方法負責更新動畫幀,最終會委托引擎Animation的update方法實現;
draw方法負責繪制精靈,炸彈人PlayerSprite會在draw方法中訪問引擎Sprite的currentAnim屬性,獲得當前動畫實例(引擎Animation實例),調用它的getCurrentFrame方法,獲得當前幀數據,然后結合bimap實例的img屬性(精靈圖片對象),繪制動畫當前幀圖片。
引擎Layer
//游戲主循環調用的方法
run: function () {
if (this._isChange()) {
…
//繪制層中所有精靈
this.draw();
//觸發onAterDraw鉤子方法
this.onAfterDraw();
…
}
},
…
draw: function () {
this.iterator("draw", this.getContext());
},
炸彈人CharacterLayer
onAfterDraw: function () {
this.___update();
},
…
___update: function () {
//調用炸彈人PlayerSprite的update方法
this.P_iterator("update");
},
引擎Sprite(炸彈人PlayerSprite的update方法由引擎Sprite實現)
update: function () {
this._updateFrame(1000 / YE.Director.getInstance().getFps());
},
…
_updateFrame: function (deltaTime) {
if (this.currentAnim) {
//委托引擎Animation的update方法更新動畫幀
this.currentAnim.update(deltaTime);
}
}
炸彈人MoveSprite(炸彈人PlayerSprite的draw方法由炸彈人MoveSprite實現)
draw: function (context) {
var frame = null;
if (this.currentAnim) {
//取出當前幀
frame = this.currentAnim.getCurrentFrame();
//從bitmap.img中獲得精靈圖片對象,繪制動畫當前幀
context.drawImage(this.bitmap.img, frame.x, frame.y, frame.width, frame.height, this.x, this.y, this.bitmap.width, this.bitmap.height);
}
},
分析問題
當前設計有四個地方可以修改:
1、可提取幀數據FrameData的通用模式
炸彈人FrameData中每幀的數據都具有相同的結構,都包括幀在精靈圖片中的位置屬性x、y和幀的大小屬性width、height以及幀播放時間duration屬性:
炸彈人FrameData
//只有一幀的動畫幀數據沒有duration屬性,其duration屬性值可以看為0
stand_right: [
{
x: offset.x, y: offset.y + 2 * height, width: sw, height: sh
}
],
…
walk_up: [
{ x: offset.x, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 },
{ x: offset.x + width, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 },
{ x: offset.x + 2 * width, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 },
{ x: offset.x + 3 * width, y: offset.y + 3 * height, width: sw, height: sh, duration: 100 }
],
可以將通用模式抽象出來,提出引擎Frame類,封裝幀數據,提供幀操作的方法。
引擎Frame為實體類,一個Frame對應動畫的一幀。
2、引擎Animation的職責不單一
現在引擎Animation既要負責幀數據的保存,又要負責幀的管理:
引擎Animation
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、Animate 、AnimationFrame三個類:
Animation為動畫容器類,負責保存一個動畫的所有幀數據。
Animate為幀管理類,負責管理一個動畫的所有幀。
AnimationFrame為精靈動畫容器類,負責保存精靈所有的動畫。
它們的對應關系為:
Animation為實體類,一個Animation對應一個動畫;
Animate為功能類,一個Animate對應一個Animation;
AnimationFrame為實體類,一個AnimationFrame對應精靈的所有動畫。
另外一個Sprite對應一個AnimationFrame,引擎Sprite委托引擎AnimationFrame保存精靈動畫。
3、引擎應該封裝動畫,提供操作API
動畫操作屬于精靈的基本操作,應該由引擎來負責動畫的管理,向用戶提供操作動畫的API。
4、修改用戶創建動畫的方式
提取了新的引擎動畫類后,需要修改用戶代碼中創建動畫的方式。
目前有兩種方式:
(1)初始化方式跟之前一樣,只是對應修改炸彈人SpriteData和FrameData,將創建引擎Animation改為創建引擎Frame和Animate實例。
(2)直接在炸彈人精靈類中創建動畫,刪除炸彈人FrameData,刪除炸彈人SpriteData的動畫數據。
考慮到這只是修改用戶代碼,跟引擎沒有關系,因此為了節省時間來,我選擇第2種方式,具體使用引擎時用戶可以自行決定初始化后動畫的方式。
初步設計的引擎領域模型
現在給出通過分析后設計的引擎領域模型:

具體實施
依次實施“分析問題”中提出的解決方案。
1、提出Frame
- 封裝幀數據
首先來看下炸彈人FrameData中一幀數據的組成:
{ x: xxx, y: xxx, width: xxx, height: xxx, duration: xxx }
x、y為幀圖片左上頂點在精靈圖片中的位置,width、height為幀圖片的大小,duration為每幀播放的時間。
因為一個動畫中的所有幀的播放時間應該都是一樣的,只是不同動畫的幀播放時間不同,因此duration不應該放在引擎Frame中,而應該放到保存動畫幀數據的Animation中。
因此,引擎Frame應該保存幀的x、y、width、height數據。
- 一個Frame對應一個Img對象
目前動畫的每幀都是從精靈的精靈圖片中“切”出來的,而精靈圖片保存在引擎Bitmap實例中,在創建精靈類實例時注入。
一個精靈只有一個bitmap實例,即一個精靈只對應1張精靈圖片。
這樣的設計導致精靈的所有動畫的所有幀都只能來自同1張精靈圖片,無法實現不同的動畫的幀來自不同的精靈圖片的需求。
因此,可以將精靈圖片對象的引用保存到Frame中,從而一個Frame對應1張精靈圖片。這樣既可以實現每個動畫對應不同的精靈圖片,還可以實現每個動畫的每幀對應不同的精靈圖片,從而實現最大的靈活性。
現在可以這樣創建引擎Frame實例:
var frame1 = YE.Frame.create(img,x, y, sw, sh);
- 提出Geometry
此處傳入create方法的參數過多,可以將后面與幀相關的4個數據提取為對象:
var frame1 = YE.Frame.create(img, YE.rect(x, y, sw, sh));
其中rect方法在新增的幾何類Geometry中定義,負責將矩形區域的數據封裝為對象。
相關代碼
引擎Geometry
YE.rect = function (x, y, w, h) {
return { origin: {x: x, y: y}, size: {width: w, height: h} };
};
引擎Frame
(function () {
namespace("YE").Frame = YYC.Class({
Init: function (img, rect) {
//保存精靈圖片對象的引用
this._img = img;
this._rect = rect;
},
Private: {
_img: null,
_rect: {}
},
Public: {
getImg: function () {
return this._img;
},
getX: function () {
return this._rect.origin.x;
},
getY: function () {
return this._rect.origin.y;
},
getWidth: function () {
return this._rect.size.width;
},
getHeight: function () {
return this._rect.size.height;
}
},
Static: {
create: function (bitmap, rect) {
return new this(bitmap, rect);
}
}
});
}());
2、分解Animation
- 改造Animation為動畫容器類,負責保存一個動畫的所有幀數據。
改造后的引擎Animation
namespace("YE").Animation = YYC.Class({
Init: function (frames, duration) {
//保存幀數據
this._frames = frames;
//保存每幀的播放時間
this._duration = duration;
},
Private: {
_frames: null,
_duration: null
},
Public: {
getFrames: function () {
return this._frames;
},
getDuration: function () {
return this._duration;
}
} ,
Static: {
create: function(frames, duration){
return new this(frames, duration);
}
}
});
- 提出Animate,負責管理動畫的所有幀
因為引擎Animate需要保存一個動畫的所有幀,所以繼承Collection,獲得集合特性:
引擎Animate
(function () {
namespace("YE").Animate = YYC.Class(YE.Collection, {
Init: function (animation) {
this.__anim = animation;
},
Private: {
__anim: null,
__frameCount: 0,
__duration: 0,
__currentFrame: null,
__currentFrameIndex: 0,
__currentFramePlayed: 0
},
Public: {
/**
* 更新當前幀
* @param deltaTime 主循環的持續時間
*/
update: function (deltaTime) {
//判斷當前幀是否播放完成,
if (this.__currentFramePlayed >= this.__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;
},
init: function () {
//調用父類Collection的API,保存動畫的所有幀
this.addChild(this.__anim.getFrames());
this.__duration = this.__anim.getDurationPerFrame();
//需要獲得幀的數量
this.__frameCount = this.getCount();
this.setCurrentFrame(0);
},
setCurrentFrame: function (index) {
this.__currentFrameIndex = index;
this.__currentFrame = this.getChildAt(index);
this.__currentFramePlayed = 0;
}
},
Static: {
create: function (animationFrame) {
return new this(animationFrame);
}
}
});
}());
因為Animate需要獲得幀的數量,因此引擎Collection需要增加getCount方法,返回容器元素的個數:
引擎Collection
getCount: function () {
return this._childs.length;
},
- 提出AnimationFrame,負責保存精靈所有的動畫
因為引擎AnimationFrame需要保存多個動畫,所以應該繼承引擎Hash類,以動畫名為key,動畫實例為value的形式保存動畫:
引擎AnimationFrame
(function () {
namespace("YE").AnimationFrame = YYC.Class({
Init: function () {
this._spriteFrames = YE.Hash.create();
},
Private: {
_spriteFrames: null
},
Public: {
//提供操作動畫API
getAnims: function () {
return this._spriteFrames.getChilds();
},
getAnim: function (animName) {
return this._spriteFrames.getValue(animName);
},
addAnim: function (animName, anim) {
//加入動畫時初始化動畫
anim.init();
this._spriteFrames.add(animName, anim);
}
},
Static: {
create: function () {
return new this();
}
}
});
}());
- 引擎Sprite與引擎AnimationFrame應該為組合關系
引擎Sprite
Init: function (data, bitmap) {
…
this._animationFrame = YE.AnimationFrame.create();
},
3、引擎封裝動畫,提供操作API
- 應該由引擎Sprite提供addAnim等操作動畫API,還是暴露引擎AnimationFrame實例給用戶,用戶直接訪問它的操作動畫API?
如果由引擎Sprite提供操作動畫API,那它的示例代碼為:
//加入動畫
addAnim: function (animName, anim) {
this._animationFrame.add(animName, anim)
}
這種方式有下面的好處:
(1)對用戶隱藏了AnimationFrame,減小了用戶負擔。
(2)增加了1層封裝,可以更靈活地插入Sprite的邏輯。
但是也有缺點,如果AnimationFrame增加操作動畫的API,則Sprite也要對應增加這些API,這樣會增加Sprite的復雜度。
考慮到:
(1)Sprite提供的操作動畫的API只是中間者方法,沒有自己的邏輯。
(2)現在操作動畫的API太少了,以后會不斷增加。
因此目前來看,直接將AnimationFrame暴露給用戶更加合適。
相關代碼
引擎Sprite
//暴露引擎AnimationFrame實例給用戶
getAnimationFrame: function () {
return this._animationFrame;
},
用戶可這樣調用動畫操作API:
var sprite = new PlayerSprite();
sprite.getAnimationFrame().addAnim(xxx,xxx);
- 引擎負責動畫的管理
在“播放動畫”中可以看到,用戶參與了更新動畫幀的機制,實現了繪制動畫當前幀的邏輯:
(1)炸彈人CharacterLayer調用了炸彈人PlayerSprite的update方法(由引擎Sprite實現)。
炸彈人CharacterLayer
___update: function () {
this.P_iterator("update");
},
…
onAfterDraw: function () {
this.___update();
},
(2)炸彈人MoveSprite實現了“繪制動畫當前幀”。
炸彈人MoveSprite
draw: function (context) {
var frame = null;
if (this.currentAnim) {
//取出當前幀
frame = this.currentAnim.getCurrentFrame();
//從bitmap.img中獲得精靈圖片對象,繪制動畫當前幀
context.drawImage(this.bitmap.img, frame.x, frame.y, frame.width, frame.height, this.x, this.y, this.bitmap.width, this.bitmap.height);
}
},
這些屬于底層機制和邏輯,應該由引擎負責。
因此,將其封裝到引擎中。
具體來說引擎需要進行下面兩個修改:
(1)封裝“更新動畫幀”的機制
引擎Layer增加update方法,在主循環中調用該方法:
引擎Layer的update方法負責調用層中所有精靈的update方法,而精靈update方法又調用引擎Animate的update方法,從而實現主循環中更新動畫幀的機制。
序列圖

引擎Layer
//游戲主循環調用的方法
run: function () {
this.update();
…
},
…
update: function () {
this.P_iterator("update");
},
引擎Sprite
update: function () {
this._updateFrame(1000 / YE.Director.getInstance().getFps());
},
…
_updateFrame: function (deltaTime) {
if (this.currentAnim) {
//調用引擎Animate的update方法
this.currentAnim.update(deltaTime);
}
}
引擎Animate
//更新當前幀
update: function (deltaTime) {
…
},
(2)實現“繪制動畫當前幀”邏輯
將炸彈人MoveSprite實現的“繪制動畫當前幀”的邏輯提到引擎Sprite中。
由于不是所有的炸彈人精靈類的繪制邏輯都為該邏輯(如炸彈人MapElementSprite沒有動畫,不需要繪制動畫幀,而是直接繪制圖片對象),所以將提取的方法命名為drawCurrentFrame,與draw方法共存,供用戶自行選擇:
引擎Sprite
//繪制動畫當前幀
drawCurrentFrame: function (context) {
//重構,提出getCurrentFrame方法
var frame = this.getCurrentFrame();
context.drawImage(
frame.getImg(),
frame.getX(), frame.getY(), frame.getWidth(), frame.getHeight(),
this.x, this.y, this.bitmap.width, this.bitmap.height
);
},
getCurrentFrame: function () {
if (this.currentAnim) {
return this.currentAnim.getCurrentFrame();
}
return null;
},
…
//保留繪制精靈圖片對象方法
draw: function (context) {
context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height);
},
炸彈人MoveSprite的draw方法改為直接調用引擎Sprite的drawCurrentFrame方法:
炸彈人MoveSprite
draw: function (context) {
this.drawCurrentFrame(context);
},
- 清理引擎包含的用戶邏輯
引擎Sprite包含的“設置精靈的初始動畫” 邏輯屬于用戶邏輯,應該由用戶類負責。
因此將引擎Sprite的defaultAnimId屬性移到炸彈人MoveSprite中。
炸彈人MoveSprite
Init: function (data, bitmap) {
…
this.defaultAnimId = data.defaultAnimId;
},
引擎Sprite刪除defaultAnimId屬性
4、在炸彈人精靈類中創建動畫,刪除炸彈人FrameData,刪除炸彈人SpriteData的動畫數據。
修改炸彈人創建動畫的方式,在炸彈人PlayerSprite和EnemySprite中創建動畫,加入到精靈類的引擎AnimationFrame實例中,并設置精靈的默認動畫。
炸彈人PlayerSprite
initData: function () {
…
var width = bomberConfig.player.WIDTH,
height = bomberConfig.player.HEIGHT,
offset = {
x: bomberConfig.player.offset.X,
y: bomberConfig.player.offset.Y
},
sw = bomberConfig.player.SW,
sh = bomberConfig.player.SH;
//創建幀,傳入精靈圖片對象和幀圖片區域大小數據
var frame1 = YE.Frame.create(this.bitmap.img, YE.rect(offset.x, offset.y, sw, sh));
var frame2 = YE.Frame.create(this.getImg(), YE.rect(offset.x + width, offset.y, sw, sh));
…
//創建動畫幀數組,加入動畫的幀
var animFrames1 = [];
animFrames1.push(frame1);
animFrames1.push(frame2);
…
//創建動畫,設置動畫的持續時間
var animation1 = YE.Animation.create(animFrames1, 100);
…
//將動畫加入到AnimationFrame實例中
var animationFrame = this.getAnimationFrame();
animationFrame.addAnim("walk_down", YE.Animate.create(animation1));
…
//設置默認動畫
this.setAnim("walk_down");
}
EnemySprite
與PlayerSprite類似,此處省略
總結
重構后的領域模型

因為在炸彈人精靈類中創建動畫,所以刪除了炸彈人FrameData和炸彈人SpriteData的動畫數據,炸彈人SpriteData不再關聯引擎動畫了。
增加了Frame類,引擎Animation被分解為AnimationFrame、Animate、Animation,它們相互之間有聚合關系。
重構后的播放動畫序列圖

引擎封裝并對用戶隱藏了“更新動畫幀”機制。
從引擎Animation中分離出來的引擎Animate負責動畫幀的管理,引擎Sprite改為與引擎Animate交互。
炸彈人PlayerSprite的draw方法(由炸彈人MoveSprite實現)直接委托引擎Sprite的drawCurrentFrame方法。
待重構點
至少還有下面幾點需要進一步修改:
1、引擎應該提供將動畫數據和動畫邏輯分離的方式,提供創建動畫的高層API。
引擎應該定義動畫數據格式,封裝創建動畫的邏輯,用戶可以按照引擎定義的數據格式,將動畫數據分離到單獨的文件中,調用高層API讀取動畫數據并創建動畫。
2、引擎應該增加更多的動畫操作API,如增加開始動畫、結束動畫等。
回顧與梳理
現在需要停下來,回顧一下之前的重構,查找并解決遺漏的問題。
使用YE.rect方法重構炸彈人矩形區域數據為對象
當前設計
在“修改動畫”的重構中,提出了Geometry類,該類有YE.rect方法,負責將矩形區域的數據封裝為對象。
引擎Geometry
YE.rect = function (x, y, w, h) {
return { origin: {x: x, y: y}, size: {width: w, height: h} };
};
炸彈人類中除了幀數據,還有其它的矩形區域數據。如炸彈人游戲中碰撞檢測的數據:
炸彈人BombSprite
collideFireWithCharacter: function (sprite) {
…
fire = {
x: range[i].x,
y: range[i].y,
width: this.getWidth(),
height: this.getHeight()
};
obj2 = {
x: sprite.getPositionX(),
y: sprite.getPositionY(),
width: sprite.getWidth(),
height: sprite.getHeight()
};
if (YE.collision.col_Between_Rects(fire, obj2)) {
return true;
}
…
},
炸彈人EnemySprite
collideWithPlayer: function (sprite2) {
var obj1 = {
x: this.getPositionX(),
y: this.getPositionY(),
width: this.getWidth(),
height: this.getHeight()
},
obj2 = {
x: sprite2.getPositionX(),
y: sprite2.getPositionY(),
width: sprite2.getWidth(),
height: sprite2.getHeight()
};
//判斷是否碰撞
if (YE.collision.col_Between_Rects(obj1, obj2)) {
throw new Error();
}
},
引擎collision
//獲得精靈的碰撞區域,
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);
…
}
分析問題
可以用YE.rect方法將矩形區域數據定義為對象。
具體實施
重構炸彈人BombSprite、EnemySprite的矩形區域數據為對象,對應修改引擎collision:
炸彈人BombSprite
collideFireWithCharacter: function (sprite) {
…
fire = YE.rect(range[i].x, range[i].y, this.getWidth(), this.getHeight());
obj2 = YE.rect(sprite.getPositionX(),sprite.getPositionY(),sprite.getWidth(),sprite.getHeight());
if (YE.collision.col_Between_Rects(fire, obj2)) {
return true;
}
…
},
炸彈人EnemySprite
collideWithPlayer: function (sprite2) {
var obj1 = YE.rect(this.getPositionX(), this.getPositionY(), this.getWidth(), this.getHeight()),
obj2 = YE.rect(sprite2.getPositionX(), sprite2.getPositionY(), sprite2.getWidth(), sprite2.getHeight());
if (YE.collision.col_Between_Rects(obj1, obj2)) {
throw new Error();
}
},
引擎collision
//根據rect的數據結構對應修改
getCollideRect:function(obj) {
return {
x1: obj.origin.x,
y1: obj.origin.y,
x2: obj.origin.x + obj.size.width,
y2: obj.origin.y + obj.size.height
}
},
封裝引擎Sprite的位置屬性x、y,提供操作API
當前設計
用戶創建精靈實例時傳入精靈初始坐標:
引擎Sprite
Init: function (data, bitmap) {
…
if (data) {
//初始坐標
this.x = data.x;
this.y = data.y;
}
…
},
用戶可直接操作精靈的坐標屬性x、y:
引擎Sprite
Public: {
…
//精靈的坐標
x: 0,
y: 0,
分析問題
- 應該封裝引擎Sprite的位置屬性x、y,向用戶提供操作坐標的API。
這是因為:
1、便于以后擴展,在API中加入引擎Sprite的邏輯。
如可以在API中增加權限控制等邏輯。
2、引擎Sprite的坐標屬性名為“x”、“y”,容易與其它object對象的屬性名同名,影響可讀性,相互干擾。
如引擎Sprite的getCellPosition方法返回包含x、y屬性的方格坐標對象:
//獲得坐標對應的方格坐標(向下取值)
getCellPosition: function (x, y) {
return {
x: Math.floor(x / YE.Config.WIDTH),
y: Math.floor(y / YE.Config.HEIGHT)
}
},
用戶容易將該方法返回的坐標對象與精靈坐標混淆,從而誤操作。
- 用戶應該使用操作坐標的方法來設置精靈的初始坐標,不應該在創建精靈實例時傳入初始坐標。
因為這樣可以:
1、增加靈活性
坐標與精靈改為關聯關系,可以不強制用戶在創建精靈實例時設置初始坐標,從而用戶可自行決定何時設置。
2、減少復雜度
簡化引擎Sprite的構造函數。
具體實施
1、引擎Sprite增加setPosition、setPositionX、setPositionY、getPositionX、getPositionY方法,將x、y屬性設為私有屬性。
2、刪除引擎Sprite構造函數的data形參。
3、對應修改炸彈人,改為使用引擎Sprite提供的操作坐標API。
引擎Sprite
namespace("YE").Sprite = YYC.AClass({
Init: function (bitmap) {
…
},
Private: {
…
_x: 0,
_y: 0
…
},
Public:{
…
setPosition: function (x, y) {
this._x = x;
this._y = y;
},
setPositionX: function (x) {
this._x = x;
},
setPositionY: function (y) {
this._y = y;
},
getPositionX: function () {
return this._x;
},
getPositionY: function () {
return this._y;
},
此處僅給出部分炸彈人類的對應修改:
引擎MoveSprite
Init: function (data, bitmap) {
//只傳入bitmap到引擎Sprite的構造函數中
this.base(bitmap);
…
this.setPosition(data.x, data.y);
},
…
__isMoving: function () {
return this.getPositionX() % bomberConfig.WIDTH !== 0 || this.getPositionY() % bomberConfig.HEIGHT !== 0
}
封裝引擎Sprite的bitmap屬性
當前設計
引擎Sprite的bitmap屬性為引擎Bitmap實例,在創建精靈實例時傳入:
引擎Sprite
Init: function (bitmap) {
this.bitmap = bitmap;
…
},
…
Public: {
//bitmap為公有屬性
bitmap: null,
現在炸彈人可以直接操作它來獲得精靈圖片的相關數據。
如炸彈人PlayerSprite可訪問bitmap屬性的img屬性來獲得精靈圖片對象:
炸彈人PlayerSprite
initData: function () {
…
var frame1 = YE.Frame.create(this.bitmap.img, YE.rect(offset.x, offset.y, sw, sh));
分析問題
應該封裝引擎Sprite的bitmap屬性,向用戶提供操作bitmap的API,這樣用戶就不需要知道bitmap的存在,減少用戶負擔。
因為精靈圖片與精靈屬于組合關系,應該在創建精靈時就設置精靈圖片,所以保留引擎Sprite構造函數中傳入bitmap實例的設計。
引擎Sprite應該增加setBitmap和setImg方法,滿足用戶更改精靈圖片的需求。
具體實施
引擎Sprite增加getImg、getWidth、getHeight、setBitmap、setImg方法,將bitmap屬性改為私有屬性:
引擎Sprite
Private: {
_bitmap: null,
…
Public:{
…
//獲得精靈圖片對象
getImg: function () {
return this._bitmap.img;
},
//獲得精靈寬度
getWidth: function () {
return this._bitmap.width;
},
//獲得精靈高度
getHeight: function () {
return this._bitmap.height;
},
setBitmap: function(bitmap){
this._bitmap = bitmap;
},
setImg: function(img){
this._bitmap.img = img;
},
對應修改用戶類,使用引擎Sprite提供的API操作bitmap:
如炸彈人PlayerSprite
initData: function () {
…
var frame1 = YE.Frame.create(this.getImg(), YE.rect(offset.x, offset.y, sw, sh));
刪除引擎Sprite的getCellPosition方法
當前設計
引擎Sprite的getCellPosition方法負責將精靈坐標轉換為炸彈人游戲中使用的方格坐標:
引擎Sprite
//獲得坐標對應的方格坐標
getCellPosition: function (x, y) {
return {
x: Math.floor(x / YE.Config.WIDTH),
y: Math.floor(y / YE.Config.HEIGHT)
}
},
分析問題
該方法的邏輯與具體的游戲相關,屬于用戶邏輯,應該由用戶實現。
具體實施
將引擎Sprite的getCellPosition方法移到對應的炸彈人類中,將其修改為從炸彈人BomberConfig中獲得方格大小:
如炸彈人MoveSprite
//獲得坐標對應的方格坐標(向下取值)
getCellPosition: function (x, y) {
return {
x: Math.floor(x / bomberConfig.WIDTH),
y: Math.floor(y / bomberConfig.HEIGHT)
}
}
刪除引擎Config
當前設計
在第一次迭代中,為了解除引擎和炸彈人Config的依賴,提出了引擎Config。
引擎Config
namespace("YE").Config = {
//方格寬度
WIDTH: 30,
//方格高度
HEIGHT: 30,
//畫布
canvas: {
//畫布寬度
WIDTH: 600,
//畫布高度
HEIGHT: 600,
//定位坐標
TOP: "0px",
LEFT: "0px"
}
};
分析問題
因為:
(1)刪除了引擎Sprite的getCellPosition方法后,引擎不再依賴引擎Config類了。
(2)引擎Config應該放置與引擎相關的配置屬性,而現在放置的配置屬性“方格大小”和“畫布大小”均屬于用戶邏輯。
所以應該刪除引擎Config。
具體實施
刪除引擎Config
引擎類不應該依賴引擎collision
當前設計
引擎Sprite定義了獲得碰撞區域數據的getCollideRect方法,依賴了引擎collision。

引擎Sprite
getCollideRect: function () {
var obj = {
x: this.x,
y: this.y,
width: this.bitmap.width,
height: this.bitmap.height
};
//調用了引擎collision的getCollideRect方法
return YE.collision.getCollideRect(obj);
},
分析問題
引擎collision為碰撞算法類,與游戲相關,應該只供用戶使用,引擎Sprite不應該依賴引擎collision。
具體實施
需要進行下面的重構:
1、刪除引擎Sprite的getCollideRect方法
現在炸彈人和引擎均沒有用到引擎Sprite的getCollideRect方法,故將其刪除。
2、引擎collision的getCollideRect方法改為私有方法
完成第一個重構后,炸彈人和引擎都不會用到引擎collision的getCollideRect方法,因此將其設為私有方法。
引擎collision
//改為私有方法
function _getCollideRect(obj) {
…
}
return {
//改為調用私有方法_getCollideRect
col_Between_Rects: function (rect1, rect2) {
var rect1 = _getCollideRect(rect1),
rect2 = _getCollideRect(rect2);
領域模型
重構后引擎collision只供用戶使用了:

修改Hash和Collection
現在回到主線,修改Hash和Collection。
封裝遍歷集合的邏輯
當前設計
引擎Hash沒有實現“遍歷集合”的邏輯。
引擎Collection實現了迭代器模式,提供了遍歷集合的迭代器方法hasNext、next、resetCursor:
引擎Collection
//迭代器模式接口
var IIterator = YYC.Interface("hasNext", "next", "resetCursor");
namespace("YE").Collection = YYC.AClass({Interface: IIterator}, {
…
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;
},
引擎Scene實現了遍歷集合的邏輯:
_iterator: function (handler, args) {
var args = Array.prototypethis.base().slice.call(arguments, 1),
i = null,
layers = this.getChilds();
for (i in layers) {
if (layers.hasOwnProperty(i)) {
layers[i][handler].apply(layers[i], args);
}
}
},
引擎Layer封裝了引擎Collection的迭代器方法,提供了外觀方法P_iterator:
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);
}
this.resetCursor();
}
由于炸彈人BombLayer要在遍歷集合時加入判斷邏輯,不能直接使用引擎Layer的P_iterator方法,所以它調用引擎Collection的迭代器方法,實現了“遍歷集合”的邏輯:
炸彈人BombLayer
___explodeInEffectiveRange: function (bomb) {
var eachBomb = null;
this.resetCursor();
while (this.hasNext()) {
eachBomb = this.next();
//加入了判斷邏輯
if (eachBomb.isInEffectiveRange.call(eachBomb, bomb)) {
this.explode(eachBomb);
}
}
this.resetCursor();
}
其它炸彈人Layer類使用引擎Layer的P_iterator方法遍歷集合:
如炸彈人CharacterLayer
___setDir: function () {
//執行集合元素的setDir方法
this.P_iterator("setDir");
},
分析問題
當前設計有下面幾個問題:
1、引擎Hash沒有實現“遍歷集合”的邏輯,導致需要繼承Hash的引擎Scene自己實現。
2、引擎Layer封裝的外觀方法P_iterator不靈活,導致炸彈人BombLayer不能直接使用,只能調用引擎Collection的迭代器方法來實現。
因為:
1、“遍歷集合”的邏輯與引擎集合類相關,應該統一由它們實現。
2、引擎Collection的迭代器方法屬于實現“遍歷集合”的底層方法,應該作為Collection的內部方法。外界只需要調用“遍歷集合”的外觀方法即可,不需要了解該方法是如何實現的。
3、用戶不應該自己實現“遍歷集合”的邏輯。
所以:
1、應該由引擎集合類統一實現“遍歷集合”邏輯。
2、引擎集合類提供改進后的“遍歷集合”的外觀方法,隱藏引擎Collection的迭代器方法,其它引擎類和用戶類直接調用外觀方法即可。
具體實施
引擎Collection刪除迭代器接口,將迭代器方法設為私有方法,實現遍歷集合的外觀方法iterator:
引擎Collection
//刪除了迭代器接口
namespace("YE").Collection = YYC.AClass({
…
//實現外觀方法iterator
iterator: function (handler, args) {
var args = Array.prototype.slice.call(arguments, 1),
nextElement = null;
this._resetCursor();
//改進設計,handler既可以為方法名,又可以為回調函數,這樣炸彈人BombLayer在遍歷集合時就可以通過傳入自定義的回調函數來加入判斷邏輯了
if (YE.Tool.judge.isFunction(arguments[0])) {
while (this._hasNext()) {
nextElement = this._next();
handler.apply(nextElement, [nextElement].concat(args));
}
this._resetCursor();
}
else {
while (this._hasNext()) {
nextElement = this._next();
nextElement[handler].apply(nextElement, args);
}
this._resetCursor();
}
},
引擎Tool實現isFunction方法
isFunction: function (func) {
return Object.prototype.toString.call(func) === "[object Function]";
},
引擎Hash實現遍歷集合的外觀方法iterator。
引擎Hash
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);
}
}
}
引擎Scene、Layer和炸彈人Layer類使用引擎集合類的iterator方法:
引擎Scene
_initLayer: function () {
this.iterator("init", this.__getLayers());
}
引擎Layer
update: function () {
this.iterator("update");
},
炸彈人BombLayer直接使用引擎Collection的iterator方法,傳入自定義的回調函數:
___explodeInEffectiveRange: function (bomb) {
var self = this;
this.iterator(function(eachBomb){
if (eachBomb.isInEffectiveRange.call(eachBomb, bomb)) {
self.explode(eachBomb);
}
});
} ,
其它炸彈人Layer類示例:
炸彈人CharacterLayer
___setDir: function () {
this.iterator("setDir");
},
組合復用引擎集合類
當前設計
目前引擎Collection、Hash為抽象類,引擎類需要繼承Collection或Hash類來獲得集合類特性。
引擎Collection
namespace("YE").Collection = YYC.AClass({
引擎Hash
namespace("YE").Hash = YYC.AClass({
引擎Layer通過繼承引擎Collection來獲得集合特性:
namespace("YE").Layer = YYC.AClass(YE.Collection, {
引擎Scene通過繼承引擎Hash來獲得集合特性:
namespace("YE").Scene = YYC.Class(YE.Hash, {
分析問題
回顧在“炸彈人游戲開發系列(3):顯示地圖”中,進行了繼承復用集合類Collection的設計。當時這樣設計的原因如下:
1、通過繼承來復用比起委托來說更方便和優雅,可以減少代碼量。
2、從概念上來說,Collection和Layer都是屬于集合類,應該屬于一個類族。Collection是從Layer中提煉出來的,它是集合類的共性,因此Collection作為父類,Layer作為子類。
然而現在這個設計已經不合適了,因為:
1、這只適用于整體具有集合特性的情況,不適用于局部具有集合特性的情況。
如引擎Animate只是功能類,不是集合類,只是因為要操作動畫的幀數據,需要一個內部容器來保存幀數據。
當前設計是讓引擎Animate繼承集合類Collection,造成整體與Collection耦合,只要兩者有一個修改了,另外一個就可能受到影響。
更好的設計是Animate增加私有屬性frames,它為Collection的實例。這樣Animate就只有該屬性與Collection耦合,當Animate的其它屬性和方法修改時,不會影響到Collection;Collection修改時,也只會影響到Animate的frames屬性。從而把影響減到了最小。
2、如果幾個有集合特性的引擎類同屬于一個類族,需要繼承某個父類時,則會有沖突。
因為它們已經繼承了集合類了,不能再繼承另一個父類。
因此,將引擎Collection、Hash改成類,改為組合復用的方式來使用。
具體實施
引擎Hash修改為類,增加create方法
namespace("YE").Hash = YYC.Class({
…
Static: {
create: function () {
return new this();
}
}
引擎Scene增加內部容器layers,組合復用Hash:
Init: function () {
this._layers = YE.Hash.create();
},
//改為通過_layers來進行集合操作
//如將“this.getChilds()”改為“this._layers.getChilds()”
Private: {
_layers:null,
_getLayers: function () {
return this._layers.getChilds();
},
_initLayer: function () {
this._layers.iterator("init", this._getLayers());
}
},
Public: {
addLayer: function (name, layer) {
this._layers.add(name, layer);
return this;
},
getLayer: function (name) {
return this._layers.getValue(name);
},
run: function () {
this._layers.iterator("onStartLoop");
this._layers.iterator("run");
this._layers.iterator("change");
this._layers.iterator("onEndLoop");
},
引擎AnimationFrame增加內部容器spriteFrames,組合復用Hash:
Init: function () {
this._spriteFrames = YE.Hash.create();
},
Private: {
_spriteFrames: null
},
Public: {
getAnims: function () {
return this._spriteFrames.getChilds();
},
getAnim: function (animName) {
return this._spriteFrames.getValue(animName);
},
addAnim: function (animName, anim) {
anim.init();
this._spriteFrames.add(animName, anim);
}
引擎Collection修改為Class,增加create方法
namespace("YE").Collection = YYC.Class({
…
Static: {
create: function () {
return new this();
}
}
引擎Layer增加內部容器_childs,組合復用Collection。
因為引擎Layer需要向用戶提供集合操作API,因此增加getChildAt、removeAll、iterator等中間者方法,封裝內部容器:
namespace("YE").Layer = YYC.AClass({
Init: function (id, zIndex, position) {
…
this._childs = YE.Collection.create();
},
…
removeAll: function () {
this._childs.removeAll();
},
addChild: function (element) {
this._childs.addChild(element);
element.init();
},
getChildAt: function (index) {
return this._childs.getChildAt(index);
},
iterator: function (handler, args) {
this._childs.iterator.apply(this._childs, arguments);
},
getChilds: function () {
return this._childs.getChilds();
},
引擎Animate增加內部容器frames,組合復用Collection:
Init: function (animation) {
…
this._frames = YE.Collection.create();
},
…
Public:{
…
init: function () {
this._frames.addChilds(this._anim.getFrames());
…
this._frameCount = this._frames.getCount();
…
},
setCurrentFrame: function (index) {
…
this._currentFrame = this._frames.getChildAt(index);
…
}
修改EventManager
用戶可指定綁定事件的對象target和處理方法的上下文handlerContext。
當前設計
用戶只能綁定全局事件,處理方法的this只能指向window:
引擎EventManager
addListener: function (event, handler) {
…
//現在寫死了,用戶只能綁定window的事件,handler的this只能指向window
YE.Tool.event.addEvent(window, eventType, handler);
…
},
分析問題
實際的游戲開發中,用戶不僅需要綁定全局事件,還需要綁定具體dom的事件,而且也可能需要指定事件處理方法的this。
因此,修改為由用戶傳入綁定事件的對象target和處理方法的上下文handlerContext。
具體實施
修改引擎EventManager的addListener方法,增加形參target和handlerContext:
引擎EventManager
addListener: function (event, handler, target, handlerContext) {
…
//如果用戶指定了handlerContext,則將handler的this指向handlerContext
if (handlerContext) {
_handler = YE.Tool.event.bindEvent(handlerContext, handler);
}
//否則handler的this指向默認的window
else {
_handler = handler;
}
//綁定事件到用戶指定的target,默認為綁定全局事件
YE.Tool.event.addEvent(target || window, eventType, _handler);
…
},
將keyListeners設為私有屬性
現在keyListeners為閉包內部成員:
引擎EventManager
(function () {
var _keyListeners = {};
namespace("YE").EventManager = {
為了方便測試(EventManager的單元測試需要修改EventManager的_keyListeners屬性),將其修改為EventManager的私有屬性。
引擎EventManager
namespace("YE").EventManager = {
_keyListeners: {},
整體重構
所有的引擎類已經重構完畢,我們需要站在整個引擎的層面進行進一步的重構。
整理文件結構
將引擎依賴的jsExtend庫引入到引擎中
現在引擎依賴了YOOP和jsExtend庫。
根據引擎設計原則“盡可能少地依賴外部文件”,考慮到jsExtend是我開發的、沒有發布的庫,可以將其引入,作為引擎的內部庫。
而YOOP是我開發的、獨立發布庫,引擎不應該引入該庫。
劃分引擎文件結構
現在所有引擎文件均在一個文件夾yEngine2D中,不方便維護,應該劃分引擎包,每個包對應一個文件夾,把引擎文件移到對應包的文件夾中。
劃分后的包圖

劃分后的文件結構

其中import文件夾放置引擎的內部庫。
整體修改
引擎類的私有和保護成員加上引擎專有前綴“ye_”
當前設計
引擎類的私有和保護成員沒有專門的前綴。
如引擎Sprite
Private: {
_animationFrame: null,
…
_updateFrame: function (deltaTime) {
…
}
},
分析問題
為了防止繼承引擎類的用戶類的私有和保護成員與引擎類成員同名,可繼承重寫的引擎類(如引擎Sprite)的私有和保護成員需要加上“ye_”前綴。
另外,為了統一引擎類的成員命名,所有的引擎類的私有和保護成員都應該加上該前綴。
然而目前引擎類沒有保護成員,因此只對引擎類私有成員加上前綴。
具體實施
如引擎Sprite
Private: {
ye_animationFrame: null,
…
ye_updateFrame: function (deltaTime) {
…
}
},
用戶可直接創建抽象引擎類Scene、Layer、Sprite的實例
當前設計
引擎Scene、Layer、Sprite為抽象類,沒有創建自身實例的create方法。
分析問題
這幾個類為抽象類,照理來說不能創建自身實例,但是為了減少用戶負擔,用戶應該在沒有自己的邏輯時,直接復用這幾個抽象引擎類,創建它們的實例。
具體實施
引擎Scene、Layer、Sprite增加create方法,創建繼承于抽象類的空子類實例。
引擎Scene
Static: {
create: function () {
var T = YYC.Class(YE.Scene, {
Init: function () {
this.base();
},
Public: {
}
});
return new T();
}
}
引擎Layer
Static: {
create: function (id, zIndex, position) {
if (arguments.length === 3) {
var T = YYC.Class(YE.Layer, {
Init: function (id, zIndex, position) {
this.base(id, zIndex, position);
}
});
return new T(id, zIndex, position);
}
else {
var T = YYC.Class(YE.Layer, {
Init: function () {
this.base();
}
});
return new T();
}
}
}
引擎Sprite
Static: {
create: function (bitmap) {
var T = YYC.Class(YE.Sprite, {
Init: function (bitmap) {
this.base(bitmap);
},
Public: {
}
});
return new T(bitmap);
}
}
如果用戶想要創建一個沒有用戶邏輯的精靈類,可以直接創建引擎Sprite的實例:
var sprite = YE.Sprite.create(bitmap);
//可以直接使用引擎Sprite自帶的方法
sprite.draw(context);
將引擎類閉包中需要用于單元測試的內部成員設為引擎類的靜態成員
當前設計
現在常量、枚舉值等是作為引擎類閉包的內部成員:
如引擎Director
(function () {
//內部變量
var _instance = null;
//內部枚舉值
var GameState = {
NORMAL: 0,
STOP: 1
};
//內部常量
var STARTING_FPS = 60;
namespace("YE").Director = YYC.Class({
分析問題
單元測試需要操作閉包的內部成員,但是現在不能直接訪問到它們,只能繞個彎,在引擎類中增加操作內部成員的測試方法,然后在單元測試中通過這些方法來操作閉包內部成員:
如引擎Director
Static: {
…
//獲得閉包內部枚舉值GameState
forTest_getGameState: function () {
return GameState
}
引擎DirectorSpec單元測試
it("設置游戲狀態為STOP", function () {
director.stop();
//調用forTest_getGameStatus方法獲得內部枚舉值GameState
expect(director. ye_gameState).toEqual(YE.Director.forTest_getGameStatus().STOP);
});
在產品代碼中增加了測試代碼,這是個壞味道,應該只有測試代碼知道產品代碼,而產品代碼不知道測試代碼。
因此,將引擎類閉包中需要用于單元測試的內部成員設為引擎類的靜態成員。
另外,因為靜態成員不會被子類繼承和覆蓋,所以靜態私有和保護成員不需要加上引擎專有前綴“ye_”。
具體實施
將引擎類閉包中需要用于單元測試的內部成員設為引擎類的靜態成員。
引擎Director將_instance、STARTING_FPS和GameState設為靜態變量:
(function () {
namespace("YE").Director = YYC.Class({
…
Private:{
…
ye_updateFps: function (time) {
…
//訪問靜態成員
this.ye_fps = YE.Director.STARTING_FPS;
…
},
…
},
…
Static: {
_instance: null,
STARTING_FPS: 60,
GameState: {
NORMAL: 0,
STOP: 1
},
getInstance: function () {
//靜態方法中可通過this直接訪問靜態成員
if (this._instance === null) {
this._instance = new this();
}
return this._instance;
}
引擎Layer將畫布狀態枚舉值State設為靜態變量
Static: {
State: {
NORMAL: 0,
CHANGE: 1
},
...
本文最終領域模型

此處省略了炸彈人中與引擎類無關的類。
高層劃分
包圖
劃分的包與文件結構對應:

對應的領域模型
- 核心包
放置引擎的核心類。- Main
- Director
- Scene
- Layer
- Sprite
- 算法包
放置通用的算法類。- AStar
- collision
- 動畫包
放置游戲動畫的相關類。- AnimationFrame
- Animation
- Animate
- Frame
- 加載包
負責游戲資源的加載和管理。- ImgLoader
- 數據結構包
放置引擎的基礎結構類。- Collection
- Hash
- Bitmap
- Geometry
- 通用工具包
放置引擎通用的方法類。- Tool
- 事件管理包
負責事件的管理。- Event
- EventManager
- 內部庫包
放置引擎引入的庫。- jsExtend
總結
經過第二次迭代,基本消除了引擎包含的用戶邏輯,從而能夠在其它游戲中使用該引擎了。
不過這只是剛開始而已,引擎還有很多待重構點,引擎的設計和功能也很不完善,相關的配套工具也沒有建立,還需要應用到實際的游戲開發中,不斷地修改引擎,加深對引擎的理解。
浙公網安備 33010602011771號