跳轉到

遊戲後端資源系統重構 + 角色解鎖系統架構設計(1M CCU)

文檔資訊

  • 分類: architecture
  • 難度: advanced
  • 預估閱讀時間: 12 分鐘
  • 標籤: game-backend, postgresql, grpc-proto, hero-unlock, resource-system, 1M-CCU, database-schema, vertical-table, idempotency, cas, luban, migration

摘要

將原本 player_currencies 扁平表重構為語義明確的兩張垂直資源表(player_resources / player_resource_pools),並在此基礎上設計支撐 1M CCU 的角色多階段解鎖系統。涵蓋資料庫設計決策、Proto 設計原則(pool 資源用 repeated 結構而非硬編欄位)、DungeonToken 等核心貨幣的處理策略,以及 BooleanAutoOpen 獨立議題記錄。Phase 1(player_resources table + ResourceRepository)已開始實作。

關鍵學習

  • player_currencies 混用導致語義不清:純餘額型資源(Gold/Diamond/DungeonToken)與池型資源(Stamina 有上限+恢復計時)不應放同一張表

  • 垂直資源表(player_resources)以 type 欄位區分資源種類,新增 ResourcesType 8/9/10 完全不需要改 code 或做 migration

  • DungeonToken 屬於核心遊戲貨幣(關卡產出 → 角色解鎖消耗),應歸入 player_resources 垂直表以支援 CAS 防超扣和 CHECK 約束

  • Proto 欄位號(field number)一旦使用就不可重用,廢棄欄位必須加 reserved 宣告避免 wire format 衝突

  • Proto 命名不需要對應 DB table 命名,應反映業務語義(PlayerCurrency 已無意義,改用 PlayerResourceState)

  • 池型資源(Stamina 等)的 proto 絕對不能硬編 stamina_max / stamina_recover_at,必須設計為 repeated PoolResource 結構以利未來擴充

  • 角色解鎖操作必須加 idempotency key(player_id + role_id + unlock_stage),防止網路重試造成重複扣款

  • BooleanAutoOpen(道具/禮包領取後是否自動打開)屬於獨立功能議題,不在角色解鎖系統範疇內

  • 調研程式碼時必須讀取最新狀態,不可依賴快取或先前的讀取結果——這是被用戶明確糾正的重要工作習慣

  • Luban 生成的 GameRoleConfig Go struct 已包含 RoleUnlockCondition 和 RoleUnlockReward,企劃配置已完整更新

技術細節

資源表重構決策

原始 player_currencies 表混用了兩種性質完全不同的資源:

  • 純餘額型(Gold, Diamond, DungeonToken):只有加減操作,需要 CAS 防超扣
  • 池型(Stamina):有 max 上限 + 時間恢復計時器,需要額外欄位

重構後拆成兩張語義明確的表(兩張表都需要清楚的 COMMENT 說明差異):

-- 純餘額型資源(只有加減,無上限無恢復)
-- 新增資源類型只需插入一行,無需 ALTER TABLE
CREATE TABLE player_resources (
    player_id  BIGINT  NOT NULL,
    type       INT     NOT NULL,  -- ResourcesType enum
    amount     BIGINT  NOT NULL DEFAULT 0,
    CHECK (amount >= 0),
    PRIMARY KEY (player_id, type)
);

-- 池型資源(有上限 + 恢復機制,如 Stamina)
-- 未來若有其他池型資源也進此表,type 欄位區分
CREATE TABLE player_resource_pools (
    player_id    BIGINT NOT NULL,
    type         INT    NOT NULL,
    current      INT    NOT NULL DEFAULT 0,
    max_val      INT    NOT NULL,
    recover_at   BIGINT,  -- unix timestamp, nullable
    PRIMARY KEY (player_id, type)
);

Proto 設計關鍵決策

Proto message 命名應反映業務語義,PlayerCurrency 已無意義(因為資源不再只是 currency),改用 PlayerResourceState

Pool 型資源絕對不能硬編欄位,否則未來新增池型資源就會有 wire format 不相容問題:

// 錯誤設計(硬編 stamina 欄位)
message PlayerResourceState {
  int32 stamina = 2;
  int32 stamina_max = 3;
  int64 stamina_recover_at = 4;  // 未來加第二種 pool 型資源就爆炸
}

// 正確設計(repeated 結構)
message PlayerResourceState {
  reserved 7, 8, 9, 10;  // 舊版 extra_currencies 相關欄位,已廢棄
  repeated ResourceEntry resources = 1;
  repeated PoolResourceEntry pool_resources = 2;
}

message ResourceEntry {
  int32  type   = 1;  // ResourcesType enum
  int64  amount = 2;
}

message PoolResourceEntry {
  int32  type        = 1;
  int32  current     = 2;
  int32  max_val     = 3;
  int64  recover_at  = 4;  // unix timestamp
}

Luban Go Struct 確認狀態

Luban 生成的 GameRoleConfig struct 已包含所有必要欄位(企劃配置已完整更新):

type GameRoleConfig struct {
    ID                  int64
    DataDescription     string
    RoleUnlockCondition []interface{}        // 每階段解鎖條件
    RoleUnlockReward    []*Beans_RewardData  // 完成全部解鎖的獎勵
    RoleFavoriteGift    []int64
}

角色解鎖系統設計

多階段解鎖流程:玩家消耗 DungeonToken 逐步解鎖角色進度,全部解鎖後獲得 RoleUnlockReward。

解鎖操作需原子性:扣除資源 + 更新進度在同一個 transaction,加 idempotency key(player_id + role_id + unlock_stage)防重複扣款。

實作進度(Phase 順序)

  • Phase 1(已開始):建立 player_resources table + ResourceRepository
  • Phase 2:遷移 Gold/Diamond 進 player_resources
  • Phase 3:實作角色解鎖 API

What Changed

將混用的 player_currencies 重構為語義明確的兩張垂直資源表:player_resources(純餘額型)與 player_resource_pools(池型含恢復機制)。這個命名組合在多次討論後確認為最清晰的方案,並要求在 schema 加清楚的 COMMENT。

Proto 設計從最初草案(硬編 stamina 欄位)修正為 repeated PoolResource 結構,確保未來新增池型資源時不會有 wire format 不相容問題。同時確認 proto 命名應反映業務語義(PlayerResourceState),不需要對應 DB table 命名。廢棄欄位 field ⅞/9/10 已加 reserved 宣告。

新增本次對話的重要發現:Luban GameRoleConfig struct 已完整更新(含 RoleUnlockCondition / RoleUnlockReward),企劃配置無缺漏。Phase 1 實作已啟動(player_resources table + ResourceRepository)。BooleanAutoOpen 確認為獨立功能議題,不納入此次設計範疇。

So What

資源垂直表設計是整個遊戲資源系統的基礎。用 type 欄位區分資源種類,未來新增任何 ResourcesType 都不需要改 code 或做 migration,極大降低了長期維護成本。

Proto 的 repeated PoolResource 設計確保了 wire format 向後相容性,避免未來 client/server 版本不一致時的靜默資料損壞。

角色解鎖系統是核心玩法之一,idempotency key 設計是 1M CCU 環境下防止重複扣款的生產級必要設計。

Trade-offs

  • 垂直資源表 vs 橫向加欄位:垂直表彈性高、不用改 code,但 JOIN 查詢複雜度略增;橫向欄位查詢直覺但每次新增資源都要 ALTER TABLE
  • JSONB extra_currencies vs 實體欄位:JSONB 無須 migration 但無法做 CAS 和 CHECK 約束,核心高頻貨幣(DungeonToken)不適用
  • Proto repeated PoolResource vs 硬編欄位:repeated 結構彈性高,但 client 解析複雜度略增;硬編欄位直覺但日後擴充有 wire format 風險
  • 分 Phase 遷移策略:降低風險但需要過渡期維護雙表寫入,增加過渡期複雜度

Try It Fast

-- CAS 防超扣:原子性扣除資源,amount 不足時直接 reject
UPDATE player_resources
SET    amount = amount - $3
WHERE  player_id = $1
  AND  type     = $2
  AND  amount  >= $3
RETURNING amount;
-- 若影響 0 rows → 餘額不足,上層回傳 INSUFFICIENT_RESOURCE error

-- 新增資源類型只需插入一行,不需要 migration
INSERT INTO player_resources (player_id, type, amount)
VALUES ($1, $new_type, 0)
ON CONFLICT (player_id, type) DO NOTHING;
// proto 廢棄欄位保留 field number 避免 wire 衝突
message PlayerResourceState {
  reserved 7, 8, 9, 10;  // 舊版 extra_currencies 相關欄位,已廢棄
  repeated ResourceEntry resources = 1;
  repeated PoolResourceEntry pool_resources = 2;
}

message ResourceEntry {
  int32  type   = 1;  // ResourcesType enum
  int64  amount = 2;
}

message PoolResourceEntry {
  int32  type        = 1;
  int32  current     = 2;
  int32  max_val     = 3;
  int64  recover_at  = 4;  // unix timestamp
}

Recommendation

  1. 所有新增的遊戲資源預設進 player_resources 垂直表(type 欄位區分),評估後才考慮 pool 型
  2. 核心高頻貨幣(每局都會扣除的,如 DungeonToken)必須走實體欄位 + CAS,不可用 JSONB
  3. Proto 每次廢棄欄位都要在 message 層加 reserved <field_number> 宣告,納入 code review checklist
  4. Pool 型資源的 proto 設計用 repeated/map 結構,不要硬編每種資源的獨立欄位
  5. Proto message 命名反映業務語義,不需要與 DB table 命名一致
  6. 角色解鎖操作加 idempotency key(player_id + role_id + unlock_stage),防止網路重試造成重複扣款
  7. 遷移現有 player_currencies 資料時分 Phase 執行,每個 Phase 保留雙寫以便 rollback
  8. BooleanAutoOpen(道具/禮包領取後自動打開)作為獨立功能議題,後續單獨設計實作
  9. 調研程式碼時每次都必須重新讀取最新檔案狀態,不可依賴對話中先前讀取的快取結果

本文檔由 Semi-Brain 自動生成

Session ID: 04c2b73d-c00e-4eaf-959f-0dbf0f224e24

分析信心度: 88%