Как сделать систему сохранений в игре: руководство для разработчиков
Система сохранений делает одно — превращает состояние игры в данные, записывает их в хранилище и потом читает обратно, восстанавливая всё как было. Сложности возникают в другом: что именно сохранять, как не допустить повреждения файла и как сохранить работоспособность старых слотов после обновления игры. Это руководство разбирает, что входит в сохранение, форматы сериализации на выбор (JSON, бинарный, protobuf), приём атомарной записи, защищающий файлы от порчи, и версионирование формата, благодаря которому обновления не ломают старые слоты. Написано для соло-разработчиков и маленьких студий, опирается на то, как хранилище и сериализация работают на самом деле, — без выдуманных процентов.
У системы сохранений одна задача: взять состояние игры, превратить его в данные, записать в хранилище и позже прочитать обратно, восстановив игру ровно такой, какой она была. Читать и писать несложно. Но настоящие трудности — другие, и их три: решить, что именно считать состоянием, заслуживающим сохранения; писать так, чтобы файл никогда не повредился; и сохранить работоспособность старых слотов после того, как вы измените игру.
Это руководство — практическая версия для соло-разработчиков и маленьких студий. Здесь разобрано, что входит в сохранение, форматы сериализации на выбор, единственный приём, защищающий от порчи, и способ версионировать данные так, чтобы обновление не сломало каждый слот, который сделали ваши игроки. Всё опирается на то, как хранилище и сериализация работают на самом деле, — без выдуманных цифр про удержание.
Сохраняйте состояние, а не производные данные
Первое решение — что сохранять. Правило простое: сохраняйте данные, которые определяют прогресс игрока, а всё остальное пересчитывайте заново.
Сохраняйте то, что игрок заработал или выбрал, — позицию персонажа, инвентарь, текущий уровень, пройденные задачи, открытые способности, сюжетные решения и настройки вроде уровней звука и схемы управления. Не сохраняйте ничего, что можно вычислить: кэшированные позиции врагов, заранее посчитанные итоги, временное визуальное состояние.
| Сохраняем | Пересчитываем заново |
|---|---|
| Позиция игрока, здоровье, инвентарь | Позиция камеры, текущий кадр анимации |
| Отметки о пройденных уровнях, открытый контент | Кэш поиска пути, производные итоги счёта |
| Сюжетные решения, настройки | Позиции частиц, временные эффекты |
Причина убрать производные данные — не только размер. Если вы храните величину, которую можно пересчитать, и логика игры меняется, сохранённое значение превращается в противоречие. Слот, в котором лежат и исходные данные, и устаревший производный итог, начнёт расходиться сам с собой. Сохраняйте источник, остальное пересчитывайте — и слот всегда остаётся внутренне непротиворечивым.
Выберите формат сериализации
Сериализация — это превращение объектов из памяти в байты, которые можно хранить, и обратно. Три формата покрывают почти любую игру.
| Формат | Читаемость | Размер | Скорость | Для чего подходит |
|---|---|---|---|---|
| JSON | Да (текст) | Крупнее | Средняя | Разработка и большинство небольших игр |
| Бинарный | Нет | Маленький | Быстрый | Объёмные сохранения, чувствительные к скорости загрузки |
| Protocol Buffers (protobuf) | Нет (но со схемой) | Маленький | Быстрый | Строгие схемы, разные языки |
Лучший выбор для небольшой игры — JSON. Файл сохранения, который можно открыть в текстовом редакторе и прочитать, стоит гораздо больше байтов, сэкономленных бинарным форматом: когда слот сломается у реального игрока, вам захочется его прочитать. Переходите на бинарный формат или protobuf, только если сохранения настолько велики или загрузка настолько медленна, что размер реально мешает. Для большинства 2D-игр этого не случается.
Одно замечание о компромиссе с читаемостью: сохранение, которое можно прочитать человеком, — это и файл, который любопытный или злонамеренный игрок может отредактировать. Для одиночной игры это нормально, а для моддерского сообщества — скорее плюс. Относитесь к этому как к обстоятельству, которое стоит учитывать, а не как к препятствию.
Приём атомарной записи
Это самый важный приём во всём руководстве — и именно его большинство инди-игр делают неправильно.
Наивный подход — открыть файл сохранения и писать прямо в него. Проблема в том, что запись не мгновенна, и если игра падает, устройство теряет питание или операционная система прерывает процесс на середине, остаётся файл, наполовину состоящий из старых данных, а наполовину — из новых: повреждённый слот, который игра не сможет прочитать.
Решение — никогда не писать в рабочий файл напрямую. Вместо этого:
- Запишите новые данные сохранения во временный файл.
- Переименуйте временный файл поверх настоящего.
Переименование файла — атомарная операция на одном томе: она либо выполняется целиком, либо не выполняется вовсе. Падение при записи оставляет временный файл недописанным, но настоящий слот сохранений не тронут и по-прежнему работоспособен. Падение при переименовании безопасно, потому что операционная система гарантирует атомарность самого переименования. Игрок либо сохраняет старый слот, либо получает новый, — но никогда не получает половину.
| Момент сбоя | Результат |
|---|---|
| Падение при записи временного файла | Старый слот цел, временный файл испорчен (его отбрасываем) |
| Падение при переименовании | Один из двух файлов цел, смесь невозможна |
| Чистое завершение | Новый слот заменяет старый |
Этот приём почти ничего не стоит реализовать и убирает практически все повреждения от потери питания и падений. Вместе с одним-двумя резервными слотами он превращает потерю сохранений из рядового обращения в поддержку в редкое событие.
Версионируйте формат сохранений
День релиза — это день, когда ваш формат сохранений становится контрактом. У игроков будут лежать слоты под каждую версию игры, которую вы когда-либо выпустите, и каждый должен загружаться.
Храните номер версии в каждом файле сохранения и никогда не предполагайте заранее структуру данных при загрузке:
- При загрузке читайте версию первой.
- Переносите старые форматы в актуальный по шагам — с версии 1 на 2, затем с 2 на 3.
- Каждому отсутствующему полю назначайте разумное значение по умолчанию.
- Переименованные поля связывайте с новыми именами.
Дисциплина в том, чтобы менять формат только через миграцию. Добавили поле — поднимите версию и напишите миграцию, которая заполнит значение по умолчанию для старых слотов. Никогда не меняйте молча смысл существующего поля: старый слот игрока загрузит старый смысл под новый код и сломается.
| Изменение | Безопасная миграция |
|---|---|
| Добавили новое поле | Значение по умолчанию для старых слотов |
| Убрали поле | Перестаём его читать; игнорируем в старых слотах |
| Переименовали поле | Связываем старое имя с новым в загрузчике |
| Изменили смысл значения | Новый номер версии, явное преобразование |
Версионируемый формат с миграциями — это и есть разница между игрой, чьи обновления сохраняют старые слоты, и игрой, чьи обновления стирают прогресс игроков. Второго игроки не прощают.
Когда и где сохранять
Два независимых вопроса: где живут данные и как часто вы их записываете.
Где — отчасти решение платформы. Локальные файлы — базовый уровень. Платформенные облачные системы (сети консолей, специфичный для платформы обмен) реплицируют сохранения между устройствами, чего игроки и ждут, но это добавляет правила разрешения конфликтов: когда облачный и локальный слот расходятся, нужна политика, обычно «побеждает самый свежий». Проектируйте разрешение конфликта до того, как столкнётесь с ним.
Как часто — проектное решение с техническими последствиями:
- Сохранение в контрольных точках пишет только в определённые моменты (конец уровня, комната сохранения). Просто, надёжно и намеренно заложено в сложность игры.
- Автосохранение пишет периодически или по значимым событиям. Удобно, но никогда не должно блокировать игру или вызывать рывок — сохраняйте в фоновом потоке или между кадрами.
- Сохранение в любом месте даёт игроку полный контроль, но обнажает каждый граничный случай в сериализации состояния, потому что игрок может сохранить в любой момент.
Техническое правило, покрывающее все три варианта: никогда не сохраняйте на горячем участке так, чтобы игрок это почувствовал. Сериализация и запись на диск занимают время; делайте их там, где в кадровом бюджете есть запас.
Частые ошибки
- Сохранять производные данные. Храните исходное состояние, остальное пересчитывайте, иначе после обновления слот начнёт противоречить сам себе.
- Писать прямо в рабочий файл сохранения. Падение на середине записи портит его. Пишите во временный файл и переименовывайте.
- Нет номера версии. Первое же обновление ломает все существующие слоты.
- Блокировать главный поток. Сохранение, из-за которого игра дёргается, игрок чувствует каждый раз.
- Один слот и без резерва. Единственное повреждённое сохранение теряет всё. Держите резервный слот.
- Рассчитывать, что облако решит конфликты. Оно их порождает; заранее определите политику разрешения.
- Сохранение везде как замена проектированию. «Сохранить в любом месте» удобно для игрока, но труднее всего сериализовать правильно — выбирайте это сознательно, а не по умолчанию.
Роль Egmatic
Egmatic построен вокруг версионируемых данных в JSON. Редактор выдаёт игровые данные в виде структурированного JSON, который движок затем читает, поэтому сериализация — полноценная часть архитектуры, а не пристройка, появившаяся под конец. Та же дисциплина напрямую переносится на сохранения: чистый, версионируемый, читаемый формат, который можно просмотреть, когда что-то пошло не так, и переносить на новые версии, когда игра меняется. Живой предпросмотр Egmatic позволяет сохранить, перезагрузить и убедиться, что состояние восстанавливается точно, — превращая самую скучную часть разработки сохранений, проверку того, что слот корректно проходит туда-обратно, в быстрый цикл вместо медленной пересборки.
За привычками сериализации, на которые опирается эта статья, наше руководство о MonoGame объясняет базу на C#, на которой построен Egmatic, а руководство по производительности 2D разбирает дисциплину удержания работы вне горячего участка — то же правило, благодаря которому сохранение не вызывает рывков.
Заключение
Система сохранений — это не функция, которую добавляют на последней неделе. Это дисциплина: сохранять исходное состояние, а не производные данные; выбирать формат, который можно отлаживать; писать атомарно, чтобы падение никогда не испортило файл; и версионировать формат, чтобы обновления никогда не ломали прогресс игрока. Сделайте эти четыре вещи правильно — и сохранения перестанут быть обузой для поддержки и станут невидимыми, какими им и положено быть.