Пару строк о средах и компонентах в Unity

Приветствую! Меня зовут Алексей и я разработчик игр на Unity. Среди моего опыта есть как и гиперказуальные игры длительностью разработки в 1 неделю, так и участие в ААА проектах. На своем пути я занимался, как от мелких задач будучи джуном, так и до постановки архитектуры и создания платформ для нескольких приложений сразу внутри одной компании, уже будучи старшим разработчиком. Я также принимал людей на работу, увольнял людей, провел не счесть сколько собеседований, сам менял места работы.

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

Начнем мы, как всегда, с азов — а именно с того, что такое КОП и в чем его фундаментальное преимущество перед ООП.

Компонентно Ориентированное Программирование — подход в котором опорный модуль — это не класс, а компонент (который в нашем случае построен на классе), что вводится и выводится из среды и является самодостаточным, путем самостоятельного извлечения зависимостей. Самодостаточность залог модульности.

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

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

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

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

Какая стоит проблема? — хрупкий класс! Что это такое в случае Unity разработки?

Делаем мы, например, RPG. Конечно у нас появляется проблема — что из себя представляет сущность юнитов и персонажа в частности? Какими свойствами ее наделить изначально так, чтобы предусмотреть максимально возможные изменения?

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

Необходимо понять следующий момент — Юнити предоставляет нам самим возможность создать как сущность среды, так и описать механизм, с помощью которого компонент будет делать то, что вы от него хотите и взаимодействовать со средой.

Для компонента определен класс, и точка входа в среду (это метод Awake, если что). Среда — свободна в реализации от слова совсем.

Схематично это можно изобразить так: рутовый класс в методе Awake отрабатывает с некоторым интерфейсом среды (пусть это будет статический класс (Environment) и берет оттуда данные для работы.

Схематично изобразим среду классом и добавим к нему абстрактное хранилище данных (им будет наша переменная object) а также метод, который вызовет компонент для их получения:

public static class Environment
{
      private static object _nextArgs;
      public static object GetArgs()
      {
          return _nextArgs;
      }
}

Далее, унаследуем MonoBehaviour и в точке ввода возьмем наши аргументы. Переопределим точку ввода для дочерних компонентов чтобы избежать постоянного вызова метода из родителя. Примерно вот так:

public class MyComponent:MonoBehaviour
{
    protected object _args;
    
    private void Awake()
    {
        _args = Environment.GetArgs();
        OnAwake();
    }
    protected virtual void OnAwake()
    { }
}

Добавим механизм, который будет вводить компоненты в среду и в тоже время пробрасывать их зависимости. Для удобства нашей схемы мы просто добавим еще 1 метод в класс среды:

public static TComponent AddMyComponent<TComponent>(GameObject container,
object args) where T : MyComponent
{
    _nextArgs = args;
    return container.AddComponent<TComponent>(); 
}

Unity устроен так, что Awake вызовется по сути прям в строчке где мы вызываем AddComponent.

И вуаля — любой компонент может получить ВСЕ, что ему нужно прямо перед началом его работы.

Конечно, это все таки схематическое изображение, и вы можете менять его на свой вкус (например, убрать приведение из object и сделать общение), но основной смысл, я думаю, понятен — не поленись и реализуй среду и свой компонент — ТЫ Ж ПРОГРАММИСТ, В КОНЦЕ КОНЦОВ!

Вы можете расширить среду. Будет хорошим тоном вести учет компонентов, и при необходимости доступа к компонентам — использовать реестр вместо тяжелых методов поиска FindObject или GetComponent. Углубившись в понятия класса, мы вспомним, что любой класс состоит из интерфейса и его реализации. Cобственно, интерфейсом выступают все публичные члены класса. Поэтому я не советую выделять интерфейс ИЗ КАЖДОГО компонента.

Этот пример также даст возможность раскрыться C# чуть больше и начать использовать кастомные атрибуты для компонентов, что не может не радовать. Для этого достаточно проверить и применить атрибуты либо в самом компоненте в момент ввода в среду, либо в месте, где вы вызываете AddMyComponent.

Все это создаст для нас некий базис разработки, на который мы сможем опереться, создавая последующую архитектуру приложения. Код, приведенный выше, только описывает, как именно мы будем использовать компоненты, но никак не определяет, как мы будет оперировать данными, их отображением и изменением.

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

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

Спасибо за внимание!

Підписуйтеся на Telegram-канал @gamedev_dou, щоб не пропустити найважливіші статті і новини про геймдев

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Мені здається статья не раскрила суть. Без справжних прикладів як мінимум не цікаво взагалі читати, просто 3 шматка кода, які нічого особливо не скажуть і не пояснять людям.

Добрый день. Статья задумывалась как начало цикла статей.
<посилання на російський ресурс хабр закрите модератором форуму>
Это вторая часть, в ней используется описанное в этой.
Следующая будет про атрибуты в Unity, где будут использоваться две предидущее) надеюсь будет понятнее.
Не стоит также исключать вероятность того, что я ужасный писатель

Навіщо вам той руський хабр? Давайте краще технічний блог на GameDev DOU або навіть серію!

Якщо у компонента декілька залежностей, а для ініціалізації передається один обджект то це треба для кожної комбінації залежностей плодити класи які їх будуть агрегувати? Чи я невірно зрозумів?

Добрый день. Все зависит от желаний и возможностей конкретного разработчика. Вы можете, например, указать в среде вместо объекта — массив объектов, а в методе в качества параметра написать «params [] object». Можно сделать реализацию с помощью кортежей. Возможностей множество. Но плодить типы-параметры лучше не стоит. Также не стоит забывать, что методы можно перегрузить так, чтобы они принимали что угодно =)

У мене питання зеленого юнця. Чому Юнька зупинила підтримку Монодевелоп в своєму інсталяторі ще десь в 2018. Невже в Візуал Студіо є якісь навороти для роботи з Юнькою? Тоді які?! Просто Монодевелоп полегше буде і по вазі і по функціоналу.

Предположу, что причина как обычно — деньги. Нет смысла поддерживать IDE которым никто не пользуется, и который явно не может конкурировать с VS и Rider

Підписатись на коментарі