SamuKata
teedeezet
teedeezet

boosty


Логическая стыковка объектов

Привет всем спонсорам!
 
В марте я занимался активной доработкой логической стыковки объектов. Теперь, когда каждый элемент системы оказался до конца продуман и даже частично реализован, я хочу рассказать вам о её полном устройстве и главных фишках.
 
Что скрывается за этим названием?
 
Логической стыковкой я называю возможность связывать объекты в цепочки логики: строить между ними иерархию, задавать владельцев, наследовать параметры видимости и коллизии.
 
Например, когда игрок кладёт предмет Мясо в Холодильник, он просто логически стыкует их между собой. Холодильник начинает "владеть" этим мясом (уничтожится холодильник - уничтожатся и все продукты внутри) и перехватывать его запрос на температуру окружающей среды.
 
Это вид стыковки "объект → объект". А есть ещё стыковка "логический компонент → объект", и с ней дела обстоят куда интереснее.
 
Стыковка логических компонентов
 
Про компоненты я
говорил и писал уже много раз, но кратко напомнить об их устройстве всё-таки надо.
 
Любой игровой объект - это не литой кусок кода, а набор "модулей" - логических компонентов, имеющих свой уникальный функционал. Здоровье, навыки, генерация энергии, свечение, инвентарь - это всё отдельные компоненты, которые мы можем свободно добавить в какой угодно объект.
 
Рассмотрим структуру уже знакомого вам предмета "Картошка":
 
То, что я вам показал - это шаблон. То есть объект, который хранится внутри ассета "Item:Potato", по образу и подобию которого создаются все копии в игровом пространстве (например, когда ваш колонист собирает урожай, и на уровне спавнится предмет "Картошка x10").
 
Шаблон содержит в себе все добавленные разработчиком компоненты и свойства. Но далеко не всё оттуда напрямую копируется в игровые объекты:
Слева - наш Шаблон (Template) из прошлого примера. Справа - мировые объекты (World Instances).
 
Некоторые данные просто нет смысла дублировать. Допустим, картофелины в мире, сколько бы тысяч их там не существовало, все дают одинаковое количество калорий при съедении. То есть они ссылаются на компонент Съедобное, но не свой личный, а один и тот же - из Шаблона. Такой вид стыковки у меня называется статическим, и он позволяет значительно сэкономить потребление игрой оперативной памяти.
 
Если же какая-то картофелина после безумных экспериментов игрока получает мутацию "большая калорийность", мы просто подменяем её ссылку на "Съедобное" на свои уникальные данные:
 
Компоненты также могут быть общими. В ролике я приводил пример с коллективным разумом (когда один разум управляет группой людей или животных), но это не единственный способ их применения. Я хочу ввести в игру модульные постройки: функционал им даётся не просто набором компонентов, а разными объектами на уровне. Например, ядерный реактор или челнок для полётов по планете.
 
Компоненты - это тоже объекты
 
В этом тексте я уже много раз написал слова "компонент" и "объект". Под первым имея ввиду модуль логики, а под вторым - самостоятельные сущности.
 
Но на самом деле, компоненты - это те же самые объекты что и "объекты". И то, и другое - унаследованные от UWorldObject классы, и вся их разница абстрактна.
 
Главное (придуманной мной) отличие компонента от игрового объекта - это направленность логики. Объект - это сущность, которая принимает логику (собирает из разных детей, то есть снизу по логической цепочке), а компонент - это сущность, которая даёт логику во внешнюю среду (родителям и владельцам).
 
Стыковать, получается, мы можем и самые обычные объекты. Например, шаблон Человека содержит в себе части тела и органы (которые могут существовать сами по себе в виде предмета). Вообще, с телами и экипировкой всё оказалось сложнее, чем я думал. У меня возникли такие проблемы, которые решать пришлось внедрением целой отдельной механики "именных слотов".
 
Именные слоты стыковки
 
(1) Можно ли пришить человеку третью ногу или вторую голову? (2) Можно ли одеть человека в 5 штанов одновременно? (3) Что делать с экипированным двуручным оружием, если человеку отрывает одну из рук?
 
Все эти спорные ситуации, требующие ограничений со стороны игры, оказалось возможно исправить с помощью системы именных слотов. Дело в том, что каждая стыковка имеет AttachmentName - своего рода ID, по которому мы на неё можем сослаться из кода. А именные слоты - это просто слоты с заранее определённым ID и условиями стыковки.
 
Решение проблемы (1): BodyComponent (хранилище частей тела и органов) позволяет стыковать к себе только объекты определённого класса (нельзя пришить человеку собачью лапу) в заранее определённые слоты (2 руки, 2 ноги, 1 голова...), и ничего больше.
 
Решение проблем (2) и (3): Части тела при стыковке добавляют владельцу слоты экипировки. При отстыковке - удаляют (если человек лишится руки, его двуручное оружие уберётся в инвентарь). Слоты экипировки, соответственно - это тоже именные слоты, в которые что угодно запихать не получится (штаны на голову не надеть - слот требует объект типа Headgear).
 
Идей для реализации ограничения стыковки у меня было много, но именно эта система оказалась для игры самой подходящей.
 
Ещё одна оптимизация (COW-компоненты)
 
Представьте, что вы начинаете игру в большой пещере (уровень размером 512x512 клеток). Вы находитесь под землей, вокруг вас одни сплошные блоки из камня. Допустим, их на уровне 100 000 штук.
  
Да, половина компонентов у них статическая. Но! У каждой каменной стены зато есть свой собственный компонент Здоровье, добавляющий ей значение текущей прочности (HP).
 
Вся сотня тысяч блоков сейчас имеет 100% прочности. Мы просто скопировали один и тот же компонент Здоровья из шаблона и замусорили оперативную память абсолютно одинаковыми и бесполезными данными.
 
И это проблема! ... Которая существовала бы в игре, если бы я всё заранее не продумал. "Если нам не нужны Per-Instance данные, значит, их не будет." - это главное правило в моей стыковке.
 
Решение проблемы такое: все индивидуальные компоненты по умолчанию существуют в режиме Copy-On-Write (COW). До тех пор, пока мы не захотим внести какие-то изменения относительно стандартного состояния (т.е. взять non-const / mutable указатель), у нас в индивидуальном слоте будет всё равно храниться ссылка на статический компонент.
 
Возвращаемся к пещере: вся сотня тысяч блоков при создании никакое Здоровье к себе не копирует. Они все продолжают ссылаться на здоровье своего Шаблона (одно на всю игру), где сказано, например, что MaxHealth = 220 и CurrentHealth = 220.
 
Какой блок игрок бы не выделил, там везде будет написано "текущее здоровье: 220 единиц (100%)". Ровно как было бы в примере без COW. Разницу с точки зрения игрока найти невозможно - разве что по значительному уменьшению потребления оперативной памяти.
 
Создаваться индивидуальное Здоровье будет при первом нанесении урона, так что размещённые где-то вдалеке от вашей базы блоки, с которыми вы никогда не будете взаимодействовать, так и останутся пустыми болванками, не потребляющими лишних ресурсов вашего компьютера.
 
Кстати, если вы сначала повредите какой-то блок, затем почините его до 100% и перезагрузите сессию, то индивидуальной копии его Здоровья снова не будет создано. Изменённых (относительно Шаблона) данных ведь у него снова нет.
  
Наслоение логической стыковки
 
Помните, недавно я
описывал алгоритм Delta Serialization, при котором разные слои информации из разных файлов последовательно накладываются друг на друга? Такая же фишка есть и у стыковки.  
 
Наследование ассетов / модификации ассетов из модов / сессионные ассеты (ГМ-культуры и модели роботов) - всё это работает на основе создания и применения слоёв данных. Мододелы просто добавляют, модифицируют или удаляют компоненты через окно Asset Editor (лезть в код для этого не надо), а игра создаёт из этих изменений новые слои.
 
Ну и конечно, сохранение и загрузка игровой сессии работает так же. На диск мы сохраняем только индивидуальные изменения объектов, а при загрузке - накладываем их на созданные из Шаблона пустышки (dummies). Это помогает сократить размер save-файла на диске.
  
Больше деталей
 
То, что я сейчас описал, звучит не так сложно. На самом деле, я оставил за скобками достаточно много деталей, и для тех, кому они интересны, я сделал эту схему:
Надеюсь, приближение на Бусти в этот раз будет работать нормально.
  
Что такая система даёт игрокам?
 
Пока рано говорить о практическом успехе или провале этой системы. Да, она идеально подходит под нужды игры и модов, но только в теории. Тестироваться "в бою" она начнёт сразу после реализации Asset Editor (то есть уже скоро). Из фишек (пока теоретических) мы имеем это:
 
• При изменении модов посреди начатой игровой сессии ничего ломаться не должно. Правильность наложения стыковки проверяется мной, а данных в полях - движком (я использую Tagged Serialization: значения сохраняются вместе с названиями полей, и при внештатной ситуации поле просто не будет загружено)
 
• Создавать контент для модов можно не просто "через редактор в главном меню", а вообще через непосредственно саму игру! Сохранение игрового объекта работает точно так же, как и сохранение модификации его Шаблона внутри ассета.
 
Это совсем безумная идея, но можно сделать так: превратить создание модов в элемент игрового процесса. Создавать новые сорта растений можно через ГМ-культуры. Модели роботов - через робо-станцию. Пушки - через оружейный стол. Предметы - через специальные верстаки. Мировые структуры - просто построив их прямо на уровне и создав из них чертёж (опять же, любая структура - это просто "мини-мир" с сохранёнными объектами)
 
• И вот вам ещё один пример, показывающий принцип работы переадресации запросов и свойств (Property Forwarding). Это ещё одна система, связанная со стыковкой, но её я пока не сделал, так что поговорим про это позже.
  
Мясо: портится, если температура окружающей среды выше 0°C. Этот запрос она отправляет выше по цепочке (рекурсивно родителям, пока не найдётся тот, кто этот запрос примет), то есть:
1) На земле: обращается к клетке
2) В инвентаре пешки: обращается к клетке (через пешку)
3) В холодильнике: обращается к холодильнику
4) В инвентаре пешки, которая лежит в крио-капсуле: обращается к крио-капсуле (через пешку)
5) В переносном холодильнике внутри инвентаря пешки, лежащей в крио-капсуле: обращается к переносному холодильнику. Если он не работает, то обращение идёт дальше по цепочке в крио-капсулу. Если и она не работает, то на клетку, где она стоит
 
Пока это всё, о чём я хотел вам сказать. Система ещё не готова на 100%, но основные механики из поста уже сделаны и частично протестированы.
 
Спасибо за внимание!

Логическая стыковка объектов Логическая стыковка объектов Логическая стыковка объектов Логическая стыковка объектов

Comments

<div ><div><span class="text">Итоги марта тоже будут, но немного позже (я устал писать этот пост  </span><span class="smile"><img id="9db7bb0d-1148-4686-9d0c-643d2c94837b" src="/thumbnail/boosty_smile/ExplodingHead.webp" title="exploding_head" class="smile"></span><span class="text">)</span></div><div></div></div>

teedeezet


More Creators