当前位置: 代码迷 >> .NET相关 >> 下一篇博文
  详细解决方案

下一篇博文

热度:211   发布时间:2016-04-24 03:00:21.0
提炼游戏引擎系列:第二次迭代(上)

前言

上文完成了引擎提炼的第一次迭代,搭建了引擎的整体框架,本文会进行引擎提炼的第二次迭代,进一步提高引擎的通用性,完善引擎框架。

由于第二次迭代内容过多,因此分为上、下两篇博文,本文为上篇。

本文目的

1、提高引擎的通用性,完善引擎框架。
2、对应修改炸弹人游戏。

本文主要内容

第一次迭代后的引擎领域模型

开发策略

本文会对引擎领域模型从左到右一一进行分析, 二次提炼和重构引擎类。

本文迭代步骤

迭代步骤说明

  • 确定要重构的引擎类

按照第一次迭代提出的引擎领域模型,从左往右一一分析,判断是否需要重构。

  • 发现问题

从是否包含用户逻辑、是否违反引擎设计原则、是否可从炸弹人类中提炼更多的通用模式等方面来审视引擎类,如果存在问题则给出引擎类与问题相关的当前设计。

  • 分析问题

分析当前设计,指出其中存在的问题,给出问题的解决方案。

  • 具体实施

按照解决方案修改当前设计。

  • 通过游戏的运行测试
  • 修改并通过引擎的单元测试

通过游戏运行测试和引擎单元测试后,继续分析该引擎类,发现并解决下一个问题。

  • 完成本次迭代

解决了引擎类所有的问题后,就可以确定下一个要重构的引擎类,进入新一轮迭代。

不讨论测试

因为测试并不是本系列的主题,所以本系列不会讨论专门测试的过程,“本文源码下载”中也没有单元测试代码。
您可以在最新的引擎版本中找到引擎完整的单元测试代码: YEngine2D

修改Main

改为继承重写

上文对用户使用引擎的方式进行了思考,给出了“引擎Main、Director采用实例重写的方式”的设计。
但是现在重新思考后,发现Main采用实例重写的方式并不合适。

当前设计

领域模型

引擎Main

(function () {    var _instance = null;    namespace("YE").Main = YYC.Class({        Init: function () {            this._imgLoader = new YE.ImgLoader();        },        Private: {         _imgLoader: null,            _prepare: function () {                this.loadResource();                this._imgLoader.onloading = this.onloading;                this._imgLoader.onload = this.onload;                this._imgLoader.onload_game = function () {                    var director = YE.Director.getInstance();                    director.init();                    director.start();                }            }        },        Public: {            init: function () {                this._prepare();                this._imgLoader.done();            },            getImg: function (id) {                return this._imgLoader.get(id);            },            load: function (images) {                this._imgLoader.load(images);            },            //* 钩子            loadResource: function () {            },            onload: function () {            },            onloading: function (currentLoad, imgCount) {            }        },        Static: {            getInstance: function () {                if (_instance === null) {                    _instance = new this();                }                return _instance;            }        }    });}());

炸弹人Main

(function(){    //获得引擎Main实例    var main = YE.Main.getInstance();    var _getImg = function () {        …    };    var _addImg = function (urls, imgs) {        …    };    var _hideBar = function () {        …    };    //重写引擎Main实例的钩子    main.loadResource = function () {        this.load(_getImg());    };    main.onloading = function (currentLoad, imgCount) {        $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));     //调用进度条插件    };    main.onload = function () {        _hideBar();    };}());

其它炸弹人类通过调用引擎Main的getImg方法来获得加载的图片对象。

var img = YE.Main.getInstance().getImg(imgId);	//获得id为imgID的图片对象

页面调用引擎Main的init方法进入游戏

<script type="text/javascript">    (function () {        YE.Main.getInstance().init();    })();</script>

分析问题

因为炸弹人Main与引擎Main都属于“入口”概念,负责资源加载的管理,所以炸弹人Main与引擎Main应该为继承关系,引擎Main需要改造为可被继承的类,炸弹人Main也要改造为继于引擎Main。

具体实施

引擎Main应该为抽象类,不再为单例:

引擎Main

(function () {namespace("YE").Main = YYC.AClass({…    });}());

炸弹人Main改为单例并继承引擎Main,提供getImg方法返回图片对象,供其它用户类调用。
炸弹人Main

(function () {    var Main  = YYC.Class(YE.Main, {        Private:{            _getImg: function () {                …            },            _addImg: function (urls, imgs) {                …            },            _hideBar: function () {                …            }        },        Public:{            //返回对应id的图片对象            getImg:function(id){                return this.base(id);            },            loadResource: function () {                this.load(_getImg());            },            onloading: function (currentLoad, imgCount) {                $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));            },            onload: function () {                this._hideBar();            }        },        Static: {            getInstance: function () {                if (_instance === null) {                    _instance = new this();                }                return _instance;            }        }    });    window.Main = Main ;}());

其它炸弹人类改为调用炸弹人Main的getImg方法来获得图片数据。

var img = Main.getInstance().getImg(imgId); 

页面改为调用炸弹人Main的init方法

<script type="text/javascript">    (function () {       Main.getInstance().init();    })();</script>

引擎Main不应该封装ImgLoader

进行上面的修改后,运行测试时会报错,错误信息为炸弹人Main在重写的onload方法中调用的“this._hideBar”为undefined。
造成这个错误的原因是在第一次迭代的设计中,引擎Main封装了引擎ImgLoader,将它的onload与ImgLoader的onload绑定在了一起,导致执行炸弹人Main的onload时,this指向了引擎ImgLoader实例imgLoader,而不是指向炸弹人Main。
引擎Main

            _prepare: function () {…			    //绑定了引擎Main和引擎ImgLoader的钩子                this._imgLoader.onloading = this.onloading;                this._imgLoader.onload = this.onload;…            }        },        Public: {            init: function () {                this._prepare();…            },            getImg: function (id) {                return this._imgLoader.get(id);            },

引擎Main提供了getImg方法来获得引擎ImgLoader实例imgLoader保存的图片对象。
引擎Main改为继承重写后,由于其他炸弹人类不能直接访问到引擎Main的getImg方法,所以炸弹人必须增加getImg方法,对其它炸弹人类暴露引擎Main的getImg方法。
这样的设计是不合理的,引擎Main的getImg方法并不是设计为被用户重写的方法,而且炸弹人Main也不需要知道引擎Main的getImg方法的实现,这增加了用户的负担,违反了引擎设计原则“尽量减少用户负担”。

因此,引擎Main不再封装imgLoader,而是将其暴露给炸弹人Main,再由它暴露给其它炸弹人类。

具体来说就是,引擎Main删除getImg、load方法,将imgLoader属性设为公有属性;炸弹人Main将imgLoader设为全局属性,直接重写imgLoader的onload、onloading钩子,并删除getImg方法。。
这样其它炸弹人类可以直接访问引擎Main的imgLoader属性,调用它的get方法来获得图片数据

由于炸弹人没有要插入到引擎Main的用户逻辑,因此引擎Main删除onload、onloading钩子。

修改后相关代码
引擎Main

        Private: {			//删除了onload和onloading钩子,不再绑定引擎Main和引擎ImgLoader的钩子了            _prepare: function () {…            }        },        Public: {    		//imgLoader作为公有属性             imgLoader: null,

炸弹人Main

            loadResource: function () {				//获得引擎Main的imgloader                var loader = this.imgLoader,                    self = this;				//重写imgLoader的钩子                loader.load(this._getImg());                loader.onloading = function (currentLoad, imgCount) {                    …                };                loader.onload = function (imgCount) {…                };								//imgLoader设为全局属性,供其它炸弹人类操作                window.imgLoader = this.imgLoader;            }

其它炸弹人类通过window.imgLoader.get方法获得图片数据

重构后的领域模型

修改Director

炸弹人Game的名字与其职责不符

引擎Director暂时找不出问题,因此来看下与它相关的炸弹人Game。

当前设计

现在炸弹人Game实例重写了引擎Director。
引擎Scene不能被重写,只能提供API供炸弹人Game和引擎Director调用。

重构前领域模型

炸弹人Game

(function () {    var director = YE.Director.getInstance();    var Game = YYC.Class({…        Public: {             init: function () {                 //初始化游戏全局状态                window.gameState = window.bomberConfig.game.state.NORMAL;                window.subject = new YYC.Pattern.Subject();                this.sleep = 1000 / director.getFps();                //初始化游戏场景                this._createScene();                this._addElements();                this._initLayer();                this._initEvent();                window.subject.subscribe(this.scene.getLayer("mapLayer"), this.scene.getLayer("mapLayer").changeSpriteImg);            },			//管理游戏状态            judgeGameState: function () {   …            }        }    });    var game = new Game();    director.init = function () {        game.init();        //设置场景        this.setScene(game.scene);    };    director.onStartLoop = function () {        game.judgeGameState();    };}());

引擎Scene

//引擎Scene为普通的类,向炸弹人类和引擎类提供APInamespace("YE").Scene = YYC.Class(YE.Hash, {…

分析问题

炸弹人Game现在只负责初始化游戏场景和管理游戏状态的逻辑,该逻辑属于场景的范围,不属于统一调度的范围,因此Game应该改造为炸弹人场景类,与引擎Scene对应,而不是与引擎Director对应。
考虑到炸弹人场景类与引擎Scene同属一个概念,因此炸弹人场景类应该使用继承重写的方式来使用引擎Scene。
由于引擎Director依赖引擎Scene,而引擎Scene不依赖引擎Director,所以炸弹人场景类也不应该再依赖引擎Director。

因此,应该进行下面的重构:
1、改造引擎Scene类为可被继承重写的类。
2、将炸弹人Game改造为炸弹人场景类Scene,继承重写引擎Scene。
3、引擎Director应该改造为一个封闭的单例类,用户不能重写,向引擎类和用户类提供主循环和场景操作相关的API。将它的钩子方法移到引擎Scene类,炸弹人Game对引擎Director钩子方法的重写变为对引擎Scene钩子方法的重写,对应修改钩子方法的调用机制。

具体实施

按照下面的步骤重构:
1、改造引擎Scene为可被继承的类,将引擎Director的钩子移到其中;
2、将炸弹人Game改造为场景类Scene,继承重写引擎Scene;
3、改造引擎Director,修改钩子方法的调用机制;
4、重构相关的引擎类和炸弹人类。

改造引擎Scene类为可被继承的类

引擎Scene改为抽象类,将引擎Director的init、onStartLoop、onEndLoop钩子方法移到其中。

引擎Scene

(function () {    namespace("YE").Scene = YYC.AClass({…        Public: {…                init: function () {                },                onStartLoop: function () {                },                onEndLoop: function () {                }        }       });}());

引擎Director删除钩子方法

将炸弹人Game改造为场景类Scene,继承重写引擎Scene

Game进行下面的修改:
(1)炸弹人Game重命名为Scene。
(2)继承引擎Scene,重写钩子方法init和onStartLoop。
(3)删除scene属性,将调用scene属性的成员改为调用自身的成员(“self/this.scene.xxx”改为“self/this.xxx”)。
(4)不再创建scene实例了,对应修改_createScene方法,删除其中的“创建scene”逻辑,保留“加入层”逻辑,将其重命名为_addLayer。

炸弹人Scene

 var Scene = YYC.Class(YE.Scene, {    Private: {        _sleep: 0,        _addLayer: function () {            this.addLayer("mapLayer", layerFactory.createMap());            this.addLayer("enemyLayer", layerFactory.createEnemy(this._sleep));            this.addLayer("playerLayer", layerFactory.createPlayer(this._sleep));            this.addLayer("bombLayer", layerFactory.createBomb());            this.addLayer("fireLayer", layerFactory.createFire());        },        _addElements: function () {            var mapLayerElements = this._createMapLayerElement(),                playerLayerElements = this._createPlayerLayerElement(),                enemyLayerElements = this._createEnemyLayerElement();            this.addSprites("mapLayer", mapLayerElements);            this.addSprites("playerLayer", playerLayerElements);            this.addSprites("enemyLayer", enemyLayerElements);        },        _createMapLayerElement: function () {            …        },        _getMapImg: function (i, j, mapData) {            …        },        _createPlayerLayerElement: function () {            …        },        _createEnemyLayerElement: function () {            ….        },        _initLayer: function () {            this.initLayer();        },        _initEvent: function () {            …        },        _judgeGameState: function () {            …        },        _gameOver: function () {            …        },        _gameWin: function () {…    }},    Public: {        //重写引擎Scene的init钩子        init: function(){            window.gameState = window.bomberConfig.game.state.NORMAL;                window.subject = new YYC.Pattern.Subject();                this.sleep = 1000 / director.getFps();                this._addLayer();            this._addElements();            this._initLayer();            this._initEvent();                window.subject.subscribe(this.getLayer("mapLayer"), this.getLayer("mapLayer").changeSpriteImg);        },        //重写引擎Scene的onStartLoop钩子        onStartLoop: function(){            this._judgeGameState();        }    }});

改造引擎Director类

修改了引擎Director和引擎Scene的钩子方法后,需要对应修改这些钩子方法的调用机制。
当前设计
在修改前先来看下引擎Main、Director、Scene以及炸弹人Game之间关于场景的交互机制:

完成加载图片后会触发引擎ImgLoader的onload_game钩子,该钩子被引擎Main重写,触发引擎Director的init钩子,执行炸弹人Game插入的初始化场景的逻辑,:

引擎Main

            _prepare: function () {…				//加载图片完成后,触发引擎ImgLoader的onload_game钩子                this._imgLoader.onload_game = function () {                    var director = YE.Director.getInstance();					//触发init钩子		            director.init();					director.start();                }            }…

炸弹人Game

_createScene: function () {    this.scene = new YE.Scene();…},…init: function () {…	this. _createScene();…}var director = YE.Director.getInstance();…//重写引擎Director的init钩子director.init = function () {    game.init();    //调用引擎Director的setScene方法,设置当前场景    this.setScene(game.scene);};

然后onload_game会调用引擎Director的start方法,启动主循环,触发引擎Director的钩子方法onStartLoop和onEndLoop,执行炸弹人Game重写插入的场景逻辑:
引擎Main

            _prepare: function () {…				//加载图片完成后,触发引擎ImgLoader的onload_game钩子                this._imgLoader.onload_game = function () {                    var director = YE.Director.getInstance();					director.init();					//调用start方法					director.start();                }            }

炸弹人Game

    var director = YE.Director.getInstance();… 	//重写引擎Director的onStartLoop钩子    director.onStartLoop = function () {      game.judgeGameState();    };

引擎Director

start:function(){…    //启动主循环    window.requestNextAnimationFrame(function (time) {        self._run(time);    });…},…_run: function (time) {    var self = this;    //主循环逻辑在_loopBody方法中    this._loopBody(time);    if (this._gameState === GameState.STOP) {        return;    }    window.requestNextAnimationFrame(function (time) {        self._run(time);    });},_loopBody: function (time) {…    //触发自己的onStartLoop和onEndLoop钩子    this.onStartLoop();…    this.onEndLoop();},

修改后的设计
进行下面四个修改:
(1)onload_game不再调用引擎Director的init方法。
(2)onload_game会传入引擎Main创建的炸弹人Scene实例(这只是临时解决方案,这样的设计导致了引擎Main依赖炸弹人Scene,违反了引擎设计原则!后面会进行重构)到引擎Director的start方法中。
(3)引擎Director的start方法会触发炸弹人Scene实例的init钩子方法,并设置该实例为当前场景。
(4)引擎Director在主循环中改为触发当前场景的onStartLoop和onEndLoop钩子方法。

修改后的场景的交互机制序列图

引擎Main

            _prepare: function () {…                this. _imgLoader.onload_game = function () {                    var director = YE.Director.getInstance();	                    //传入创建的炸弹人场景实例                    director.start(new Scene());	                };            }

引擎Director

            start: function (scene) {                var self = this;				//触发场景的init钩子                scene.init();				//设置为当前场景                this.setScene(scene);…            },

引擎Director

            _loopBody: function (time) {…                this._scene.onStartLoop();…                this._scene.onEndLoop();            },

重构相关的引擎类和炸弹人类

引擎Director类的start方法重命名为runWithScene

由于start方法传入了炸弹人Scene的实例,所以将该方法重命名为runWithScene更合适:
引擎Director

runWithScene:function(scene){…}

引擎Main

            _prepare: function () {…                this._imgLoader.onload_game = function () {                    var director = YE.Director.getInstance();					//改为调用引擎Director的runWithScene方法                    director.runWithScene(new Scene());                };            }

解除引擎Main对炸弹人Scene的依赖

现在引擎Main创建了炸弹人Scene的实例:
引擎Main

            _prepare: function () {…                this. _imgLoader.onload_game = function () {                    var director = YE.Director.getInstance();						//创建并注入炸弹人Scene实例                    director.runWithScene (new Scene());	                };            }

这导致了引擎依赖用户,违反了引擎设计原则。
因为引擎ImgLoader的onload_game与onload钩子执行时间相同,所以可以将onload_game中的逻辑移到炸弹人Main重写ImgLoader的onload钩子中,由炸弹人Main创建炸弹人Scene实例,解除了引擎Main对炸弹人Scene的依赖:
炸弹人Main

                loadResource: function () {…                    loader.onload = function (imgCount) {…                            YE.Director.getInstanc.runWithScene(new Scene());                    };…                }

删除引擎ImgLoader的onload_game钩子

ImgLoader的onload_game钩子和onload钩子重复了,这是第一次迭代提出的临时解决方案。
现在onload_game钩子已经没有用了,因此将其删除。

引擎类继承重写的钩子方法都设成虚方法

继承重写的钩子方法是设计为被用户继承重写的,属于多态,应该将其设为虚方法。
对于实例重写的钩子方法,用户只是重写实例的钩子方法,并没有继承引擎类,不属于多态,不设为虚方法。

又由于用户不是必须要重写钩子方法,因此钩子方法不应该设为抽象方法。

引擎Main

         Virtual:{                loadResource: function () {                }            }

引擎Scene

            Virtual: {                init: function () {                },                onStartLoop: function () {                },                onEndLoop: function () {                }            }

游戏结束时引擎要停止所有定时器

目前引擎Director只有退出主循环的机制:
引擎Director

            _run: function (time) {                var self = this;                this._loopBody(time);				//如果游戏状态为STOP,则退出主循环                if (this._gameState === YE.Director.GameState.STOP) {                    return;                }                window.requestNextAnimationFrame(function (time) {                    self._run(time);                });            },…            stop: function () {                this._gameStatus = GameStatus.STOP;            }

用户可能会在游戏中调用setTimeout、setInterval方法设置定时器,所以引擎需要在游戏结束时停止这些定时器。
因此,引擎Director的stop方法增加停止所有定时器的逻辑:
引擎Director

            stop: function () {…                YE.Tool.async.clearAllTimer();            }

引擎Tool增加clearAllTimer方法,使用暴力清除法停止所有的定时器:
引擎Tool

namespace("YE.Tool").async = {        /**         * 清空序号在1-500范围中的定时器         */        clearAllTimer: function () {            var i = 0,                num = 0,                timerNum = 500, //最大定时器个数                firstIndex = 0;            firstIndex = 1;            num = firstIndex + timerNum;    //循环次数            for (i = firstIndex; i < num; i++) {                window.clearTimeout(i);            }            for (i = firstIndex; i < num; i++) {                window.clearInterval(i);            }        }    }

兼容IE
clearAllTimer方法在IE浏览器中有问题。虽然定时器序号在所有浏览器中都是每次只加1,但是在IE浏览器中,每次刷新浏览器后定时器起始序号会叠加,导致IE中起始序号可能很大(而在Chrome和Firefox中定时器序号的起始值始终为1),可能超出定时器的清理范围。
因此需要用户使用定时器时要保存任意一个定时器的序号到引擎中,并将clearAllTimer方法改为清空该序号前后一定范围内的定时器。

修改后代码
引擎Tool

      /**         * 清空序号在index前后timerNum范围中的定时器         * @param index 定时器序号         */        clearAllTimer: function (index) {            var i = 0,                num = 0,                timerNum = 250,                 firstIndex = 0;            //获得最小的定时器序号            firstIndex = (index - timerNum >= 1) ? (index - timerNum) : 1;			//循环次数            num = firstIndex + timerNum * 2;                for (i = firstIndex; i < num; i++) {                window.clearTimeout(i);            }            for (i = firstIndex; i < num; i++) {                window.clearInterval(i);            }	}

引擎Director增加保存定时器序号的_timeIndex属性,在stop方法中将_timeIndex传入clearAllTimer,并增加设置定时器序号的方法setTimerIndex:
引擎Director

		    _timerIndex: 0,…            stop: function () {…                YE.Tool.async.clearAllTimer(this._timerIndex);            },            setTimerIndex: function (index) {                this._timerIndex = index;            }

对应修改炸弹人源码,调用引擎Director的setTimerIndex方法保存任意一个定时器的序号到引擎中:
炸弹人BombLayer

            explode: function (bomb) {…                index = setTimeout(function () {…                }, 300);				//保存定时器序号                YE.Director.getInstance().setTimerIndex(index);            },

重构后的领域模型

修改Scene

删除change方法

当前设计

主循环调用了引擎Scene的change方法,它又调用了场景内层的change方法。

引擎Scene

            change: function () {                this.__iterator("change");            },            run: function () {                this.__iterator("run");            },

引擎Director

        _loopBody: function (time) {…                this._scene.run();				this._scene.change();…            },

分析问题

引擎Scene的change方法没有自己的逻辑。因此删除change方法,将其合并到引擎Scene的主循环方法run中。

具体实施

引擎Scene

            run: function () {                this.__iterator("run");                this.__iterator("change");            },

引擎Director

        _loopBody: function (time) {…				//不再调用场景的change方法了                this._scene.run();…            },

不应该关联引擎Sprite

当前设计

现在引擎Scene提供了addSprites方法,负责将精灵加入到层中:

引擎Scene

            addSprites: function (name, elements) {                this.getLayer(name).addChilds(elements);            },

炸弹人Scene

            _addElements: function () {                var mapLayerElements = this._createMapLayerElement(),                    playerLayerElements = this._createPlayerLayerElement(),                    enemyLayerElements = this._createEnemyLayerElement();                this.addSprites("mapLayer", mapLayerElements);                this.addSprites("playerLayer", playerLayerElements);                this.addSprites("enemyLayer", enemyLayerElements);            },

分析问题

引擎Director、Scene、Layer、Sprite分别对应不同的层面,上层不应该跨层依赖下层(引擎Director是个特例,因为其它引擎类可能需要调用它提供的操作主循环的API,因此它可被下层跨层依赖):

当前设计造成了引擎Scene关联引擎Sprite,应该去掉两者的关联:

具体实施

引擎Scene删除addSprites方法。
炸弹人Scene改为先获得layer,然后再调用layer的addChilds方法来实现加入精灵到层中:
炸弹人Scene

            _addLayer: function () {                this.getLayer("mapLayer").addChilds(this._createMapLayerElement());                this.getLayer("playerLayer").addChilds(this._createPlayerLayerElement());                this.getLayer("enemyLayer").addChilds(this._createEnemyLayerElement());            },

修改Layer

封装画布操作

当前设计

现在画布的操作由用户负责,用户需要实现setCanvas方法,指定层对应的画布,将画布dom保存到引擎Layer的P_canvas属性中,并设置画布的位置。引擎Layer则直接通过用户设置好的P_canvas属性来操作画布:

引擎Layer

            Abstract: {				//抽象方法,由用户实现                setCanvas: function () {                },…

炸弹人BombLayer

    var BombLayer = YYC.Class(YE.Layer, {…            setCanvas: function () {                this.P_canvas = document.getElementById("bombLayerCanvas");                var css = {                    "position": "absolute",                    "top": bomberConfig.canvas.TOP,                    "left": bomberConfig.canvas.LEFT,                    "z-index": 1                };                $("#bombLayerCanvas").css(css);            },

引擎Layer还将画布canvas的context属性暴露给了用户:

引擎Layer

            __getContext: function () {				//获得画布的context,暴露给用户                this.P_context = this. P_canvas.getContext("2d");            },

炸弹人BombLayer

            draw: function () {				//炸弹人可直接访问画布的context                this.iterator("draw", this.P_context);            },

分析问题

画布操作属于底层逻辑,不应该由用户实现,应该由引擎封装,向用户提供操作画布的API。

因此,进行下面的重构:
(1)引擎Layer封装画布,向用户提供操作画布的API。
(2)引擎Layer封装画布的context属性,向用户提供操作context的API。

具体实施

按照下面的步骤重构:
1、封装画布
(1)将P_canvas属性改为私有属性。
(2)引擎Layer增加操作画布的API。
(3)修改用户Layer类的setCanvas方法,用户不再直接操作画布,而是通过引擎Layer提供的API来操作画布。
(4)引擎Layer的构造函数增加设置画布的逻辑,这样用户就可以通过“创建用户Layer实例时传入画布参数”来设置画布。
(5)引擎Layer删除setCanvas方法,不再限定用户在setCanvas方法中设置画布。

2、封装context。
将P_context改为私有属性,并提供getContext方法。

封装canvas

**1、将保护属性P_canvas改成私有属性__canvas**
引擎Layer

Private:{    __canvas: null,…},

2、增加setCanvasByID、setWidth、setHeight、setZIndex、setPosition方法

相关代码

引擎Layer

Public:{    //保存对应id的画布    setCanvasByID: function (canvasID) {        this.__canvas = document.getElementById(canvasID);    },    //设置画布宽度    setWidth: function (width) {        this.__canvas.width = width;    },    //设置画布高度    setHeight: function (height) {        this.__canvas.height = height;    },    //设置画布层级顺序    setZIndex: function (zIndex) {        this.__canvas.style.zIndex = zIndex;    },    //设置画布坐标    setPosition: function (x, y) {        this.__canvas.style.top = x.toString() + "px";        this.__canvas.style.left = y.toString() + "px";    },

引擎Layer的setPosition方法对top和left值加上了“px”字符串,因此需要对应修改炸弹人Config设置的画布坐标:
炸弹人Config
修改前

    canvas: {…        TOP: "0px",        LEFT: "0px"    },

修改后

    canvas: {…        TOP: 0,        LEFT: 0    },

3、修改用户Layer类的setCanvas方法,用户不再直接操作画布,而是通过引擎Layer提供的API来操作画布
相关代码
炸弹人BombLayer

            setCanvas: function () {                this.setCanvasByID("bombLayerCanvas");                this.setPosition(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);                this.setZIndex(1);            },

炸弹人EnemyLayer

            setCanvas: function () {                this.setCanvasByID("enemyLayerCanvas");                this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);                this.setZIndex(3);            },

炸弹人FireLayer

            setCanvas: function () {                this.setCanvasByID("fireLayerCanvas");                this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);                this.setZIndex(2);            },

炸弹人MapLayer

            setCanvas: function () {…                this.setCanvasByID("mapLayerCanvas");                this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);                this.setZIndex(0);            },

炸弹人PlayerLayer

            setCanvas: function () {                this.setCanvasByID("playerLayerCanvas");                this.setAnchor(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);                this.setZIndex(3);            },

4、引擎Layer的构造函数增加设置画布的逻辑

在构造函数中判断是否传入了画布参数,如果传入则调用操作画布API设置画布:
引擎Layer

        Init: function (id, zIndex, position) {            if (arguments.length === 3) {                this.setCanvasByID(id);                this.setZIndex(zIndex);                this.setPosition (position.x, position.y);            }        },

这样用户就有两种方式设置画布了:
(1)创建用户Layer实例时传入画布参数。
(2)在setCanvas方法中调用画布操作API。

5、引擎Layer删除setCanvas方法,不再限定用户必须在setCanvas方法中设置画布
因为用户可以在创建用户Layer实例时设置画布,所以“强迫用户在setCanvas抽象方法中设置画布”的设计就不合适了。
因此,引擎Layer删除setCanvas方法,对应修改引擎Scene,初始化层时不再调用layer的setCanvas方法了:
引擎Scene

            initLayer: function () {                //this.__iterator("setCanvas");…            }
  • 用户需要什么时候设置画布?

因为引擎Layer初始化时需要获得画布的context属性,所以用户需要在这之前设置画布:
引擎Layer

                init: function () {                    this.__getContext();                },

因此,用户除了可在创建用户Layer实例时设置画布,还可以在引擎Layer初始化之前设置画布。

如炸弹人BombLayer可重写引擎Layer的init方法,在执行引擎Layer初始化前设置画布:

            ___setCanvas: function () {                this.setCanvasByID("bombLayerCanvas");                this.setPosition(bomberConfig.canvas.TOP, bomberConfig.canvas.LEFT);                this.setZIndex(1);            },…            init: function (layers) {				this. ___setCanvas();	//在执行引擎初始化逻辑前设置画布…				this.base();	//执行父类引擎Layer的init方法            },

封装context

将P_context改为私有属性__context,并提供getContext方法
引擎Layer

            __context: null,…            getContext: function () {                return this.__context;            },

对应修改用户Layer类,使用getContext来获得__context
如炸弹人BombLayer

            draw: function () {                this.iterator("draw", this.getContext());            },

引擎执行层的初始化

当前设计

引擎Scene提供初始化层的方法initLayer,由炸弹人Scene在场景初始化时调用,执行场景内层的初始化:

引擎Scene

           initLayer: function () {                this.__iterator("init", this.__getLayers());            },

炸弹人Scene

            init: function () { …                this._initLayer();…            },…            _initLayer: function () {…                this.initLayer();            },

分析问题

“执行层的初始化”属于底层逻辑,应该由引擎负责,引擎类Scene应该对用户隐藏initLayer方法。

具体实施

将引擎Scene的initLayer设为私有方法,并在引擎Scene的init钩子方法中调用:
引擎Scene

            __initLayer: function () {                this.__iterator("init", this.__getLayers());            }…            init: function () {                this.__initLayer();            },

不过这样修改后,炸弹人Scene在重写init钩子时就需要先执行引擎Scene的初始化逻辑,再执行自己的用户逻辑,违反了引擎设计原则“尽量减少用户负担”,会在后面进行重构。
炸弹人Scene

            init: function () {                //执行引擎类初始化逻辑                this.base();                               //用户初始化逻辑                 …            }

分离引擎的初始化逻辑与用户的初始化逻辑

当前设计

现在引擎Scene、引擎Layer、引擎Sprite提供了init钩子方法,负责引擎类的初始化。该方法为虚方法,用户可重写,加入自己的初始化逻辑。

用户代码示例:
炸弹人Scene

              init: function () {            	//执行引擎类初始化逻辑                this.base();                        	//用户初始化逻辑                this._sleep = 1000 / director.getFps();                …            }

炸弹人BombLayer

            init: function (layers) {				//执行引擎类初始化逻辑                this.base();				//用户初始化逻辑                this.fireLayer = layers.fireLayer;                …            }

炸弹人MoveSprite

            init: function () {				//执行引擎类初始化逻辑                this.base();				//用户初始化逻辑                this.P_context.setPlayerState(this.__getCurrentState());                …            }

分析问题

用户在加入自己的初始化逻辑时,需要先执行引擎类的初始化逻辑,导致用户不仅需要知道引擎类的初始化逻辑,还需要知道用户初始化逻辑和引擎初始化逻辑的调用顺序,违反了引擎设计原则“尽量减少用户负担”。

因此,引擎Scene、Layer、Sprite类的初始化应该由引擎负责并对用户隐藏,将引擎的初始化逻辑与用户的初始化逻辑分离。

具体实施

引擎Sprite、Layer、Sprite增加initData钩子方法,用户可重写它来插入自己的初始化逻辑。而引擎的init方法不再作为钩子方法供用户重写,它负责引擎的初始化和调用initData方法执行用户的初始化。

关于“引擎的init方法中调用initData方法的顺序”的思考
因为用户依赖于引擎,所以照理说应该先进行引擎类的初始化,然后再调用initData方法进行用户的初始化,这样用户初始化时就可获得引擎类初始化后的状态。

然而对于引擎Layer来说,它的初始化逻辑需要操作画布,需要用户先设置好画布。
用户可以在创建用户Layer实例时设置画布,也可以在重写的initData方法中设置画布。对于引擎来说要做最坏的假设,即假设用户在initData方法中设置画布,这样的话引擎Layer就必须在init方法中先调用initData方法,再进行自己的初始化。

同样,引擎Scene也需要用户先加入层到场景中,然后才能执行自己的场景初始化逻辑。
所以Scene和Layer应该先调用initData钩子方法,然后再执行自己的初始化逻辑。

而引擎Sprite的初始化逻辑与用户没有顺序依赖,因而引擎Sprite可以先进行引擎类的初始化,然后再调用initData进行用户的初始化。

相关代码

引擎Scene

			init: function () {                //需要用户先加入层到场景中后,才能初始化层                this.initData();                                this.__initLayer();            },            //*钩子            Virtual: {                initData: function(){                },

引擎Layer

            init: function (layers) {				//需要用户设置画布后,才能初始化画布				//这里将layers传入initData中                this.initData(layers);                this.__getContext();				this.__initCanvas();            },            Virtual: {                initData: function (layers) {                },

引擎Sprite

            init: function () {                //引擎可以先执行自己的初始化逻辑,再执行用户的初始化逻辑                this.setAnim(this.defaultAnimId);                this.initData();            },…            Virtual: {                initData: function () {                },

用户代码示例:
炸弹人Scene

            initData: function () {                //执行用户初始化逻辑                …            }

炸弹人BombLayer

            initData: function (layers) {                //执行用户初始化逻辑                …            }

炸弹人MoveSprite

            initData: function () {                //执行用户初始化逻辑                …            }

clear方法只负责清除画布

当前设计

引擎Layer的clear方法会根据参数个数来判断是清除所有的精灵,还是清除指定的精灵:
引擎Layer

                clear: function (sprite) {                    if (arguments.length === 0) {						//清除所有层内精灵                        this.P_iterator("clear", this.__context);                    }                    else if (arguments.length === 1) {						//清除指定的精灵                        sprite.clear(this.__context);                    }                },

用户代码示例:
炸弹人BombLayer

            ___removeBomb: function (bomb) {				//从画布中清除bomb精灵                this.clear(bomb);	…            },

分析问题

引擎Layer的clear方法的判断逻辑是多余的,因为引擎Sprite的clear方法是供用户调用的,如果用户想要清除某个精灵,可以直接调用该精灵的clear方法。
又因为引擎Layer最清楚层内的所有精灵,所以它的clear方法保留“清除层内所有精灵”的逻辑。

具体实施

引擎Layer的clear方法只负责清除层内所有精灵。

引擎Layer

                clear: function () {                    this. P_iterator ("clear", this.__context);                }

炸弹人BombLayer

            ___removeBomb: function (bomb) {				//直接调用bomb精灵的clear方法                bomb.clear(this.getContext());…            },

继续修改引擎Layer和Sprite的clear方法

当前设计

引擎Layer的clear方法通过调用层内所有精灵的clear方法,达到清空画布的目的:
引擎Layer

                clear: function () {                    this.iterator("clear", this._context);                },

引擎Sprite的clear方法直接清空画布:
引擎Sprite

                clear: function (context) {                    //直接清空画布区域                    context.clearRect(0, 0, YE.Config.canvas.WIDTH, YE.Config.canvas.HEIGHT);                }

炸弹人MapLayer实现了“清空画布”的逻辑:
炸弹人MapLayer

            clear: function () {                this.P_context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);…            },

分析问题

当前设计有下面几个问题:
(1)引擎Sprite的clear方法应该只负责从画布中清除自己,“清空画布”的逻辑应该由引擎Layer的clear方法负责。
(2)引擎Layer的clear方法应该直接清空画布。
(3)“清空画布”属于底层逻辑,不应该由用户类实现。

具体实施

引擎Layer的clear方法负责清空画布:
引擎Layer

                clear: function () {                    this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);                },

引擎Sprite的clear方法负责从画布中清除自己:
引擎Sprite

                clear: function (context) {                    context.clearRect(this.x, this.y, this.bitmap.width, this.bitmap.height);                }

因为用户类可能需要知道画布大小,因此引擎Layer增加getCanvasWidth、getCanvasHeight方法:
引擎Layer

                getCanvasWidth: function () {                    return this._canvas.width;                },                getCanvasHeight: function () {                    return this._canvas.height;                },…                //对应修改clear方法 				clear: function () {                    this._context.clearRect(0, 0, this. getCanvasWidth(), this. getCanvasHeight());                },

修改炸弹人MapLayer,直接调用引擎Layer的clear方法清除画布:
炸弹人MapLayer

            clear: function () {                this.base();…            },

封装run方法

当前设计

引擎类的run方法封装了引擎类在主循环中的逻辑,该方法由上层引擎类在主循环中调用。
(关于引擎run方法的作用,可参考《炸弹人游戏开发系列(4)》的“增加run方法”一节])

引擎Director

            _loopBody: function (time) {…				//调用场景的run方法                this._scene.run();…            },…            _run: function (time) {                var self = this;                this._loopBody(time);…                window.requestNextAnimationFrame(function (time) {                    self._run(time);                });            },

引擎Scene

            run: function () {				//调用场景内层的run方法                this.__iterator("run");…            },

现在引擎Layer向用户提供了P_render方法,而它的run方法为抽象方法,由用户实现:
引擎Layer

            P_render: function () {                if (this.P_isChange()) {                    this.clear();                    this.draw();                    this.setStateNormal();                }            }…        Abstract: {…            run: function () {            }

我们来看下炸弹人Layer类实现的run方法:
炸弹人BombLayer

            run: function () {                this.P_render();            }

炸弹人FireLayer

            run: function () {                this.P_render();            }

炸弹人MapLayer

            run: function () {                if (this.P_isChange()) {                    this.clear();                    this.draw();                }            }

炸弹人CharacterLayer

run: function () {    this.___setDir();    this.___move();    this.___render();}…___render: function () {    if (this.P_isChange()) {        this.clear();        this.___update(this.___deltaTime);        this.draw();        this.setStateNormal();    }}

炸弹人EnemyLayer

            run: function () {                if (this.collideWithPlayer()) {                    window.gameState = window.bomberConfig.game.state.OVER;                    return;                }                this.__getPath();				//调用Character->run                this.base();            }

炸弹人PlayerLayer

            run: function () {                if (keyState[YE.Event.KeyCodeMap.SPACE]) {                    this.createAndAddBomb();                    keyState[YE.Event.KeyCodeMap.SPACE] = false;                }				//调用Character->run                this.base();            }

分析问题

引擎Layer应该实现主循环逻辑,并且由于这属于底层逻辑,应该对用户隐藏。
因此,引擎Layer应该实现并对用户隐藏run方法。
下面从三个步骤进行分析:
1、识别出炸弹人Layer类的run方法的通用模式。
2、将其提到引擎Layer的run方法中。
3、在引擎Layer的run方法中调用增加的钩子方法,执行炸弹人Layer类插入的逻辑。

识别出炸弹人Layer的run方法的通用模式

分析炸弹人Layer类的相关代码,可以看到炸弹人BombLayer、FireLayer的run方法直接调用了引擎Layer的P_render方法;
炸弹人MapLayer的run方法与P_render方法相比,虽然少调用了引擎Layer的setStateNormal方法,但因为引擎Scene的run方法会调用MapLayer的change方法,而它又会调用引擎Layer的setStateNormal方法,所以MapLayer的run方法也等效于调用了P_render方法。
引擎Scene

            run: function () {                this.__iterator("run");                this.__iterator("change");            },

炸弹人MapLayer

            change: function () {                this.setStateNormal();            },

再来看下CharaterLayer的run方法,它调用了_render方法,该方法与P_render方法相比,多调用了“_update”方法。

而EnemyLayer、PlayerLayer继承CharacterLayer,它们的run方法都调用CharacterLayer的run方法,也就是说都调用了___render方法。

由此可见,炸弹人Layer类的run方法的通用模式是都调用了引擎Layer的P_render方法,只是有些炸弹人Layer类还有自己要插入的逻辑。

提取通用模式到引擎Layer的run方法中

再来看下引擎Layer的P_render方法是否需要重构:

            P_render: function () {                if (this.P_isChange()) {                    this.clear();                    this.draw();                    this.setStateNormal();                }            }

(1)判断是否包含用户逻辑
它调用的都是引擎类Layer的方法,没有包含用户逻辑。
(2)判断是否具有通用性
所有用户Layer类在每次主循环中都要先判断画布的状态,如果状态为CHANGE,表明画布更改过,则先清除画布,然后绘制画布,最后设置画布状态为NORMAL,因此该方法具有通用性。

综上所述,可以将P_render方法直接合并到引擎Layer的run方法中。

增加onAfterDraw钩子方法

炸弹人CharacterPlayer的run方法调用了自己的“___update”方法,该方法需要在引擎Layer的run方法中执行。
为了能让CharacterPlayer及其子类直接使用引擎Layer的run方法,引擎Layer需要增加onAfterDraw钩子方法,并在run方法中调用该钩子。

具体实施

将引擎Layer的P_render方法合并到run方法中,增加onAfterDraw钩子方法:
引擎Layer

            run: function () {                if (this.P_isChange()) {                    this.clear();                    this.draw();					//触发onAfterDraw钩子                    this.onAfterDraw();                    this.setStateNormal();                }            },            Virtual: {…                onAfterDraw: function () {                }            },

对应修改炸弹人BombLayer、FireLayer、MapLayer,删除run方法

炸弹人CharacterLayer、EnemyLayer、PlayerLayer由于还有其它的用户逻辑需要在引擎Layer的run方法之前执行,所以暂时保留run方法(后面会重构):
炸弹人CharacterLayer

                run: function () {                    this.___setDir();                    this.___move();					//调用引擎Layer的run方法					this.base();                },                onAfterDraw: function () {                    this.___update(this.___deltaTime);                }

炸弹人EnemyLayer

            run: function () {                if (this.collideWithPlayer()) {                    window.gameState = window.bomberConfig.game.state.OVER;                    return;                }                this.__getPath();				//调用Character的run方法                this.base();            }

炸弹人PlayerLayer

            run: function () {                if (keyState[YE.Event.KeyCodeMap.SPACE]) {                    this.createAndAddBomb();                    keyState[YE.Event.KeyCodeMap.SPACE] = false;                }				//调用Character的run方法                this.base();            }

增加onStartLoop、onEndLoop钩子

当前设计

经过上一步的修改后,炸弹人CharacterLayer、EnemyLayer、PlayerLayer仍然要重写引擎Layer的run方法,没有达到“引擎Layer对用户隐藏run方法”的设计目的。

分析问题

引擎和用户的run方法的逻辑现在混杂到了一起。
在前面的“应该将引擎的初始化逻辑与用户的初始化逻辑分离”重构中,我们已经看到这种设计很不好,应该将引擎逻辑和用户的逻辑分离。

具体实施

参考引擎Scene,引擎Layer也提出onStartLoop、onEndLoop钩子方法,这两个钩子分别在引擎Layer的run方法执行前、后触发。

引擎Layer

            Virtual:{…                onStartLoop: function () {                },                onEndLoop: function () {                }

在引擎Scene的run方法中触发引擎Layer的钩子:
引擎Scene

            run: function () {                this.__iterator("onStartLoop");                this.__iterator("run");                this.__iterator("change");                this.__iterator("onEndLoop");            },

对应修改炸弹人CharacterLayer、EnemyLayer、PlayerLayer,将自己的逻辑放到钩子中,不再重写引擎Layer的run方法:
炸弹人CharacterLayer

                onStartLoop: function () {                    this.___setDir();                    this.___move();                },

炸弹人EnemyLayer

            onStartLoop: function () {                if (this.collideWithPlayer()) {                    window.gameState = window.bomberConfig.game.state.OVER;                    return;                }                this.__getPath();				//调用CharacterLayer的onStartLoop                this.base();            }

炸弹人PlayerLayer

            onStartLoop: function () {                if (keyState[YE.Event.KeyCodeMap.SPACE]) {                    this.createAndAddBomb();                    keyState[YE.Event.KeyCodeMap.SPACE] = false;                }				//调用CharacterLayer的onStartLoop                this.base();            }

将P_isNorml、P_isChange改成私有方法

分析问题

经过“封装run方法”的修改后,用户Layer类不会再用到引擎Layer的P_isChange、P_isNorml方法了,因此将其设为私有方法。

具体实施

引擎Layer

            __isChange: function () {                return this.__state === State.CHANGE;            },            __isNormal: function () {                return this.__state === State.NORMAL;            }

提取炸弹人draw方法的通用模式

继续从炸弹人Layer类中提取通用模式。

当前设计

现在引擎Layer的draw方法为抽象方法,由用户实现:
引擎Layer

        Abstract: {…            draw: function () {            },

炸弹人BombLayer、CharacterLayer、FireLayer的draw方法具有共同的模式,都是绘制所有精灵:

            draw: function () {                this.iterator("draw", this.getContext());            },

分析问题

可将通用模式提到引擎Layer的draw方法中。
又由于不是所有炸弹人Layer类的绘制逻辑都是“绘制所有精灵”,所以将draw方法设为虚方法,用户可重写该方法实现不同的逻辑。

具体实施

实现引擎Layer的draw方法,对应删除炸弹人BombLayer、CharacterLayer、FireLayer的draw方法。
引擎Layer

			Virtual:{…                draw: function () {                    this.iterator("draw", this.getContext());                },

增加钩子方法isChange,change方法不再为抽象方法

当前设计

现在引擎Layer的change方法为抽象方法,由用户实现,通过调用引擎Layer提供的setStateChange和setStateNormal方法来设置画布状态。

画布状态的作用
引擎Layer在主循环中会判断画布状态,如果为CHANGE,则重绘画布,否则不重绘。

引擎Layer

        Abstract: {            change: function () {            }        }

用户代码示例:
如炸弹人BombLayer

            change: function () {                //如果炸弹人放置了炸弹,则设置画布状态为CHANGE,从而在下次主循环时重绘画布,显示炸弹                if (this.___hasBomb()) {                    this.setStateChange();                }            }

分析问题

其实用户只需要决定下次主循环时是否重绘画布,而不需要知道画布状态。根据引擎设计原则“尽量减少用户负担”,引擎Layer应该对用户隐藏“画布状态”。

具体实施

引擎Layer增加虚方法isChange,用户可以重写该方法,如果需要重绘则返回true,否则返回false。
引擎Layer的change方法会调用isChange方法,根据返回值判断是调用setStateChange方法,还是调用setStateNormal方法。

因为用户可能需要在isChange方法之外设置画布状态,所以引擎Layer保留setStateNormal、setStateChange方法供用户调用。

引擎Layer

            change: function () {                if(this.isChange() === true){                    this.setStateChange();                }                else{                    this.setStateNormal();                }            },            Virtual: {…                isChange: function(){                    return true;                },

炸弹人只需要重写isChange方法
如炸弹人BombLayer

            isChange: function () {                if (this.___hasBomb()) {                    return true;                }            }

思考

  • 引擎Layer现在没有抽象方法了,但仍然应该为抽象类

如果引擎Layer为类,则用户就不能有继承引擎Layer的抽象子类。

例如:用户可能有多个Layer类,对应多个画布,可能需要从中提出抽象基类,抽象基类也需要继承引擎Layer。如果引擎Layer为类,则提出抽象基类不能继承它。

修改Sprite

引擎执行精灵的初始化

当前设计

目前由用户负责执行精灵的初始化:
炸弹人Scene

           _createPlayerLayerElement: function () {                var element = [],                    player = spriteFactory.createPlayer();                    				//执行玩家精灵的初始化                player.init();                …            },            _createEnemyLayerElement: function () {                var element = [],                    enemy = spriteFactory.createEnemy(),                    enemy2 = spriteFactory.createEnemy2();				//执行敌人精灵的初始化                enemy.init();                enemy2.init();                …            },

分析问题

“执行精灵的初始化”属于底层逻辑,应该由引擎负责执行。

由哪个引擎类负责
因为引擎Layer负责管理层内精灵,所以应该由它负责。

在哪里执行精灵的初始化
有两个选择:
1、在初始化层时执行层中的所有精灵的初始化。
2、在加入精灵到层中时执行精灵的初始化。

因为在初始化层时,不一定加入了精灵到层中,所以应该选择在加入精灵到层中时执行精灵的初始化。

具体实施

引擎Layer重写引擎Collection的addChilds方法,加入精灵到层中时执行精灵的初始化:
引擎Layer

        namespace("YE").Layer = YYC.AClass(YE.Collection, {…            addChilds: function (elements) {                this.base(elements);                elements.forEach(function(e){                    //执行精灵的初始化                    e.init();	                });            },

炸弹人Scene不再负责执行精灵的初始化了。

修改后,游戏运行测试会报错。因为在加入地图精灵到层中时,会执行地图精灵的初始化,设置地图精灵的默认动画。然而地图精灵没有动画,其defaultAnimId为undefined,所以执行setAnim方法时会报错。

引擎Sprite

            init: function () {                    //显示默认动画                    this.setAnim(this.defaultAnimId);…            },

为了让游戏运行通过,暂时在引擎Sprite的init方法中加入defaultAnimId的判断:
引擎Sprite

            init: function () {                //如果有默认动画Id,则显示默认动画                if (this.defaultAnimId) {                    this.setAnim(this.defaultAnimId);                }…            },

其实可以看到,引擎Sprite的defaultAnimId属性是默认动画的id,属于用户逻辑,后面会进行重构,去除该用户逻辑。

提取炸弹人中每次主循环持续时间的计算逻辑到引擎Sprite的update方法中

当前设计

游戏需要计算每次主循环持续时间deltaTime,用于在动画管理中计算当前帧播放的时间,确定是否对当前帧进行切换等操作。
目前由炸弹人实现deltaTime的计算。炸弹人Scene计算deltaTime,然后传入炸弹人Layer,然后再传入炸弹人精灵的update方法(引擎Sprite实现),最后传入引擎Animation的update方法。

炸弹人Scene

initData: function(){…    this._sleep = 1000 / director.getFps();	//计算本次主循环持续时间,保存到_sleep属性中…},…_addLayer: function () {…    //deltaTime传入layer    this.addLayer("enemyLayer", layerFactory.createEnemy(this._sleep));    this.addLayer("playerLayer", layerFactory.createPlayer(this._sleep));…},

炸弹人CharacterLayer

Init: function (deltaTime) {    this.___deltaTime = deltaTime;},…___update: function (deltaTime) {    //deltaTime传入炸弹人精灵的update方法    this.iterator("update", deltaTime);},…onAfterDraw: function () {    this.___update(this.___deltaTime);}

引擎Sprite

update: function (deltaTime) {    this._updateFrame(deltaTime);},…_updateFrame: function (deltaTime) {    if (this.currentAnim) {        //deltaTime传入引擎Animation的update方法        this.currentAnim.update(deltaTime);    }}

引擎Animation

            update: function (deltaTime) {…                    //根据deltaTime,计算当前帧的已播放时间                    this._currentFramePlayed += deltaTime;…            },

分析问题

1、引擎负责计算帧率fps,所以它知道如何计算deltaTime。
2、deltaTime与主循环密切相关,而主循环是由引擎来负责的。

因此,应该由引擎计算deltaTime。

由哪个引擎类负责?
(1)只有引擎Animation需要用到deltaTime,而它又是由引擎Sprite的update方法传入的,引擎Sprite是直接关联方。
(2)引擎Scene和引擎Layer都只是传递deltaTime值,没有自己的逻辑。
(3)计算deltaTime需要获得引擎Director的帧率,引擎Sprite能够访问引擎Director,从而能够计算deltaTime。

因此应该由引擎Sprite负责。

具体实施

引擎Sprite的update方法负责计算deltaTime:
引擎Sprite

            update: function () {     			this._updateFrame(1000 / YE.Director.getInstance().getFps());            },

对应修改炸弹人Scene和炸弹人CharacterLayer,不再负责计算和传递deltaTime了。

本文源码下载

GitHub

参考资料

炸弹人游戏系列

上一篇博文

提炼游戏引擎系列:第一次迭代

下一篇博文

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

  相关解决方案