今天的目標是要讓爆爆王有一個可以玩的版本出來。其實我前面在開發的時候就時不時看到一些需要修改的問題,但我刻意留到這個時間,主要是因為我不想要打亂前面的思考主軸。
現在其實我整理了一些需要修改的項目,不過我比較期待和大家一起親眼見證會發生什麼問題,所以今天的主題是:先玩 10 遍,不行的話就 20 遍。
意思是說我們就直接從玩遊戲開始,每次遊戲時發現問題,就馬上停下來修改,再玩下一次,如果玩 10 遍修不好,我們就玩 20 遍!這樣是不是比起單純照著已知的清單修改還要有趣一點呢,或許還可以順便修正一些之前沒注意到的錯誤。
那就馬上開始吧!
第一遍我們就發現儀表板沒有隨著敵人減少,但是在 Day15 的時候實測是有成功扣除的,所以問題是出在哪裡呢?一樣不知道的時候不卡住,馬上請 AI 來幫忙除錯。
然後發現原來是因為我們踩到了 JavaScript 傳值與傳址的大坑裡面去了,有興趣深入研究的人可以參考我找到的這篇參考資料,感謝鐵人前輩的貢獻。總之由於 enemies 是用下面這種寫法:
const enemies = [enemy1, enemy2, enemy3];
這裡面每個 enemy 都有自己原本的位址,但我們這幾天做了重新賦值:
function moveEnemy(map, boxes, enemy, player,bombs,explodeCells) {
// 計算並重新賦予座標
if (enemy === enemy1) {
enemy1 = pickNextEnemy1(map, boxes, enemy1);
}
if (enemy === enemy2) {
enemy2 = pickNextEnemy2(map, boxes, enemy2, player);
}
if (enemy === enemy3) {
enemy3 = pickNextEnemy3(map, boxes, enemy3, player,bombs,explodeCells);
}
}
在這裡每個 enemy 都新的位址給覆蓋掉了,但是 enemies 裡面保存的還是舊的位址,所以才會沒更新到。
那我們就馬上來修正吧!
// 把 explodeBomb 裡面的這一句
enemyNum = enemies.filter(enemy => enemy.live === true).length;
// 改成直接用新的 enemy 計算
enemyNum = [enemy1, enemy2, enemy3].filter(enemy => enemy.live === true).length;
這次我們發現了兩個問題:
先從簡單的開始,扣個 HP 吧!不過該在哪裡寫這個邏輯比較好呢?
好像都怪怪的,那萬一敵人移動、玩家也移動,不就扣兩次?
所以我們寫在 render 裡面好了!這樣每次 render 的時候拿當下的座標比較,如果重疊了就可以扣血。
試試看,大家先別跟我說這個可以怎麼精簡,我就說了要從最爛的方法寫起嘛! clean code 是後面的事!
if(
(player.x=== enemy1.x && player.y === enemy1.y) ||
(player.x=== enemy2.x && player.y === enemy2.y) ||
(player.x=== enemy3.x && player.y === enemy3.y)
) {
HP -= 1;
}
第三次發現 HP 都扣到 -5 了,結果遊戲還沒結束!原來是因為遊戲結束目前被寫在炸彈爆炸的邏輯裡面,那我們現在來看看哪些東西一起搬到 render 會更乾淨:
// 勝負判定
if (HP <= 0) {
process.stdout.write("\n失去所有HP,遊戲結束\n");
process.exit();
}
if (enemyNum <= 0) {
process.stdout.write("\n所有敵人都被消滅,恭喜獲勝\n");
process.exit();
}
我把這兩個勝負判定的邏輯一起移動到 render 印出畫面的最下方,然後再來嘗試第四次:
第四次我們發現到敵人根本還沒碰到玩家,就扣了 HP,經過 AI 排查原因是因為這兩個計時器發生了競態:
setTimeout
setInterval
我要來用上之前曾經用過的方法,先 commit 現在的修改,然後讓 AI 來修正這個錯誤

從這裡我們可以看到 AI 加了一個 explodeAt 的變數,目的在於計算炸彈什麼時候會爆炸

接著在敵人的 setInterval 裡面加上了判斷,如果有炸彈在這次移動中要爆炸,敵人會跳過這一次的移動,確保炸彈的傷害先判定完成。
來嘗試第五次
這次終於可以玩了,不過敵人可以穿過炸彈還是太可怕了一點,很容易被逼到死角,我們要趕快處理這個問題。
這邊又想到一個有趣的設計問題:如果敵人不能穿越炸彈,那玩家可以嗎?玩家目前放置炸彈一定會跟炸彈重疊,但如果離開了這顆炸彈,就不能再穿越呢?來試試看這個做法。
一樣先 commit 然後讓 AI 來做:

首先我們看到它加了判斷下一步是不是炸彈,如果是的話就直接終止移動。

接著我們看到它在 enemy1 和 enemy2 多傳入了 bombs ,我猜目的是為了進到裡面去判斷前方是不是炸彈。

驗證了確實它加入了這個判斷的邏輯以避免 AI 走入炸彈, enemy2 和 enemy3 都有做類似的判斷,如同先前所說,我這邊先不考慮如何精簡,我只要確定可以動就好。
試玩第六次了!!
唉,第六次確實敵人跟玩家都不能夠穿越炸彈,但是為什麼敵人在遠方、也沒有炸彈,卻莫名其妙死掉了,我們得排查這個問題,經過 AI 排查之後發現事情是這樣發生的:
setTimeout。setTimeout 並沒有取消,之後仍會呼叫 explodeBomb()。也就是說玩家會被幽靈爆炸給炸傷啦!!!
那一樣先 commit 前面的修改之後交給 AI 實作:

這邊可以看到它在放置炸彈的時候也去判斷,如果該格已經是炸彈就不能再放置。另外它制定了 bomb 傳入 explodeBomb ,目的是要在最後「只清除這顆炸彈」,原本的邏輯是會清除掉這個座標全部的炸彈。
那我們就再玩第七次:
什麼啊!!!! 結果還是莫名其妙死了,原來主要是因為死掉的敵人也還在移動,而且會造成傷害,以下是修正:

這邊 AI 修改讓還活著的敵人才能造成傷害。

這一段也是它修改如果敵人已死亡就不進行移動。
真希望這是最後一次測試XD 第八次來囉!
第八次的遊玩終於比較順利一點了,但是炸彈有時候爆炸會看不見就消失了,或者很快的閃過去還來不及反應,顯然是重繪的時間點不對,我們要修正一下這個問題:

這邊把原本寫在 setTimeout 之前的清除爆炸邏輯,移動到 setTimeout 裡面,以保留爆炸至少顯示一秒。
第九次!!!快結束了!!!
歷經千辛萬苦,我們終於是可以正常遊玩了!!!可喜可賀。你是不是也鬆一口氣了呢XD
今天的內容稍微繁瑣了一些,記錄了我每次修正的過程,當中遇到的部分問題其實可以在開發時先預防,不過我想我不是那種天才型的開發者,也還沒有累積足夠多的經驗以去判斷哪些邏輯這樣寫會出錯、那樣寫才安全。
所以我想藉由今天一邊遊玩一邊更新來發現:原來我這樣寫會導致幽靈炸彈、幽靈敵人的產生,又或者注意到在有多個計時器的狀況下要去避免掉競態的問題。
所以我想我們可以很自豪的說今天又多累積了一些經驗了!
那麼到這邊爆爆王的遊戲就做完啦,不過我建議還是多玩個幾次,以免有些 bug 其實是在還沒遇到的情境下觸發的。
明天我們又要做些什麼呢,大家有想到哪些遊戲可以做嗎,或者是有想要在現有的架構上做那些調整呢?我們明天見!
如果你想要瀏覽完整的程式碼,可以參考這裡:第 19 天程式碼