Сохранение прогресса
Позиция персонажа, проверка данных и резервная копия файла.
Запись положения
ConfigFile хранит значения по секциям и ключам. SetValue() меняет данные в памяти; Save() записывает файл. Для прогресса игрока используйте user://.
Методы ниже разместите внутри своего C#-скрипта. player — существующая Entity3D в дереве игры, path — например "user://progress.cfg".
public Error SavePosition(Entity3D player, string path)
{
using var data = new ConfigFile();
data.SetValue("player", "position", player.GlobalPosition);
return data.Save(path);
}Проверяйте результат: Error.Ok означает успешную запись. Этот короткий вариант перезаписывает один файл без резервной копии.
Загрузка и проверка
Сначала загрузите файл и проверьте тип данных. При ошибке положение персонажа остаётся прежним. После переноса сбросьте интерполяцию, а для CharacterBody3D — скорость.
public Error LoadPosition(Entity3D player, string path)
{
using var data = new ConfigFile();
Error error = data.Load(path);
if (error != Error.Ok)
return error;
if (!data.HasSectionKey("player", "position"))
return Error.InvalidData;
using Variant value = data.GetValue("player", "position");
if (value.VariantType != Variant.Type.Vector3)
return Error.InvalidData;
Vector3 position = value.AsVector3();
if (!position.IsFinite())
return Error.InvalidData;
player.GlobalPosition = position;
player.ResetPhysicsInterpolation();
if (player is CharacterBody3D character)
character.Velocity = Vector3.Zero;
return Error.Ok;
}Отсутствующий файл возвращает Error.FileNotFound: это обычная ситуация при первом запуске. Не заменяйте существующий прогресс начальными данными автоматически при любой ошибке загрузки.
Готовый скрипт с резервной копией
Добавьте в уровень обычную Entity, прикрепите ProgressSave.cs, соберите проект и назначьте персонажа свойству Target. В Input Map добавьте save_game на F2 и load_game на F3.
ProgressSave.cs — запись, проверка формата и резервная копия
using System;
using System.Text;
using Reckfell;
using IO = System.IO;
public partial class ProgressSave : Entity
{
[Export] public Entity3D Target { get; set; } = null!;
[Export] public string SavePath { get; set; } = "user://progress.cfg";
public Error SaveProgress()
{
if (!ReckfellObject.IsInstanceValid(Target))
return Error.Unconfigured;
if (string.IsNullOrWhiteSpace(SavePath) || SavePath.IndexOf('\0') >= 0 ||
!SavePath.StartsWith("user://", StringComparison.Ordinal) || SavePath.Length <= 7)
return Error.FileBadPath;
Vector3 position = Target.GlobalPosition;
if (!position.IsFinite())
return Error.InvalidData;
using var data = new ConfigFile();
data.SetValue("save", "format", 1);
data.SetValue("player", "position", position);
try
{
string path = ProjectSettings.GlobalizePath(SavePath);
byte[] bytes = Encoding.UTF8.GetBytes(data.EncodeToText());
using (var stream = new IO.FileStream(path + ".tmp", IO.FileMode.Create, IO.FileAccess.Write))
{
stream.Write(bytes);
stream.Flush(flushToDisk: true);
}
if (IO.File.Exists(path))
IO.File.Replace(path + ".tmp", path, path + ".bak");
else
IO.File.Move(path + ".tmp", path);
return Error.Ok;
}
catch (UnauthorizedAccessException)
{
return Error.FileNoPermission;
}
catch (IO.IOException)
{
return Error.CantCreate;
}
catch (ArgumentException)
{
return Error.FileBadPath;
}
catch (NotSupportedException)
{
return Error.FileBadPath;
}
}
public Error LoadProgress(bool backup = false)
{
if (!ReckfellObject.IsInstanceValid(Target))
return Error.Unconfigured;
if (string.IsNullOrWhiteSpace(SavePath) || SavePath.IndexOf('\0') >= 0 ||
!SavePath.StartsWith("user://", StringComparison.Ordinal) || SavePath.Length <= 7)
return Error.FileBadPath;
string path = backup ? SavePath + ".bak" : SavePath;
if (!IO.File.Exists(ProjectSettings.GlobalizePath(path)))
return Error.FileNotFound;
using var data = new ConfigFile();
Error error = data.Load(path);
if (error != Error.Ok)
return error;
using Variant format = data.GetValue("save", "format", 0);
if (format.VariantType != Variant.Type.Int || format.AsInt64() != 1)
return Error.InvalidData;
if (!data.HasSectionKey("player", "position"))
return Error.InvalidData;
using Variant value = data.GetValue("player", "position");
if (value.VariantType != Variant.Type.Vector3)
return Error.InvalidData;
Vector3 position = value.AsVector3();
if (!position.IsFinite())
return Error.InvalidData;
Target.GlobalPosition = position;
Target.ResetPhysicsInterpolation();
if (Target is CharacterBody3D character)
character.Velocity = Vector3.Zero;
return Error.Ok;
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("save_game"))
ReckfellApi.Print($"Save: {SaveProgress()}");
else if (@event.IsActionPressed("load_game"))
ReckfellApi.Print($"Load: {LoadProgress()}");
else
return;
GetViewport().SetInputAsHandled();
}
}F2 сохраняет положение, F3 восстанавливает его. Папку сохранений можно открыть через Project → Open User Data Folder.
Что происходит с файлом
- Новые данные целиком записываются в
.tmpи сбрасываются на диск. - При замене основной файл становится
.bak. Первое сохранение резервной копии ещё не имеет. LoadProgress(backup: true)явно загружает резервную копию. Автоматического восстановления и автосохранения в примере нет.
Здесь намеренно используется System.IO.File.Replace(). Не заменяйте его на DirAccess.Rename() или RenameAbsolute(): в Windows 1.0.0 существующий файл назначения удаляется до попытки перемещения. При отсутствующем источнике назначение может пропасть, хотя метод вернёт ошибку. Это воспроизведено на одноразовых файлах; см. ограничения DirAccess.
При отсутствующем .tmp и при эксклюзивной блокировке слота проверенный File.Replace() сохранил основной файл и .bak; при блокировке остался и новый .tmp. После ошибки не загружайте временный файл автоматически: он может быть устаревшим или неполным. Устраните причину, повторите сохранение либо явно загрузите проверенную резервную копию. Пример не поддерживает несколько одновременных записывающих задач.
Flush(true) и успешная замена не доказывают восстановление после отключения питания. Проверены обычные операции и указанные случаи отказа на Windows; аварийное завершение, другие файловые системы и платформы здесь не проверялись.
format = 1 — версия структуры сохранения. Загрузка проверяет её, наличие поля, тип Vector3 и конечность координат до изменения персонажа.
Для нескольких уровней сохраняйте также идентификатор уровня. Координаты одного уровня нельзя применять к другому без проверки. См. переходы между уровнями.
Ошибки
| Результат | Действие |
|---|---|
Ok | Операция завершена |
FileNotFound | Слота или выбранной резервной копии ещё нет |
Unconfigured | Назначьте действующий Target |
InvalidData | Проверьте формат, обязательное поле и тип; не меняйте текущий прогресс |
FileBadPath | Укажите непустой допустимый путь к файлу в user:// |
FileNoPermission | Выберите доступный путь в user:// |
CantCreate | Проверьте путь, доступное место и доступ к файлу |
Запись синхронная: пример рассчитан на небольшой слот. Большой снимок мира требует отдельной организации сериализации и ввода-вывода. Замена с резервной копией проверена на Windows x64.
Справочник: ConfigFile, ProjectSettings.