<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      提煉游戲引擎系列:第二次迭代(上)

      前言

      上文完成了引擎提煉的第一次迭代,搭建了引擎的整體框架,本文會進行引擎提煉的第二次迭代,進一步提高引擎的通用性,完善引擎框架。

      由于第二次迭代內容過多,因此分為上、下兩篇博文,本文為上篇。

      本文目的

      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

      參考資料

      炸彈人游戲系列

      上一篇博文

      提煉游戲引擎系列:第一次迭代

      下一篇博文

      提煉游戲引擎系列:第二次迭代(下)

      posted @ 2014-12-15 08:15  楊元超  閱讀(1411)  評論(2)    收藏  舉報
      主站蜘蛛池模板: 亚洲美女厕所偷拍美女尿尿| 性无码专区无码| 亚亚洲视频一区二区三区| 97人人添人澡人人爽超碰| 2022最新国产在线不卡a| 亚洲 自拍 另类小说综合图区| 国产成人高清亚洲综合| 欧美丰满熟妇hdxx| 自拍偷拍一区二区三区四| 少妇人妻偷人一区二区| 高清自拍亚洲精品二区| 精品无码国产不卡在线观看| 草草线在成年免费视频2| 亚洲av午夜福利大精品| 少妇人妻88久久中文字幕| 亚洲男人天堂东京热加勒比| 国产综合久久久久鬼色| 亚洲色偷偷色噜噜狠狠99| 久久久久国精品产熟女久色| 亚洲综合久久精品哦夜夜嗨| 高h纯肉无码视频在线观看| 中文人妻av高清一区二区| 精品久久精品午夜精品久久 | 亚洲 一区二区 在线| 国产精品人妻熟女男人的天堂 | 国产熟睡乱子伦视频在线播放| 国产免费又黄又爽又色毛| 青青草无码免费一二三区| 亚洲欧洲日产国码久在线| 国产成人精品亚洲资源| 亚洲va久久久噜噜噜久久狠狠| 国产99视频精品免费视频36| 亚洲高清乱码午夜电影网| 在线成人国产天堂精品av| 国产精品无码成人午夜电影 | 国内精品久久久久影院薰衣草| 亚洲伊人久久精品影院| 无码专区 人妻系列 在线| 丰满岳乱妇一区二区三区| 亚洲熟女乱色综合亚洲图片| 无码国产偷倩在线播放|