Skip to content

Сохранение игры

Вначале важно понять, что сохранение игры во время выполнения — это сохранение динамического состояния, которое возникло в результате игрового процесса и поэтому сериализация это плохое решение.

Почему не сработает просто сериализовать сцену?

На первый взгляд, идея сохранить игру выглядит просто: взять все игровые объекты (gameobject) на сцене, записать их в файл и потом восстановить. К сожалению, этот подход фундаментально несовместим с архитектурой Unity и требованиями реальных проектов. Попытка его реализовать приведёт не к упрощению, а к поломке сохранений.

Из сохранения, Вы загружаете не состояние игры, а её «труп»

Вместо этого нужно научится восстанавливать gameobject из префаба

Когда вы сериализуете GameObject, вы фиксируете его текущее техническое представление в движке: координаты Transform, ссылки на компоненты, настройки MeshRenderer и BoxCollider. Подавляющие большинство этих данных - это статический декор, т.е. статические или технические свойства (коллайдеры, рендереры). Поэтому сериализовать их все — это гигабайты ненужных данных и минуты загрузки. Вместо этого нужно сохранять только состояние игры. Например, агент — это не его меш и коллайдер, а его имя, профессия, текущая цель и список задач. Восстанавливая только нужные для логики поведения данные мы не просто экономим, но делаем прозрачной модель игры.

Ссылки превратятся в «битые» указатели

Вместо этого нужно проиндексировать все объекты и после этого их пересвязать по индексам

В коде объекты ссылаются друг на друга: здание знает своего владельца (public Person owner). При наивной сериализации сохраняется ссылка на конкретный экземпляр объекта в памяти. После загрузки создаются совершенно новые экземпляры GameObject, и все старые ссылки указывают в никуда. Это гарантированный NullReferenceException и падение игры.

"Жизнь" в игре замрет

Вместо этого нужно снова подписаться на события и востановить ход игры

События не сериализуются, все объекты, которые ждут сигналов их не получат, и в лучшем случае замрут, а в худшем игра упадет. Агент, который ушел в здание, gameobject которого нет на сцене уже не появится, когда наступит момент уходить из здания.

Рейкаст должен замереть и продолжиться с того же места

Самое важное на этом этапе понять, что происходит в момент загрузки? Не продолжение, а реконструкция. Рейкаст замереть не может, запущенные короутины продолжают работать, управляющая логик и представляющие её классы, заменяться не будут. Отсюда у нас возникает задача выделить минимально возможное количество данных, которые нужно сохранить, чтобы при загрузке стало бы возможным реконструировать систему заново с прерванного момента.

Почему сериализация это вообще плохая идея? Почему сохранение в базу данных это правильно?

В отличии от сериализации, базы данных, а конкретно реляционная модель разрабатывалась давно и стабильно работает для хранения данных. Мнение, что сериализация может быть чем то выиграшнее появляется от упрощенного взгляда на сохранение. Именно, реляционный подход позволяет оптимально хранить данные. Когда мы говорим "оптимально" это не значит, что "сохранение происходит быстро". Именно от этого впечатления - "мы сохраним всё в один файл объекты как есть" и происходит непонимание. Первая нормальная форма в реляционной модели говорит, что каждая ячейка таблицы должна хранить одно неделимое (атомарное) значение. Это важно потому, что это позволяет вам отбирать и анализировать данные в любом порядке. Совсем другое дело у вас с файлом сериализации - вы не можете получить доступ к нужным данным, пока полностью не прочитаете этот файл.

Такие развитые базы данных как MS SQL Server требуют работы отдельного процесса - систему управления базой данных (СУБД). И именно это останавливает разработчиков в применении баз данных для сохранения, особенно если речь не идет о сетевой игре, где нужен общий сервер. Но от самих игр логика применения БД для сохранения не зависит, как правило любая игра может стать сетевой. И тогда менять способ сохранения становится накладно, модель сохранения игры - это базис на котором строится сама игра. Поэтому пока игра еще не развита, не имеет четко очерченной модели можно применять клиентскую базу данных SqlLite. Это позволит в нужный момент просто сменить провайдера на SQLServer и не затронит самого кода игры и модели сохранения.

Но если вы еще смотрите в сторону сериализации, подумайте вот о чем. База данных SqlLite - это тоже просто файл, в который по сути так же происходит сериализация. Разница заключается лишь в том, что данные в этом файле хранятся реляционно и у вас есть целый слой программного обеспечения для сохраниния и, что не менее важно, анализа этих данных.

Что может предоставить вместо этого сериализация? Кажущуюся быстроту сохранения данных в бинарном виде без метаинформации? В этом случае каждая версия формата данных должна иметь у вас отдельную версию загрузки, и как мы говорили выше восстановления состояния игры. Т.е. вы не можете просто взять сохранение версии 1.02 и открыть его в версии игры 1.03, или 1.01. Что еще? Избыточный текстовый формат json или т.н. объектную базу данных? Они по определению будут дублировать данные, т.к. они не переводят их в реляционную модель.

Если вы попытаетесь „починить“ наивную сериализацию, вы неизбежно придёте к тому, что будете хранить атомарные данные с идентификаторами и явными связями — то есть к реляционной модели. Но вместо того чтобы взять готовую, проверенную реляционную базу данных (SQLite), вы начинаете изобретать свой велосипед: собственный формат файла, собственные алгоритмы связывания, собственную систему запросов. Вы создаёте кустарный аналог того, что уже есть в любой БД. И если вы попытаетесь автоматизировать этот процесс маппинга, вы получите самодельный ORM, который будет работать хуже профессиональных решений. Так зачем тратить время и силы на изобретение того, что уже существует и отлично работает?

Entity Framework и Object-Relational Mapping (ORM)

После того как мы поняли, что привлекательность сериализации заключается не в самой сериализации как таковой, а в желании автоматизировать процесс сохранения в реляционную модель, т.е. реализовать ORM мы можем поискать существующие решения. Отдельно от игр Entity Framework (EF) представляет хороший инструмент, позволяющий заменить ручной процесс написания рутинных, типовых sql-скриптов. Но к сожалению, его дефолтный подход совершенно не верен. С другой стороны, EF позволяет переопределить маппинг под нужные требования и именно недопонимание этого сложного процесса, является причиной отказа от хранения сохранений в базах данных.

Почему дефолтный подход не верен? По умолчанию, EF мапит свойства (properties), а не поля (fields). Корнями этот спор уходит в догму о самоинкапсуляции, которая хороша для бизнес-логики, но губительна для хранения. Реальные данные в объекте живут в полях, а свойства — это лишь публичный интерфейс доступа к ним. Более того, свойства могут содержать вычисляемую логику (например, public bool IsDead => Health <= 0;). На языке баз данных это выглядит как VIEW или вычисляемое поле. А теперь вопрос: зачем сохранять в таблицу то, что можно пересчитать из первичных данных? Для сохранения нужны исключительно первичные, атомарные факты: здоровье, координаты, идентификатор цели. Всё остальное — это поведение, которое должно восстанавливаться из этих фактов при загрузке.

Следует отметить, что в Unity, при работе с компонентами и сериализацией сцен, используется прямо противоположный подход: по умолчанию сериализуются поля, а не свойства. Это не случайность — это осознанное решение разработчиков движка. Поля — это хранилище данных, они не содержат логики, они атомарны. Свойства же в Unity — это поведение, доступ к данным, вычисления, и они не сериализуются, потому что их сохранение не имеет смысла (и даже вредно).

TacUnityEF (адаптация EF под Unity от tac)

Из предыдущих разделов вы уже поняли, что сериализация объектов — это тупик, а реляционная модель — единственный правильный способ хранения игрового состояния. В нашей библиотеке мы пошли дальше: мы не просто используем SQLite как хранилище, мы настраиваем EF так, что он берёт на себя рутину ORM, но при этом оставляет вам полный контроль выбирать какие свойства будут сохранятся.