炸彈人游戲開(kāi)發(fā)系列(8):放炸彈
前言
上文中我們加入了1個(gè)敵人,使用A*算法尋路。本文會(huì)給我們的炸彈人增加放炸彈的能力。
說(shuō)明
名詞解釋
- xx類族
是指以xx為基類的繼承樹(shù)上的所有類。
本文目的
實(shí)現(xiàn)“放炸彈”功能
增加1個(gè)敵人,即一共有2個(gè)敵人追蹤炸彈人
本文主要內(nèi)容
- 開(kāi)發(fā)策略
- 顯示炸彈和火焰
- 使用觀察者模式
- 重構(gòu)
- 炸彈可以炸死炸彈人和敵人
- 移動(dòng)時(shí)放炸彈
- 放置多個(gè)炸彈
- 改變地圖
- 小結(jié)
- 加入1個(gè)敵人
- 本文最終領(lǐng)域模型
- 演示
- 本文參考資料
回顧上文更新后的領(lǐng)域模型

對(duì)領(lǐng)域模型進(jìn)行思考
Layer類族的render方法改名為run
Layer的render方法負(fù)責(zé)統(tǒng)一調(diào)用Layer的方法,在概念上屬于Actor,因此將其改名為run。
開(kāi)發(fā)策略
首先實(shí)現(xiàn)“放炸彈”功能。把這個(gè)功能分解成很多個(gè)子功能,一個(gè)一個(gè)地實(shí)現(xiàn)子功能。
然后再加入1個(gè)敵人。實(shí)際上就是在Game中往EnemyLayer集合中再加入一個(gè)EnemySprite實(shí)例,SpriteData增加第2個(gè)敵人的數(shù)據(jù),SpriteFactory增加工廠方法createEnemy2。
放炸彈流程

功能分解

顯示炸彈和火焰
顯示炸彈
首先來(lái)實(shí)現(xiàn)“地圖上顯示炸彈”的功能,目前最多顯示1個(gè)炸彈,玩家、敵人不能穿過(guò)炸彈。如果玩家處于炸彈方格中,則敵人會(huì)原地等待,玩家離開(kāi)后,敵人繼續(xù)追蹤。
增加圖片
增加圖片bomb.png:

增加BomberSprite
增加炸彈精靈類BomberSprite:
(function () { var BombSprite = YYC.Class(Sprite, { Init: function (data, bitmap) { this.base(null, bitmap); }, Public: { draw: function (context) { context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height); }, clear: function (context) { context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } } }); window.BombSprite = BombSprite; }());
增加BombLayer
在畫(huà)布上增加炸彈層。同時(shí)增加對(duì)應(yīng)的BombLayer類,它的集合元素為BombSprite類的實(shí)例。
將玩家、敵人畫(huà)布Canvas的zIndex設(shè)為3,炸彈畫(huà)布的zIndex設(shè)為1,使得,炸彈畫(huà)布位于地圖畫(huà)布(zIndex為0)之上,玩家和敵人畫(huà)布之下。
BomberLayer
(function () { var BombLayer = YYC.Class(Layer, { Private: { ___hasBomb: function () { return this.getChilds().length > 0; }, ___render: function () { if (this.___hasBomb()) { this.clear(); this.draw(); } } }, Public: { 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); }, draw: function () { this.P__iterator("draw", this.P__context); }, clear: function () { this.P__iterator("clear", this.P__context); }, run: function () { this.___render(); } } }); window.BombLayer = BombLayer; }());
增加工廠方法
SpriteFactory增加創(chuàng)建炸彈精靈類實(shí)例的工廠方法。
LyaerFactory增加創(chuàng)建炸彈層實(shí)例的工廠方法。
SpriteFactory
createBomb: function (playerSprite) { return new BombSprite(playerSprite, bitmapFactory.createBitmap({ img: window.imgLoader.get("bomb"), width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT })); },
LayerFactory
createBomb: function () { return new BombLayer(); },
修改PlayerSprite
PlayerSprite增加createBomb方法:
bombNum: 0, ... createBomb: function () { if (this.moving || this.bombNum === 1) { return null; } var bomb = spriteFactory.createBomb(); bomb.x = this.x; bomb.y = this.y; this.bombNum += 1; return bomb; }
修改PlayerLayer
PlayerLayer增加getBomb和createAndAddBomb方法:
bombLayer: null, ... getBomb: function (bombLayer) { this.bombLayer = bombLayer; }, createAndAddBomb: function () { var bomb = this.getChildAt(0).createBomb(); if (!bomb) { return false; } this.bombLayer.appendChild(bomb); }
監(jiān)聽(tīng)空格鍵
空格鍵用于炸彈人放炸彈。
KeyCodeMap增加空格鍵枚舉值:
var keyCodeMap = { Left: 65, // A鍵 Right: 68, // D鍵 Down: 83, // S鍵 Up: 87, // W鍵 Space: 32 //空格鍵 }; keyState[keyCodeMap.A] = false; keyState[keyCodeMap.D] = false; keyState[keyCodeMap.W] = false; keyState[keyCodeMap.S] = false; keyState[keyCodeMap.Space] = false;
然后在PlayerLayer中對(duì)KeyState的空格鍵進(jìn)行判定:
run: function () { if (keyState[keyCodeMap.Space]) { this.createAndAddBomb(); keyState[keyCodeMap.Space] = false; } this.base(); }
領(lǐng)域模型

顯示火焰
火力范圍設(shè)為1格,分為上下左右四個(gè)方向。地圖的墻對(duì)火焰有阻斷作用。
增加圖片
爆炸中心為圖片boom.png:

火焰為圖片explode.png:

增加FireSprite
增加火焰精靈類。
增加FireLayer
在畫(huà)布上增加火焰畫(huà)布,同時(shí)對(duì)應(yīng)的FireLayer類。
該畫(huà)布位于地圖和炸彈畫(huà)布之上,玩家和敵人畫(huà)布之下。
增加工廠方法
SpriteFactory增加創(chuàng)建爆炸中心火焰精靈類實(shí)例和創(chuàng)建火焰精靈類實(shí)例的工廠方法。
LayerFactory增加創(chuàng)建火焰層實(shí)例的工廠方法。
領(lǐng)域模型

相關(guān)代碼
Sprite
(function () { var Sprite = YYC.AClass({ Init: function (data, bitmap) { this.bitmap = bitmap; if (data) { this.x = data.x; this.y = data.y; this.defaultAnimId = data.defaultAnimId; this.anims = data.anims; } }, Private: { //更新幀動(dòng)畫(huà) _updateFrame: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } } }, Public: { bitmap: null, //精靈的坐標(biāo) x: 0, y: 0, //精靈包含的所有 Animation 集合. Object類型, 數(shù)據(jù)存放方式為" id : animation ". anims: null, //默認(rèn)的Animation的Id , string類型 defaultAnimId: null, //當(dāng)前的Animation. currentAnim: null, //設(shè)置當(dāng)前Animation, 參數(shù)為Animation的id, String類型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, //重置當(dāng)前幀 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, //取得精靈的碰撞區(qū)域, getCollideRect: function () { return { x1: this.x, y1: this.y, x2: this.x + this.bitmap.width, y2: this.y + this.bitmap.height } }, Virtual: { //初始化方法 init: function () { //設(shè)置當(dāng)前Animation this.setAnim(this.defaultAnimId); }, // 更新精靈當(dāng)前狀態(tài). update: function (deltaTime) { this._updateFrame(deltaTime); }, //獲得坐標(biāo)對(duì)應(yīng)的方格坐標(biāo) getCellPosition: function (x, y) { return { x: x / bomberConfig.WIDTH, y: y / bomberConfig.HEIGHT } }, draw: function (context) { context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height); }, clear: function (context) { //直接清空畫(huà)布區(qū)域 context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } } }, Abstract: { } }); window.Sprite = Sprite; }());
FireSprite
(function () { var FireSprite = YYC.Class(Sprite, { Init: function (data, bitmap) { this.base(null, bitmap); } }); window.FireSprite = FireSprite; }());
BombSprite
(function () { var BombSprite = YYC.Class(Sprite, { Init: function (playerSprite, bitmap) { this.playerSprite = playerSprite; this.base(null, bitmap); }, Protected: { }, Private: { __createFire: function () { var fires = [], up = null, down = null, left = null, right = null; this.__createCenter(fires); this.__createUp(fires); this.__createDown(fires); this.__createLeft(fires); this.__createRight(fires); return fires; }, __createCenter: function (fires) { var center = spriteFactory.createExplode(); center.x = this.x; center.y = this.y; fires.push(center); }, __createUp: function (fires) { this.__createOneDir(fires, this.x, this.y - bomberConfig.HEIGHT); }, __createDown: function (fires) { this.__createOneDir(fires, this.x, this.y + bomberConfig.HEIGHT); }, __createLeft: function (fires) { this.__createOneDir(fires, this.x - bomberConfig.WIDTH, this.y); }, __createRight: function (fires) { this.__createOneDir(fires, this.x + bomberConfig.WIDTH, this.y); }, __createOneDir: function (fires, x, y) { var fire = null; var position = this.getCellPosition(x, y); if (this.__isNotBorder(position) && this.__isGround(position)) { fire = spriteFactory.createFire(); fire.x = x; fire.y = y; fires.push(fire); } }, __isNotBorder: function (position) { if (position.x < 0 || position.y < 0) { return false; } if (position.x >= window.mapData[0].length || position.y >= window.mapData.length) { return false; } return true; }, __isGround: function (position) { return window.mapData[position.y][position.x] === window.bomberConfig.map.type.GROUND; }, __changeTerrainData: function () { var pass = bomberConfig.map.terrain.pass, position = this.getCellPosition(this.x, this.y); window.terrainData[position.y][position.x] = pass; } }, Public: { playerSprite: null, explode: function () { this.playerSprite.bombNum -= 1; this.__changeTerrainData(); return this.__createFire(); } } }); window.BombSprite = BombSprite; }());
PlayerSprite
(function () { var PlayerSprite = YYC.Class(MoveSprite, { Init: function (data, bitmap) { this.base(data, bitmap); this.P__context = new Context(this); }, Private: { __allKeyUp: function () { return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false; }, __judgeAndSetDir: function () { if (window.keyState[keyCodeMap.A] === true) { this.P__context.walkLeft(); } else if (window.keyState[keyCodeMap.D] === true) { this.P__context.walkRight(); } else if (window.keyState[keyCodeMap.W] === true) { this.P__context.walkUp(); } else if (window.keyState[keyCodeMap.S] === true) { this.P__context.walkDown(); } }, __changeTerrainData: function () { var stop = bomberConfig.map.terrain.stop, position = this.getCurrentCellPosition(); window.terrainData[position.y][position.x] = stop; } }, Public: { //已放置的炸彈數(shù) bombNum: 0, move: function () { this.P__context.move(); }, setDir: function () { if (this.moving) { return; } if (this.__allKeyUp()) { this.P__context.stand(); } else { this.__judgeAndSetDir(); } }, createBomb: function () { if (this.moving || this.bombNum === 1) { return null; } var bomb = spriteFactory.createBomb(this); bomb.x = this.x; bomb.y = this.y; this.bombNum += 1; this.__changeTerrainData(); return bomb; } } }); window.PlayerSprite = PlayerSprite; }());
Layer
//層類(抽象類) //職責(zé): ////負(fù)責(zé)層內(nèi)組件的統(tǒng)一draw (function () { var Layer = YYC.AClass(Collection, { Init: function () { }, Private: { __state: bomberConfig.layer.state.CHANGE, //默認(rèn)為change __getContext: function () { this.P__context = this.P__canvas.getContext("2d"); } }, Protected: { //*共用的變量(可讀、寫(xiě)) P__canvas: null, P__context: null, //*共用的方法(可讀) P__isChange: function () { return this.__state === bomberConfig.layer.state.CHANGE; }, P__isNormal: function () { return this.__state === bomberConfig.layer.state.NORMAL; }, P__setStateNormal: function () { this.__state = bomberConfig.layer.state.NORMAL; }, P__setStateChange: function () { this.__state = bomberConfig.layer.state.CHANGE; }, P__iterator: function (handler) { var args = Array.prototype.slice.call(arguments, 1), nextElement = null; while (this.hasNext()) { nextElement = this.next(); nextElement[handler].apply(nextElement, args); //要指向nextElement } this.resetCursor(); }, P__render: function () { if (this.P__isChange()) { this.clear(); this.draw(); this.P__setStateNormal(); } } }, Public: { addElements: function (elements) { this.appendChilds(elements); }, Virtual: { init: function () { this.__getContext(); }, //更改狀態(tài) change: function () { this.__state = bomberConfig.layer.state.CHANGE; } } }, Abstract: { setCanvas: function () { }, clear: function () { }, //統(tǒng)一繪制 draw: function () { }, //游戲主線程調(diào)用的函數(shù) run: function () { } } }); window.Layer = Layer; }());
FireLayer
(function () { var FireLayer = YYC.Class(Layer, { Private: { ___hasFire: function(){ return this.getChilds().length > 0; } }, Public: { setCanvas: function () { this.P__canvas = document.getElementById("fireLayerCanvas"); var css = { "position": "absolute", "top": bomberConfig.canvas.TOP, "left": bomberConfig.canvas.LEFT, "z-index": 2 }; $("#fireLayerCanvas").css(css); }, draw: function () { this.P__iterator("draw", this.P__context); }, clear: function () { this.P__iterator("clear", this.P__context); }, change: function () { if (this.___hasFire()) { this.base(); } }, run: function () { this.P__render(); } } }); window.FireLayer = FireLayer; }());
BombLayer
(function () { var BombLayer = YYC.Class(Layer, { Private: { ___hasBomb: function(){ return this.getChilds().length > 0; }, ___removeBomb: function (bomb) { //*注意順序! this.clear(); this.remove(bomb); }, ___removeAllFire: function () { //*注意順序! this.fireLayer.clear(); this.fireLayer.removeAll(); } }, Public: { fireLayer: null, 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); }, draw: function () { this.P__iterator("draw", this.P__context); }, clear: function () { this.P__iterator("clear", this.P__context); }, getFire: function (fireLayer) { this.fireLayer = fireLayer; }, explode: function (bomb) { var self = this; this.fireLayer.addElements(bomb.explode()); this.___removeBomb(bomb); //定時(shí)清空f(shuō)ireLayer(火焰消失) setTimeout(function () { self.___removeAllFire(); }, 300); }, change: function(){ if (this.___hasBomb()) { this.base(); } }, run: function () { this.P__render(); } } }); window.BombLayer = BombLayer; }());
SpriteFactory
createFire: function () { return new FireSprite(null, bitmapFactory.createBitmap({ img: window.imgLoader.get("fire"), width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT })); }, createExplode: function () { return new FireSprite(null, bitmapFactory.createBitmap({ img: window.imgLoader.get("explode"), width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT })); }
LayerFactory
createFire: function () { return new FireLayer(); }
“顯示炸彈和火焰”演示
使用觀察者模式
觀察者模式介紹
詳見(jiàn)Javascript設(shè)計(jì)模式之我見(jiàn):觀察者模式。
應(yīng)用場(chǎng)景
墻被炸掉后,會(huì)變成空地。
實(shí)現(xiàn)思路
Maplayer的changeSpriteImg負(fù)責(zé)更改地圖圖片,BombSprite的explode負(fù)責(zé)處理爆炸邏輯。
需要在explode中調(diào)用Maplayer的changeSpriteImg。
因此,決定在Game中訂閱Maplayer的changeSpriteImg方法,然后在BombSprite的explode方法中發(fā)布。
為什么此處用觀察者模式
因?yàn)镸apLayer的Layer類族在BombSprite的Sprite類族的上層,我不希望下層BombSprite與上層MapLayer耦合。
因此,采用觀察者模式來(lái)解除兩者的耦合。
領(lǐng)域模型
使用觀察模式前

使用觀察模式后

相關(guān)代碼
Subject
(function () { if (!Array.prototype.forEach) { Array.prototype.forEach = function (fn, thisObj) { var scope = thisObj || window; for (var i = 0, j = this.length; i < j; ++i) { fn.call(scope, this[i], i, this); } }; } if (!Array.prototype.filter) { Array.prototype.filter = function (fn, thisObj) { var scope = thisObj || window; var a = []; for (var i = 0, j = this.length; i < j; ++i) { if (!fn.call(scope, this[i], i, this)) { continue; } a.push(this[i]); } return a; }; } Subject = function () { this._events = []; } Subject.prototype = (function () { return { //訂閱方法 subscribe: function (context, fn) { if (arguments.length == 2) { this._events.push({ context: arguments[0], fn: arguments[1] }); } else { this._events.push(arguments[0]); } }, //發(fā)布指定方法 publish: function (context, fn, args) { var args = Array.prototype.slice.call(arguments, 2); //獲得函數(shù)參數(shù) var _context = null; var _fn = null; this._events.filter(function (el) { if (el.context) { _context = el.context; _fn = el.fn; } else { _context = context; _fn = el; } if (_fn === fn) { return _fn; } }).forEach(function (el) { //指定方法可能有多個(gè) el.apply(_context, args); //執(zhí)行每個(gè)指定的方法 }); }, unSubscribe: function (fn) { var _fn = null; this._events = this._events.filter(function (el) { if (el.fn) { _fn = el.fn; } else { _fn = el; } if (_fn !== fn) { return el; } }); }, //全部發(fā)布 publishAll: function (context, args) { var args = Array.prototype.slice.call(arguments, 1); //獲得函數(shù)參數(shù) var _context = null; var _fn = null; this._events.forEach(function (el) { if (el.context) { _context = el.context; _fn = el.fn; } else { _context = context; _fn = el; } _fn.apply(_context, args); //執(zhí)行每個(gè)指定的方法 }); }, dispose: function () { this._events = []; } } })(); YYC.Pattern.Subject = Subject; })();
MapLayer
//改變指定精靈類的img對(duì)象 //參數(shù): //x:x坐標(biāo)(方格對(duì)應(yīng)值);y:y坐標(biāo)(方格對(duì)應(yīng)值);img:要替換的img對(duì)象 changeSpriteImg: function (x, y, img) { var index = y * window.bomberConfig.map.COL + x; this.getChildAt(index).bitmap.img = img; },
BombSprite
__destroyOneDir: function (x, y) { ... window.observer.publishAll(null, position.x, position.y, groundImg); ... },
Game
//觀察者全局實(shí)例 window.observer = null var Game = YYC.Class({ Init: function () { window.observer = new YYC.Pattern.Observer(); }, ... init: function () { ... //觀察者模式 -> 訂閱 window.observer.subscribe(this.layerManager.getLayer("mapLayer"), this.layerManager.getLayer("mapLayer").changeSpriteImg); },
重構(gòu)
增加TerrainDataOperate
增加TerrainData地形數(shù)據(jù)操作類TerrainDataOperate
領(lǐng)域模型
重構(gòu)前

重構(gòu)后

相關(guān)代碼
TerrainDataOperate
(function () { var terrainDataOperate = { getTerrainData: function () { return YYC.Tool.array.clone(window.terrainData); }, setTerrainData: function (x, y, data) { window.terrainData[y][x] = data; } }; window.terrainDataOperate = terrainDataOperate; }());
增加火力范圍
將范圍從1格改為2格,方便演示游戲。
增加游戲全局狀態(tài)GameState
在Game的run方法中,需要判斷敵人是否抓住了玩家(是否與玩家碰撞):
run: function () { if (this.layerManager.getLayer("enemyLayer").collideWidthPlayer()) { YYC.Tool.asyn.clearAllTimer(this.mainLoop); alert("Game Over!"); return; } ... }
這里注意到,Game需要知道EnemyLayer的collideWidthPlayer方法:

但Game類只應(yīng)該知道LayerManager,而不應(yīng)該知道Layer(見(jiàn)“炸彈人游戲開(kāi)發(fā)系列(1):準(zhǔn)備工作”中的概念層次結(jié)構(gòu))。
因此,增加游戲全局狀態(tài)GameState,在Game的run判斷GameState,然后把與炸彈人的碰撞檢測(cè)的任務(wù)放到EnemyLayer的run方法中:

重構(gòu)后相關(guān)代碼
Config
game: { state: { NORAML: 1, OVER: 2 } },
Game
//游戲全局狀態(tài) window.gameState = window.bomberConfig.game.state.NORMAL; ... run: function () { if (window.gameState === window.bomberConfig.game.state.OVER) { this.gameOver(); return; } ... } , gameOver: function () { YYC.Tool.asyn.clearAllTimer(this.mainLoop); alert("Game Over!"); }
EnemyLayer
run: function () { if (this.collideWidthPlayer()) { window.gameState = window.bomberConfig.game.state.OVER; return; }
...
炸彈可以炸死炸彈人和敵人
在炸彈爆炸時(shí),判斷與炸彈人、敵人是否碰撞并進(jìn)行相應(yīng)處理。
領(lǐng)域模型

相關(guān)代碼
BombLayer
___collideFireWithPlayer: function (bomb) { if (bomb.collideFireWithCharacter(this.playerLayer.getChildAt(0))) { window.gameState = window.bomberConfig.game.state.OVER; } }, ___collideFireWithEnemy: function (bomb) { var i = 0, len = 0, enemySprites = this.enemyLayer.getChilds(); for (i = 0, len = enemySprites.length ; i < len; i++) { if (bomb.collideFireWithCharacter(enemySprites[i])) { this.___removeEnemy(enemySprites[i]); } } }, ___removeEnemy: function (enemy) { //*注意順序! this.enemyLayer.clear(); this.enemyLayer.remove(enemy); }, ___handleCollid: function (bomb) { //判斷與炸彈人碰撞 this.___collideFireWithPlayer(bomb) //判斷與每個(gè)敵人碰撞 this.___collideFireWithEnemy(bomb); } ... enemyLayer: null, playerLayer: null, ... explode: function (bomb) { var self = this, result = null; //處理碰撞 this.___handleCollid(bomb); ...
移動(dòng)時(shí)放炸彈
因?yàn)檎◤椚艘苿?dòng)時(shí),根據(jù)炸彈人狀態(tài)的不同,炸彈放置的坐標(biāo)策略也不同(即如果炸彈人往上走,則炸彈放在炸彈人所在方格的上面相鄰方格;如果往左走,則炸彈放在炸彈人所在方格的左側(cè)相鄰方格)。所以將PlayerSprite的createBomb方法委托給狀態(tài)類處理。具體來(lái)說(shuō),就是把createBomb方法移到狀態(tài)類的WalkState類和Stand類中來(lái)分別處理。
領(lǐng)域模型

分析
因?yàn)镻layerSprite、EnemySprite都使用了狀態(tài)類,因此兩者都與BombSprite耦合。但只有PlayerSprite需要使用createBomb方法,EnemySprite并不需要使用該方法。所以此處違反了迪米特法則。
目前這種情況在可以接受的范圍之內(nèi)。如果在后面的開(kāi)發(fā)中EnemySprite與BombSprite耦合得很?chē)?yán)重,再來(lái)考慮解耦。
放置多個(gè)炸彈
可以最多放3個(gè)炸彈,炸彈爆炸時(shí)會(huì)引爆在火力范圍內(nèi)的炸彈。
不能在一個(gè)方格疊加多個(gè)炸彈
在狀態(tài)類WalkState類族、StandState類族的createBomb中判斷方格是否有炸彈(判斷地形數(shù)據(jù)TerrainData來(lái)實(shí)現(xiàn))。
改變地圖
炸掉墻
如果墻處于火焰范圍內(nèi),則修改MapData,將墻的圖片換成空地圖片,同時(shí)對(duì)應(yīng)修改TerrainData,將墻所在的方格設(shè)成可通過(guò)。
刷新地圖
在炸掉墻后,在BombLayer中需要調(diào)用MapLayer的setStateChange方法,將MapLayer的state設(shè)為change,從而能夠在游戲的下一個(gè)主循環(huán)中,刷新地圖,從而顯示為空地。
領(lǐng)域模型

相關(guān)代碼
BombLayer
___mapChange: function (mapChange) { if (mapChange) { this.mapLayer.setStateChange(); } }
小結(jié)
現(xiàn)在我們就完成了“放炸彈”的功能,來(lái)看下成果吧~
“放炸彈”演示
相關(guān)代碼
FireSprite
(function () { var FireSprite = YYC.Class(Sprite, { Init: function (data, bitmap) { this.base(null, bitmap); } }); window.FireSprite = FireSprite; }());
FireLayer
(function () { var FireLayer = YYC.Class(Layer, { Private: { ___hasFire: function(){ return this.getChilds().length > 0; } }, Public: { setCanvas: function () { this.P__canvas = document.getElementById("fireLayerCanvas"); var css = { "position": "absolute", "top": bomberConfig.canvas.TOP, "left": bomberConfig.canvas.LEFT, "z-index": 2 }; $("#fireLayerCanvas").css(css); }, draw: function () { this.P__iterator("draw", this.P__context); }, clear: function () { this.P__iterator("clear", this.P__context); }, change: function () { if (this.___hasFire()) { this.setStateChange(); } }, run: function () { this.P__render(); } } }); window.FireLayer = FireLayer; }());
PlayerSprite
(function () { var PlayerSprite = YYC.Class(MoveSprite, { Init: function (data, bitmap) { this.base(data, bitmap); this.P__context = new Context(this); }, Private: { __allKeyUp: function () { return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false; }, __judgeAndSetDir: function () { if (window.keyState[keyCodeMap.A] === true) { this.P__context.walkLeft(); } else if (window.keyState[keyCodeMap.D] === true) { this.P__context.walkRight(); } else if (window.keyState[keyCodeMap.W] === true) { this.P__context.walkUp(); } else if (window.keyState[keyCodeMap.S] === true) { this.P__context.walkDown(); } }, __changeTerrainData: function () { var stop = bomberConfig.map.terrain.stop, position = this.getCurrentCellPosition(); terrainDataOperate.setTerrainData(position.x, position.y, stop); } }, Public: { //已放置的炸彈數(shù) bombNum: 0, move: function () { this.P__context.move(); }, setDir: function () { if (this.moving) { return; } if (this.__allKeyUp()) { this.P__context.stand(); } else { this.__judgeAndSetDir(); } }, createBomb: function () { if (this.bombNum === 3) { return null; } return this.P__context.createBomb(); } } }); window.PlayerSprite = PlayerSprite; }());
BomberSprite
(function () { var BombSprite = YYC.Class(Sprite, { Init: function (playerSprite, bitmap) { this.playerSprite = playerSprite; this.base(null, bitmap); }, Protected: { }, Private: { //返回火焰范圍 //返回順序?yàn)閇center、[up]、[down]、[left]、[right]] __getFireAllRange: function () { return [ { x: this.x, y: this.y }, [ { x: this.x, y: this.y - bomberConfig.HEIGHT }, { x: this.x, y: this.y - bomberConfig.HEIGHT * 2 } ], [ { x: this.x, y: this.y + bomberConfig.HEIGHT }, { x: this.x, y: this.y + bomberConfig.HEIGHT * 2 } ], [ { x: this.x - bomberConfig.WIDTH, y: this.y }, { x: this.x - bomberConfig.WIDTH * 2, y: this.y } ], [ { x: this.x + bomberConfig.WIDTH, y: this.y }, { x: this.x + bomberConfig.WIDTH * 2, y: this.y } ] ]; }, __getCenterEffectiveRange: function (effectiveRange, center) { effectiveRange.center = { x: center.x, y: center.y }; }, __getFourDirEffectiveRange: function (effectiveRange, allRange) { var i = 0, j = 0, len1 = 0, len2 = 0, firePos = null, cellPos = null, groundRange = [], wallRange = []; for (i = 0, len1 = allRange.length; i < len1; i++) { for (j = 0, len2 = allRange[i].length; j < len2; j++) { firePos = allRange[i][j]; cellPos = this.getCellPosition(firePos.x, firePos.y); if (this.__isNotBorder(cellPos)) { if (this.__isGround(cellPos)) { groundRange.push(firePos); } else if (this.__isWall(cellPos)) { wallRange.push(firePos); break; } else { throw new Error("未知的地圖類型"); } } } } effectiveRange.groundRange = groundRange; effectiveRange.wallRange = wallRange; }, __createFire: function (effectiveRange) { var fires = []; this.__createCenter(fires, effectiveRange); this.__createFourDir(fires, effectiveRange); return fires; }, __createCenter: function (fires, effectiveRange) { var center = spriteFactory.createExplode(); center.x = effectiveRange.center.x; center.y = effectiveRange.center.y; fires.push(center); }, __createFourDir: function (fires, effectiveRange) { var i = 0, len = 0, fire = null, groundRange = effectiveRange.groundRange; for (i = 0, len = groundRange.length; i < len; i++) { fire = spriteFactory.createFire(); fire.x = groundRange[i].x; fire.y = groundRange[i].y; fires.push(fire); } }, __isNotBorder: function (position) { if (position.x < 0 || position.y < 0) { return false; } if (position.x >= window.mapData[0].length || position.y >= window.mapData.length) { return false; } return true; }, __isGround: function (position) { return window.mapDataOperate.getMapData()[position.y][position.x] === window.bomberConfig.map.type.GROUND; }, __bombPass: function () { var pass = bomberConfig.map.terrain.pass, position = this.getCellPosition(this.x, this.y); terrainDataOperate.setTerrainData(position.x, position.y, pass); }, __destroyWall: function (effectiveRange) { var i = 0, len = 0, mapChange = false, wallRange = effectiveRange.wallRange, cellPos = null, ground = bomberConfig.map.type.GROUND, groundImg = window.imgLoader.get("ground"), wall = bomberConfig.map.type.WALL, pass = bomberConfig.map.terrain.pass, stop = bomberConfig.map.terrain.stop; for (i = 0, len = wallRange.length; i < len; i++) { cellPos = this.getCellPosition(wallRange[i].x, wallRange[i].y); window.mapDataOperate.setMapData(cellPos.x, cellPos.y, ground); window.terrainDataOperate.setTerrainData(cellPos.x, cellPos.y, pass); //觀察者模式 -> 發(fā)布 //調(diào)用mapLayer.changeSpriteImg,改變地圖層對(duì)應(yīng)精靈類的img對(duì)象 window.observer.publishAll(null, cellPos.x, cellPos.y, groundImg); if (!mapChange) { mapChange = true; } } return mapChange; }, __isWall: function (position) { return window.mapDataOperate.getMapData()[position.y][position.x] === window.bomberConfig.map.type.WALL; }, __isInEffectiveRange: function (effectiveRange) { var range = null; range = effectiveRange.groundRange.concat(effectiveRange.wallRange); range.push(effectiveRange.center); if (this.__isInRange(range)) { return true; } else { return false; } }, __isInRange: function (range) { var i = 0, len = 0; for (i = 0, len = range.length; i < len; i++) { if (range[i].x === this.x && range[i].y === this.y) { return true; } } return false; } }, Public: { playerSprite: null, //是否已爆炸標(biāo)志 exploded: false, explode: function () { var fires = null, mapChange = false, effectiveRange = []; this.playerSprite.bombNum -= 1; this.exploded = true; this.__bombPass(); effectiveRange = this.getFireEffectiveRange(); fires = this.__createFire(effectiveRange); mapChange = this.__destroyWall(effectiveRange); return { fires: fires, mapChange: mapChange }; }, //檢測(cè)火焰與玩家人物、敵人的碰撞 collideFireWithCharacter: function (sprite) { var effectiveRange = this.getFireEffectiveRange(), range = [], fire = {}, obj2 = {}, i = 0, len = 0; //放到數(shù)組中 range.push(effectiveRange.center); range = range.concat(effectiveRange.groundRange, effectiveRange.wallRange); for (i = 0, len = range.length; i < len; i++) { fire = { x: range[i].x, y: range[i].y, width: this.bitmap.width, height: this.bitmap.height }; obj2 = { x: sprite.x, y: sprite.y, width: sprite.bitmap.width, height: sprite.bitmap.height }; if (YYC.Tool.collision.col_Between_Rects(fire, obj2)) { return true; } } return false; }, //返回有效范圍。(考慮墻、邊界阻擋等問(wèn)題) //返回值形如:{center: {x: 1,y: 1}}, {groundRange: [{{x: 1,y: 1}]}, {wallRange: [{{x: 1,y: 1}]} getFireEffectiveRange: function () { var effectiveRange = {}, allRange = this.__getFireAllRange(); this.__getCenterEffectiveRange(effectiveRange, allRange.shift()); this.__getFourDirEffectiveRange(effectiveRange, allRange); return effectiveRange; }, isInEffectiveRange: function (bomb) { return this.__isInEffectiveRange(bomb.getFireEffectiveRange()); } } }); window.BombSprite = BombSprite; }());
Sprite
(function () { var Sprite = YYC.AClass({ Init: function (data, bitmap) { this.bitmap = bitmap; if (data) { this.x = data.x; this.y = data.y; this.defaultAnimId = data.defaultAnimId; this.anims = data.anims; } }, Private: { //更新幀動(dòng)畫(huà) _updateFrame: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } } }, Public: { bitmap: null, //精靈的坐標(biāo) x: 0, y: 0, //精靈包含的所有 Animation 集合. Object類型, 數(shù)據(jù)存放方式為" id : animation ". anims: null, //默認(rèn)的Animation的Id , string類型 defaultAnimId: null, //當(dāng)前的Animation. currentAnim: null, //設(shè)置當(dāng)前Animation, 參數(shù)為Animation的id, String類型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, //重置當(dāng)前幀 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, //取得精靈的碰撞區(qū)域, getCollideRect: function () { var obj = { x: this.x, y: this.y, width: this.bitmap.width, height: this.bitmap.height }; return YYC.Tool.collision.getCollideRect(obj); }, Virtual: { //初始化方法 init: function () { //設(shè)置當(dāng)前Animation this.setAnim(this.defaultAnimId); }, // 更新精靈當(dāng)前狀態(tài). update: function (deltaTime) { this._updateFrame(deltaTime); }, //獲得坐標(biāo)對(duì)應(yīng)的方格坐標(biāo)(向下取值) getCellPosition: function (x, y) { return { x: Math.floor(x / bomberConfig.WIDTH), y: Math.floor(y / bomberConfig.HEIGHT) } }, draw: function (context) { context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height); }, clear: function (context) { //直接清空畫(huà)布區(qū)域 context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } } } }); window.Sprite = Sprite; }());
PlayerLayer
(function () { var PlayerLayer = YYC.Class(CharacterLayer, { Init: function (deltaTime) { this.base(deltaTime); }, Private: { ___keyDown: function () { if (keyState[keyCodeMap.A] === true || keyState[keyCodeMap.D] === true || keyState[keyCodeMap.W] === true || keyState[keyCodeMap.S] === true) { return true; } else { return false; } }, ___spriteMoving: function () { return this.getChildAt(0).moving }, ___spriteStand: function () { if (this.getChildAt(0).stand) { this.getChildAt(0).stand = false; return true; } else { return false; } } }, Public: { bombLayer: null, setCanvas: function () { this.P__canvas = document.getElementById("playerLayerCanvas"); $("#playerLayerCanvas").css({ "position": "absolute", "top": bomberConfig.canvas.TOP, "left": bomberConfig.canvas.LEFT, "border": "1px solid red", "z-index": 3 }); }, init: function (layers) { this.bombLayer = layers.bombLayer; this.base(); }, change: function () { if (this.___keyDown() || this.___spriteMoving() || this.___spriteStand()) { this.base(); } }, createAndAddBomb: function () { var bomb = this.getChildAt(0).createBomb(); var self = this; if (!bomb) { return false; } this.bombLayer.appendChild(bomb); //3s后炸彈爆炸 setTimeout(function () { if (!bomb.exploded) { self.bombLayer.explode(bomb); } }, 3000); return bomb; }, run: function () { if (keyState[keyCodeMap.Space]) { this.createAndAddBomb(); keyState[keyCodeMap.Space] = false; } this.base(); } } }); window.PlayerLayer = PlayerLayer; }());
BomberLayer
(function () { var BombLayer = YYC.Class(Layer, { Private: { ___hasBomb: function(){ return this.getChilds().length > 0; }, ___removeBomb: function (bomb) { //*注意順序! this.clear(bomb); this.remove(bomb); }, ___removeAllFire: function () { //*注意順序! this.fireLayer.clear(); this.fireLayer.removeAll(); }, ___removeEnemy: function (enemy) { //*注意順序! this.enemyLayer.clear(enemy); this.enemyLayer.remove(enemy); }, ___mapChange: function (mapChange) { if (mapChange) { this.mapLayer.setStateChange(); } }, ___collideFireWithPlayer: function (bomb) { if (bomb.collideFireWithCharacter(this.playerLayer.getChildAt(0))) { window.gameState = window.bomberConfig.game.state.OVER; } }, ___collideFireWithEnemy: function (bomb) { var i = 0, len = 0, enemySprites = this.enemyLayer.getChilds(); for (i = 0, len = enemySprites.length ; i < len; i++) { if (bomb.collideFireWithCharacter(enemySprites[i])) { this.___removeEnemy(enemySprites[i]); } } }, ___handleCollid: function (bomb) { //判斷與玩家人物碰撞 this.___collideFireWithPlayer(bomb) //判斷與每個(gè)敵人碰撞 this.___collideFireWithEnemy(bomb); }, ___explodeInEffectiveRange: function (bomb) { var eachBomb = null; this.resetCursor(); while (this.hasNext()) { eachBomb = this.next(); if (eachBomb.isInEffectiveRange.call(eachBomb, bomb)) { this.explode(eachBomb); } } this.resetCursor(); } }, Public: { fireLayer: null, mapLayer: null, playerLayer: null, enemyLayer: null, 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); }, init: function(layers){ this.fireLayer = layers.fireLayer; this.mapLayer = layers.mapLayer; this.playerLayer = layers.playerLayer; this.enemyLayer = layers.enemyLayer; this.base(); }, draw: function () { this.P__iterator("draw", this.P__context); }, explode: function (bomb) { var self = this, result = null; //處理碰撞 this.___handleCollid(bomb); result = bomb.explode(); this.fireLayer.addSprites(result.fires); this.___mapChange(result.mapChange); this.___removeBomb(bomb); //炸彈爆炸時(shí)會(huì)引爆在火力范圍內(nèi)的炸彈。 this.___explodeInEffectiveRange(bomb); //定時(shí)清空f(shuō)ireLayer(火焰消失) setTimeout(function () { self.___removeAllFire(); }, 300); }, change: function(){ if (this.___hasBomb()) { this.setStateChange(); } }, run: function () { this.P__render(); } } }); window.BombLayer = BombLayer; }());
Layer
(function () { var Layer = YYC.AClass(Collection, { Init: function () { }, Private: { __state: bomberConfig.layer.state.CHANGE, //默認(rèn)為change __getContext: function () { this.P__context = this.P__canvas.getContext("2d"); } }, Protected: { //*共用的變量(可讀、寫(xiě)) P__canvas: null, P__context: null, //*共用的方法(可讀) P__isChange: function(){ return this.__state === bomberConfig.layer.state.CHANGE; }, P__isNormal: function () { return this.__state === bomberConfig.layer.state.NORMAL; }, P__iterator: function (handler) { var args = Array.prototype.slice.call(arguments, 1), nextElement = null; while (this.hasNext()) { nextElement = this.next(); nextElement[handler].apply(nextElement, args); //要指向nextElement } this.resetCursor(); }, P__render: function () { if (this.P__isChange()) { this.clear(); this.draw(); this.setStateNormal(); } } }, Public: { remove: function (sprite) { this.base(function (e, obj) { if (e.x === obj.x && e.y === obj.y) { return true; } return false; }, sprite); }, addSprites: function(elements){ this.appendChilds(elements); }, //設(shè)置狀態(tài)為NORMAL setStateNormal: function () { this.__state = bomberConfig.layer.state.NORMAL; }, //設(shè)置狀態(tài)為CHANGE setStateChange: function () { this.__state = bomberConfig.layer.state.CHANGE; }, Virtual: { init: function () { this.__getContext(); }, clear: function (sprite) { if (arguments.length === 0) { this.P__iterator("clear", this.P__context); } else if (arguments.length === 1) { sprite.clear(this.P__context); } } } }, Abstract: { setCanvas: function () { }, //判斷并更改狀態(tài) change: function () { }, //統(tǒng)一繪制 draw: function () { }, //游戲主線程調(diào)用的函數(shù) run: function () { } } }); window.Layer = Layer; }());
SpriteFactory
createBomb: function (playerSprite) { return new BombSprite(playerSprite, bitmapFactory.createBitmap({ img: window.imgLoader.get("bomb"), width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT })); }, createFire: function () { return new FireSprite(null, bitmapFactory.createBitmap({ img: window.imgLoader.get("fire"), width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT })); }, createExplode: function () { return new FireSprite(null, bitmapFactory.createBitmap({ img: window.imgLoader.get("explode"), width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT })); }
LayerFactory
createBomb: function () { return new BombLayer(); }, createFire: function () { return new FireLayer(); }
加入1個(gè)敵人
往EnemyLayer集合中再加入一個(gè)EnemySprite實(shí)例,SpriteData增加第2個(gè)敵人的數(shù)據(jù),SpriteFactory增加工廠方法createEnemy2。
相關(guān)代碼
Game
_createEnemyLayerElement: function () { var element = [], enemy = spriteFactory.createEnemy(), enemy2 = spriteFactory.createEnemy2(); enemy.init(); enemy2.init(); element.push(enemy); element.push(enemy2); return element; },
SpriteData
enemy2: { //初始坐標(biāo) x: bomberConfig.WIDTH * 10, //x: 0, y: bomberConfig.HEIGHT * 10, //定義sprite走路速度的絕對(duì)值 walkSpeed: bomberConfig.enemy.speed.NORMAL, //速度 speedX: 1, speedY: 1, minX: 0, maxX: bomberConfig.canvas.WIDTH - bomberConfig.player.IMGWIDTH, minY: 0, maxY: bomberConfig.canvas.HEIGHT - bomberConfig.player.IMGHEIGHT, defaultAnimId: "stand_left", anims: { "stand_right": new Animation(getFrames("enemy", "stand_right")), "stand_left": new Animation(getFrames("enemy", "stand_left")), "stand_down": new Animation(getFrames("enemy", "stand_down")), "stand_up": new Animation(getFrames("enemy", "stand_up")), "walk_up": new Animation(getFrames("enemy", "walk_up")), "walk_down": new Animation(getFrames("enemy", "walk_down")), "walk_right": new Animation(getFrames("enemy", "walk_right")), "walk_left": new Animation(getFrames("enemy", "walk_left")) } }
SpriteFactory
createEnemy2: function () { return new EnemySprite(getSpriteData("enemy2"), bitmapFactory.createBitmap({ img: window.imgLoader.get("enemy"), width: bomberConfig.player.IMGWIDTH, height: bomberConfig.player.IMGHEIGHT })); },
炸死所有敵人后,提示游戲勝利
GameState增加WIN枚舉值。在BombLayer中判斷是否將敵人都炸死了,如果都炸死了則設(shè)置GameState為WIN。在Game中判斷GameState,調(diào)用相應(yīng)的方法。
領(lǐng)域模型

相關(guān)代碼
BombLayer
___collideFireWithEnemy: function (bomb) { var i = 0, len = 0, enemySprites = this.enemyLayer.getChilds(); for (i = 0, len = enemySprites.length ; i < len; i++) { if (bomb.collideFireWithCharacter(enemySprites[i])) { this.___removeEnemy(enemySprites[i]); } } //如果敵人都被炸死了,則游戲勝利! if (this.enemyLayer.getChilds().length === 0) { window.gameState = window.bomberConfig.game.state.WIN; } },
Game
_judgeGameState: function () { switch (window.gameState) { case window.bomberConfig.game.state.NORMAL: break; case window.bomberConfig.game.state.OVER: this.gameOver(); break; case window.bomberConfig.game.state.WIN: this.gameWin(); break; default: throw new Error("未知的游戲狀態(tài)"); } return; } ... run: function () { this._judgeGameState(); this.layerManager.run(); this.layerManager.change(); },
本文最終領(lǐng)域模型

高層劃分
炸彈層和炸彈精靈、火焰層和火焰精靈應(yīng)該放到哪個(gè)包?
炸彈層和玩家層、炸彈精靈和玩家精靈緊密關(guān)聯(lián),火焰層和火焰精靈與炸彈層和炸彈精靈緊密關(guān)聯(lián),因此將炸彈層和炸彈精靈、火焰層和火焰精靈移到人物包中。
新增包
- 全局包
GameState - 觀察者模式包
Subject - 炸彈實(shí)現(xiàn)包
BombSprite、FireSprite、BombLayer、FireLayer
層、包

對(duì)應(yīng)領(lǐng)域模型
- 輔助操作層
- 控件包
PreLoadImg - 配置包
Config
- 控件包
- 用戶交互層
- 入口包
Main
- 入口包
- 業(yè)務(wù)邏輯層
- 輔助邏輯
- 工廠包
BitmapFactory、LayerFactory、SpriteFactory - 事件管理包
KeyState、KeyEventManager - 抽象包
Layer、Sprite、Hash、Collection - 全局包
GameState
- 工廠包
- 游戲主邏輯
- 主邏輯包
Game
- 主邏輯包
- 層管理
- 層管理包
LayerManager
- 層管理包
- 實(shí)現(xiàn)
- 人物實(shí)現(xiàn)包
PlayerLayer、MoveSprite、PlayerSprite、EnemySprite、CharacterLayer、PlayerLayer、EnemyLayer、Context、PlayerState、WalkState、StandState、WalkState_X、WalkState_Y、StandLeftState、StandRightState、StandUpState、StandDownState、WalkLeftState、WalkRightState、WalkUpState、WalkDownState - 炸彈實(shí)現(xiàn)包
BombSprite、FireSprite、BombLayer、FireLayer - 地圖實(shí)現(xiàn)包
MapLayer、MapElementSprite - 算法包
FindPath - 動(dòng)畫(huà)包
Animation、GetSpriteData、SpriteData、GetFrames、FrameData - 觀察者模式包
Subject
- 人物實(shí)現(xiàn)包
- 輔助邏輯
- 數(shù)據(jù)操作層
- 地圖數(shù)據(jù)操作包
MapDataOperate、TerrainDataOperate - 路徑數(shù)據(jù)操作包
GetPath - 圖片數(shù)據(jù)操作包
Bitmap
- 地圖數(shù)據(jù)操作包
- 數(shù)據(jù)層
- 地圖包
MapData、TerrainData - 圖片路徑包
ImgPathData
- 地圖包
本文參考資料
深入理解JavaScript系列(32):設(shè)計(jì)模式之觀察者模式
《設(shè)計(jì)模式之禪》
浙公網(wǎng)安備 33010602011771號(hào)