Дмитрий Назаров

Дмитрий Назаров 

GameDev

21subscribers

166posts

Showcase and bundles

4
A bundle is a collection, compilation, playlist, or catalog of posts that saves you from searching through the blog and lets you buy the entire content series in a single click.
goals2
3 of 100 paid subscribers
Так я смогу понять, что канал кому-то интересен и у меня будет мотивация делаться контентом!
$0 of $6 278 raised
Смогу ГОД разрабатывать игру фултайм, стримить, записывать ролики без отвлечения на наемную работу.

Создал систему сохранения

По итогу двух месяцев создал-таки систему сохранения.
Намучился, конечно, но в целом, не было непреодолимых трудностей. С построением StateTree местами сложней бывало...
В Телеграм уже ни раз писал и здесь напишу, что система у меня не стандартная и не базируется ни на одном из видео-уроков. 90% видео уроков что я видел в сети базируются на использовании UStruct, т.е. структур данных. А я нелюблю структуры. Они мне ни раз доставляли проблемы на ровном месте и поэтому я отказался от них и почти выпилил из проекта...
Я привык работать на Object (UObject). У меня Инвентарь на них построен, Память, Потребности и частично профессии AI персонажей.
Кто не в курсе, это по сути базовый класс для всех блюпринтов в Unreal. Но в отличие от actor (его дочерних классов) не создается в мире, а создается (Construct) в режиме реального времени (runtime).
Были большие проблемы с сохранением самих Object.
Не буду вдоваться в подробности, кому интересно, почитайте Telegram - там много об этом написано.
Здесь скажу лишь что я навайбкодил плагин JsonObjectSerializer, который позволяет сериализовать и десериализовать object в JSON туда и обратно. Т.е. это чуть ли не единственный легитимный способ сохранить Uobject со всеми его переменными в т.ч. другими Objects и воссоздать обратно при загрузке. Плагин имеет две ноды MakeJsonFromObject для создания json из Object и SpawnObjectFromJson - для воссоздания object из json строки. При спавне object можно указать Outer и тем самым закрепить object за нужным владельцем.
Весь процесс сохранения и загрузки фактически происходит внутри самого actor. В GameInstance вызывается лишь блюпринт интерфейс на данные события.
Я создал базис для системы. Множество раз тестировал - все работает и отлажено. Есть мелкие баги - но я их пофикшу потом, т.к. кое-какие механики еще нужно дорабатывать.

Как устроена система сохранения

ChatGPT подготовила изображение а я следом дам описание:
У меня есть BP_SaveGameInstance - это блюпринт с классом GameInstance.
В нем происходит основная логика сохранения и загрузки.
На старте создается BP_SaveGame - это блюпринт с BP_SaveGame с классом SaveGame. В нем происходят сохранения данных. Он записывается в слот сохранения.
В нем есть переменные:
  • Save_ObjectAICharacter - Map с String и BP_ObjectSave - Массив object заспавненных AI Character и их ID
  • Save_ObjectAICharacterSpawned - Map с String и BP_ObjectSave - Массив object заспавненных actor и их ID
  • Save_ObjectBPMesh - Map с String и BP_ObjectSave - Массив object заспавненных BP Mesh и их ID
  • Save_ObjectBPMeshSpawned - Map с String и BP_ObjectSave - Массив object заспавненных actor и их ID
  • Save_ObjectBaseInventoryItem - Map с String и BP_ObjectSave - Массив object заспавненных BaseInventoryItem и их ID
  • Save_ObjectBaseInventoryItemSpawned - Map с String и BP_ObjectSave - Массив object заспавненных actor и их ID
  • Save_ObjectLoadedDestroyActor - Map с String и BP_ObjectSave - Массив object actor которые нужно удалять т.к. они были удалены с уровня на момент создания последнего сохранения
  • Save_ObjectTPCharacter - переменная класса BP_ObjectSave_TPCharacter главного героя для сохранения данных главного героя (игрока) класса BP_TPCharacter.
  • Save_ObjectLevelActorTime - переменная класса BP_ObjectSave_LevelActorTime для сохранения управления временем суток
BP_ObjectSave - это базовый Uobject в котором храняться данные которые я сохраняю. Хранит transform своего actor его ActorClass, ID своего actor и многое другое.
У него есть дочерние Uobject:
  • BP_ObjectSave_AICharacter - создан на базе BP_ObjectSave, но дополнен особенными переменными для AI Character персонажа типа gameplay tags, данных скелетных сеток экипировки, предметов инвентаря, характеристик, потребностей и много другого.
  • BP_ObjectSave_BaseInventoryItem - создан на базе BP_ObjectSave и дополнен некоторыми переменными для actor типа BP Base Inventory Item, например transform, gameplay tags, скелетные и статические сетки и др.
  • BP_ObjectSave_BPMesh - создан на базе BP_ObjectSave, но дополнен особенными переменными для actor типа BP Mesh. Преимущественно это постройки и какие-то абстрактные объекты, например, леса, поля, сады - не имеющие своей скелетной сетки, а чисто логику. Здесь есть переменные типа gameplay tags, данные скелетных и статических сеток и др.
  • BP_ObjectSave_LevelActorTime - создан на базе BP_ObjectSave, содержит Integer переменную дня CurrentDay и float переменную TimeOfDay для времени суток.
  • BP_ObjectSave_TPCharacter - создан на базе BP_ObjectSave и имеет переменные для главного героя которые мы сохраняем и загружаем. Например, gameplay tags, скелетные сетки, инвентарь, экипировку и т.п.
В BP_SaveGame как я отмечал выше есть карты - Map для всех сущностей и две переменные - для BP_TPCharacter и для BP_LevelActorTime. Эти карты используеются внутри сущностей.
При первом запуске игры происходит сохранение внутри самих сущностей экторов. На Begin Play каждого типа сущности повешан ивент блюпринт интерфейса EventSaveObjectInitialize - он производит первичное сохранение переменных.
Покажу на примере AI Character (персонажа искусственного интеллекта в моей игре).
При старте этого ивента из BP_SaveGameInstance получается BP_SaveGame текущего файла сохранения. Из него переменные карт Save_ObjectAICharacter и Save_ObjectAICharacterSpawned подключатся в ноду Select. В Select подается булевое значение из HasTag геймплейного тега Status.Spawned (заспавненный actor) контейнера Gameplay Tags. Далее если булевый результат true (т.е. actor помечен как Status.Spawned, т.е. заспавненный), то выбирается карат Save_ObjectAICharacterSpawned, а если не помечен, то Save_ObjectAICharacter. Дальше выбранная карта подается в Find где через в пин String подается ID текущего actor. Т.е. таким образом мы находим нужный BP_ObjectSave в выбранной Map по его ID, который уникален. Если данный BP_ObjectSave найден в карте, то это говорит о том, что игра уже запускалась и данный object был записан. Если он найден мы через Branch в его true выход подаем ивент SaveLoadData где происходит загрузка основных переменных из BP_ObjectSave. Если же BP_ObjectSave не найден в выбранной ранее Map, то идет создание данного BP_ObjectSave через Construct ноду. Важно: в пин Outer ноды Construct подается BP_SaveGame который по сути является файлом сохранения и не будет уничтожен, а значит на создаваемый через констракт BP_ObjectSave будет жить! После чего мы опять выбираем Map по тому же принципу Spawned геймплейного тега что и ранее, но на этот раз используем не Find, а Add чтобы добавить только что созданный через Construct переменную BP_ObjectSave. Таким образом он помещается в карту и дальше если игра будет загружена, на Begin Play вновь начнется процесс инициализации, но уже не будет Construct, а будет загрузка через true выход бранча проверки (как я и описывал ранее).
Если происходит загрузка игры через главное меню, то мы внутри BP_SaveGameInstance получаем наш текущий слот/файл сохранения BP_SaveGame и запускаем через Sequencer разветвление внутри блюпринта для каждой карты внутри BP_SaveGame (переменные я приводил выше) через For Each Loop проверку на валидность actor по ID которого был создан object сохранения. Если actor валиден, т.е. есть на карте, запускается событие блюпринт интерфейса для каждого из actor под названием EventSaveLoading и происходит загрузка переменных. Если actor не валиден, значит запускается ивент EventSaveLoadSpawn для в котором мы из BP_ObjectSave полученного из карты (actor которого не прошел валидность) получаем булевую переменную LoadedDestroyActor и проверяем. Если true, то ничего не происходит и actor удален (в загрузке не нуждается) а если false, значит actor не был удален, но не находился при стартовом состоянии уровня на нем, т.е. был заспавнен в результате игры по каким-то причинам. Мы подключаем к false бранча проверки булевой ноду SpawnActor и передаем туда для спавна ActorClass переменную прямо из BP_ObjectSave сохранения данного actor. Таким образом он будет создан заново при загрузке игры.
Сохранение через главное меню происходит аналогичным образом: через Sequencer происходит запуск через For Each Loop для каждой карты проверку на валидность actor и дальше сохраняем если он найден на уровне. Если валидация не проходит, значит actor был удален по каким-то причинам: в результате скриптов или уничтожения, то мы берем из BP_ObjectSave булевую переменную LoadedDestroyActor и помечаем ее true через Set. Это говорит нам о том, что при следующей загрузке данный actor будет удален если он имеет true для переменной LoadedDestroyActor.
Это все что касается системы сохранения. Она универсальна и насколько я понял поддерживает любые типы переменных - нужно просто добавить их в нужный ивент сохранения к нужному actor. 
Времени потрачено не мало, но зато теперь я смогу при необходимости расширять ее.
Subscription levels2

Геймер

$1.26 per month
Подпишись и получай на 50% больше контента по разработке игры.

Разработчик

$3.2 per month
- Подпишись и получай на 100% больше информации и опыта разработчика.
- Детали из процесса разработки механик и дизайнерских решений.
- Полезные лайфхаки для разработчика
Go up