iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Vibe Coding

從 Vibe Coding 到系統架構:Gemini x Claude Code 雙 AI 協同開發 Godot 2D Roguelike 卡牌遊戲實戰系列 第 14 篇

Day 14:【程式碼理解】同樣是 duplicate(true),保護的東西完全不同?

  • 分享至 

  • xImage
  •  

Claude Code 在《異世界救援》的程式碼裡,前後兩次用了 duplicate(true)。

一次在 CardDatabase.gd,一次在 BattleScene.gd。

我一開始只知道它是在「複製資料」。但追問「為什麼要複製?」之後,才發現這兩個地方雖然用了同一個方法,真正要保護的東西完全不同。

先簡單說一下 duplicate(true)。

duplicate() 可以把一份資料複製成另一份:

var original = ["斬擊", "招架"]
var copydata = original.duplicate()

這樣 copydata 就是另一份資料。

而 duplicate(true) 裡面的 true,代表做「深層複製」。如果資料裡面還有 Array、Dictionary 等巢狀資料,也會一起複製。

例如卡牌資料其實比較像:

Array
 ├─ Dictionary
 ├─ Dictionary
 ├─ Dictionary
 └─ Dictionary

所以:

duplicate()
→ 複製外層

duplicate(true)
→ 連裡面的資料也一起複製

知道這件事後,再回頭看 Claude Code 為什麼在兩個地方都使用它,就很有意思了。

卡牌資料庫:保護原始資料

先看 CardDatabase.gd。

卡牌資料庫裡有一個固定的 CARD_POOL:

const CARD_POOL: Array = [
    {
        "id": "slash",
        "name": "斬擊",
        "cost": 1,
        "type": "attack",
        "value": 0,
        "desc": "造成攻擊力點傷害。"
    },

    # ...其餘卡牌
]

對外提供資料時,Claude Code 寫成:

static func get_all_cards() -> Array:
    return CARD_POOL.duplicate(true)

static func get_card_by_id(card_id: String) -> Dictionary:
    for card in CARD_POOL:
        if card.get("id", "") == card_id:
            return card.duplicate(true)

    return {}

我一開始的疑問是:

CARD_POOL 都已經宣告成 const 了,為什麼還需要複製?

我實際測了一下,發現 Godot 的 const 陣列和 Dictionary 具有唯讀狀態。

即使把裡面的 Dictionary 拿出來放進另一個變數,也不能因此直接修改:

var card = CARD_POOL[0]
card["value"] = 55

Godot 仍然會報:

ERROR: Dictionary is in read-only state.
SCRIPT ERROR: Invalid assignment on read-only value

所以如果其他地方需要拿到一份可以自由使用、修改的卡牌資料,就不能直接把原本的資料交出去。

這時候就需要複製。

而且普通的:

POOL.duplicate()

只會複製最外層的 Array,裡面的 Dictionary 還是原本的資料。

所以這裡才使用:

POOL.duplicate(true)

我實際測試後,複製出來的 Dictionary 就可以修改,而且不會影響原本的 POOL:

var deck2: Array = []

deck2.append(POOL[0].duplicate(true))

deck2[0]["value"] = 7

print(deck2[0])
print(POOL[0])

結果:

{ "id": "slash", "value": 7 }
{ "id": "slash", "value": 1 }

所以 CardDatabase 裡的 duplicate(true),是在保護:

原本的卡牌資料不要被其他地方改掉。


戰鬥場景:保護玩家建立好的牌組

第二個地方完全不同。

玩家前 9 關開寶箱、選卡,最後會慢慢建立自己的牌組:

Global.deck

進入戰鬥後,遊戲需要把這個牌組拿來洗牌、抽牌、移動到棄牌堆。

所以 Claude Code 寫:

draw_pile = Global.deck.duplicate(true)
draw_pile.shuffle()

這裡我問的問題就變成:

為什麼不能直接使用 Global.deck?

我做了一個很簡單的測試:

var global_deck: Array = [
    {"id": "slash"},
    {"id": "parry"}
]

var draw_pile_ref: Array = global_deck

draw_pile_ref.pop_back()

print(global_deck)

結果:

[{ "id": "slash" }]

明明操作的是 draw_pile_ref,原本的 global_deck 卻也少了一張。

因為這兩個變數其實指向同一份資料。

但遊戲裡,我希望:

Global.deck
→ 玩家目前建立好的牌組

draw_pile
→ 這一場戰鬥暫時使用的牌堆

戰鬥中的洗牌、抽牌、棄牌,都只是這一場戰鬥的操作。

所以應該先複製:

draw_pile = Global.deck.duplicate(true)

這樣戰鬥怎麼操作 draw_pile,都不會直接改掉玩家原本的牌組。

所以第二個 duplicate(true) 保護的是:

戰鬥中的暫時操作不要改到玩家原本建立好的牌組。


同樣是複製,為什麼兩邊都需要?

現在把兩個地方放在一起看:

位置 複製什麼 為什麼要複製
CardDatabase 原始卡牌資料 保護原始資料
BattleScene 玩家建立的牌組 讓戰鬥中的操作不要改到原牌組

兩邊都是:

.duplicate(true)

但它們解決的是不同問題。

第一個是在保護「原本不能被改的資料」。
第二個是在保護「戰鬥中不應該被永久修改的資料」。

這也讓我發現,看到 AI 寫:

some_data.duplicate(true)

不能只記住:「這裡要複製。」
還要問:「為什麼這裡需要一份新的資料?」

如果接下來的操作會修改資料,而這些修改不應該回寫到原本那一份,就需要複製。
反過來,如果修改本來就應該留下來,就不需要複製。
例如 賣血狂暴 會直接修改:

Global.player_attack += value

因為攻擊力增加本來就是玩家的新狀態,這個修改就應該留下來。

所以這次我學習到的,不只是 duplicate(true) 這一行程式碼。
而是看到「複製」時,可以多問一句:
「這份資料為什麼不能直接改?」

這個問題,有時候比記住一個函式更有用。


Day 15 又會遇到另一種問題。
護甲每回合要不要歸零?
魔王的攻擊意圖要不要提前告訴玩家?
這兩個設定看起來只是遊戲規則,實際上會直接影響玩家每一回合怎麼做決定。

參考


上一篇
Day 13:【Godot 實作】明確設定 mouse_filter 的預設值
下一篇
Day 15:【事件系統】RewardScene 三選一:9 關構築牌組的最小實作
系列文
從 Vibe Coding 到系統架構:Gemini x Claude Code 雙 AI 協同開發 Godot 2D Roguelike 卡牌遊戲實戰 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言