godot-save-load-systems
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSave/Load Systems
存档/读档系统
JSON serialization, version migration, and PERSIST group patterns define robust data persistence.
JSON序列化、版本迁移以及PERSIST组模式是构建可靠数据持久化的核心。
Available Scripts
可用脚本
save_migration_manager.gd
save_migration_manager.gd
Expert save file versioning with automatic migration between schema versions.
专业的存档文件版本控制,支持不同架构版本间的自动迁移。
save_system_encryption.gd
save_system_encryption.gd
AES-256 encrypted saves with compression to prevent casual save editing.
MANDATORY - For Production: Read save_migration_manager.gd before shipping to handle schema changes.
AES-256加密存档并附带压缩功能,防止随意修改存档。
生产环境强制要求:发布前请阅读save_migration_manager.gd以处理架构变更。
NEVER Do in Save Systems
存档系统中的绝对禁忌
- NEVER save without version field — Game updates, old saves break. MUST include + migration logic for schema changes.
"version": "1.0.0" - NEVER use absolute paths — breaks on other machines. Use
FileAccess.open("C:/Users/...")protocol (maps to OS-specific app data folder).user:// - NEVER save Node references — ? Nodes aren't serializable. Extract data via
save_data["player"] = $Playermethod instead.player.save_data() - NEVER forget to close FileAccess — without
var file = FileAccess.open(...)? File handle leak = save corruption. Use.close()OR GDScript auto-close on scope exit.close() - NEVER use JSON for large binary data — 10MB texture as base64 in JSON? Massive file size + slow parse. Use binary format () OR separate asset files.
store_var - NEVER trust loaded data — Save file edited by user? prevents crash if field missing. VALIDATE all loaded values.
data.get("health", 100) - NEVER save during physics/animation frames — trigger save? File corruption if game crashes mid-write. Save ONLY on explicit events (level complete, menu).
_physics_process
- 绝对不要在无版本字段的情况下存档 — 游戏更新后,旧存档会失效。必须包含字段,并为架构变更编写迁移逻辑。
"version": "1.0.0" - 绝对不要使用绝对路径 — 在其他设备上会失效。请使用
FileAccess.open("C:/Users/...")协议(映射到操作系统特定的应用数据文件夹)。user:// - 绝对不要保存Node引用 — ?节点无法被序列化。请通过
save_data["player"] = $Player方法提取数据。player.save_data() - 绝对不要忘记关闭FileAccess — 后不调用
var file = FileAccess.open(...)?文件句柄泄漏会导致存档损坏。请使用.close()方法或利用GDScript的作用域自动关闭特性。close() - 绝对不要用JSON存储大型二进制数据 — 将10MB纹理以base64格式存入JSON?会导致文件体积过大且解析缓慢。请使用二进制格式()或单独的资源文件。
store_var - 绝对不要信任加载的数据 — 存档可能被用户修改?使用可防止字段缺失时崩溃。请验证所有加载的值。
data.get("health", 100) - 绝对不要在物理/动画帧期间存档 — 在中触发存档?若游戏在写入过程中崩溃会导致存档损坏。仅在明确事件(关卡完成、菜单操作)时存档。
_physics_process
Pattern 1: JSON Save System (Recommended for Most Games)
模式1:JSON存档系统(多数游戏推荐)
Step 1: Create SaveManager AutoLoad
步骤1:创建SaveManager自动加载脚本
gdscript
undefinedgdscript
undefinedsave_manager.gd
save_manager.gd
extends Node
const SAVE_PATH := "user://savegame.save"
extends Node
const SAVE_PATH := "user://savegame.save"
Save data to JSON file
Save data to JSON file
func save_game(data: Dictionary) -> void:
var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if save_file == null:
push_error("Failed to open save file: " + str(FileAccess.get_open_error()))
return
var json_string := JSON.stringify(data, "\t") # Pretty print
save_file.store_line(json_string)
save_file.close()
print("Game saved successfully")func save_game(data: Dictionary) -> void:
var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if save_file == null:
push_error("Failed to open save file: " + str(FileAccess.get_open_error()))
return
var json_string := JSON.stringify(data, "\t") # Pretty print
save_file.store_line(json_string)
save_file.close()
print("Game saved successfully")Load data from JSON file
Load data from JSON file
func load_game() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
push_warning("Save file does not exist")
return {}
var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if save_file == null:
push_error("Failed to open save file: " + str(FileAccess.get_open_error()))
return {}
var json_string := save_file.get_as_text()
save_file.close()
var json := JSON.new()
var parse_result := json.parse(json_string)
if parse_result != OK:
push_error("JSON Parse Error: " + json.get_error_message())
return {}
return json.data as Dictionaryfunc load_game() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
push_warning("Save file does not exist")
return {}
var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if save_file == null:
push_error("Failed to open save file: " + str(FileAccess.get_open_error()))
return {}
var json_string := save_file.get_as_text()
save_file.close()
var json := JSON.new()
var parse_result := json.parse(json_string)
if parse_result != OK:
push_error("JSON Parse Error: " + json.get_error_message())
return {}
return json.data as DictionaryDelete save file
Delete save file
func delete_save() -> void:
if FileAccess.file_exists(SAVE_PATH):
DirAccess.remove_absolute(SAVE_PATH)
print("Save file deleted")
undefinedfunc delete_save() -> void:
if FileAccess.file_exists(SAVE_PATH):
DirAccess.remove_absolute(SAVE_PATH)
print("Save file deleted")
undefinedStep 2: Save Player Data
步骤2:保存玩家数据
gdscript
undefinedgdscript
undefinedplayer.gd
player.gd
extends CharacterBody2D
var health: int = 100
var score: int = 0
var level: int = 1
func save_data() -> Dictionary:
return {
"health": health,
"score": score,
"level": level,
"position": {
"x": global_position.x,
"y": global_position.y
}
}
func load_data(data: Dictionary) -> void:
health = data.get("health", 100)
score = data.get("score", 0)
level = data.get("level", 1)
if data.has("position"):
global_position = Vector2(
data.position.x,
data.position.y
)
undefinedextends CharacterBody2D
var health: int = 100
var score: int = 0
var level: int = 1
func save_data() -> Dictionary:
return {
"health": health,
"score": score,
"level": level,
"position": {
"x": global_position.x,
"y": global_position.y
}
}
func load_data(data: Dictionary) -> void:
health = data.get("health", 100)
score = data.get("score", 0)
level = data.get("level", 1)
if data.has("position"):
global_position = Vector2(
data.position.x,
data.position.y
)
undefinedStep 3: Trigger Save/Load
步骤3:触发存档/读档
gdscript
undefinedgdscript
undefinedgame_manager.gd
game_manager.gd
extends Node
func save_game_state() -> void:
var save_data := {
"player": $Player.save_data(),
"timestamp": Time.get_unix_time_from_system(),
"version": "1.0.0"
}
SaveManager.save_game(save_data)
func load_game_state() -> void:
var data := SaveManager.load_game()
if data.is_empty():
print("No save data found, starting new game")
return
if data.has("player"):
$Player.load_data(data.player)undefinedextends Node
func save_game_state() -> void:
var save_data := {
"player": $Player.save_data(),
"timestamp": Time.get_unix_time_from_system(),
"version": "1.0.0"
}
SaveManager.save_game(save_data)
func load_game_state() -> void:
var data := SaveManager.load_game()
if data.is_empty():
print("No save data found, starting new game")
return
if data.has("player"):
$Player.load_data(data.player)undefinedPattern 2: Binary Save System (Advanced, Faster)
模式2:二进制存档系统(进阶版,速度更快)
For large save files or when human-readability isn't needed:
gdscript
const SAVE_PATH := "user://savegame.dat"
func save_game_binary(data: Dictionary) -> void:
var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if save_file == null:
return
save_file.store_var(data, true) # true = full objects
save_file.close()
func load_game_binary() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if save_file == null:
return {}
var data: Dictionary = save_file.get_var(true)
save_file.close()
return data适用于大型存档文件或无需人类可读性的场景:
gdscript
const SAVE_PATH := "user://savegame.dat"
func save_game_binary(data: Dictionary) -> void:
var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if save_file == null:
return
save_file.store_var(data, true) # true = full objects
save_file.close()
func load_game_binary() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if save_file == null:
return {}
var data: Dictionary = save_file.get_var(true)
save_file.close()
return dataPattern 3: PERSIST Group Pattern
模式3:PERSIST组模式
For auto-saving nodes with the group:
persistgdscript
undefined适用于自动保存标记为组的节点:
persistgdscript
undefinedAdd nodes to "persist" group in editor or via code:
Add nodes to "persist" group in editor or via code:
add_to_group("persist")
add_to_group("persist")
Implement save/load in each persistent node:
Implement save/load in each persistent node:
func save() -> Dictionary:
return {
"filename": get_scene_file_path(),
"parent": get_parent().get_path(),
"pos_x": position.x,
"pos_y": position.y,
# ... other data
}
func load(data: Dictionary) -> void:
position = Vector2(data.pos_x, data.pos_y)
# ... load other data
func save() -> Dictionary:
return {
"filename": get_scene_file_path(),
"parent": get_parent().get_path(),
"pos_x": position.x,
"pos_y": position.y,
# ... other data
}
func load(data: Dictionary) -> void:
position = Vector2(data.pos_x, data.pos_y)
# ... load other data
SaveManager collects all persist nodes:
SaveManager collects all persist nodes:
func save_all_persist_nodes() -> void:
var save_nodes := get_tree().get_nodes_in_group("persist")
var save_dict := {}
for node in save_nodes:
if not node.has_method("save"):
continue
save_dict[node.name] = node.save()
save_game(save_dict)undefinedfunc save_all_persist_nodes() -> void:
var save_nodes := get_tree().get_nodes_in_group("persist")
var save_dict := {}
for node in save_nodes:
if not node.has_method("save"):
continue
save_dict[node.name] = node.save()
save_game(save_dict)undefinedBest Practices
最佳实践
1. Use user://
Protocol
user://1. 使用user://
协议
user://gdscript
undefinedgdscript
undefined✅ Good - platform-independent
✅ Good - platform-independent
const SAVE_PATH := "user://savegame.save"
const SAVE_PATH := "user://savegame.save"
❌ Bad - hardcoded path
❌ Bad - hardcoded path
const SAVE_PATH := "C:/Users/Player/savegame.save"
**`user://` paths:**
- **Windows**: `%APPDATA%\Godot\app_userdata\[project_name]`
- **macOS**: `~/Library/Application Support/Godot/app_userdata/[project_name]`
- **Linux**: `~/.local/share/godot/app_userdata/[project_name]`const SAVE_PATH := "C:/Users/Player/savegame.save"
**`user://`路径对应:**
- **Windows**: `%APPDATA%\Godot\app_userdata\[project_name]`
- **macOS**: `~/Library/Application Support/Godot/app_userdata/[project_name]`
- **Linux**: `~/.local/share/godot/app_userdata/[project_name]`2. Version Your Save Format
2. 为存档格式添加版本
gdscript
const SAVE_VERSION := "1.0.0"
func save_game(data: Dictionary) -> void:
data["version"] = SAVE_VERSION
# ... save logic
func load_game() -> Dictionary:
var data := # ... load logic
if data.get("version") != SAVE_VERSION:
push_warning("Save version mismatch, migrating...")
data = migrate_save_data(data)
return datagdscript
const SAVE_VERSION := "1.0.0"
func save_game(data: Dictionary) -> void:
data["version"] = SAVE_VERSION
# ... save logic
func load_game() -> Dictionary:
var data := # ... load logic
if data.get("version") != SAVE_VERSION:
push_warning("Save version mismatch, migrating...")
data = migrate_save_data(data)
return data3. Handle Errors Gracefully
3. 优雅处理错误
gdscript
func save_game(data: Dictionary) -> bool:
var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if save_file == null:
var error := FileAccess.get_open_error()
push_error("Save failed: " + error_string(error))
return false
save_file.store_line(JSON.stringify(data))
save_file.close()
return truegdscript
func save_game(data: Dictionary) -> bool:
var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if save_file == null:
var error := FileAccess.get_open_error()
push_error("Save failed: " + error_string(error))
return false
save_file.store_line(JSON.stringify(data))
save_file.close()
return true4. Auto-Save Pattern
4. 自动存档模式
gdscript
var auto_save_timer: Timer
func _ready() -> void:
# Auto-save every 5 minutes
auto_save_timer = Timer.new()
add_child(auto_save_timer)
auto_save_timer.wait_time = 300.0
auto_save_timer.timeout.connect(_on_auto_save)
auto_save_timer.start()
func _on_auto_save() -> void:
save_game_state()
print("Auto-saved")gdscript
var auto_save_timer: Timer
func _ready() -> void:
# Auto-save every 5 minutes
auto_save_timer = Timer.new()
add_child(auto_save_timer)
auto_save_timer.wait_time = 300.0
auto_save_timer.timeout.connect(_on_auto_save)
auto_save_timer.start()
func _on_auto_save() -> void:
save_game_state()
print("Auto-saved")Testing Save Systems
测试存档系统
gdscript
func _ready() -> void:
if OS.is_debug_build():
test_save_load()
func test_save_load() -> void:
var test_data := {"test_key": "test_value", "number": 42}
save_game(test_data)
var loaded := load_game()
assert(loaded.test_key == "test_value")
assert(loaded.number == 42)
print("Save/Load test passed")gdscript
func _ready() -> void:
if OS.is_debug_build():
test_save_load()
func test_save_load() -> void:
var test_data := {"test_key": "test_value", "number": 42}
save_game(test_data)
var loaded := load_game()
assert(loaded.test_key == "test_value")
assert(loaded.number == 42)
print("Save/Load test passed")Common Gotchas
常见陷阱
Issue: Saved Vector2/Vector3 not loading correctly
gdscript
undefined问题:保存的Vector2/Vector3无法正确加载
gdscript
undefined✅ Solution: Store as x, y, z components
✅ Solution: Store as x, y, z components
"position": {"x": pos.x, "y": pos.y}
"position": {"x": pos.x, "y": pos.y}
Then reconstruct:
Then reconstruct:
position = Vector2(data.position.x, data.position.y)
**Issue**: Resource paths not resolving
```gdscriptposition = Vector2(data.position.x, data.position.y)
**问题**:资源路径无法解析
```gdscript✅ Store resource paths as strings
✅ Store resource paths as strings
"texture_path": texture.resource_path
"texture_path": texture.resource_path
Then reload:
Then reload:
texture = load(data.texture_path)
undefinedtexture = load(data.texture_path)
undefinedReference
参考资料
Related
相关内容
- Master Skill: godot-master
- Master Skill: godot-master