炸彈人游戲開發系列(6):實現碰撞檢測,設置移動步長
前言
上文中我們實現了“玩家控制炸彈人”的功能,本文將實現碰撞檢測,讓炸彈人不能穿過墻。在實現的過程中會發現炸彈人移動的問題,然后會通過設置移動步長來解決。
說明
名詞解釋
- 具體狀態類
指應用于炸彈人移動狀態的狀態模式的ConcreState角色的類。這里具體包括WalkLeftState、WalkRightState、WalkUpState、WalkDownState、StandLeftState等類。
本文目的
實現碰撞檢測
本文主要內容
回顧上文更新后的領域模型

對領域模型進行思考
重構PlayerSprite
重構前代碼
(function () { var PlayerSprite = YYC.Class({ //供子類構造函數中調用 Init: function (data) { this.x = data.x; this.y = data.y; this.minX = data.minX; this.maxX = data.maxX; this.minY = data.minY; this.maxY = data.maxY; this.defaultAnimId = data.defaultAnimId; this.anims = data.anims; this.walkSpeed = data.walkSpeed; this._context = new Context(this); }, Private: { _context: null, _setCoordinate: function (deltaTime) { this.x = this.x + this.speedX * deltaTime; this.y = this.y + this.speedY * deltaTime; this._limitMove(); }, _limitMove: function () { this.x = Math.max(this.minX, Math.min(this.x, this.maxX)); this.y = Math.max(this.minY, Math.min(this.y, this.maxY)); }, _getCurrentState: function () { var currentState = null; switch (this.defaultAnimId) { case "stand_right": currentState = Context.standRightState; break; case "stand_left": currentState = Context.standLeftState; break; case "stand_down": currentState = Context.standDownState; break; case "stand_up": currentState = Context.standUpState; break; case "walk_down": currentState = Context.walkDownState; break; case "walk_up": currentState = Context.walkUpState; break; case "walk_right": currentState = Context.walkRightState; break; case "walk_left": currentState = Context.walkLeftState; break; default: throw new Error("未知的狀態"); break; }; return currentState; } }, Public: { //精靈的坐標 x: 0, y: 0, //精靈的速度 walkSpeed: 0, speedX: 0, speedY: 0, //精靈的坐標區間 minX: 0, maxX: 9999, minY: 0, maxY: 9999, anims: null, //默認的Animation的Id , string類型 defaultAnimId: null, //當前的Animation. currentAnim: null, init: function () { this._context.setPlayerState(this._getCurrentState()); //設置當前Animation this.setAnim(this.defaultAnimId); }, //重置當前幀 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, //設置當前Animation, 參數為Animation的id, String類型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, // 更新精靈當前狀態. update: function (deltaTime) { //每次循環,改變一下繪制的坐標 this._setCoordinate(deltaTime); if (this.currentAnim) { this.currentAnim.update(deltaTime); } }, draw: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight); } }, clear: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } }, handleNext: function () { this._context.walkLeft(); this._context.walkRight(); this._context.walkUp(); this._context.walkDown(); this._context.stand(); } } }); window.PlayerSprite = PlayerSprite; }());
handleNext改名為changeDir
反思handleNext方法。從方法名來看,它的職責應該為處理本次循環的所有邏輯。然而,經過數次重構后,現在handleNext的職責只是調用狀態類的方法,更具體的來說,它的職責為判斷和設置炸彈人移動方向。
因此,應該將handleNext改名為changeDir,從而能夠反映出它的職責。
從update方法中分離出move方法
再來審視update方法,發現它有兩個職責:
- 更新坐標
- 更新動畫
進一步思考,此處“更新坐標”的職責更抽象地來說應該為"炸彈人移動“的職責。應該將其提出,形成move方法。然后去掉”__setCoordinate“方法,將其代碼直接寫到move方法中
刪除deltaTime
_setCoordinate: function (deltaTime) { this.x = this.x + this.speedX * deltaTime; this.y = this.y + this.speedY * deltaTime; this._limitMove(); },
這里deltaTime其實沒有什么作用,因此將其刪除。
重構后相關代碼
PlayerSprite
update: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } }, draw: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight); } }, clear: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } }, move: function () { this.x = this.x + this.speedX; this.y = this.y + this.speedY; this._limitMove(); }, changeDir: function () { this._context.walkLeft(); this._context.walkRight(); this._context.walkUp(); this._context.walkDown(); this._context.stand(); }
要對應修改PlayerLayer
__changeDir: function () { this.___iterator("changeDir"); }, ___move: function () { this.___iterator("move"); }, ... render: function () { if (this.P__isChange()) { this.clear(this.P__context); this.__changeDir(); this.___move(); this.___update(); this.draw(this.P__context); this.P__setStateNormal(); } }
分離speedX/speedY屬性的語義,提出“方向向量”概念dirX/dirY
狀態類WalkLeftState
walkLeft: function () { var sprite = null; if (window.keyState[keyCodeMap.A] === true) { sprite = this.P_context.sprite; sprite.speedX = -sprite.walkSpeed; sprite.speedY = 0; sprite.setAnim("walk_left"); } },
目前是通過在具體狀態類中改變speedX/speedY的正負(如+sprite.walkSpeed或-sprite.walkSpeed),來實現炸彈人移動方向的改變。因此,我發現speedX/speedY屬性實際上有兩個語義:
- 炸彈人移動速度
- 炸彈人移動方向
這樣會造成speed語義混淆,不便于閱讀和維護。因此,將“炸彈人移動方向”提出來,形成新的屬性dirX/dirY,而speedX/speedY則保留“炸彈人移動速度”語義。
重構后相關代碼
PlayerSprite
dirX: 0, dirY: 0, ... move: function () { this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; this._limitMove(); },
WalkLeftState(其它具體狀態類也要做類似的修改)
walkLeft: function () { var sprite = null; if (window.keyState[keyCodeMap.A] === true) { sprite = this.P_context.sprite; sprite.dirX = -1; sprite.dirY = 0; sprite.setAnim("walk_left"); } },
開發策略
首先查閱相關資料,確定碰撞檢測的方法,然后再實現炸彈人與地圖磚墻的碰撞檢測。
初步實現碰撞檢測
提出“碰撞檢測”的概念
在第2篇博文中提出了“碰撞檢測”的概念:
用于檢測炸彈人與磚墻、炸彈人與怪物等之間的碰撞。碰撞檢測包括矩形碰撞、多邊形碰撞等,一般使用矩形碰撞即可。
此處我采用矩形碰撞檢測。
增加地形數據TerrainData
首先,我們需要一個存儲地圖中哪些區域能夠通過,哪些區域不能通過的數據結構。
通過參考地圖數據mapData,我決定數據結構選用二維數組,且地形數組與地圖數組一一對應。
相關代碼
地圖數據MapData
(function () { var ground = bomberConfig.map.type.GROUND, wall = bomberConfig.map.type.WALL; var mapData = [ [ground, wall, ground, ground], [ground, ground, ground, ground], [ground, wall, ground, ground], [ground, wall, ground, ground] ]; window.mapData = mapData; }());
地形數據TerrainData
//地形數據 (function () { //0表示可以通過,1表示不能通過 var terrainData = [ [0, 1, 0, 0], [0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0] ]; window.terrainData = terrainData; }());
重構TerrainData
受到MapData的啟示,可以在Config中加入地形數據的枚舉值(pass、stop),然后直接在TerrainData中使用枚舉值。這樣做有以下的好處:
- 增強可讀性
- 枚舉值放到Config中,方便統一管理
相關代碼
Config
map: { ... terrain: { pass: 0, stop: 1 } },
TerrainData
//地形數據 (function () { var pass = bomberConfig.map.terrain.pass, stop = bomberConfig.map.terrain.stop; var terrainData = [ [pass, stop, pass, pass], [pass, pass, pass, pass], [pass, stop, pass, pass], [pass, stop, pass, pass] ]; window.terrainData = terrainData; }());
在PlayerSprite中實現矩形碰撞檢測
實現checkCollideWithMap方法:
_checkCollideWithMap: function () { var i1 = Math.floor((this.y) / bomberConfig.HEIGHT), i2 = Math.floor((this.y + bomberConfig.player.IMGHEIGHT - 1) / bomberConfig.HEIGHT), j1 = Math.floor((this.x) / bomberConfig.WIDTH), j2 = Math.floor((this.x + bomberConfig.player.IMGWIDTH - 1) / bomberConfig.WIDTH), terrainData = window.terrainData, pass = bomberConfig.map.terrain.pass, stop = bomberConfig.map.terrain.stop; if (terrainData[i1][j1] === pass && terrainData[i1][j2] === pass && terrainData[i2][j1] === pass && terrainData[i2][j2] === pass) { return false; } else { return true; } },
在move中判斷:
move: function () { var origin_x = this.x, origin_y = this.y; this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; this._limitMove(); if (this._checkCollideWithMap()) { this.x = origin_x; this.y = origin_y; } },
領域模型

設置移動步長
發現問題
如果炸彈人每次移動0.2個方格,炸彈人想通過兩個障礙物之間的空地,則炸彈人所在矩形區域必須與空地區域平行時才能通過。這通常導致玩家需要調整多次才能順利通過。
如圖所示:

不能通過

可以通過
引入”移動步長“概念
結合參考資料”html5游戲開發-零基礎開發RPG游戲-開源講座(二)-跑起來吧英雄“,這里可以引出“移動步長”的概念:
即炸彈人一次移動一個地圖方格(炸彈人一次會移動多步)。即如果一個方格長為10px,而游戲每次主循環輪詢時炸彈人移動2px,則炸彈人一次需要移動5步。在炸彈人的一個移動步長完成之前,玩家不能操作炸彈人,直到炸彈人完成一個移動步長(即移動了一個方格),玩家才能操作炸彈人。
實現移動步長
提出概念
這里先提出以下概念:
- step
移動步數,炸彈人移動一個方格需要的步數
- completeOneMove(該標志會在后面重構中被刪除)
炸彈人完成一個移動步長的標志
- moving
炸彈人正在移動的標志
- moveIndex
炸彈人在一次移動步長中已經移動的次數
具體實現
首先在游戲開始時,計算一次炸彈人移動一個方格需要的步數;然后在移動前,先判斷是否完成一次移動步長,如果正在移動且沒有完成一次步長,則moveIndex加1;在移動后,判斷該次移動是否完成移動步長,并相應更新移動標志和moveIndex。
重構
將“moveIndex加1”移到狀態類中
具體狀態類的職責為:負責本狀態的邏輯以及決定狀態過渡。“moveIndex加1”這個職責屬于“本狀態的邏輯”,因此應該將其移到具體狀態類中,封裝為addIndex方法。
將按鍵判斷移到PlayerSprite中
“按鍵判斷”是狀態轉換事件的判斷,這里因為炸彈人不同狀態轉換為同一狀態的觸發事件相同,所以可以將其移到上一層的客戶端(調用具體狀態類的地方)中,即移到PlayerSprite的changeDir方法中。具體分析詳見Javascript設計模式之我見:狀態模式中的“將觸發狀態的事件判斷移到Warrior類中”。
相關代碼
PlayerSprite
... _computeCoordinate: function () { this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; this._limitMove(); //因為移動次數是向上取整,可能會造成移動次數偏多(如stepX為2.5,取整則stepX為3), //坐標可能會偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整數倍), //因此此處需要向下取整。 if (this.completeOneMove) { this.x -= this.x % bomberConfig.WIDTH; this.y -= this.y % bomberConfig.HEIGHT; } }, //計算移動次數 _computeStep: function () { this.stepX = Math.ceil(bomberConfig.WIDTH / this.speedX); this.stepY = Math.ceil(bomberConfig.HEIGHT / this.speedY); }, _allKeyUp: function () { return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false; }, _judgeCompleteOneMoveByIndex: function () { if (!this.moving) { return; } if (this.moveIndex_x >= this.stepX) { this.moveIndex_x = 0; this.completeOneMove = true; } else if (this.moveIndex_y >= this.stepY) { this.moveIndex_y = 0; this.completeOneMove = true; } else { this.completeOneMove = false; } }, _judgeAndSetDir: function () { if (window.keyState[keyCodeMap.A] === true) { this._context.walkLeft(); } else if (window.keyState[keyCodeMap.D] === true) { this._context.walkRight(); } else if (window.keyState[keyCodeMap.W] === true) { this._context.walkUp(); } else if (window.keyState[keyCodeMap.S] === true) { this._context.walkDown(); } } ... //一次移動步長中的需要移動的次數 stepX: 0, stepY: 0, //一次移動步長中已經移動的次數 moveIndex_x: 0, moveIndex_y: 0, //是否正在移動標志 moving: false, //完成一次移動標志 completeOneMove: false, init: function () { this._context.setPlayerState(this._getCurrentState()); this._computeStep(); this.setAnim(this.defaultAnimId); }, ... move: function () { this._judgeCompleteOneMoveByIndex(); this._computeCoordinate(); }, changeDir: function () { if (!this.completeOneMove && this.moving) { this._context.addIndex(); return; } if (this._allKeyUp()) { this._context.stand(); } else { this._judgeAndSetDir(); } }
...
Context
(function () { var Context = YYC.Class({ Init: function (sprite) { this.sprite = sprite; }, Private: { _state: null }, Public: { sprite: null, setPlayerState: function (state) { this._state = state; this._state.setContext(this); }, walkLeft: function () { this._state.walkLeft(); }, walkRight: function () { this._state.walkRight(); }, walkUp: function () { this._state.walkUp(); }, walkDown: function () { this._state.walkDown(); }, stand: function () { this._state.stand(); }, addIndex: function () { this._state.addIndex(); } }, Static: { walkLeftState: new WalkLeftState(), walkRightState: new WalkRightState(), walkUpState: new WalkUpState(), walkDownState: new WalkDownState(), standLeftState: new StandLeftState(), standRightState: new StandRightState(), standUpState: new StandUpState(), standDownState: new StandDownState() } }); window.Context = Context; }());
WalkLeftState(此處只舉一個狀態類說明,其它狀態類與該類類似):
...
walkLeft: function () { var sprite = this.P_context.sprite; sprite.dirX = -1; sprite.dirY = 0; sprite.setAnim("walk_left"); sprite.moving = true; this.addIndex(); }, addIndex: function () { this.P_context.sprite.moveIndex_x += 1; }
...
繼續完成碰撞檢測
對地圖障礙物檢測進行了修改,并將碰撞檢測和邊界檢測移到具體狀態類中。
相關代碼
WalkLeftState(此處只舉一個狀態類說明,其它狀態類與該類類似)
... walkLeft: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_left"); if (!this.checkPassMap()) { sprite.moving = false; sprite.dirX = 0; return; } sprite.dirX = -1; sprite.dirY = 0; sprite.moving = true; this.addIndex(); }, ... //檢測是否可通過該地圖。可以通過返回true,不能通過返回false checkPassMap: function () { return !this.checkCollideWithBarrier(); }, checkCollideWithBarrier: function () { var pass = bomberConfig.map.terrain.pass, stop = bomberConfig.map.terrain.stop; //計算目的地地形數組下標 var target_x = this.P_context.sprite.x / bomberConfig.WIDTH - 1, target_y = this.P_context.sprite.y / bomberConfig.HEIGHT; //超出邊界 if (target_x >= terrainData.length || target_y >= terrainData[0].length) { return true; } if (target_x < 0) { return true; } //碰撞 if (window.terrainData[target_y][target_x] === stop) { return true; } return false; } ...
重構
重構PlayerSprite
將move移到狀態類中
PlayerSprite的move方法負責炸彈人的移動,其應該屬于具體狀態類的職責(負責本狀態的邏輯),故將PlayerSprite的move移到具體狀態類中。
進一步分析
將PlayerSprite的move移到具體狀態類中,從職責上來進一步分析,實質是將“炸彈人移動”的職責分散到各個具體狀態類中了(如WalkLeftState、WalkRightState只負責X方向的移動,WalkUpState、WalkDownState只負責Y方向的移動)
優點
增加了細粒度的控制。可以控制各個具體狀態類下炸彈人的移動。
缺點
不好統一管理。當想修改“炸彈人移動”的邏輯時,可能需要修改每個具體狀態類的move。
不過這個缺點可以在后面的提取具體狀態類的基類的重構中解決。因為該重構會將具體狀態類中“炸彈人移動”的職責匯聚到基類中。
重構addIndex
現在PlayerSprite -> changeDir中不用調用addIndex方法了,可以直接在具體狀態類的move方法中調用。
這樣做的好處是具體狀態類不用再公開addIndex方法了,而是將其私有化。
為什么把公有方法addIndex改為私有方法比較好?
這是因為改動一個類的私有成員時,只會影響到該類,而不會影響到與該類關聯的其它類;而改動公有成員則可能會影響與之關聯的其它類。特別當我們是在創建供別人使用的類庫時,如果發布后再來修改公有成員,會對很多人造成影響!這也是符合“高內聚低耦合”的思想。
我們應該對公有權限保持警惕的態度,能設成私有的就私有,只公開必要的接口成員。
相關代碼
PlayerSprite
move: function () { this._context.move(); },
WalkLeftState(WalkRightState與之類似)
move: function () { if (this.P_context.sprite.moving) { this.addIndex(); } this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); }, __addIndex: function(){ this.P_context.sprite.moveIndex_x += 1; }, __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (!sprite.moving) { return; } if (sprite.moveIndex_x >= sprite.stepX) { sprite.moveIndex_x = 0; sprite.completeOneMove = true; } else { sprite.completeOneMove = false; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.x = sprite.x + sprite.speedX * sprite.dirX; //因為移動次數是向上取整,可能會造成移動次數偏多(如stepX為2.5,取整則stepX為3), //坐標可能會偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整數倍), //因此此處需要向下取整。 //x、y為bomberConfig.WIDTH/bomberConfig.HEIGHT的整數倍(向下取整) if (sprite.completeOneMove) { sprite.x -= sprite.x % bomberConfig.WIDTH; } }
WalkUpState(WalkDownState與之類似)
move: function () { if (this.P_context.sprite.moving) { this.addIndex(); } this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); }, __addIndex: function(){ this.P_context.sprite.moveIndex_y += 1; }, __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (!sprite.moving) { return; } if (sprite.moveIndex_y >= sprite.stepY) { sprite.moveIndex_y = 0; sprite.completeOneMove = true; } else { sprite.completeOneMove = false; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.y = sprite.y + sprite.speedY * sprite.dirY; //因為移動次數是向上取整,可能會造成移動次數偏多(如stepX為2.5,取整則stepX為3), //坐標可能會偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整數倍), //因此此處需要向下取整。 //x、y為bomberConfig.WIDTH/bomberConfig.HEIGHT的整數倍(向下取整) if (sprite.completeOneMove) { sprite.y -= sprite.y % bomberConfig.HEIGHT; } }
重構狀態模式
讓我們來看看狀態類。
思路
我發現具體狀態類有很多重復的代碼,有些方法有很多相似之處。這促使我提煉出一個高層的共同模式。具體的方法就是提煉出基類,然后用模板模式,在子類中實現不同點。
提煉出WalkState、StandState
因此,我從WalkLeftState,WalkRightState,WalkDownState,WalkUpState中提煉出基類WalkState,從StandLeftState、StandRightState、StandDownState、StandUpState中提煉出基類StandState。
提煉出WalkState_X、WalkState_Y
我發現在WalkLeftState,WalkRightState中和WalkDownState,WalkUpState中,它們分別有共同的模式,而這共同模式不能提到WalkState中。因此,我又從WalkLeftState,WalkRightState中提煉出WalkState_X,WalkDownState,WalkUpState中提煉出WalkState_Y,然后讓WalkState_X和WalkState_Y繼承于WalkState。
狀態模式最新的領域模型

相關代碼
PlayerState
(function () { var PlayerState = YYC.AClass({ Protected: { P_context: null }, Public: { setContext: function (context) { this.P_context = context; } }, Abstract: { stand: function () { }, walkLeft: function () { }, walkRight: function () { }, walkUp: function () { }, walkDown: function () { }, move: function () { } } }); window.PlayerState = PlayerState; }());
WalkState
(function () { var WalkState = YYC.AClass(PlayerState, { Protected: { //*子類可復用的代碼 P__checkMapAndSetDir: function () { var sprite = this.P_context.sprite; this.P__setDir(); if (!this.__checkPassMap()) { sprite.moving = false; //sprite.dirX = 0; this.P__stop(); } else { sprite.moving = true; } }, Abstract: { P__setPlayerState: function () { }, //計算并返回目的地地形數組下標 P__computeTarget: function () { }, //檢測是否超出地圖邊界。 //超出返回true,否則返回false P__checkBorder: function () { }, //設置方向 P__setDir: function () { }, //停止 P__stop: function () { } } }, Private: { //檢測是否可通過該地圖。可以通過返回true,不能通過返回false __checkPassMap: function () { //計算目的地地形數組下標 var target = this.P__computeTarget(); if (this.P__checkBorder(target)) { return false; } return !this.__checkCollideWithBarrier(target); }, //地形障礙物碰撞檢測 __checkCollideWithBarrier: function (target) { var stop = bomberConfig.map.terrain.stop; //碰撞 if (window.terrainData[target.y][target.x] === stop) { return true; } return false; } }, Public: { stand: function () { this.P__setPlayerState(); this.P_context.stand(); this.P_context.sprite.resetCurrentFrame(0); this.P_context.sprite.stand = true; }, Virtual: { walkLeft: function () { this.P_context.setPlayerState(Context.walkLeftState); this.P_context.walkLeft(); this.P_context.sprite.resetCurrentFrame(0); }, walkRight: function () { this.P_context.setPlayerState(Context.walkRightState); this.P_context.walkRight(); this.P_context.sprite.resetCurrentFrame(0); }, walkUp: function () { this.P_context.setPlayerState(Context.walkUpState); this.P_context.walkUp(); this.P_context.sprite.resetCurrentFrame(0); }, walkDown: function () { this.P_context.setPlayerState(Context.walkDownState); this.P_context.walkDown(); this.P_context.sprite.resetCurrentFrame(0); } } }, Abstract: { move: function () { } } }); window.WalkState = WalkState; }());
WalkState_X
(function () { var WalkState_X = YYC.AClass(WalkState, { Protected: { }, Private: { __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (sprite.moveIndex_x >= sprite.stepX) { sprite.moveIndex_x = 0; sprite.moving = false; } else { sprite.moving = true; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.x = sprite.x + sprite.speedX * sprite.dirX; }, __roundingDown: function () { this.P_context.sprite.x -= this.P_context.sprite.x % bomberConfig.WIDTH; } }, Public: { move: function () { if (!this.P_context.sprite.moving) { this.__roundingDown(); return; } this.P_context.sprite.moveIndex_x += 1; this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); } }, Abstract: { } }); window.WalkState_X = WalkState_X; }());
WalkState_Y
(function () { var WalkState_Y = YYC.AClass(WalkState, { Protected: { }, Private: { __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (sprite.moveIndex_y >= sprite.stepY) { sprite.moveIndex_y = 0; sprite.moving = false; } else { sprite.moving = true; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.y = sprite.y + sprite.speedY * sprite.dirY; }, __roundingDown: function () { this.P_context.sprite.y -= this.P_context.sprite.y % bomberConfig.WIDTH; } }, Public: { move: function () { if (!this.P_context.sprite.moving) { this.__roundingDown(); return; } this.P_context.sprite.moveIndex_y += 1; this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); } }, Abstract: { } }); window.WalkState_Y = WalkState_Y; }());
WalkLeftState
(function () { var WalkLeftState = YYC.Class(WalkState_X, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standLeftState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH - 1, y: sprite.y / window.bomberConfig.HEIGHT }; }, P__checkBorder: function (target) { if (target.x < 0) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_left"); sprite.dirX = -1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; } }, Public: { walkLeft: function () { this.P__checkMapAndSetDir(); } } }); window.WalkLeftState = WalkLeftState; }());
WalkRightState
(function () { var WalkRightState = YYC.Class(WalkState_X, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standRightState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH + 1, y: sprite.y / window.bomberConfig.HEIGHT }; }, P__checkBorder: function (target) { if (target.x >= window.terrainData[0].length) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_right"); sprite.dirX = 1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; } }, Public: { walkRight: function () { this.P__checkMapAndSetDir(); } } }); window.WalkRightState = WalkRightState; }());
WalkDownState
(function () { var WalkDownState = YYC.Class(WalkState_Y, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standDownState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH, y: sprite.y / window.bomberConfig.HEIGHT + 1 }; }, P__checkBorder: function (target) { if (target.y >= window.terrainData.length) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_down"); sprite.dirY = 1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; } }, Private: { }, Public: { walkDown: function () { this.P__checkMapAndSetDir(); } } }); window.WalkDownState = WalkDownState; }());
WalkUpState
(function () { var WalkUpState = YYC.Class(WalkState_Y, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standUpState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH, y: sprite.y / window.bomberConfig.HEIGHT - 1 }; }, P__checkBorder: function (target) { if (target.y < 0) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_up"); sprite.dirY = -1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; } }, Public: { walkUp: function () { this.P__checkMapAndSetDir(); } } }); window.WalkUpState = WalkUpState; }());
StandState
(function () { var StandState = YYC.AClass(PlayerState, { Protected: { }, Public: { walkLeft: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkLeftState); this.P_context.walkLeft(); }, walkRight: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkRightState); this.P_context.walkRight(); }, walkUp: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkUpState); this.P_context.walkUp(); }, walkDown: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkDownState); this.P_context.walkDown(); }, move: function () { } }, Abstract: { } }); window.StandState = StandState; }());
StandLeftState
(function () { var StandLeftState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; sprite.setAnim("stand_left"); sprite.moving = false; } } }); window.StandLeftState = StandLeftState; }());
StandRightState
(function () { var StandRightState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; sprite.setAnim("stand_right"); sprite.moving = false; } } }); window.StandRightState = StandRightState; }());
StandDownState
(function () { var StandDownState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; sprite.setAnim("stand_down"); sprite.moving = false; } } }); window.StandDownState = StandDownState; }());
StandUpState
(function () { var StandUpState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; sprite.setAnim("stand_up"); sprite.moving = false; } } }); window.StandUpState = StandUpState; }());
重構PlayerSprite
changeDir改名為setDir
該方法會在游戲主循環中調用,并不會每次輪詢時都改變炸彈人移動方向,因此changDir這個方法名不合理,改為setDir更為合適。
刪除completeOneMove
現在可以不需要completeOneMove標志了,故將其刪除。
重構后的PlayerSprite
(function () { var PlayerSprite = YYC.Class({ Init: function (data) { //初始坐標 this.x = data.x; this.y = data.y; this.speedX = data.speedX; this.speedY = data.speedY; //x/y坐標的最大值和最小值, 可用來限定移動范圍. this.minX = data.minX; this.maxX = data.maxX; this.minY = data.minY; this.maxY = data.maxY; this.defaultAnimId = data.defaultAnimId; this.anims = data.anims; this.walkSpeed = data.walkSpeed; this.speedX = data.walkSpeed; this.speedY = data.walkSpeed; this._context = new Context(this); }, Private: { //狀態模式上下文類 _context: null, //更新幀動畫 _updateFrame: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } }, _computeCoordinate: function () { this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; //因為移動次數是向上取整,可能會造成移動次數偏多(如stepX為2.5,取整則stepX為3), //坐標可能會偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整數倍), //因此此處需要向下取整。 //x、y為bomberConfig.WIDTH/bomberConfig.HEIGHT的整數倍(向下取整) if (this.completeOneMove) { this.x -= this.x % bomberConfig.WIDTH; this.y -= this.y % bomberConfig.HEIGHT; } }, _getCurrentState: function () { var currentState = null; switch (this.defaultAnimId) { case "stand_right": currentState = Context.standRightState; break; case "stand_left": currentState = Context.standLeftState; break; case "stand_down": currentState = Context.standDownState; break; case "stand_up": currentState = Context.standUpState; break; case "walk_down": currentState = Context.walkDownState; break; case "walk_up": currentState = Context.walkUpState; break; case "walk_right": currentState = Context.walkRightState; break; case "walk_left": currentState = Context.walkLeftState; break; default: throw new Error("未知的狀態"); break; }; return currentState; }, //計算移動次數 _computeStep: function () { this.stepX = Math.ceil(bomberConfig.WIDTH / this.speedX); this.stepY = Math.ceil(bomberConfig.HEIGHT / this.speedY); }, _allKeyUp: function () { return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false; }, _judgeCompleteOneMoveByIndex: function () { if (!this.moving) { return; } if (this.moveIndex_x >= this.stepX) { this.moveIndex_x = 0; this.completeOneMove = true; } else if (this.moveIndex_y >= this.stepY) { this.moveIndex_y = 0; this.completeOneMove = true; } else { this.completeOneMove = false; } }, _judgeAndSetDir: function () { if (window.keyState[keyCodeMap.A] === true) { this._context.walkLeft(); } else if (window.keyState[keyCodeMap.D] === true) { this._context.walkRight(); } else if (window.keyState[keyCodeMap.W] === true) { this._context.walkUp(); } else if (window.keyState[keyCodeMap.S] === true) { this._context.walkDown(); } } }, Public: { //精靈的坐標 x: 0, y: 0, //精靈的速度 speedX: 0, speedY: 0, //精靈的坐標區間 minX: 0, maxX: 9999, minY: 0, maxY: 9999, //精靈包含的所有 Animation 集合. Object類型, 數據存放方式為" id : animation ". anims: null, //默認的Animation的Id , string類型 defaultAnimId: null, //當前的Animation. currentAnim: null, //精靈的方向系數: //往下走dirY為正數,往上走dirY為負數; //往右走dirX為正數,往左走dirX為負數。 dirX: 0, dirY: 0, //定義sprite走路速度的絕對值 walkSpeed: 0, //一次移動步長中的需要移動的次數 stepX: 0, stepY: 0, //一次移動步長中已經移動的次數 moveIndex_x: 0, moveIndex_y: 0, //是否正在移動標志 moving: false, //站立標志 //用于解決調用WalkState.stand后,PlayerLayer.render中P__isChange返回false的問題 //(不調用draw,從而仍會顯示精靈類walk的幀(而不會刷新為更新狀態后的精靈類stand的幀))。 stand: false, //設置當前Animation, 參數為Animation的id, String類型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, //重置當前幀 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, init: function () { this._context.setPlayerState(this._getCurrentState()); this._computeStep(); //設置當前Animation this.setAnim(this.defaultAnimId); }, // 更新精靈當前狀態 update: function (deltaTime) { this._updateFrame(deltaTime); }, draw: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight); } }, clear: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); //直接清空畫布區域 context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } }, move: function () { this._context.move(); }, setDir: function () { if (this.moving) { return; } if (this._allKeyUp()) { this._context.stand(); } else { this._judgeAndSetDir(); } } } }); window.PlayerSprite = PlayerSprite; }());
本文最終領域模型

高層劃分
與上文相同,沒有增加新的包
層、包

對應領域模型
- 輔助操作層
- 控件包
PreLoadImg - 配置包
Config
- 控件包
- 用戶交互層
- 入口包
Main
- 入口包
- 業務邏輯層
- 輔助邏輯
- 工廠包
BitmapFactory、LayerFactory、SpriteFactory - 事件管理包
KeyState、KeyEventManager
- 工廠包
- 游戲主邏輯
- 主邏輯包
Game
- 主邏輯包
- 層管理
- 層管理實現包
PlayerLayerManager、MapLayerManager - 層管理抽象包
- LayerManager
- 層管理實現包
- 層
- 層實現包
PlayerLayer、MapLayer - 層抽象包
Layer - 集合包
Collection
- 層實現包
- 精靈
- 精靈包
PlayerSprite、Context、PlayerState、WalkState、StandState、WalkState_X、WalkState_Y、StandLeftState、StandRightState、StandUpState、StandDownState、WalkLeftState、WalkRightState、WalkUpState、WalkDownState - 動畫包
Animation、GetSpriteData、SpriteData、GetFrames、FrameData
- 精靈包
- 輔助邏輯
- 數據操作層
- 地圖數據操作包
MapDataOperate - 路徑數據操作包
GetPath - 圖片數據操作包
Bitmap
- 地圖數據操作包
- 數據層
- 地圖包
MapData、TerrainData - 圖片路徑包
ImgPathData
- 地圖包
本文參考資料
html5游戲開發-零基礎開發RPG游戲-開源講座(二)-跑起來吧英雄
浙公網安備 33010602011771號