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

时间:2022-02-17 18:09:28

前言

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

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

本文目的

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

本文主要内容

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

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

开发策略

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

本文迭代步骤

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

迭代步骤说明

  • 确定要重构的引擎类

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

  • 发现问题

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

  • 分析问题

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

  • 具体实施

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

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

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

  • 完成本次迭代

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

不讨论测试

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

修改Main

改为继承重写

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

当前设计

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

引擎Main

(function () {
    var _instance = null;

    namespace("YE").Main = YYC.Class({
        Init: function () {
            this._imgLoader = new YE.ImgLoader();
        },
        Private: {
         _imgLoader: null,

            _prepare: function () {
                this.loadResource();

                this._imgLoader.onloading = this.onloading;
                this._imgLoader.onload = this.onload;

                this._imgLoader.onload_game = function () {
                    var director = YE.Director.getInstance();

                    director.init();
                    director.start();
                }
            }
        },
        Public: {
            init: function () {
                this._prepare();
                this._imgLoader.done();
            },
            getImg: function (id) {
                return this._imgLoader.get(id);
            },
            load: function (images) {
                this._imgLoader.load(images);
            },

            //* 钩子

            loadResource: function () {
            },
            onload: function () {
            },
            onloading: function (currentLoad, imgCount) {
            }
        },
        Static: {
            getInstance: function () {
                if (_instance === null) {
                    _instance = new this();
                }
                return _instance;
            }
        }
    });
}());

炸弹人Main

(function(){
    //获得引擎Main实例
    var main = YE.Main.getInstance();

    var _getImg = function () {};

    var _addImg = function (urls, imgs) {};

    var _hideBar = function () {};
    //重写引擎Main实例的钩子

    main.loadResource = function () {
        this.load(_getImg());
    };
    main.onloading = function (currentLoad, imgCount) {
        $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));     //调用进度条插件
    };
    main.onload = function () {
        _hideBar();
    };
}());

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

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

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

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

分析问题

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

具体实施

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

引擎Main

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

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

(function () {
    var Main  = YYC.Class(YE.Main, {
        Private:{
            _getImg: function () {},

            _addImg: function (urls, imgs) {},

            _hideBar: function () {}
        },
        Public:{
            //返回对应id的图片对象
            getImg:function(id){
                return this.base(id);
            },

            loadResource: function () {
                this.load(_getImg());
            },
            onloading: function (currentLoad, imgCount) {
                $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));
            },
            onload: function () {
                this._hideBar();
            }
        },
        Static: {
            getInstance: function () {
                if (_instance === null) {
                    _instance = new this();
                }
                return _instance;
            }
        }

    });

    window.Main = Main ;
}());

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

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

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

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

引擎Main不应该封装ImgLoader

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

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

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

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

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

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

修改后相关代码
引擎Main

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

炸弹人Main

            loadResource: function () {
                //获得引擎Main的imgloader
                var loader = this.imgLoader,
                    self = this;

                //重写imgLoader的钩子
                loader.load(this._getImg());

                loader.onloading = function (currentLoad, imgCount) {};
                loader.onload = function (imgCount) {};
                
                //imgLoader设为全局属性,供其它炸弹人类操作
                window.imgLoader = this.imgLoader;
            }

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

重构后的领域模型

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

修改Director

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

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

当前设计

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

重构前领域模型
提炼游戏引擎系列:第二次迭代(上)

炸弹人Game

(function () {
    var director = YE.Director.getInstance();

    var Game = YYC.Class({Public: {
      
       init: function () {
                 //初始化游戏全局状态
                window.gameState = window.bomberConfig.game.state.NORMAL;


                window.subject = new YYC.Pattern.Subject();

                this.sleep = 1000 / director.getFps();

                //初始化游戏场景

                this._createScene();
                this._addElements();
                this._initLayer();
                this._initEvent();

                window.subject.subscribe(this.scene.getLayer("mapLayer"), this.scene.getLayer("mapLayer").changeSpriteImg);
            },
            //管理游戏状态
            judgeGameState: function () {}
        }
    });

    var game = new Game();

    director.init = function () {
        game.init();

        //设置场景
        this.setScene(game.scene);
    };
    director.onStartLoop = function () {
        game.judgeGameState();
    };
}());

引擎Scene

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

分析问题

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

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

具体实施

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

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

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

引擎Scene

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

引擎Director删除钩子方法

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

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

炸弹人Scene

 var Scene = YYC.Class(YE.Scene, {
    Private: {
        _sleep: 0,

        _addLayer: function () {
            this.addLayer("mapLayer", layerFactory.createMap());
            this.addLayer("enemyLayer", layerFactory.createEnemy(this._sleep));
            this.addLayer("playerLayer", layerFactory.createPlayer(this._sleep));
            this.addLayer("bombLayer", layerFactory.createBomb());
            this.addLayer("fireLayer", layerFactory.createFire());
        },
        _addElements: function () {
            var mapLayerElements = this._createMapLayerElement(),
                playerLayerElements = this._createPlayerLayerElement(),
                enemyLayerElements = this._createEnemyLayerElement();

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

改造引擎Director类

修改了引擎Director和引擎Scene的钩子方法后,需要对应修改这些钩子方法的调用机制。
当前设计
在修改前先来看下引擎Main、Director、Scene以及炸弹人Game之间关于场景的交互机制:
提炼游戏引擎系列:第二次迭代(上)

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

引擎Main

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

炸弹人Game

_createScene: function () {
    this.scene = new YE.Scene();},
…
init: function () {this. _createScene();}

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

    //调用引擎Director的setScene方法,设置当前场景
    this.setScene(game.scene);
};

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

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

炸弹人Game

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

引擎Director

start:function(){//启动主循环
    window.requestNextAnimationFrame(function (time) {
        self._run(time);
    });},
…
_run: function (time) {
    var self = this;
    //主循环逻辑在_loopBody方法中
    this._loopBody(time);

    if (this._gameState === GameState.STOP) {
        return;
    }

    window.requestNextAnimationFrame(function (time) {
        self._run(time);
    });
},
_loopBody: function (time) {//触发自己的onStartLoop和onEndLoop钩子
    this.onStartLoop();this.onEndLoop();
},

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

修改后的场景的交互机制序列图
提炼游戏引擎系列:第二次迭代(上)

引擎Main

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

引擎Director

            start: function (scene) {
                var self = this;

                //触发场景的init钩子
                scene.init();
                //设置为当前场景
                this.setScene(scene);},

引擎Director

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

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

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

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

runWithScene:function(scene){}

引擎Main

            _prepare: function () {this._imgLoader.onload_game = function () {
                    var director = YE.Director.getInstance();

                    //改为调用引擎Director的runWithScene方法
                    director.runWithScene(new Scene());
                };
            }

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

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

            _prepare: function () {this. _imgLoader.onload_game = function () {
                    var director = YE.Director.getInstance();   

                    //创建并注入炸弹人Scene实例
                    director.runWithScene (new Scene());    
                };
            }

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

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

删除引擎ImgLoader的onload_game钩子

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

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

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

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

引擎Main

         Virtual:{
                loadResource: function () {
                }
            }

引擎Scene

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

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

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

            _run: function (time) {
                var self = this;

                this._loopBody(time);
                //如果游戏状态为STOP,则退出主循环
                if (this._gameState === YE.Director.GameState.STOP) {
                    return;
                }

                window.requestNextAnimationFrame(function (time) {
                    self._run(time);
                });
            },
…
            stop: function () {
                this._gameStatus = GameStatus.STOP;
            }

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

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

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

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

            firstIndex = 1;
            num = firstIndex + timerNum;    //循环次数

            for (i = firstIndex; i < num; i++) {
                window.clearTimeout(i);
            }
            for (i = firstIndex; i < num; i++) {
                window.clearInterval(i);
            }
        }
    }

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

修改后代码
引擎Tool

      /**
 * 清空序号在index前后timerNum范围中的定时器
 * @param index 定时器序号
 */
        clearAllTimer: function (index) {
            var i = 0,
                num = 0,
                timerNum = 250, 
                firstIndex = 0;

            //获得最小的定时器序号
            firstIndex = (index - timerNum >= 1) ? (index - timerNum) : 1;
            //循环次数
            num = firstIndex + timerNum * 2;    

            for (i = firstIndex; i < num; i++) {
                window.clearTimeout(i);
            }
            for (i = firstIndex; i < num; i++) {
                window.clearInterval(i);
            }
    }

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

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

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

            explode: function (bomb) {
…

                index = setTimeout(function () {}, 300);

                //保存定时器序号
                YE.Director.getInstance().setTimerIndex(index);
            },

重构后的领域模型

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

修改Scene

删除change方法

当前设计

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

引擎Scene

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

引擎Director

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

分析问题

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

具体实施

引擎Scene

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

引擎Director

        _loopBody: function (time) {//不再调用场景的change方法了

                this._scene.run();},

不应该关联引擎Sprite

当前设计

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

引擎Scene

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

炸弹人Scene

            _addElements: function () {
                var mapLayerElements = this._createMapLayerElement(),
                    playerLayerElements = this._createPlayerLayerElement(),
                    enemyLayerElements = this._createEnemyLayerElement();

                this.addSprites("mapLayer", mapLayerElements);
                this.addSprites("playerLayer", playerLayerElements);
                this.addSprites("enemyLayer", enemyLayerElements);
            },

分析问题

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

当前设计造成了引擎Scene关联引擎Sprite,应该去掉两者的关联:
提炼游戏引擎系列:第二次迭代(上)

具体实施

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

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

修改Layer

封装画布操作

当前设计

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

引擎Layer

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

炸弹人BombLayer

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

                $("#bombLayerCanvas").css(css);
            },

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

引擎Layer

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

炸弹人BombLayer

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

分析问题

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

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

具体实施

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

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

封装canvas

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

Private:{
    __canvas: null,},

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

相关代码

引擎Layer

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

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

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

修改后

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

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

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

炸弹人EnemyLayer

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

炸弹人FireLayer

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

炸弹人MapLayer

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

炸弹人PlayerLayer

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

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

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

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

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

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

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

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

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

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

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

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

封装context

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

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

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

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

引擎执行层的初始化

当前设计

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

引擎Scene

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

炸弹人Scene

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

分析问题

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

具体实施

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

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

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

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

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

当前设计

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

用户代码示例:
炸弹人Scene

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

炸弹人BombLayer

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

                //用户初始化逻辑
                this.fireLayer = layers.fireLayer;}

炸弹人MoveSprite

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

                //用户初始化逻辑
                this.P_context.setPlayerState(this.__getCurrentState());}

分析问题

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

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

具体实施

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

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

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

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

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

相关代码

引擎Scene

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

            //*钩子
            Virtual: {
                initData: function(){
                },

引擎Layer

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

                this.__getContext();
                this.__initCanvas();
            },
            Virtual: {
                initData: function (layers) {
                },

引擎Sprite

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

                this.initData();
            },
…
            Virtual: {
                initData: function () {
                },

用户代码示例:
炸弹人Scene

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

炸弹人BombLayer

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

炸弹人MoveSprite

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

clear方法只负责清除画布

当前设计

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

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

用户代码示例:
炸弹人BombLayer

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

分析问题

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

具体实施

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

引擎Layer

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

炸弹人BombLayer

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

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

当前设计

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

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

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

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

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

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

分析问题

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

具体实施

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

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

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

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

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

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

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

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

封装run方法

当前设计

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

引擎Director

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

                this._loopBody(time);window.requestNextAnimationFrame(function (time) {
                    self._run(time);
                });
            },

引擎Scene

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

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

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

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

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

炸弹人FireLayer

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

炸弹人MapLayer

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

炸弹人CharacterLayer

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

炸弹人EnemyLayer

            run: function () {
                if (this.collideWithPlayer()) {
                    window.gameState = window.bomberConfig.game.state.OVER;
                    return;
                }

                this.__getPath();

                //调用Character->run
                this.base();
            }

炸弹人PlayerLayer

            run: function () {
                if (keyState[YE.Event.KeyCodeMap.SPACE]) {
                    this.createAndAddBomb();
                    keyState[YE.Event.KeyCodeMap.SPACE] = false;
                }

                //调用Character->run
                this.base();
            }

分析问题

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

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

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

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

炸弹人MapLayer

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

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

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

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

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

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

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

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

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

增加onAfterDraw钩子方法

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

具体实施

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

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

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

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

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

炸弹人EnemyLayer

            run: function () {
                if (this.collideWithPlayer()) {
                    window.gameState = window.bomberConfig.game.state.OVER;
                    return;
                }

                this.__getPath();

                //调用Character的run方法
                this.base();
            }

炸弹人PlayerLayer

            run: function () {
                if (keyState[YE.Event.KeyCodeMap.SPACE]) {
                    this.createAndAddBomb();
                    keyState[YE.Event.KeyCodeMap.SPACE] = false;
                }

                //调用Character的run方法
                this.base();
            }

增加onStartLoop、onEndLoop钩子

当前设计

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

分析问题

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

具体实施

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

引擎Layer

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

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

            run: function () {
                this.__iterator("onStartLoop");

                this.__iterator("run");
                this.__iterator("change");

                this.__iterator("onEndLoop");
            },

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

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

炸弹人EnemyLayer

            onStartLoop: function () {
                if (this.collideWithPlayer()) {
                    window.gameState = window.bomberConfig.game.state.OVER;
                    return;
                }

                this.__getPath();
                //调用CharacterLayer的onStartLoop
                this.base();
            }

炸弹人PlayerLayer

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

将P_isNorml、P_isChange改成私有方法

分析问题

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

具体实施

引擎Layer

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

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

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

当前设计

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

        Abstract: {draw: function () {
            },

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

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

分析问题

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

具体实施

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

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

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

当前设计

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

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

引擎Layer

        Abstract: {
            change: function () {
            }
        }

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

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

分析问题

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

具体实施

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

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

引擎Layer

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

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

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

思考

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

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

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

修改Sprite

引擎执行精灵的初始化

当前设计

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

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

                //执行敌人精灵的初始化
                enemy.init();
                enemy2.init();},

分析问题

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

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

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

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

具体实施

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

        namespace("YE").Layer = YYC.AClass(YE.Collection, {addChilds: function (elements) {
                this.base(elements);

                elements.forEach(function(e){
                    //执行精灵的初始化
                    e.init();   
                });
            },

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

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

引擎Sprite

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

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

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

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

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

当前设计

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

炸弹人Scene

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

炸弹人CharacterLayer

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

引擎Sprite

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

引擎Animation

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

分析问题

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

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

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

因此应该由引擎Sprite负责。

具体实施

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

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

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

本文源码下载

GitHub

参考资料

炸弹人游戏系列

上一篇博文

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

下一篇博文

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