Save / Load — сохранение прогресса
FileAccess, ConfigFile, ResourceSaver, JSON, encryption, user:// пути.
В Godot есть несколько встроенных способов сохранять прогресс. Каждый подходит под свою задачу.
Пути и user:// schema
В Godot для всех пользовательских данных есть специальный путь — user://. На разных
платформах он указывает в правильное место:
- Windows:
%APPDATA%\Godot\app_userdata\[name]\ - macOS:
~/Library/Application Support/Godot/app_userdata/[name]/ - Linux:
~/.local/share/godot/app_userdata/[name]/ - Android:
/data/data/com.example.app/files/ - iOS:
Documents/ - Web:
IndexedDBчерез виртуальную ФС
Главное правило: никогда не пишите в res:// (это read-only после билда). Всё пользовательское
идёт в user://.
localStorage — для primitive (как Godot ConfigFile). IndexedDB — для большого
структурированного (как Godot FileAccess + JSON).
ConfigFile — для настроек
ConfigFile сохраняет в INI-формате (как .ini-файлы Windows). Идеально для player-config’а:
громкость, разрешение, бинды клавиш.
extends Node
const SETTINGS_PATH := "user://settings.cfg"
func save_settings(master_volume: float, fullscreen: bool, player_name: String) -> void:
var config := ConfigFile.new()
config.set_value("audio", "master_volume", master_volume)
config.set_value("video", "fullscreen", fullscreen)
config.set_value("player", "name", player_name)
config.save(SETTINGS_PATH)
func load_settings() -> Dictionary:
var config := ConfigFile.new()
var err := config.load(SETTINGS_PATH)
if err != OK:
return {} # файл ещё не существует
return {
"master_volume": config.get_value("audio", "master_volume", 1.0), # default 1.0
"fullscreen": config.get_value("video", "fullscreen", false),
"player_name": config.get_value("player", "name", "Player"),
}
На диске получится:
[audio]
master_volume=0.8
[video]
fullscreen=true
[player]
name="Konstantin"
Это читаемо, версионируемо, легко редактируется руками — идеально для настроек, где безопасность не важна.
FileAccess — для произвольных файлов
Для структурированных сейвов (с массивами, словарями, custom data) — FileAccess + JSON:
extends Node
const SAVE_PATH := "user://save.json"
func save_game(state: Dictionary) -> Error:
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file == null:
return FileAccess.get_open_error()
file.store_string(JSON.stringify(state, "\t")) # \t — pretty-print indent
return OK
func load_game() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if file == null:
push_error("Failed to open save file: %s" % FileAccess.get_open_error())
return {}
var content := file.get_as_text()
var parsed = JSON.parse_string(content)
return parsed if parsed != null else {}
Использование:
var state := {
"version": 1,
"player": {
"name": "Konstantin",
"level": 5,
"hp": 80,
"position": [10.5, 2.0, -3.2], # Vector3 не JSON-сериализуем напрямую
},
"inventory": ["sword", "potion", "potion"],
"play_time_seconds": 3600.5,
"active_quests": ["q_main_01", "q_side_03"],
}
save_game(state)
# Позже:
var loaded := load_game()
print(loaded.player.name)
JSON.stringify не сериализует Godot-типы (Vector3, Color, Transform3D). Конвертируйте
в массивы:
var pos := player.global_position
state.player.position = [pos.x, pos.y, pos.z]
# При загрузке:
player.global_position = Vector3(loaded.player.position[0],
loaded.player.position[1],
loaded.player.position[2]) ResourceSaver / ResourceLoader — нативно для Godot Resource
Если ваше состояние представимо как Resource-подкласс, можно использовать встроенную сериализацию:
# save_data.gd
class_name SaveData extends Resource
@export var player_name: String = "Player"
@export var level: int = 1
@export var hp: int = 100
@export var position: Vector3 = Vector3.ZERO
@export var inventory: Array[String] = []
# save_system.gd
const SAVE_PATH := "user://save.tres"
func save_game(data: SaveData) -> void:
ResourceSaver.save(data, SAVE_PATH)
func load_game() -> SaveData:
if not ResourceLoader.exists(SAVE_PATH):
return SaveData.new()
return load(SAVE_PATH) as SaveData
Плюсы:
- Vector3, Color, и другие Godot-типы сериализуются автоматически.
- Файл человекочитаемый (
.tres). - Можно открывать в Godot Editor для отладки.
Минусы:
- Жёсткая привязка к structure — миграция между версиями требует осторожности.
- Безопасность нулевая (файл легко правится).
Бинарный формат — .res
Если хотите компактный и быстрый формат:
ResourceSaver.save(data, "user://save.res", ResourceSaver.FLAG_COMPRESS)
.res — бинарный (быстрее парсится), плюс FLAG_COMPRESS — компрессия zstd. Это в десятки
раз компактнее JSON для больших сейвов.
Шифрование
Простой XOR
const KEY := 0x5A
func encrypt_xor(text: String) -> PackedByteArray:
var bytes := text.to_utf8_buffer()
for i in bytes.size():
bytes[i] ^= KEY
return bytes
func decrypt_xor(bytes: PackedByteArray) -> String:
for i in bytes.size():
bytes[i] ^= KEY
return bytes.get_string_from_utf8()
AES через FileAccess (встроенный в Godot)
Godot имеет встроенный FileAccess.open_encrypted_with_pass:
func save_encrypted(state: Dictionary, password: String) -> void:
var file := FileAccess.open_encrypted_with_pass(
"user://save.dat", FileAccess.WRITE, password)
file.store_string(JSON.stringify(state))
func load_encrypted(password: String) -> Dictionary:
if not FileAccess.file_exists("user://save.dat"):
return {}
var file := FileAccess.open_encrypted_with_pass(
"user://save.dat", FileAccess.READ, password)
if file == null:
push_warning("Wrong password or corrupted file")
return {}
var content := file.get_as_text()
return JSON.parse_string(content)
⚠️ Как и в Unity: пароль в коде → reverse-engineering найдёт. Это защита от казуальных читеров, не более.
Версионирование сейвов
const CURRENT_VERSION := 3
func load_game() -> Dictionary:
var data := _read_save()
if data.is_empty():
return _default_state()
var version: int = data.get("version", 1)
if version < CURRENT_VERSION:
data = _migrate(data, version)
return data
func _migrate(data: Dictionary, from_version: int) -> Dictionary:
if from_version == 1:
# v1 → v2: добавили inventory
data.inventory = []
from_version = 2
if from_version == 2:
# v2 → v3: переименовали поле
data.player_level = data.get("level", 1)
data.erase("level")
from_version = 3
data.version = CURRENT_VERSION
return data
Версионируйте с самого начала. Добавить поле version после релиза — больно.
Async-сохранение через WorkerThreadPool
Если сейв большой и стопорит main thread:
extends Node
var _save_task_id: int = -1
func save_async(state: Dictionary) -> void:
var json := JSON.stringify(state)
_save_task_id = WorkerThreadPool.add_task(_write_to_disk.bind(json))
func _write_to_disk(json: String) -> void:
var file := FileAccess.open("user://save.json", FileAccess.WRITE)
file.store_string(json)
func _process(_delta: float) -> void:
if _save_task_id != -1 and WorkerThreadPool.is_task_completed(_save_task_id):
WorkerThreadPool.wait_for_task_completion(_save_task_id)
_save_task_id = -1
print("Save complete")
См. главу про Threading для деталей.
Auto-save паттерн
extends Node
@export var interval_seconds: float = 60.0
@onready var _timer := Timer.new()
func _ready() -> void:
_timer.wait_time = interval_seconds
_timer.timeout.connect(_on_autosave)
add_child(_timer)
_timer.start()
func _on_autosave() -> void:
var state := GameState.capture_state()
SaveSystem.save_game(state)
# show toast "💾 Сохранено"
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST or what == NOTIFICATION_APPLICATION_PAUSED:
_on_autosave()
get_tree().quit()
NOTIFICATION_APPLICATION_PAUSED срабатывает на mobile при сворачивании — гарантированно
успеваем сохранить.
Cloud Saves
Godot core не имеет встроенного Cloud Save. Решения:
- Steam Cloud — через GodotSteam модуль (community).
- Google Play Games Services — через AssetLib плагин.
- Custom backend — пишете API → HTTPRequest узел общается с сервером.
Подводные камни
res://— read-only в билде. Толькоuser://.Vector3и др. Godot-типы — НЕ JSON. Конвертируйте в массивы или используйте Resource.- JSON.parse_string возвращает null при ошибке. Всегда проверяйте.
- Тестируйте миграцию — добавили поле → проверьте старый сейв.
- WebGL — IndexedDB лимиты. Браузер может ограничить ваш storage (~50MB+). Большие сохранения дробите.