๊ฒŒ์ž„์„ ๋งŒ๋“ค๋‹ค๋ณด๋ฉด ์ˆ˜์ •ํ•  ์ผ์ด ์ž์ฃผ ์žˆ๋‹ค

๋งŒ์•ฝ ์ถœ์‹œ ์ „์ด๋ผ๋ฉด ์ด์ „ ๋ฒ„์ „์—์„œ ์ €์žฅํ•œ ์„ธ์ด๋ธŒ ๋ฐ์ดํ„ฐ๋Š” ์‹ ๊ฒฝ ์“ธ ํ•„์š” ์—†์ง€๋งŒ ์ด๋ฏธ ์ถœ์‹œ ํ–ˆ๋‹ค๋ฉด?

๊ธฐ์กด ์„ธ์ด๋ธŒ ๋ฐ์ดํ„ฐ๋กœ๋„ ๋กœ๋”ฉ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•ด์•ผ ํ• ๊ฒƒ์ด๋‹ค.
๊ทธ๋ž˜์„œ ์ด์ „์— ๋งŒ๋“ค๋˜ ์ฝ”๋“œ์—์„œ ์„ธ์ด๋ธŒ ํŒŒ์ผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถ€๋ถ„์„ ๋“ค๊ณ ์™”๋‹ค.

์ „์ž‘์€ ๋ฑ ๋นŒ๋”ฉ ๋กœ๊ทธ๋ผ์ดํฌ๋ผ์„œ ์„ธ์ด๋ธŒ ํŒŒ์ผ์˜ ์ค‘์š”๋„๊ฐ€ ๋ณด๋‹ค ๋–จ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์—ย 
ํ•ด๋‹น ์„ธ์ด๋ธŒ๋ฅผ ํŒŒ๊ธฐํ• ์ง€ ์‚ฌ์šฉํ• ์ง€ ์—ฌ๋ถ€๋„ ๋“ค์–ด๊ฐ€๋Š” can_migration ํ•จ์ˆ˜๋„ ์žˆ๋Š”๊ฑธ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

@abstract
class_name Migration_Base

func migrate(data: Dictionary) -> Dictionary:
data.version = update_version()

return data

@abstract
func update_version() -> String

@abstract
func can_migration() -> bool



# class_name Migration_0_0_1
extends Migration_Base

# PlayerData.base_data.speed_increment ์ถ”๊ฐ€๋จ
func migrate(data: Dictionary) -> Dictionary:
# 0.0.1 -> 0.0.2
data = super.migrate(data)

data.player.base_data.speed_increment = 0.1
return data

func update_version() -> String:
return "0.0.2"

func can_migration() -> bool:
return true


์ง€๊ธˆ ๋งŒ๋“œ๋Š” ๊ฒŒ์ž„์€ ๋ฐฉ์น˜ํ˜• ๊ฒŒ์ž„์œผ๋กœ ์•„๋ฌด๋ฆฌ ๋ชปํ•ด๋„ 4์‹œ๊ฐ„ ์ด์ƒ์˜ ํ”Œ๋ ˆ์ด ํƒ€์ž„์„ ๋ณด์žฅํ• ๊ฑด๋ฐ

์ด๊ฑธ ๋ฒ„์ „ ์˜ฌ๋ผ๊ฐ”๋‹ค๊ณ  ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ํ•˜๋ผ๊ณ  ํ•˜๋ฉด ์•ˆ๋œ๋‹ค

์ผ๋‹จ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์—… ์ „์— ์„ธ์ด๋ธŒ ํŒŒ์ผ ๊ตฌ์กฐ๋ถ€ํ„ฐ ๋ณด์ž

class_name SaveLoadUtils

const SAVE_DIR_PATH := "user://save"

static func save_dict_file(dict: Dictionary, file_name: String) -> void:
if not DirAccess.dir_exists_absolute(SAVE_DIR_PATH):
var error := DirAccess.make_dir_absolute(SAVE_DIR_PATH)
if error != OK:
prints("make_dir_error", file_name, error)
return

var file_path := SAVE_DIR_PATH.path_join(file_name)
var file := FileAccess.open(file_path, FileAccess.WRITE)
if not file:
prints("file_error", file_name, FileAccess.get_open_error())
return

var bytes := var_to_bytes(dict)
file.store_buffer(bytes)
file.close()

static func load_from_file(file_name: String) -> Dictionary:
var file_path := SAVE_DIR_PATH.path_join(file_name)
if not FileAccess.file_exists(file_path):
prints("file_not_found", file_name)
return {}

var file := FileAccess.open(file_path, FileAccess.READ)
if not file:
prints("file_error", file_name, FileAccess.get_open_error())
return {}

var bytes := file.get_buffer(file.get_length())
file.close()

var data: Variant = bytes_to_var(bytes)
if typeof(data) != TYPE_DICTIONARY:
prints("invalid_save_data", file_name)
return {}

return data as Dictionary


์ง€๊ธˆ ํ”„๋กœ์ ํŠธ๋Š” ์œ„ ์ฝ”๋“œ๋กœ ์„ธ์ด๋ธŒ ๋กœ๋“œ๋ฅผ ๋‹ด๋‹นํ•˜๊ณ  ์žˆ๋‹ค.

๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹จ์ผ Dictionary๋กœ ๋ณ€ํ™˜ ํ›„ var_to_bytes ํ•˜๋Š” ๊ตฌ์กฐ์ด๋‹ค.
json ์„ ์‚ฌ์šฉํ• ๊นŒ ํ–ˆ์ง€๋งŒ ์–ด์งœํ”ผ ์™ธ๋ถ€์—์„œ ์—ด๊ณ  ์ˆ˜์ •ํ•  ํ•„์š”๋„ ์—†๊ณ  json์ด ๊ฐ€์ง€๋Š” ํŠน๋ณ„ํ•œ ์ด์ ๋„ ์—†์–ด์„œ ์ด๋Ÿฐ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

์„ธ์ด๋ธŒ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋Š” ์•„๋ž˜ 2๊ฐ€์ง€ ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ ย  GameManager ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด to_dict๋ฅผ ํ˜ธ์ถœํ•ด ๊ฐ€๋Š” ๋ฐฉ์‹์ด๋‹ค

func to_dict() -> Dictionary:
static func deserialize(dict: Dictionary) -> T:


๊ทธ๋ž˜์„œ ๋งŒ๋“  0.0.1 => 0.0.2 ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค

extends Migration_Base

# GARBAGE_COLLECTION => BUBBLE_DESTROY ์œผ๋กœ ๋ณ€๊ฒฝ๋จ
# ์ผ๋ถ€ ์ˆ˜์น˜ ๋ณ€๊ฒฝํ•จ
func migrate(data: Dictionary) -> Dictionary:
# 0.0.1 -> 0.0.2
data = super.migrate(data)

# Enum.Upgrade_Type.CRITICAL_HITBOX = 1163
# Enum.Upgrade_Mod_Target.CORNER_GOLD = 1102
if data.upgrade.upgrades.has(1163) and data.upgrade.upgrades[1163].current_level > 0:
update_value(data, 1102, "critical_hitbox", Enum.Upgrade_Mod_Op.ADD, 400)
...
# Enum.Upgrade_Mod_Target.BUBBLE_GOLD_PER_SECOND = 3008
# Enum.Upgrade_Mod_Target.BUBBLE_DESTROY_GOLD = 3009
update_value(data, 3008, "default_value_placeholder", Enum.Upgrade_Mod_Op.ADD, 0.0)
update_value(data, 3009, "default_value_placeholder", Enum.Upgrade_Mod_Op.ADD, 0.0)
return data

func update_value(data: Dictionary, stat_id: int, id: String, op: Enum.Upgrade_Mod_Op, value: float) -> Dictionary:
var temp_states: Dictionary = data.upgrade.stat_states[stat_id]
for entry_dict in temp_states.get("applied_modifiers", []):
if entry_dict.source_id == id:
entry_dict.value = value
entry_dict.op = op
break
return data

func update_version() -> String:
return "0.0.2"

func can_migration() -> bool:
return true



์ž˜ ๋™์ž‘ํ•˜๋‚˜ ์ƒ์ˆ˜ ํˆฌ์„ฑ์ด๊ณ ย 
Upgrade_BubbleDestroy ์ฐธ์กฐ๊ฐ€ ์žˆ์–ด ๋‚˜์ค‘์— ์–ด๋–ป๊ฒŒ ๋ ์ง€ ๋ชจ๋ฅด๋Š” ๋‹จ์ ์ด ์žˆ์—ˆ๋‹ค

100% ํ™•๋ฅ ๋กœ ๋‚˜์ค‘์— ํ•œ๋‘๊ฐœ ๋นผ๋จน์–ด์„œ ์˜๋„ํ•œ ์ˆ˜์น˜๊ฐ€ ์•„๋‹Œ ์ž˜๋ชป๋œ ๊ฐ’์ด ๋“ค์–ด๊ฐˆ ํ™˜๊ฒฝ์ด์˜€๋‹ค.

์ด๋Ÿฐ์‹์œผ๋กœ ๊ตฌํ˜„ ํ•  ์ˆ˜ ๋ฐ–์— ์—†๋˜๊ฒŒ ์—…๊ทธ๋ ˆ์ด๋“œ์‹œ ์ค‘์•™ ์—…๊ทธ๋ ˆ์ด๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋ณ€์กฐํ•˜๋Š” ๊ตฌ์กฐ๋ผ์„œ
์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฏธ ๊ตฌ๋งคํ•˜์˜€์ง€๋งŒ ์ˆ˜์ •๋œ ์—…๊ทธ๋ ˆ์ด๋“œ๋ฅผ ์ ์šฉํ•˜๋ ค๋ฉด ์ด๊ฒŒ ์ตœ์„ ์ด์˜€๋‹ค.

func _on_purchased() -> void:
data.current_value += level_upgrade_amounts[current_level - 1]
GameManager.upgrade.add_stat_modifier(
self,
Enum.Upgrade_Mod_Target.ALL_GOLD,
Enum.Upgrade_Mod_Op.MUL,
level_upgrade_amounts[current_level - 1]
)


์ด๋Ÿฐ์‹์œผ๋กœ _on_purchased์—์„œ ์ˆ˜์ •ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค

๋”ฐ๋กœ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋กœ ๋ฝ‘์•˜์œผ๋ฉด ์‰ฝ๊ฒŒ ๊ฐ€๋Šฅํ•œ๋ฐ modifier ๋ง๊ณ  ๋‹ค๋ฅธ ๋™์ž‘ ํ•  ๊ฒฝ์šฐ๋„ ์žˆ์–ด ํ•˜๋“œ์ฝ”๋”ฉ ํ–ˆ๋‹ค

๊ทธ๋ž˜์„œ ๊ตฌ๋งค์‹œ ๋™์ž‘์„ ์ˆ˜์ •ํ•˜์˜€๋‹ค
๋จผ์ € UpgradePurchaseEffectContext๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค

@abstract
class_name UpgradePurchaseEffectContext
extends RefCounted

@abstract
func add_stat_modifier(
source_upgrade: Upgrade_Base,
target: Enum.Upgrade_Mod_Target,
op: Enum.Upgrade_Mod_Op,
value: float
) -> void


๊ทธ๋‹ค์Œ ์—…๊ทธ๋ž˜์ด๋“œ ๊ตฌ์ž…์‹œ ์‚ฌ์šฉํ•  UpgradeDataPurchaseEffectContext ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค ์ƒ์„ฑ์ž์—์„œ upgrade_data ๋ฐ›์•„์„œ ์“ฐ๋Š”์‹์ด๋‹ค

class_name UpgradeDataPurchaseEffectContext
extends UpgradePurchaseEffectContext

var upgrade_data

func _init(p_upgrade_data) -> void:
upgrade_data = p_upgrade_data

func add_stat_modifier(
source_upgrade: Upgrade_Base,
target: Enum.Upgrade_Mod_Target,
op: Enum.Upgrade_Mod_Op,
value: float
) -> void:
upgrade_data.add_stat_modifier(source_upgrade, target, op, value)



๊ทธ๋ฆฌ๊ณ  ์ด๊ฑด ์—…๊ทธ๋ ˆ์ด๋“œ ๋ณต๊ตฌ์— ์‚ฌ์šฉํ• ๊ฑฐ

class_name RawPurchaseEffectContext
extends UpgradePurchaseEffectContext

var modifier_entries_by_target := {}

func add_stat_modifier(
source_upgrade: Upgrade_Base,
target: Enum.Upgrade_Mod_Target,
op: Enum.Upgrade_Mod_Op,
value: float
) -> void:
if value == 0.0:
return

if not modifier_entries_by_target.has(target):
modifier_entries_by_target[target] = []

var modifier_entries: Array = modifier_entries_by_target[target]
for entry_dict in modifier_entries:
if entry_dict["source_id"] == source_upgrade.id and entry_dict["op"] == op:
entry_dict["value"] = float(entry_dict["value"]) + value
return

modifier_entries.append({
"source_id": source_upgrade.id,
"source_name": source_upgrade.upgrade_name,
"op": op,
"value": value,
})


๊ทธ๋‹ค์Œ์€ ๊ตฌ๋งค์‹œ ๋™์ž‘์„ ์ˆ˜์ •ํ•˜์ž

func _apply_purchase_effect(context: UpgradePurchaseEffectContext, level: int) -> void:
data.current_value += level_upgrade_amounts[level - 1]
context.add_stat_modifier(
self,
Enum.Upgrade_Mod_Target.ALL_GOLD,
Enum.Upgrade_Mod_Op.MUL,
level_upgrade_amounts[level - 1]
)


context๋ฅผ ๋ฐ›์•„์„œ ๋™์ž‘ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Œ
์ด๋Ÿฌ๋ฉด ๋‹ค๋ฅธ context๋ฅผ ์ „๋‹ฌํ•ด์„œ ๋ญ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค

๋ฌด์กฐ๊ฑด ๊ตฌ์ž…์—๋งŒ ๋™์ž‘ํ•˜๊ณ  ๋‹ค์‹œ ๋กœ๋”ฉํ•˜๋ฉด ์•ˆ๋˜๋Š”๊ฑด ์ด๊ฑธ ๋งŒ๋“ค์–ด์„œ ๋Œ๋ฆฌ๊ธฐ๋กœ ํ–ˆ์Œ

func _on_purchased_side_effect() -> void:
pass


๊ทธ๋Ÿผ ๋กœ์ง์„ ์ด๋Ÿฐ์‹์œผ๋กœ ์ˆ˜์ • ํ•  ์ˆ˜ ์žˆ์Œ

Upgrade_BubbleDestroy๋Š” class ์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์ธ๋ฐ ์ด๊ฑด ์ž์ฃผ ์žˆ์ง„ ์•Š์„๊ฒƒ ๊ฐ™์•„์„œ ๊ทธ๋ƒฅ ๋ฝ‘์•„์„œ ๋„ฃ๋Š”์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•จ


extends Migration_Base

func migrate(data: Dictionary) -> Dictionary:
# 0.0.1 -> 0.0.2
data = super.migrate(data)
# Enum.Upgrade_Type.ZERO_PIXEL = 1042
# Enum.Upgrade_Mod_Target.CORNER_GOLD = 1102
if data.upgrade.upgrades.has(1042):
mark_purchase_effect_dirty(data, 1042)
...
# Enum.Upgrade_Type.CRITICAL_HITBOX = 1163
# Enum.Upgrade_Mod_Target.CORNER_GOLD = 1102
if data.upgrade.upgrades.has(1163):
mark_purchase_effect_dirty(data, 1163)

# Enum.Upgrade_Mod_Target.BUBBLE_GOLD_PER_SECOND = 3008
# Enum.Upgrade_Mod_Target.BUBBLE_DESTROY_GOLD = 3009
update_value(data, 3008, "default_value_placeholder", Enum.Upgrade_Mod_Op.ADD, 0.0)
update_value(data, 3009, "default_value_placeholder", Enum.Upgrade_Mod_Op.ADD, 0.0)

return data

func update_version() -> String:
return "0.0.2"

func can_migration() -> bool:
return true




์ตœ์ข…์ ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์—์„  ์—…๋ฐ์ดํŠธ ๋œ ๊ฒƒ๋“ค ๋”ํ‹ฐ ํ”Œ๋ž˜๊ทธ๋งŒ ์„ค์ •ํ•˜๋ฉดย 
์•Œ์•„์„œ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ•ด์„œ ์ˆ˜์น˜ ๋ฒˆ๊ฒฝ์—๋Š” ๋Œ€์‘ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค

์ด๋Ÿฌ๋ฉด ์ƒ์ˆ˜ ์‚ฌ์šฉ์„ ์ค„์—ฌ์„œ ํœด๋จผ์—๋Ÿฌ๋ฅผ ๊ฐ์†Œ์‹œํ‚ฌ ์ˆ˜ ์žˆ์„๊ฒƒ์ด๋‹ค

ํ˜น์‹œ ๊ฐ’ ์ž˜๋ชป ์ž…๋ ฅํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๋นผ๋จน๋Š” ๊ฒฝ์šฐ๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฝ”๋“œ ์ž‘์„ฑ์„ AI์— ์œ„์ž„ํ•˜๋Š” ์‹์œผ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค.

git diff ํ™•์ธํ•˜๊ณ  ์•Œ์•„์„œ ์ž‘์„ฑํ•ด์คŒ


extends Migration_Base

# ๋ถ„์„ ๋ฒ”์œ„: f75ece8a24772e0de99864a34e3beb722e0c1706 -> ํ˜„์žฌ HEAD/์ž‘์—… ์ƒํƒœ
# ๋ณ€๊ฒฝ ์š”์•ฝ:
# - FEVER_CHARGE_DURATION(2201)์„ FEVER_CHARGE_PER_SECOND(2201) ์˜๋ฏธ๋กœ ์ „ํ™˜
func migrate(data: Dictionary) -> Dictionary:
# 0.0.2 -> 0.0.3
data = super.migrate(data)
# Enum.Upgrade_Type.FEVER_TIME = 1122
# Enum.Upgrade_Mod_Target.FEVER_CHARGE_PER_SECOND = 2201
# source_id: fever_time
if data.upgrade.upgrades.has(1122):
mark_purchase_effect_dirty(data, 1122)
...