昨天我們完成了放置炸彈的功能,今天要讓炸彈可以在三秒鐘後爆炸,並且可以判斷在爆炸當下的範圍內有誰在裡面、要不要扣血或者消失。
說到 JavaScript 延遲或是間隔執行的方法就是我們先前有使用過的 setInterval 和另外一個 setTimeout 這兩個方法。
如果你有點忘記這兩個方法的差別的話,快速的回顧一下, setInterval 是間隔一段時間重複執行,就像我們前面做的:每隔一秒就讓 player 往右邊一步;setTimeout 則是延遲一個指定的時間之後只執行一次,以這次炸彈爆炸來說,讀者們會選擇哪一個呢?
我選擇的是 setTimeout ,因為我們是要在放置炸彈後三秒爆炸,這件事只會執行一次而不會重複執行。
雖然決定好方法了,但我還是發現自己的腦袋裡一片空白,完全不知道從哪裡開始啊!所以我們來盤點一下至今為止使用過哪些方法來幫助自己繼續下去:
嗯…先試著拆解步驟,先不批判自己,試著把第一個想到的點子寫下來:
「寫一個函式來執行這個 setTimeout 爆炸的邏輯」
好,那這個想法向上和向下又可以延伸出兩個想法
接著相信各位讀者都很熟悉,要依序來回答這兩個問題:
應該放在放置炸彈的邏輯內
function putBomb(){
let bombX = player.x;
let bombY = player.y;
bombs.push({x:bombX,y:bombY})
explodeBomb();
}
函式內應該要做哪些事情
此時我又想到兩個問題:
player 被清除之後要找一個位置重生?這又會延伸出要隨機重生還是定點重生的問題,晚點再來處理render 的時機點,適不適合把 explodeBomb 函式放在 putBomb 裡面呢?開始變得複雜了對嗎?試著整理上面想到的資料變成流程圖吧!順手也把繪製過程中想到的想法寫進便利貼裡面。


這邊我貼了兩組便利貼,第一個是繪製過程中我想到:為什麼判斷敵人、箱子、玩家在不在爆炸範圍內,必須要等第一次渲染之後才執行,這是因為爆炸以前如果可以逃離爆炸範圍就不會死亡,所以這個判斷必須要在爆炸之後才來計算.
第二個問題則是想到敵人目前只記錄了單純的座標 {x: 數字, y: 數字} ,箱子的話還可以從陣列中移除,但是每一個敵人都是一個變數的情況下,我們該怎麼去記錄這個敵人已經陣亡了呢?我也把我想到的解答貼進去:新增一個 live 的 key,讓物件變成{x: 數字, y: 數字, live: true} 來表達敵人生存與否。
畫完圖之後,重新觀察程式碼,我們知道在這段監聽器內,按任何一個按鍵,都是在修改座標,而每一次修改座標之後,都會觸發 render:
process.stdin.on('data', (key) => {
// 前面這邊是按按鍵之後,會對應去修改座標的位置
// 這裡一定會重新渲染
render();
});
不過這是在「有按鍵輸入」的情況下,所以我們在第三秒觸發爆炸的重新渲染以及第四秒觸發清空爆炸區域,都需要另外呼叫 render。
唉,是不是真的蠻複雜的,不管了,既然地圖都有了,我們先來寫看看吧,俗話說船到橋頭自然直,我們就勇敢把船開出去!!!
一開始我們先完成到三秒後 render 渲染出爆炸畫面就好。
從剛剛的流程圖中我們可以知道,這裡至少有兩個步驟:
* 符號那既然最後一個步驟是執行渲染,我們從 render 開始改起好了。來看一下下面這一段,你們認為爆炸的符號應該要放在哪一個順位才能夠蓋掉 player 、enemy 和 box 呢?我們先放到最上面試試看好了。
function render() {
let frame = ''
for (let y = 0; y < map.length; y++) {
let row = ''
for (let x = 0; x < map[y].length; x++) {
const cell = map[y][x]
if (player.x === x && player.y === y) {
row += `\x1b[33m@\x1b[0m`
}else if (enemy1.x === x && enemy1.y === y) {
row += `\x1b[35m&\x1b[0m`
}else if (enemy2.x === x && enemy2.y === y) {
row += `\x1b[35m&\x1b[0m`
}else if (enemy3.x === x && enemy3.y === y) {
row += `\x1b[35m&\x1b[0m`
}else if (boxes.some(box => box.x === x && box.y === y)) {
row += `\x1b[34mX\x1b[0m` // 用藍色和 X 來呈現箱子
}else if (bombs.some(bomb => bomb.x === x && bomb.y === y)) {
row += `\x1b[31mB\x1b[0m` // 用紅色和 B 來呈現炸彈
} else {
row += `\x1b[32m${cell}\x1b[0m`
}
}
frame += row + '\n'
}
process.stdout.write(`\x1b[${map.length +1 }A`); // 加上儀表板這一行也要一起重新渲染
process.stdout.write(`\x1b[31mHP:${HP}\x1b[35m 敵人:${enemyNum}\x1b[0m\n`); // 儀表版
process.stdout.write(frame); // 印出畫面
}
先來制定一下變數,寫一些模擬的座標進去,順便寫個註解
let bombs = []; // 尚未爆炸的炸彈
let explodeCells = [{x:3,y:1},{x:3,y:2},{x:3,y:3}]; // 爆炸中的格子
接著把 explode 放到 render 的最上面
if ( explodeCells.some(explodeCell => explodeCell.x === x && explodeCell.y === y) ){
row += `\x1b31m*\x1b[0m` // 用紅色 * 來呈現爆炸
}else if (player.x === x && player.y === y) {
row += `\x1b[33m@\x1b[0m`
}else if (enemy1.x === x && enemy1.y === y) {
row += `\x1b[35m&\x1b[0m`
}else if (enemy2.x === x && enemy2.y === y) {
row += `\x1b[35m&\x1b[0m`
}else if (enemy3.x === x && enemy3.y === y) {
row += `\x1b[35m&\x1b[0m`
}else if (boxes.some(box => box.x === x && box.y === y)) {
row += `\x1b[34mX\x1b[0m` // 用藍色和 X 來呈現箱子
}else if (bombs.some(bomb => bomb.x === x && bomb.y === y)) {
row += `\x1b[31mB\x1b[0m` // 用紅色和 B 來呈現炸彈
} else {
row += `\x1b[32m${cell}\x1b[0m`
}

現在這樣可以印出爆炸畫面了!!
接著我們要動態把炸彈周圍十字型的座標扣除牆壁剩餘的寫進 explodeCells 裡面,先試著把要做的事情寫下來
function explodeBomb(){
// 找出影響範圍座標
// 扣除牆壁
// 寫入 explodeCells
}
然後再逐步去完善它,這邊因為我在內層的 explodeBomb 傳入了 map ,所以我也在外層的 putBomb 加入了這個 map , bombX 和 bombY 則是從 putBomb 拉進來就可以。
function putBomb(map){
let bombX = player.x;
let bombY = player.y;
bombs.push({x:bombX,y:bombY})
explodeBomb(map,bombX,bombY);
}
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])
}
}
}
記得要把剛剛我們寫的假資料先清空
let bombs = []; // 尚未爆炸的炸彈
let explodeCells = []; // 爆炸中的格子
測試看看
現在按下空白鍵就會直接看到爆炸畫面,那是因為目前完整的流程會是:執行空白鍵之後 → 進到 putBomb 放置 B 炸彈→ 進到 explodeBomb 儲存爆炸範圍 → 此時 putBomb 的工作結束 → 回到監聽器內跑完 render
在放置炸彈和爆炸之間沒有另外一個 render 的流程,這就是典型的同步流程,所以我們看不見 B,所以我們先把爆炸延後三秒鐘,讓爆炸進入到 setTimeout 的非同步流程裡面,過程中使用者如果離開原地就會觸發另一個監聽器的 render,應該就能看到 B 炸彈了
把 explodeBomb 包進一個 3 秒的 setTimeout 裡面
function putBomb(map){
let bombX = player.x;
let bombY = player.y;
bombs.push({x:bombX,y:bombY})
setTimeout(()=>explodeBomb(map,bombX,bombY),3000);
}
測試一下之後發現他不會自動在 3 秒後爆炸,他的順序是 放置炸彈 → 三秒後將爆炸範圍寫入陣列 → 使用者下一次執行移動的時候才觸發 render,這就提到了我們前面所說的,我們要補一個 render 進去,讓他主動在把爆炸範圍寫入陣列的時候就觸發 render
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();
}
測試一下
那麼明天我們再繼續完成爆炸之後該怎麼讓敵人陣亡吧!讀者們也可以挑戰看看畫出流程圖唷!
如果你想要瀏覽完整的程式碼,可以參考這裡:第 13 天程式碼