前言css
上文中咱們加入了1個敵人,使用A*算法尋路。本文會給咱們的炸彈人增長放炸彈的能力。html
實現「放炸彈」功能算法
增長1個敵人,即一共有2個敵人追蹤炸彈人canvas
查看大圖設計模式
Layer的render方法負責統一調用Layer的方法,在概念上屬於Actor,所以將其更名爲run。數組
開發策略app
首先實現「放炸彈」功能。把這個功能分解成不少個子功能,一個一個地實現子功能。ide
而後再加入1個敵人。實際上就是在Game中往EnemyLayer集合中再加入一個EnemySprite實例,SpriteData增長第2個敵人的數據,SpriteFactory增長工廠方法createEnemy2。函數
顯示炸彈和火焰oop
首先來實現「地圖上顯示炸彈」的功能,目前最多顯示1個炸彈,玩家、敵人不能穿過炸彈。若是玩家處於炸彈方格中,則敵人會原地等待,玩家離開後,敵人繼續追蹤。
增長圖片bomb.png:
增長炸彈精靈類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類,它的集合元素爲BombSprite類的實例。
將玩家、敵人畫布Canvas的zIndex設爲3,炸彈畫布的zIndex設爲1,使得,炸彈畫布位於地圖畫布(zIndex爲0)之上,玩家和敵人畫布之下。
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增長建立炸彈精靈類實例的工廠方法。
LyaerFactory增長建立炸彈層實例的工廠方法。
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增長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增長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); }
空格鍵用於炸彈人放炸彈。
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中對KeyState的空格鍵進行斷定:
run: function () { if (keyState[keyCodeMap.Space]) { this.createAndAddBomb(); keyState[keyCodeMap.Space] = false; } this.base(); }
火力範圍設爲1格,分爲上下左右四個方向。地圖的牆對火焰有阻斷做用。
爆炸中心爲圖片boom.png:
火焰爲圖片explode.png:
增長火焰精靈類。
在畫布上增長火焰畫布,同時對應的FireLayer類。
該畫布位於地圖和炸彈畫布之上,玩家和敵人畫布之下。
SpriteFactory增長建立爆炸中心火焰精靈類實例和建立火焰精靈類實例的工廠方法。
LayerFactory增長建立火焰層實例的工廠方法。
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: { //更新幀動畫 _updateFrame: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } } }, Public: { bitmap: null, //精靈的座標 x: 0, y: 0, //精靈包含的全部 Animation 集合. Object類型, 數據存放方式爲" id : animation ". anims: null, //默認的Animation的Id , string類型 defaultAnimId: null, //當前的Animation. currentAnim: null, //設置當前Animation, 參數爲Animation的id, String類型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, //重置當前幀 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, //取得精靈的碰撞區域, getCollideRect: function () { return { x1: this.x, y1: this.y, x2: this.x + this.bitmap.width, y2: this.y + this.bitmap.height } }, Virtual: { //初始化方法 init: function () { //設置當前Animation this.setAnim(this.defaultAnimId); }, // 更新精靈當前狀態. update: function (deltaTime) { this._updateFrame(deltaTime); }, //得到座標對應的方格座標 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) { //直接清空畫布區域 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: { //已放置的炸彈數 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
//層類(抽象類) //職責: ////負責層內組件的統一draw (function () { var Layer = YYC.AClass(Collection, { Init: function () { }, Private: { __state: bomberConfig.layer.state.CHANGE, //默認爲change __getContext: function () { this.P__context = this.P__canvas.getContext("2d"); } }, Protected: { //*共用的變量(可讀、寫) 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(); }, //更改狀態 change: function () { this.__state = bomberConfig.layer.state.CHANGE; } } }, Abstract: { setCanvas: function () { }, clear: function () { }, //統一繪製 draw: function () { }, //遊戲主線程調用的函數 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); //定時清空fireLayer(火焰消失) 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(); }
使用觀察者模式
牆被炸掉後,會變成空地。
Maplayer的changeSpriteImg負責更改地圖圖片,BombSprite的explode負責處理爆炸邏輯。
須要在explode中調用Maplayer的changeSpriteImg。
所以,決定在Game中訂閱Maplayer的changeSpriteImg方法,而後在BombSprite的explode方法中發佈。
由於MapLayer的Layer類族在BombSprite的Sprite類族的上層,我不但願下層BombSprite與上層MapLayer耦合。
所以,採用觀察者模式來解除二者的耦合。
使用觀察模式前
使用觀察模式後
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]); } }, //發佈指定方法 publish: function (context, fn, args) { var args = Array.prototype.slice.call(arguments, 2); //得到函數參數 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) { //指定方法可能有多個 el.apply(_context, args); //執行每一個指定的方法 }); }, 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; } }); }, //所有發佈 publishAll: function (context, args) { var args = Array.prototype.slice.call(arguments, 1); //得到函數參數 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); //執行每一個指定的方法 }); }, dispose: function () { this._events = []; } } })(); YYC.Pattern.Subject = Subject; })();
MapLayer
//改變指定精靈類的img對象 //參數: //x:x座標(方格對應值);y:y座標(方格對應值);img:要替換的img對象 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
//觀察者全局實例 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); },
重構
增長TerrainData地形數據操做類TerrainDataOperate
重構前
重構後
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格,方便演示遊戲。
在Game的run方法中,須要判斷敵人是否抓住了玩家(是否與玩家碰撞):
run: function () { if (this.layerManager.getLayer("enemyLayer").collideWidthPlayer()) { YYC.Tool.asyn.clearAllTimer(this.mainLoop); alert("Game Over!"); return; } ... }
這裏注意到,Game須要知道EnemyLayer的collideWidthPlayer方法:
但Game類只應該知道LayerManager,而不該該知道Layer(見「炸彈人遊戲開發系列(1):準備工做」中的概念層次結構)。
所以,增長遊戲全局狀態GameState,在Game的run判斷GameState,而後把與炸彈人的碰撞檢測的任務放到EnemyLayer的run方法中:
Config
game: { state: { NORAML: 1, OVER: 2 } },
Game
//遊戲全局狀態 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; }
...
炸彈能夠炸死炸彈人和敵人
在炸彈爆炸時,判斷與炸彈人、敵人是否碰撞並進行相應處理。
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) //判斷與每一個敵人碰撞 this.___collideFireWithEnemy(bomb); } ... enemyLayer: null, playerLayer: null, ... explode: function (bomb) { var self = this, result = null; //處理碰撞 this.___handleCollid(bomb); ...
移動時放炸彈
由於炸彈人移動時,根據炸彈人狀態的不一樣,炸彈放置的座標策略也不一樣(即若是炸彈人往上走,則炸彈放在炸彈人所在方格的上面相鄰方格;若是往左走,則炸彈放在炸彈人所在方格的左側相鄰方格)。因此將PlayerSprite的createBomb方法委託給狀態類處理。具體來講,就是把createBomb方法移到狀態類的WalkState類和Stand類中來分別處理。
由於PlayerSprite、EnemySprite都使用了狀態類,所以二者都與BombSprite耦合。但只有PlayerSprite須要使用createBomb方法,EnemySprite並不須要使用該方法。因此此處違反了迪米特法則。
目前這種狀況在能夠接受的範圍以內。若是在後面的開發中EnemySprite與BombSprite耦合得很嚴重,再來考慮解耦。
放置多個炸彈
能夠最多放3個炸彈,炸彈爆炸時會引爆在火力範圍內的炸彈。
在狀態類WalkState類族、StandState類族的createBomb中判斷方格是否有炸彈(判斷地形數據TerrainData來實現)。
改變地圖
若是牆處於火焰範圍內,則修改MapData,將牆的圖片換成空地圖片,同時對應修改TerrainData,將牆所在的方格設成可經過。
在炸掉牆後,在BombLayer中須要調用MapLayer的setStateChange方法,將MapLayer的state設爲change,從而可以在遊戲的下一個主循環中,刷新地圖,從而顯示爲空地。
BombLayer
___mapChange: function (mapChange) { if (mapChange) { this.mapLayer.setStateChange(); } }
小結
如今咱們就完成了「放炸彈」的功能,來看下成果吧~
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: { //已放置的炸彈數 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: { //返回火焰範圍 //返回順序爲[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); //觀察者模式 -> 發佈 //調用mapLayer.changeSpriteImg,改變地圖層對應精靈類的img對象 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, //是否已爆炸標誌 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 }; }, //檢測火焰與玩家人物、敵人的碰撞 collideFireWithCharacter: function (sprite) { var effectiveRange = this.getFireEffectiveRange(), range = [], fire = {}, obj2 = {}, i = 0, len = 0; //放到數組中 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; }, //返回有效範圍。(考慮牆、邊界阻擋等問題) //返回值形如:{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: { //更新幀動畫 _updateFrame: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } } }, Public: { bitmap: null, //精靈的座標 x: 0, y: 0, //精靈包含的全部 Animation 集合. Object類型, 數據存放方式爲" id : animation ". anims: null, //默認的Animation的Id , string類型 defaultAnimId: null, //當前的Animation. currentAnim: null, //設置當前Animation, 參數爲Animation的id, String類型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, //重置當前幀 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, //取得精靈的碰撞區域, 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 () { //設置當前Animation this.setAnim(this.defaultAnimId); }, // 更新精靈當前狀態. update: function (deltaTime) { this._updateFrame(deltaTime); }, //得到座標對應的方格座標(向下取值) 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) { //直接清空畫布區域 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) //判斷與每一個敵人碰撞 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); //炸彈爆炸時會引爆在火力範圍內的炸彈。 this.___explodeInEffectiveRange(bomb); //定時清空fireLayer(火焰消失) 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, //默認爲change __getContext: function () { this.P__context = this.P__canvas.getContext("2d"); } }, Protected: { //*共用的變量(可讀、寫) 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); }, //設置狀態爲NORMAL setStateNormal: function () { this.__state = bomberConfig.layer.state.NORMAL; }, //設置狀態爲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 () { }, //判斷並更改狀態 change: function () { }, //統一繪製 draw: function () { }, //遊戲主線程調用的函數 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個敵人
往EnemyLayer集合中再加入一個EnemySprite實例,SpriteData增長第2個敵人的數據,SpriteFactory增長工廠方法createEnemy2。
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: { //初始座標 x: bomberConfig.WIDTH * 10, //x: 0, y: bomberConfig.HEIGHT * 10, //定義sprite走路速度的絕對值 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中判斷是否將敵人都炸死了,若是都炸死了則設置GameState爲WIN。在Game中判斷GameState,調用相應的方法。
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("未知的遊戲狀態"); } return; } ... run: function () { this._judgeGameState(); this.layerManager.run(); this.layerManager.change(); },
本文最終領域模型
高層劃分
炸彈層和玩家層、炸彈精靈和玩家精靈緊密關聯,火焰層和火焰精靈與炸彈層和炸彈精靈緊密關聯,所以將炸彈層和炸彈精靈、火焰層和火焰精靈移到人物包中。
本文參考資料
深刻理解JavaScript系列(32):設計模式之觀察者模式
《設計模式之禪》