iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
JavaScript

重新拿回思考力!一起來用 JavaScript 打造 CLI 好玩遊戲區!系列 第 14 篇

Day14 爆爆王6:把隱藏在背景程序中的執行順序整理成時序圖

  • 分享至 

  • xImage
  •  

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

https://ithelp.ithome.com.tw/upload/images/20260927/20182439HLq3jKndNa.png

https://ithelp.ithome.com.tw/upload/images/20260927/20182439qzIiM8po1w.png

乍看之下今天的事情應該很輕鬆吧!!! 不過我們還是先寫一下步驟:

  1. 調整資料結構,照流程圖所說的新增一個 live: true 的屬性到物件中
  2. 測試是否可以隱藏掉該敵人
  3. 動態在偵測到敵人位於爆炸區時,將 live 改為 false
  4. 重新渲染

從第一點開始寫!

調整資料結構

因為 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};

https://ithelp.ithome.com.tw/upload/images/20260927/20182439VELw3odYUl.png

執行後發現確實是少一個敵人,那麼再把 false 改回 true,因為我們初始化都是要顯示出來的。

位於爆炸區的敵人,爆炸後 true 改為 false

這個部分有兩件事要做:

  1. 判斷是否位於爆炸區
  2. 改 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);

    // 在這邊如何呢?
}

嗯…似乎行不通,首先是因為非同步邏輯的執行順序,畫張時序圖來幫助我們自己理解:

https://ithelp.ithome.com.tw/upload/images/20260927/20182439UK0CfFWuMt.png

可以想像由於非同步邏輯會將 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 ,時間上就會被疊加,時序圖像這樣:

https://ithelp.ithome.com.tw/upload/images/20260927/20182439FeUTWUa8F0.png

來執行看看吧

day14-沒有清空畫面

結果我們發現一直停留在爆炸畫面,原來是忘記要清空 explodeCells 了!!!

趕快來清空一下:

// 逐一篩選掉這顆炸彈的爆炸範圍
for(let i = 0 ; i < surroundingCells.length; i++){
  explodeCells = explodeCells.filter(cell => cell !== surroundingCells[i])
}

setTimeout(()=>render(),1000);

再試玩一下

day14 炸彈爆炸後還在

這時候又發現炸彈爆炸之後怎麼還在啊!!!!

趕快再來改一下:

// 清除炸彈,一樣記得要放在 render 之前
bombs = bombs.filter(item => !(item.x === bombX && item.y === bombY));

setTimeout(()=>render(),1000);

這時候終於成功啦,昨天我們藉由流程圖來幫助自己釐清過於複雜的功能,但今天練習的是時序圖,雖然也是流程的一種,但是更關注在事情發生的先後順序,特別是函式的執行順序,有時也必須依賴對於 JavaScript 這個語言的理解。未來如果有遇到一樣是同步與非同步的問題,我們就知道時序圖會很有幫助了!

明天我們要完成炸彈爆炸最後的幾步路,你知道還有哪些還沒做完嗎?試著寫看看吧!明天見!

如果你想要瀏覽完整的程式碼,可以參考這裡:第14天程式碼


上一篇
Day13 爆爆王5:把過於複雜的步驟繪製成功能流程圖
下一篇
Day15 爆爆王7:來個中場休息小回顧
系列文
重新拿回思考力!一起來用 JavaScript 打造 CLI 好玩遊戲區! 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言