avatar
1 год назад

Кратко о декомпозиции в Unreal Engine. Быстропост.

Сразу обговорю формат изложения. Я не претендую на абсолютную истину. Цель поста - развлечение. Я пишу пост исходя из собственного опыта только Unreal Engine. 


Unreal Engine из его корневой архитектуры, требует определенного разбития кода на различные сущности. Это, насколько мне известно, отличает UE от того же Unity, где можно пол игры написать в единой сущности. Сегодня хотелось бы поговорить об том, как именно разбивать код и обеспечивать грамотную архитектуру с фокусом на рост приложения. 


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


Декомпозиция включает в себя разбиение на компоненты. Мы делим общий код проекта на две системы. Система компонентов и система владельцев этих компонентов. Компоненты могут ссылаться только на другие компоненты, не ключевые акторы и могут получать данные с владельцев только косвенным путем через интерфейсы. Владельцы же владеют компонентами, могут то же самое + ссылаться на другие ключевые акторы, хотя избыточная ссылочность в системе всё еще не рекомендована. В проекте можно систему владельцев разбить на три ключевые подгруппы -Gamemode система, AI система и Player система. AI и Player System независимы друг от друга и не могут знать об состоянии Gamemode, а Gamemode система владеет всем и почти не содержит ограничений по связыванию. 


Все компоненты подразделяются на пассивные и активные. Активные выполняют действия сами, используя движковые события. Например, компоненты движения, видимости, выполнения инструкций ИИ являются активными. Так как те используют Tick, BT, Player Input, ect. Пассивные компоненты являются продолжением активных. Они используют делегаты, интерфейсы и тд и содержат логику, выполняющуюся исключительно как продолжение активных компонентов. В качестве примера приведем Health Component. Тот выполняется исключительно когда персонаж получил урон и, соответственно, полностью пассивен без получения урона. Чем меньшее количество активных компонентов содержит программа, тем лучше. Стоит, правда, отметить, что активность компонента во многом понятие растяжимое и решается непосредственно разработчиком. 


Все компоненты имеют свои зависимости. Количество зависимостей также оптимально должно стремится к минимуму. 
Каждый компонент по умолчанию рекомендован к автоматической инициализации, если та корректна и возможна. То есть вся логика подключения к делегатам, основные эвенты и тд должны быть проиграны за счёт внутренней логики компонента. Задача Owner Actor в этой концепции - обеспечивать дополнительную инициализацию, если такая требуется. 


В данной модели нас не так много инструментов связи между сущностями. При чем каждый со своими ограничениями. Но рассмотрим основные, доступные из коробки- Cast, GetComponent, Interface. 


1. Cast- рекомендуется только для больших объектов самого движка, например, приведение к персонажу, если вам нужно получить скорость персонажа или размер его коллайдера. В целом, желательно в выполнять Cast к исключительно абстрактным объектам. Если вам нужно связаться с игроком или конкретным не абстрактным классом , используйте другие методы или убедитесь, что ваша логика не требуется для других объектов. Cast - самая быстрая операция среди альтернатив. Я почти уверен в этом, хотя ряд моих экспериментов показал, что в случае несобранного проекта,это не совсем так. Помните, что Cast повышает связность кода при неправильном использовании. 
В целом, основная задача Actor в декомпозиции связывать компоненты между собой, если на то есть необходимость.


2. Компонент. Ваш основной инструмент компоновки. Основное преимущество Component заключается в том, что он позволяет вам реализовывать ваши методы без слишком большой связи, как в Cast, и без ненужных проблем с инициализацией, как в Interface.Компонент с помощью делегатов может использоваться как интерфейс, а также может быть автоматически добавлен или удален. Компонент отлично подходит для простого добавления сущностей ко всему.Из за того, что у нас редко используется в проекте больше одного компонента одного класса на один Actor, для получения компонента часто стандартизируют Get Component By Class или Find Component By Class в качестве метода получения компонента, либо, если обращение к компоненту чрезвычайно часто, например, один раз в тик, через специализированный интерфейс. 
Основные проблемы с компонентом заключаются в том, что они немного дороже альтернатив как по объему памяти, так и по времени, которое требуется для получения компонента с помощью Get Component By Class. Кроме того, компонент можно повесить только на Actor. Другим важным недостатком является то, что компонент не может быть удален, если компонент был добавлен в родительский actor в редакторе в bullprints. 


Тем не менее, я настоятельно рекомендую -

Если количество переменных, которые вам нужно реализовать, больше 2. Если присутствует хотя бы одна локальная функция.

Если у вас нет строгой уверенности в том, что код не понадобится в других частях программы (например, так сказал технический руководитель, глава проекта или планировщик)

Если нет частичной реализации задачи альтернативными методами.

Если вы можете строго определить цель этого компонента. (например, этот компонент отвечает за Health Point). 
Тогда используйте компоненты, отдавая предпочтение другим методам коммуникации. 


Интерфейсы.
Основным преимуществом интерфейсов является их универсальность и скорость по сравнению с компонентами и Castами.Основным недостатком является необходимость прописывания реализации, которая часто снижает скорость разработки. Интерфейсы используются, когда

- вы по какой-либо причине не используете Cast. 

- вы не используете компонент - обычно либо из-за малого объема логики, содержащейся в этом компоненте, либо потому, что компонент недоступен (например, для UObjectов), либо потому, что вам нужно перезаписать какую-то часть компонента в наследнике актера.

- вы используете его, чтобы получить указатель на нужный компонент. Важно, что любой компонент, который может быть получен, например, каждый кадр, должен иметь интерфейсную прослойку, так как получение компонента GetComponentByClass или Find Component By Class имеет проблемы со скоростью выполнения. 

Напоследок - планирование проекта напрямую зависит от ваших навыков, как разработчика и вашей цели. В некоторых случаях, как например создание простейших прототипов для тестирования идей, заморочки с декомпозицией и прочими элементами архитектуры вредны вашему времени. 

2комментария