提炼游戏引擎系列:第二次迭代(下)

前言

本文为引擎提炼第二次迭代的下篇,将会完成引擎中动画、集合和事件管理相关类的重构。

本文目的

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"),  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,  sw, height: sh
                    }
                ],
…
                walk_up: [
                    { x: offset.x, y: offset.y + 3 * height,  sw, height: sh, duration: 100 },
                    { x: offset.x + width, y: offset.y + 3 * height,  sw, height: sh, duration: 100 },
                    { x: offset.x + 2 * width, y: offset.y + 3 * height,  sw, height: sh, duration: 100 },
                    { x: offset.x + 3 * width, y: offset.y + 3 * height,  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,  sw, height: sh
    }
],
…
walk_up: [
    { x: offset.x, y: offset.y + 3 * height,  sw, height: sh, duration: 100 },
    { x: offset.x + width, y: offset.y + 3 * height,  sw, height: sh, duration: 100 },
    { x: offset.x + 2 * width, y: offset.y + 3 * height,  sw, height: sh, duration: 100 },
    { x: offset.x + 3 * width, y: offset.y + 3 * height,  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,  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: { 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: { w, height: h} };
    };

炸弹人类中除了帧数据,还有其它的矩形区域数据。如炸弹人游戏中碰撞检测的数据:
炸弹人BombSprite

                collideFireWithCharacter: function (sprite) {
…
                    fire = {
                        x: range[i].x,
                        y: range[i].y,
                         this.getWidth(),
                        height: this.getHeight()
                    };
                    obj2 = {
                        x: sprite.getPositionX(),
                        y: sprite.getPositionY(),
                         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(),
                         this.getWidth(),
                        height: this.getHeight()
                    },
                    obj2 = {
                        x: sprite2.getPositionX(),
                        y: sprite2.getPositionY(),
                         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,
                     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

总结

经过第二次迭代,基本消除了引擎包含的用户逻辑,从而能够在其它游戏中使用该引擎了。
不过这只是刚开始而已,引擎还有很多待重构点,引擎的设计和功能也很不完善,相关的配套工具也没有建立,还需要应用到实际的游戏开发中,不断地修改引擎,加深对引擎的理解。

本文源码下载

GitHub

参考资料

炸弹人游戏系列

上一篇博文

提炼游戏引擎系列:第二次迭代(上)

原文地址:https://www.cnblogs.com/chaogex/p/4175837.html