遊戲後端限時道具系統設計:ExemptResourceType 架構決策¶
文檔資訊
- 分類: architecture
- 難度: intermediate
- 預估閱讀時間: 5 分鐘
- 標籤:
game-backend,item-system,time-limited-items,stamina-pass,luban-config,architecture-pattern
摘要¶
討論遊戲後端限時道具(通行證)的判斷邏輯與架構設計。核心結論:道具是否可無限使用由「次要類型枚舉 + 限時時間欄位」共同判斷;同類型但不同時間戳的道具為獨立實體不可疊加;選擇方案 A 在 ItemConfig 加入 ExemptResourceType 欄位並於 PlayerService 啟動時建立 mapping,達到 O(1) 查找效率。
關鍵學習¶
-
限時道具的「可無限使用」判斷由次要類型枚舉(SecondaryType)+ 時間欄位共同決定,非單純枚舉
-
相同類型但不同時間戳的道具在後端是獨立實體,無法自動疊加;前端可合併顯示剩餘時間
-
多個服務入口都需要判斷道具豁免資源類型時,應集中管理避免 bug,不要分散在各個 API 呼叫點
-
方案 A(ItemConfig 加 ExemptResourceType 欄位)優於分散式設計,因為配置層集中、查找 O(1)
-
未來擴充(3分鐘無限體力、不消耗金幣等)只需在 ItemConfig 配置新的 ExemptResourceType,不改程式碼
技術細節¶
道具判斷邏輯¶
限時道具的「是否可無限使用」判斷流程:
- 根據
DungeonMainConfig中配置的 ID 掃描玩家背包 - 查找哪些次要類型(SecondaryType)屬於限時道具
- 確認玩家身上是否持有這些限時道具
- 同時滿足「次要類型枚舉匹配」和「時間欄位有效」才判定可無限使用
道具疊加行為¶
相同類型道具(例如兩個「飛機杯包5分鐘」)若在不同時間獲得,後端會視為兩個獨立實體(不同時間戳),不會自動疊加時間。前端若需顯示合計剩餘時間,需自行合併計算。
方案 A:ExemptResourceType 架構¶
在 Luban ItemConfig 新增欄位:
PlayerService 啟動時建立一次性 mapping:
// resourceType → []secondaryType
var exemptMapping map[int32][]int32
func (s *PlayerService) buildExemptMapping(configs []*ItemConfig) {
for _, cfg := range configs {
if cfg.ExemptResourceType != 0 {
exemptMapping[cfg.ExemptResourceType] = append(
exemptMapping[cfg.ExemptResourceType],
cfg.SecondaryType,
)
}
}
}
查詢時 O(1) lookup,不需每次掃全表。啟動成本低(僅遍歷一次 ItemConfig),實際效能影響可忽略。
What Changed¶
架構決策¶
選擇方案 A:在 ItemConfig(Luban 配置)中新增 ExemptResourceType 欄位,集中管理「哪種道具豁免哪種資源消耗」的對應關係。
解決的問題¶
原本的設計讓多個 API 入口點各自判斷道具豁免邏輯,容易因為遺漏導致 bug。方案 A 將邏輯集中到配置層,PlayerService 啟動時建立 resourceType → []secondaryType 的 mapping,統一查找入口。
未來擴充規劃¶
3 分鐘無限體力(企劃尚未配置)、不消耗金幣等需求,均可沿用此架構,只需在 ItemConfig 新增對應的 ExemptResourceType 配置即可,不需修改程式碼。
So What¶
為何值得記錄¶
遊戲道具系統的「限時豁免」邏輯是常見但容易設計錯誤的模式。這個討論明確了:
- 不要把豁免邏輯散落在各個業務 API 中,應集中管理
- 配置驅動(Config-Driven)的設計讓擴充成本接近零
- 後端道具實體模型(不同時間戳 = 不同實體)是易踩的坑,前後端需對齊理解
這個模式可複用於任何「持有特定道具期間豁免某資源消耗」的遊戲系統設計。
Trade-offs¶
- 方案 A 優點:配置集中、O(1) 查找、擴充只需改 Config 不改程式碼
- 方案 A 缺點:ItemConfig 欄位增加,Luban 需重新生成;豁免邏輯與道具定義耦合
- 不拆獨立模組的風險:若未來通行證邏輯複雜化(跨服務、多條件),可能需要重構
- 道具不疊加的設計:對玩家體驗較不友好,但後端實作簡單;前端合併顯示是折衷方案
Try It Fast¶
// PlayerService 啟動時建立 exemptMapping
func (s *PlayerService) InitExemptMapping() {
s.exemptMapping = make(map[int32][]SecondaryType)
for _, cfg := range s.itemConfigs {
if cfg.ExemptResourceType != 0 {
s.exemptMapping[cfg.ExemptResourceType] = append(
s.exemptMapping[cfg.ExemptResourceType],
cfg.SecondaryType,
)
}
}
}
// 查詢玩家是否持有豁免 stamina 的通行證
func (s *PlayerService) HasStaminaPass(player *Player) bool {
exemptTypes := s.exemptMapping[ResourceType_Stamina] // O(1)
for _, secType := range exemptTypes {
if player.HasActiveItem(secType) {
return true
}
}
return false
}
Recommendation¶
- 確認 Luban
ItemConfig的ExemptResourceType欄位已加入並重新生成配置代碼 - 在
PlayerService啟動初始化中呼叫InitExemptMapping(),確保 mapping 在處理任何請求前就已就緒 - 所有需要判斷「豁免資源消耗」的 API,統一透過
HasXxxPass()方法查詢,禁止各自實作掃描邏輯 - 前端需自行計算「同類型通行證」的合計剩餘時間,後端不提供自動疊加
- 當「3分鐘無限體力」企劃配置完成時,只需在 ItemConfig 設定
ExemptResourceType = stamina,不需修改程式碼