前言
在開發遊戲的過程中,裝備的去留問題讓我困擾了一段時間:行動結束後,裝備應該丟失還是保留?如果保留,是留在角色身上還是回到倉庫?這些邊界條件散落在各處,邏輯越來越難維護。
最後我決定做一次重構,導入狀態機來統一管理裝備的生命週期,這篇文章記錄我怎麼解決這個問題。
背景:問題在哪?
這個遊戲中,玩家出發前會從倉庫挑選裝備帶進行動(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 | { |
兩個欄位分別記錄裝備的來源和當前位置:
| 欄位 | 值 | 說明 |
|---|---|---|
origin |
loadout |
這局從倉庫帶出去的裝備 |
origin |
displaced |
被 loadout 裝備替換下來的舊裝備 |
state |
inventory |
目前在玩家背包(未裝備) |
state |
equipped |
目前穿在玩家身上 |
state |
missing |
玩家身上找不到(保險用途) |
只有 loadout 和 displaced 的裝備會被追蹤,行動中撿到的掉落裝備另外管理,不混入 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=displaced、state=inventory 加入 ledger:
new_item是 ledger 追蹤的裝備(即 loadout 裝備換上去了)old_item不在 ledger 中(沒被追蹤過)old_item不是這局撿到的掉落裝備
結算前同步
resolve_operation_equipment_for_lobby(player) 執行前,先呼叫 _sync_operation_equipment_states(player) 掃描玩家當下的真實狀態,將 ledger 中每筆記錄的 state 更新為 equipped、inventory 或 missing。
回大廳結算
根據最終 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回大廳]
不變量(設計保證)
這個設計有三條不能被打破的規則:
- Ledger 只追蹤
loadout和displaced裝備,run 掉落絕對不混入。 - 回倉決策只看結算當下的真實位置,不看裝備歷史上是否曾被穿過。
- 回倉時一律 duplicate(複製)到 stash,避免引用共享造成副作用。