Unity компонентно-ориентированный подход часть вторая

Ранее я уже писал статью про компонентно-ориентированный подход в Unity. Та реализация оказалась совершенно не рабочей для любых проектов.

В этой статье я расскажу о новой реализации. Чем она лучше. Когда стоит её использовать. И какие проблемы могут возникнуть.

Время прочтения: 5 мин

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

Ссылка на первую часть: https://habr. com/ru/articles/733288/

Ссылка на проект: https://github. com/Favellangel/TowerDefence2D

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

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

Теперь в проекте нет отдельных скриптов с данными, который можно подставлять. Это повысило понимание кода и объектов в Unity.

Для проекта который я здесь использую, такой подход уместен. Проект не очень сложный. Для дизайнеров довольно удобно добавлять и изменять префабы.

Unity компонентно-ориентированный подход часть вторая

Далее расскажу проблемы, которые возникнут при таком подходе.

Первая проблема — инициализации скриптов. Unity вызывает сначала awake, start для одного скрипта, а потом только для другого. Это можно проверить выводами на консоль для разных скриптов.

Unity компонентно-ориентированный подход часть вторая

Для решения этой проблемы я написал код использующий рефлексию. Рефлексия, это когда код самостоятельно анализирует себя и выполняет какие-то действия. Рефлексия очень сильно бьёт по производительности. Поэтому её не стоит использовать во время игры. Либо в начале для инициализации, либо в конце.

Я создал несколько интерфейсов IBindable, IAwakable, IEnable. Интерфейсы содержат по одному методу. Идея была в том, чтобы сделать инициализацию в том порядке, который мы изначально ожидали от Unity от методов Awake и Start.

public interface IBindable { public void Bind(); }

Далее в скрипте Entry Point Game я собираю все MonoBehaviors. И вызываю Awake скрипта EntryPoint.

public class EntryPointGame : EntryPoint { protected override void Awake() { GameTime.StartGame(); monoBehaviours = FindObjectsOfType<MonoBehaviour>(true); base.Awake(); } }

В скрипте Entry Point я создаю списки. В методе GetLinks я пытаюсь преобразовать каждый MonoBehavior скрипт в конкретный интерфейс. Если преобразование удалось, добавляю его в соответствующий список.

public abstract class EntryPoint : MonoBehaviour { private List<IBindable> bindList = new List<IBindable>(); private List<IAwakable> initList = new List<IAwakable>(); private List<IEnable> enebleList = new List<IEnable>(); protected MonoBehaviour[] monoBehaviours; protected virtual void Awake() { GetLinks(); foreach (var item in initList) item.Initialize(); foreach (var item in bindList) item.Bind(); } private void OnEnable() { foreach (var item in enebleList) item.Enable(); } private void GetLinks() { IBindable bindable; IAwakable initializeble; IEnable enable; foreach (var monoBehaviour in monoBehaviours) { bindable = monoBehaviour as IBindable; enable = monoBehaviour as IEnable; initializeble = monoBehaviour as IAwakable; if(bindable != null) bindList.Add(bindable); if (enable != null) enebleList.Add(enable); if (initializeble != null) initList.Add(initializeble); bindable = null; enable = null; initializeble = null; } monoBehaviours = null; GC.Collect(); } }

После на конкретных MonoBehavior за место метода Awake или start можно наследоваться от интерфейсов. И эти методы будут вызываться по порядку. В методе Initialize можно собрать ссылки на компоненты Unity или свои компоненты на этом объекте. В методе Bind можно собрать все внешние ссылки.

public class PlayerGameOverBehavior : Subscriber, IAwakable, IBindable { private GameObject panelGameOver; public void Initialize() { onAction = GetComponent<HealthComponent>(); } public void Bind() { PanelGameOverController gameOverController = FindObjectOfType<PanelGameOverController>(true); panelGameOver = gameOverController.gameObject; } public override void Execute() { panelGameOver.SetActive(true); GameTime.StopGame(); } }

IEnable в результате лучше удалить и добавить скрипт EntryPointGame в порядок вызовов скриптов (Script Execution Order) вторым в списке после Event System. Тогда этот скрипт будет выполняться самым первым. А значит выполнит все IBindable и IAwakable. И только потом выполнит OnEnable.

Unity компонентно-ориентированный подход часть вторая

Плюсы такой реализации:

  • Быстро и легко добавлять новые механики;

  • Легко изменить механику;

Этот подход перестанет работать, как только:

  • На один Gameobject будет повешено 15 и более скриптов. Начнётся путаница;

  • Добавятся сложные механики: Инвентарь, система диалогов и прочие;

  • Начнут появляться баги, а написать тесты для MonoBehavior особо не получится;

  • В команде появится больше 1-2 программистов;

  • На GameObjects появится очень много скриптов, из-за чего возникнут проблемы с производительностью и памятью.

Этот подход хорошо подходит:

  • Для изучения Unity и С#;

  • Для создания прототипов;

  • Для создания простых игр;

В остальных случаях лучше воспользоваться другими подходами. Тем более в компаниях вы ни увидите такую реализацию. Даже для простых hyperCasual.

ИТОГ

Проблемы, которые были описаны данной статье лишь верхушка айсберга.

Если нет понимания как можно разделить код на небольшие блоки и заставить это работать без использования GameManagers и прочих. То этот пример очень хорошо это показывает.

Если хотите начать работать в игровой студии. То для создания больших и средних проектов необходимо писать код не зависимый от Monobehavior. То есть обычные С# скрипты. А Monobehavior использовать только для использования этой игровой логики.

Спасибо за прочтение статьи.

5 комментариев

Какой-то сомнительный костыль, где в плюсах находится всего 2 одинаковых пункта. Зачем так заморачиваться, пока что не понятно.

Суть в не заморачиваться. А в том, что я хотел использовать то что Unity задумывала в своём движке изначально.
И в принципе до какого-то момента это возможно. Пока проект совсем простой. А для прототипирования это способ очень даже удобный.

Монобехи в принципе не стоит использовать для бизнес логики ни в больших ни в маленьких проектах. Для этого подходят простые с# классы где программист контролирует время жизни и порядок инициализации.