今天要實作在炸彈中被波及敵人的邏輯,開始前我們先回顧一下這個流程圖


乍看之下今天的事情應該很輕鬆吧!!! 不過我們還是先寫一下步驟:
live: true 的屬性到物件中live 改為 false
從第一點開始寫!
因為 pickRandomEmptyCell(map,[player]) 這個出來的內容會是像這樣的物件
{X: 1,Y:1}
那我們期待它其實應該要長這樣:
{X: 1,Y:1,live:true}
所以我們利用解構的方式,把原本的 pickRandomEmptyCell(map,[player] 物件解構出來,並加上 live 這個 key 值,全部都先預設為 true,表示這個敵人還活著
let enemy1 = {...pickRandomEmptyCell(map,[player]),live:true};
let enemy2 = {...pickRandomEmptyCell(map,[player,enemy1]),live:true};
let enemy3 = {...pickRandomEmptyCell(map,[player,enemy1,enemy2]),live:true};
所有會接觸到畫面的邏輯,都跟 render 有關,所以我們直接來看 render 的邏輯,有關於 enemy 的這一段 (先以 enemy1 舉例)
else if (enemy1.x === x && enemy1.y === y && enemy1.live === true) {
row += `\x1b[35m&\x1b[0m`
}
然後很驚喜地發現,這邊其實只要加上 live 是不是為 true 的判斷,就可以順利的顯示活著的敵人,那麼 live 為 false 的敵人自然就看不見了。
我們可以先試著把 live 改為 false ,然後執行看看畫面上是不是少一個敵人
let enemy1 = {...pickRandomEmptyCell(map,[player]),live:false};

執行後發現確實是少一個敵人,那麼再把 false 改回 true,因為我們初始化都是要顯示出來的。
這個部分有兩件事要做:
true 為 false
那這時候開始要來思考我們應該把這段邏輯寫在 putBomb 還是 explodeBomb 呢?
先來觀察一下這兩段:
function putBomb(map){
let bombX = player.x;
let bombY = player.y;
bombs.push({x:bombX,y:bombY})
setTimeout(()=>explodeBomb(map,bombX,bombY),3000);
}
function explodeBomb(map,bombX,bombY){
// 找出影響範圍座標
const surroundingCells = [
{x:bombX, y:bombY},
{x:bombX - 1, y:bombY},
{x:bombX + 1, y:bombY},
{x:bombX, y:bombY - 1},
{x:bombX, y:bombY + 1},
]
// 扣除牆壁
for(let i = 0 ; i < surroundingCells.length; i++){
const thisX = surroundingCells[i].x;
const thisY = surroundingCells[i].y;
if(map[thisY][thisX] !== '#'){
explodeCells.push(surroundingCells[i])
}
}
render();
}
從時間順序來講,我們知道按下空白鍵之後 → 執行 putBomb 放炸彈 → 三秒鐘後計算出爆炸座標並重新渲染
那麼在第四秒要發生的事情,我們可以選擇接在這裡
function putBomb(map){
let bombX = player.x;
let bombY = player.y;
bombs.push({x:bombX,y:bombY})
setTimeout(()=>explodeBomb(map,bombX,bombY),3000);
// 在這邊如何呢?
}
嗯…似乎行不通,首先是因為非同步邏輯的執行順序,畫張時序圖來幫助我們自己理解:

可以想像由於非同步邏輯會將 setTimeout 拉到旁邊去先擱置,先繼續往下處理可以處理的部分,等到三秒後才執行 setTimeout ,所以如果直接寫在 putBomb 裡面的話,反而會先清空畫面而不是先爆炸。
另外一點則是因為 explodeBomb 函式內已經有計算好的影響範圍 surroundingCells,可以直接拿來計算是否有敵人在範圍內。
既然決定好了我們就往下寫 explodeBomb 內的邏輯:
function explodeBomb(map,bombX,bombY){
// 找出影響範圍座標 這段不變
// 扣除牆壁 這段也不變
// 三秒後第一個觸發的 render
render();
// 觸發 render 之後才做敵人的判斷
const hasEnemy1 = surroundingCells.find(cell => cell.x === enemy1.x && cell.y === enemy1.y);
const hasEnemy2 = surroundingCells.find(cell => cell.x === enemy2.x && cell.y === enemy2.y);
const hasEnemy3 = surroundingCells.find(cell => cell.x === enemy3.x && cell.y === enemy3.y);
// 如果爆炸範圍存在敵人的話,該敵人的 live 改為 false
if (hasEnemy1) {
enemy1.live = false;
}
if (hasEnemy2) {
enemy2.live =false;
}
if (hasEnemy3) {
enemy3.live=false;
}
// 這邊要再使用 setTimeout
setTimeout(()=>render(),1000);
}
這段邏輯的最後再使用一次 setTimeout ,時間上就會被疊加,時序圖像這樣:

來執行看看吧
結果我們發現一直停留在爆炸畫面,原來是忘記要清空 explodeCells 了!!!
趕快來清空一下:
// 逐一篩選掉這顆炸彈的爆炸範圍
for(let i = 0 ; i < surroundingCells.length; i++){
explodeCells = explodeCells.filter(cell => cell !== surroundingCells[i])
}
setTimeout(()=>render(),1000);
再試玩一下
這時候又發現炸彈爆炸之後怎麼還在啊!!!!
趕快再來改一下:
// 清除炸彈,一樣記得要放在 render 之前
bombs = bombs.filter(item => !(item.x === bombX && item.y === bombY));
setTimeout(()=>render(),1000);
這時候終於成功啦,昨天我們藉由流程圖來幫助自己釐清過於複雜的功能,但今天練習的是時序圖,雖然也是流程的一種,但是更關注在事情發生的先後順序,特別是函式的執行順序,有時也必須依賴對於 JavaScript 這個語言的理解。未來如果有遇到一樣是同步與非同步的問題,我們就知道時序圖會很有幫助了!
明天我們要完成炸彈爆炸最後的幾步路,你知道還有哪些還沒做完嗎?試著寫看看吧!明天見!
如果你想要瀏覽完整的程式碼,可以參考這裡:第14天程式碼