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


    前言

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

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

    本文目的

    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

    参考资料

    炸弹人游戏系列

    上一篇博文

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

    下一篇博文

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

  • 相关阅读:
    vim字体设置
    windows下eclipse打不开
    ubuntu的无线网无法连上
    将ubuntu系统录到u盘上
    使用UltraISO刻录系统到U盘可能会出现打不开的情况
    windows安装程序制作
    安装ubuntu双系统
    数据库插入数据时间比较
    LeetCode 189. Rotate Array
    LeetCode 228. Summary Ranges
  • 原文地址:https://www.cnblogs.com/chaogex/p/4164074.html
Copyright © 2020-2023  润新知