godot-save-load-systems

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Save/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
    "version": "1.0.0"
    + migration logic for schema changes.
  • NEVER use absolute paths
    FileAccess.open("C:/Users/...")
    breaks on other machines. Use
    user://
    protocol (maps to OS-specific app data folder).
  • NEVER save Node references
    save_data["player"] = $Player
    ? Nodes aren't serializable. Extract data via
    player.save_data()
    method instead.
  • NEVER forget to close FileAccess
    var file = FileAccess.open(...)
    without
    .close()
    ? File handle leak = save corruption. Use
    close()
    OR GDScript auto-close on scope exit.
  • NEVER use JSON for large binary data — 10MB texture as base64 in JSON? Massive file size + slow parse. Use binary format (
    store_var
    ) OR separate asset files.
  • NEVER trust loaded data — Save file edited by user?
    data.get("health", 100)
    prevents crash if field missing. VALIDATE all loaded values.
  • NEVER save during physics/animation frames
    _physics_process
    trigger save? File corruption if game crashes mid-write. Save ONLY on explicit events (level complete, menu).

  • 绝对不要在无版本字段的情况下存档 — 游戏更新后,旧存档会失效。必须包含
    "version": "1.0.0"
    字段,并为架构变更编写迁移逻辑。
  • 绝对不要使用绝对路径
    FileAccess.open("C:/Users/...")
    在其他设备上会失效。请使用
    user://
    协议(映射到操作系统特定的应用数据文件夹)。
  • 绝对不要保存Node引用
    save_data["player"] = $Player
    ?节点无法被序列化。请通过
    player.save_data()
    方法提取数据。
  • 绝对不要忘记关闭FileAccess
    var file = FileAccess.open(...)
    后不调用
    .close()
    ?文件句柄泄漏会导致存档损坏。请使用
    close()
    方法或利用GDScript的作用域自动关闭特性。
  • 绝对不要用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
undefined
gdscript
undefined

save_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 Dictionary
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 Dictionary

Delete save file

Delete save file

func delete_save() -> void: if FileAccess.file_exists(SAVE_PATH): DirAccess.remove_absolute(SAVE_PATH) print("Save file deleted")
undefined
func delete_save() -> void: if FileAccess.file_exists(SAVE_PATH): DirAccess.remove_absolute(SAVE_PATH) print("Save file deleted")
undefined

Step 2: Save Player Data

步骤2:保存玩家数据

gdscript
undefined
gdscript
undefined

player.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 )
undefined
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 )
undefined

Step 3: Trigger Save/Load

步骤3:触发存档/读档

gdscript
undefined
gdscript
undefined

game_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)
undefined
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)
undefined

Pattern 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 data

Pattern 3: PERSIST Group Pattern

模式3:PERSIST组模式

For auto-saving nodes with the
persist
group:
gdscript
undefined
适用于自动保存标记为
persist
组的节点:
gdscript
undefined

Add 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)
undefined
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)
undefined

Best Practices

最佳实践

1. Use
user://
Protocol

1. 使用
user://
协议

gdscript
undefined
gdscript
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 data
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 data

3. 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 true
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 true

4. 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
```gdscript
position = 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)
undefined
texture = load(data.texture_path)
undefined

Reference

参考资料

Related

相关内容

  • Master Skill: godot-master
  • Master Skill: godot-master