0%

用狀態機解決 Roguelike 裝備去留問題

前言

在開發遊戲的過程中,裝備的去留問題讓我困擾了一段時間:行動結束後,裝備應該丟失還是保留?如果保留,是留在角色身上還是回到倉庫?這些邊界條件散落在各處,邏輯越來越難維護。

最後我決定做一次重構,導入狀態機來統一管理裝備的生命週期,這篇文章記錄我怎麼解決這個問題。

背景:問題在哪?

這個遊戲中,玩家出發前會從倉庫挑選裝備帶進行動(loadout),行動過程中也可能撿到新裝備並替換身上的舊裝。

問題出在行動結束時

  • 帶出去的裝備要回倉嗎?
  • 如果玩家中途換掉了它,那個被替換下來的裝備呢?
  • 玩家失敗了,裝備要全部消失還是部分保留?

原本這些規則分散在 UI、Player 腳本等多個地方,條件判斷越加越多,邊界情況越來越難控制。

解決思路:三個設計原則

重構的核心是把「誰決定裝備去留」這件事收斂成一個來源,用三個原則組合實現:

1. Single Source of Truth(單一資料來源)

行動期間,所有需要被追蹤的裝備都記錄在 GameManager.operation_equipment_ledger(以下簡稱 ledger)。
任何關於裝備的決策都從這裡查,不從 UI 或 Player 腳本分散讀取。

2. Event-driven Transition(事件驅動狀態更新)

裝備的狀態不由外部主動輪詢,而是透過 EventBus.equipment_changed 事件被動更新。
裝備被穿上、換下時,系統自動更新 ledger,不需要每個地方都手動寫一次。

3. End-of-run Policy Resolver(結算器)

行動結束時,只執行一次結算,根據 ledger 的最終狀態決定每件裝備的命運,不在多個地方各自判斷。


資料結構

Ledger 中每筆裝備記錄長這樣(位於 scripts/autoload/game_manager.gd):

1
2
3
4
5
{
"item": EquipmentData,
"origin": "loadout" | "displaced",
"state": "inventory" | "equipped" | "missing"
}

兩個欄位分別記錄裝備的來源當前位置

欄位 說明
origin loadout 這局從倉庫帶出去的裝備
origin displaced 被 loadout 裝備替換下來的舊裝備
state inventory 目前在玩家背包(未裝備)
state equipped 目前穿在玩家身上
state missing 玩家身上找不到(保險用途)

只有 loadoutdisplaced 的裝備會被追蹤,行動中撿到的掉落裝備另外管理,不混入 ledger。


狀態機:裝備的一生

一件被追蹤的裝備,在一局行動中會經歷以下狀態變化:

            stateDiagram-v2
            [*] --> inventory : 進入行動 加入背包
inventory --> equipped : 穿上裝備
equipped --> inventory : 卸下裝備
inventory --> missing : 結算前找不到
equipped --> missing : 結算前找不到
equipped --> [*] : 結算 保留在身上
inventory --> [*] : 結算 複製回倉庫
missing --> [*] : 結算 略過
          

初始進入

呼叫 apply_operation_loadout_to_player() 時,成功加入玩家背包的 loadout 裝備會寫入 ledger,初始 state=inventory

裝備交換

監聽 _on_equipment_changed(slot, old_item, new_item) 事件,滿足以下條件時,被替換的舊裝會以 origin=displacedstate=inventory 加入 ledger:

  • new_item 是 ledger 追蹤的裝備(即 loadout 裝備換上去了)
  • old_item 不在 ledger 中(沒被追蹤過)
  • old_item 不是這局撿到的掉落裝備

結算前同步

resolve_operation_equipment_for_lobby(player) 執行前,先呼叫 _sync_operation_equipment_states(player) 掃描玩家當下的真實狀態,將 ledger 中每筆記錄的 state 更新為 equippedinventorymissing

回大廳結算

根據最終 state 決定裝備去向:

state 結果
equipped 保留在角色身上
inventory 從玩家移除,複製回 stash_loot.equipment
missing 略過(防禦性處理)

結算完畢後呼叫 clear_operation_equipment_ledger() 清空記錄。


與行動掉落裝備的關係

行動中撿到的裝備(run 掉落)由 run_backpack_loot 另外管理,不進 ledger。兩套系統各自獨立,結算時有執行順序:

            flowchart TD
            A[行動結束] --> B{結果}
B -->|成功撤離| C[保留穿著中的 run 裝備]
B -->|失敗| F[移除所有 run 掉落裝備]
C --> D[移除背包中其餘 run 戰利品]
D --> E[存入倉庫]
E --> G[同步 ledger 狀態]
F --> G
G --> H{ledger 裝備狀態}
H -->|equipped| I[保留在角色身上]
H -->|inventory| J[複製回倉庫]
H -->|missing| K[略過]
I --> L[清除 ledger 回大廳]
J --> L
K --> L
          

整體流程總覽

            flowchart LR
            A[大廳選好loadout] --> B[進入行動]
B --> C[loadout裝備寫入ledger]
C --> D[局內遊玩]
D --> E{換了裝備?}
E -->|是| F[舊裝以displaced登記]
F --> D
E -->|否| G[行動結束]
G --> H[run掉落結算]
H --> I[同步ledger狀態]
I --> J[ledger結算]
J --> K[清除ledger回大廳]
          

不變量(設計保證)

這個設計有三條不能被打破的規則:

  1. Ledger 只追蹤 loadoutdisplaced 裝備,run 掉落絕對不混入。
  2. 回倉決策只看結算當下的真實位置,不看裝備歷史上是否曾被穿過。
  3. 回倉時一律 duplicate(複製)到 stash,避免引用共享造成副作用。