做一款即時戰略《狼煙》(下):兩個 bug 教我的「別想太複雜」
2026年6月13日 · wemee (with AI assistant)
🔥 《做一款即時戰略》 這是一個兩篇的系列,記錄我替遊戲區做的第三款中型遊戲《狼煙》。
- 設計篇:怎麼讓「自動出兵」變得有得玩
- 手感篇:兩個 bug 教我的「別想太複雜」(本篇)
上一篇講完設計的四根支柱,核心也做出來了——從「⚔️ 開戰!」打到分出勝負,整條流程都通。於是我做了每次都該做的事:坐下來,自己玩。
然後馬上撞到兩個讓人哭笑不得的 bug。
Bug 一:盾兵卡在弓兵後面,動彈不得
我先出了弓兵,再出盾兵。結果盾兵就卡在弓兵後面,怎樣都擠不過去、無法往前走。明明這類拉鋸遊戲的常識是:所有兵應該能自由重疊穿插才對。
Bug 二:出的兵繞過城下敵軍,然後互相無視
更詭異的是第二個。當敵方近戰已經貼在我城下猛砍時,我出的新兵居然出現在那個敵兵的「前方」(更靠戰場那側),然後頭也不回地往前走,完全無視背後正在拆我家的敵人。那個敵兵也一樣無視我的新兵,繼續砍城。兩邊像幽靈一樣互相穿過。
揪根源:兩個 bug,其實是同一個
debug 到最後發現,這兩個現象同源於我把移動模型設計得「太巧」了。我當初想要一條漂亮、整齊的戰線,於是寫了兩件事。
第一,排隊成戰列——每個兵前進時,會跟前方友軍保持最小間距:
// 與前方友軍保持間距,自然排成戰列
const ahead = this.nearestAllyAhead(u);
if (ahead) {
if (dir > 0) newX = Math.min(newX, ahead.x - UNIT_SPACING);
else newX = Math.max(newX, ahead.x + UNIT_SPACING);
}
第二,只往前方索敵——找敵人時,跳過在自己「後方」的:
for (const o of this.units) {
if (o.side === u.side || o.isDead) continue;
if ((o.x - u.x) * dir < -12) continue; // 只看前方
// …找最近的前方敵人
}
這兩段各自看都很合理,合起來就生出那兩個 bug:
- 間距夾擠讓後排的兵被前面的友軍卡死——盾兵動不了的元兇。
- 只看前方+出兵點在敵軍前方,讓新兵根本「看不到」貼在它背後的敵人;那個敵人也覺得新兵在它後面 → 互相跳過 → 各走各的。
轉念:不需要那麼巧
我盯著這兩段程式,意識到一件事:我在用程式去「喬」一個其實靠擺位置就能解決的問題。
關鍵的轉念很簡單——把出兵點挪到主堡「後方」。城門就一個,所有兵都從城堡背後出來、往前推進。這樣:
// 從城堡後方出兵:玩家在主堡左側、敵方在主堡右側
const x = side === 'player'
? KEEP_PLAYER_X - SPAWN_BEHIND
: KEEP_ENEMY_X + SPAWN_BEHIND;
一旦兵從城後出來,任何新兵永遠出現在「貼城敵軍」的後方(自家這側)。它往前走,第一個遇到的就是那個敵人——「只看前方」瞬間就完全正確了,不必再為「敵人在背後」寫任何特例。
接著我把排隊間距整段刪掉,讓單位自由重疊:
private advance(u: Unit, dtSec: number): void {
// 部隊可自由重疊(不互相阻擋):只朝敵方主堡方向前進。
// 前排後排會因射程差異自然形成——近戰擠到最前線、遠程在後方放。
const dir = u.side === 'player' ? 1 : -1;
const speedMul = u.rallyMs > 0 ? 1 + RALLY.speedBonus : 1;
const newX = u.x + dir * u.def.speed * speedMul * dtSec;
u.x = Phaser.Math.Clamp(newX, 12, BOARD_WIDTH - 12);
}
盾兵不再被卡,前後排則自動長出來——因為近戰射程短會擠到最前線、弓兵法師射程長會自然停在後面放。我原本想用 UNIT_SPACING 硬排的戰線,其實射程差異本來就會免費給我。
一個空間上的安排(城後出兵),消掉了兩個 bug、一整段排隊程式(nearestAllyAhead 直接刪除)、還有一堆「敵人在背後怎麼辦」的特例。
教訓:工程師最該警惕的,是把簡單問題想複雜
這件事讓我很有感觸。那兩個 bug 不是「沒寫到」,而是「寫太多」造成的——我為了一個我以為要的效果(整齊戰線),主動加了間距系統跟方向性索敵,結果這些聰明的機制自己打架。
很多時候最好的修法不是「再加一段邏輯去處理例外」,而是退一步,把問題重新擺一下,讓例外根本不存在。城門往後挪 100 像素,比我寫一個全方位索敵 + 排隊避讓的系統,乾淨太多了。少,即是多。
加碼:牧師該怎麼聰明地走位
修完移動後又抓到一個相關的:牧師不會攻擊主堡(牠只治療)。所以當前方沒有敵兵時,牧師會一路往前走,越過我的前線、一直走到地圖邊緣發呆——反而脫離了該被治療的部隊。
第一個念頭是「沒敵人就原地待命」。但再想一下,有個更聰明的行為:有友軍就跟在前線後方治療,沒友軍就退回出生點待命。
// 找自家最前線的戰鬥單位(非牧師)
let front: Unit | null = null;
for (const o of this.units) {
if (o === u || o.side !== u.side || o.isDead || o.def.kind === 'cleric') continue;
if (!front || o.x * dir > front.x * dir) front = o;
}
// 有友軍 → 跟在前線後方固定距離;沒友軍 → 退回出生點
const desiredX = front ? front.x - dir * CLERIC_FOLLOW_GAP : spawnX;
退回出生點還有個附帶好處:那裡有自家主堡的防禦箭罩著,而且剛好接得住你接下來新出的兵。一個小小的「往後退」邏輯,讓牧師從「會脫隊的拖油瓶」變成「聰明的隨軍醫療」。
收尾
做《防線》時我學到「手感是玩出來的,不是想出來的」。這次又補了一課:手感的修正,有時不是去調一個數字,而是去刪掉一段你自以為聰明的程式。
把兵種對撞的邏輯交給最樸素的規則(自由重疊、只看前方、城後出兵),它反而長出了我想要的一切——整齊的前線、合理的接戰、會防守的後方。設計上我用四根支柱把賽局撐起來,工程上卻是靠不斷地「拿掉」,才把手感磨順。
繁,容易;簡,難。這款《狼煙》到目前為止,教我最多的就是後面這四個字。
留言 0
留言載入中…