godot-save-load-systems by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-save-load-systemsJSON 序列化、版本迁移和 PERSIST 组模式定义了健壮的数据持久化方案。
"version": "1.0.0" 字段并实现迁移逻辑。C:/Users/... 在其他每台机器上都会失效。始终使用 user:// 协议,Godot 会将其映射到正确的、特定于操作系统的应用程序数据文件夹。Dictionary 或 Resource 中。close() 更安全。store_var() 或单独的专用资源文件。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
data.get("field", default_value) 并验证数字是否在预期范围内,以防止崩溃。erase() 或 add() 会导致迭代错误。使用 data.duplicate() 来安全地迭代。FileAccess.open_encrypted_with_pass() 来保护它。ResourceLoader.load_threaded_request() 在后台加载关卡。String 类型 UUID。true 允许完全的对象解码,对于从网络下载的存档来说,这是一个重大的安全风险。Vector3 转换为字符串或字典。对于严格的数据类型,请使用 var_to_bytes() 或二进制格式。强制要求:在实现相应模式之前,请阅读对应的脚本。
10 个专家级模式:PERSIST 组序列化、二进制快照、JSON 安全解析和线程化加载。
专家级存档文件版本管理,支持模式版本间的自动迁移。
AES-256 加密存档,带压缩功能,防止随意修改存档。
# save_manager.gd
extends Node
const SAVE_PATH := "user://savegame.save"
## 将数据保存到 JSON 文件
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") # 美化输出
save_file.store_line(json_string)
save_file.close()
print("Game saved successfully")
## 从 JSON 文件加载数据
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 delete_save() -> void:
if FileAccess.file_exists(SAVE_PATH):
DirAccess.remove_absolute(SAVE_PATH)
print("Save file deleted")
# 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
)
# 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)
适用于大型存档文件或不需要人类可读性的情况:
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 = 完整对象
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
用于自动保存带有 persist 组的节点:
# 在编辑器或通过代码将节点添加到 "persist" 组:
add_to_group("persist")
# 在每个持久化节点中实现保存/加载:
func save() -> Dictionary:
return {
"filename": get_scene_file_path(),
"parent": get_parent().get_path(),
"pos_x": position.x,
"pos_y": position.y,
# ... 其他数据
}
func load(data: Dictionary) -> void:
position = Vector2(data.pos_x, data.pos_y)
# ... 加载其他数据
# SaveManager 收集所有持久化节点:
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)
user:// 协议# ✅ 良好 - 平台无关
const SAVE_PATH := "user://savegame.save"
# ❌ 糟糕 - 硬编码路径
const SAVE_PATH := "C:/Users/Player/savegame.save"
user:// 路径:
%APPDATA%\Godot\app_userdata\[project_name]~/Library/Application Support/Godot/app_userdata/[project_name]~/.local/share/godot/app_userdata/[project_name]const SAVE_VERSION := "1.0.0"
func save_game(data: Dictionary) -> void:
data["version"] = SAVE_VERSION
# ... 保存逻辑
func load_game() -> Dictionary:
var data := # ... 加载逻辑
if data.get("version") != SAVE_VERSION:
push_warning("Save version mismatch, migrating...")
data = migrate_save_data(data)
return data
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
var auto_save_timer: Timer
func _ready() -> void:
# 每 5 分钟自动保存一次
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")
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")
问题:保存的 Vector2/Vector3 无法正确加载
# ✅ 解决方案:存储为 x, y, z 分量
"position": {"x": pos.x, "y": pos.y}
# 然后重建:
position = Vector2(data.position.x, data.position.y)
问题:资源路径无法解析
# ✅ 将资源路径存储为字符串
"texture_path": texture.resource_path
# 然后重新加载:
texture = load(data.texture_path)
每周安装数
95
仓库
GitHub 星标数
59
首次出现
2026年2月10日
安全审计
安装于
gemini-cli91
opencode91
github-copilot89
codex89
kimi-cli87
amp87
JSON serialization, version migration, and PERSIST group patterns define robust data persistence.
"version": "1.0.0" field and implement migration logic.C:/Users/... will break on every other machine. Always use the user:// protocol, which Godot maps to the correct OS-specific app data folder.Dictionary or Resource instead.close() is safer for long-running logic.store_var() or separate dedicated asset files.data.get("field", default_value) and validate that numbers are within expected ranges to prevent crashes.erase() or add() inside a loop over the same dictionary causes iteration errors. Use data.duplicate() to iterate safely.FileAccess.open_encrypted_with_pass() to secure it.ResourceLoader.load_threaded_request() to load levels in the background.String UUIDs for game objects.true allows full object decoding, which is a major security risk for saves downloaded from the web.Vector3 to a string or dictionary. For strict data types, use var_to_bytes() or a binary format.MANDATORY : Read the appropriate script before implementing the corresponding pattern.
10 Expert patterns: PERSIST group serialization, binary snapshots, JSON safe-parsing, and threaded loading.
Expert save file versioning with automatic migration between schema versions.
AES-256 encrypted saves with compression to prevent casual save editing.
# save_manager.gd
extends Node
const SAVE_PATH := "user://savegame.save"
## 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")
## 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
## Delete save file
func delete_save() -> void:
if FileAccess.file_exists(SAVE_PATH):
DirAccess.remove_absolute(SAVE_PATH)
print("Save file deleted")
# 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
)
# 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)
For large save files or when human-readability isn't needed:
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
For auto-saving nodes with the persist group:
# Add nodes to "persist" group in editor or via code:
add_to_group("persist")
# 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
# 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)
user:// Protocol# ✅ Good - platform-independent
const SAVE_PATH := "user://savegame.save"
# ❌ Bad - hardcoded path
const SAVE_PATH := "C:/Users/Player/savegame.save"
user:// paths:
%APPDATA%\Godot\app_userdata\[project_name]~/Library/Application Support/Godot/app_userdata/[project_name]~/.local/share/godot/app_userdata/[project_name]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
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
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")
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")
Issue : Saved Vector2/Vector3 not loading correctly
# ✅ Solution: Store as x, y, z components
"position": {"x": pos.x, "y": pos.y}
# Then reconstruct:
position = Vector2(data.position.x, data.position.y)
Issue : Resource paths not resolving
# ✅ Store resource paths as strings
"texture_path": texture.resource_path
# Then reload:
texture = load(data.texture_path)
Weekly Installs
95
Repository
GitHub Stars
59
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
gemini-cli91
opencode91
github-copilot89
codex89
kimi-cli87
amp87
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
157,400 周安装