Реализация системы сохранения и загрузки данных игр
PlayerPrefs.SetFloat("health", 100) работает для прототипа. Для продакшена — это тупик. Когда объём сохраняемых данных вырастает до десятков переменных, PlayerPrefs превращается в неструктурированную свалку без версионирования, без возможности нескольких слотов и без защиты от корруптации. Перейти с PlayerPrefs на нормальную систему в середине разработки — болезненная задача.
Что должна уметь система сохранения
Минимальный production-ready набор:
- Несколько слотов сохранения с метаданными (дата, имя персонажа, уровень, скриншот)
- Атомарная запись: файл либо записан полностью, либо не записан вообще (промежуточный краш не корруптирует данные)
- Версионирование: при обновлении игры старые сохранения должны мигрировать, а не ломаться
- Асинхронная запись: сохранение не должно фризить игру на 200ms
Архитектура: ISaveable и SaveManager
Паттерн: каждый компонент, который хочет сохраняться, реализует интерфейс ISaveable:
public interface ISaveable
{
string SaveId { get; }
object CaptureState();
void RestoreState(object state);
}
SaveManager при сохранении находит все ISaveable на сцене (через FindObjectsOfType или регистрацию), вызывает CaptureState() у каждого, собирает результат в Dictionary<string, object>, сериализует и пишет на диск. При загрузке — обратный процесс.
SaveId — уникальная строка для каждого компонента. Используется GUID, генерируемый в инспекторе через [SerializeField] private string _saveId. Важно не использовать имя объекта сцены как ID: оно не уникально и может измениться.
Сериализация: JSON vs Binary
JSON (Newtonsoft.Json) — читаем, легко дебажится, совместим с разными платформами. Минусы: больше объём файла, немного медленнее, нужны кастомные конвертеры для типов Unity (Vector3, Quaternion, Color). JsonConvert.SerializeObject(data, Formatting.None) с кастомным UnityTypeConverter — рабочий подход.
BinaryFormatter — Unity-встроенный, быстрый, компактный. Но: deprecated в .NET 5+, имеет уязвимости безопасности (не критично для offline игр), нечитаем для дебага. Для новых проектов не рекомендуется.
MessagePack-CSharp — бинарный формат с производительностью лучше JSON и без проблем BinaryFormatter. Хороший выбор для мобильных игр с большими объёмами данных.
Путь к файлу сохранения: Application.persistentDataPath + "/saves/slot_{index}.sav". persistentDataPath гарантированно доступен для записи на всех платформах (iOS, Android, PC, Console).
Атомарная запись и защита от корруптации
Прямая перезапись файла File.WriteAllText(path, json) может оставить файл в невалидном состоянии при крэше в момент записи. Атомарная запись:
- Записать данные во временный файл
slot_0.sav.tmp - Если запись успешна — переименовать
File.Move(tmpPath, finalPath)(атомарная операция на большинстве ОС) - Старый файл предварительно переименовать в
slot_0.sav.bak— резервная копия
При загрузке: если основной файл не найден или невалиден — попробовать .bak. Это элементарная защита, которая экономит тысячи часов поддержки после релиза.
Асинхронное сохранение
Сериализация 5 МБ JSON синхронно — это 50–200ms задержки на среднем PC, на мобильных устройствах хуже. Решение: async/await с File.WriteAllTextAsync():
public async Task SaveAsync(int slot)
{
var data = CollectSaveData();
string json = JsonConvert.SerializeObject(data);
await File.WriteAllTextAsync(GetSavePath(slot), json);
}
В Unity async Task методы работают корректно с ConfigureAwait(false) для фоновых потоков. UI индикатор сохранения показывается до вызова, скрывается в finally блоке.
Версионирование и миграция
Каждый файл сохранения содержит "saveVersion": 3. При загрузке версия сравнивается с currentSaveVersion. Если версии не совпадают — запускается цепочка мигреторов:
ISaveMigrator[] migrators = {
new SaveMigratorV1ToV2(),
new SaveMigratorV2ToV3()
};
Каждый мигратор знает как обновить JObject от своей версии к следующей. Это позволяет обновлять формат сохранений без потери данных игроков. Без версионирования первое же обновление игры с изменением структуры данных инвалидирует все существующие сохранения.
Автосохранение и checkpoint система
Автосохранение через InvokeRepeating("AutoSave", 300f, 300f) — каждые 5 минут в специальный autosave слот. Checkpoint-сохранение: при входе в триггерную зону публикуется событие OnCheckpointReached, SaveManager сохраняет в checkpoint-слот без UI.
Критично: не сохранять в момент боя или нагруженной сцены — выбирать момент сохранения так, чтобы фоновый поток записи не конкурировал с пиком CPU геймплея. Флаг isSafeToSave снимается на время интенсивных сцен.
Ориентировочные сроки
| Масштаб | Состав | Срок |
|---|---|---|
| Простой | JSON, один слот, без версионирования | 2–4 дня |
| Базовый | ISaveable паттерн, несколько слотов, атомарная запись | 1–2 недели |
| Полный | Async, версионирование, миграция, cloud sync | 3–5 недель |
| С облачными сохранениями | + Unity Cloud Save / Steam Cloud интеграция | +1–2 недели |
Процесс
Сначала пишем SaveManager с тестом в изоляции (Unity Test Runner): сохранить данные, загрузить, проверить идентичность. Потом интегрируем ISaveable в существующие компоненты по одному. Имитируем краш записи во время тестирования — специально прерываем процесс сохранения и проверяем, что данные не корруптировались. Cloud sync реализуется последним — только после стабильной работы локального сохранения.





