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