Управление состоянием и событиями между компонентами в GameObject
Как известно всем, более или менее знакомых с платформой Unity, каждый игровой объект GameObject состоит из компонентов (встроенных или пользовательских, который обычно называют «скрипт»). Компоненты наследуются от базового класса MonoBehavior.
И обычно, ну или часто, для связывания компонентов осуществляется прямая связь.
Т.е. в одном компоненте, для получения данных другого компонента, мы получаем последний с помощью метода GetComponent<…>(), например так:
В данном примере в переменную someComponent будет помещена ссылка на компонент типа SomeComponent.
При таком «сильно связанном» подходе, особенно, при наличии большого количества компонентов, довольно просто запутаться и поддерживать целостность такой связи. К примеру, если изменится название свойства или метода в одном компоненте, то придется исправлять во всех компонентах, использующих этот. И это гемор.
Под катом много картинок
Создание решения на базе «сильной связанности» компонентов
Создадим пустой проект для воспроизведения обычной ситуации, когда у нас есть некие компоненты и каждый из них ссылается друг на друга, для получения данных либо для управления.
Я добавил два скрипта FirstComponent и SecondComponent, которые будут использованы как компоненты в игровом объекте:
Теперь я определю простую структуру для каждого из компонентов, необходимую для экспериментов.
Теперь представим ситуацию, при которой нам бы понадобилось, получить значения полей state1 из компонента FirstComponent и вызвать его метод ChangeState(…) в компоненте SecondComponent. Для этого нужно получить ссылку на компонент и запросить нужные данные в компоненте SecondComponent:
После того как мы запустим игру в консоли будет видно, что мы получили данные из FisrtComponent из SecondComponent и изменили состояние первого
Теперь точно также мы можем получить данные и в обратном направлении из компонента FirstComponent получить данные компонента SecondComponent.
После запуска игры также будет видно что данные получаем и можем управлять компонентом SecondComponent из FirstComponent.
Это был довольно простой пример, и чтобы понять что за проблему я хочу описать, понадобилось бы сильно усложнить структуру и связи всех компонентов, но смысл понятен. Сейчас связь между компонентами выглядит следующим образом:
Расширять даже один игровой объект новыми компонентами, если им нужно будет взаимодействовать с уже существующими будет довольно рутинно. А особенно, если, например, название поля state1 в компоненте FirstComponent изменится, например, на state_1 и придется во всех компонентах менять название, где это используется. Или когда полей у компонента становится слишком много, то тогда довольно сложно становится по ним ориентироваться.
Создание решения на базе «Общего состояния» между компонентами
Теперь представим, что нам не нужно было бы получать ссылку на каждый интересующий компонент и получения у него данных, а был бы некий объект, который содержит состояния и данные всех компонентов в игровом объекте. На схеме это выглядело бы следующим образом:
Общее состояние или Объект общего состояния (SharedState) это тоже компонент, который будет играть роль служебного компонента и хранить состояния всех компонентов игрового объекта.
Я создам новый компонент и назову его SharedState:
И определю код этого универсального компонента. Он будет хранить закрытый словарь и индексатор для более удобной работы со словарем компонента, также это будет инкапсуляция и напрямую со словарем из других компонент работать не получится.
Теперь этот компонент нужно разместить на игровом объекте, чтобы остальные компоненты могли получить к нему доступ:
Далее нужно внести некоторые правки в компоненты FirstComponent и SecondComponent, чтобы они использовали компонент SharedState для хранения своего состояния или данных:
Как видно по коду компонентов, мы больше не храним поля, вместо этого мы используем общее состояние и имеем доступ к его данным по ключу «state1» или «counter». Теперь эти данные не привязаны ни к одному компоненту, и если появится третий компонент, то получив доступ к SharedState он сможет иметь доступ ко всем этим данным.
Теперь для демонстрации работы этой схемы, нужно изменить методы Update в обоих компонентах. В FisrtComponent:
И в компоненте SecondComponent:
Теперь компоненты не знают происхождения этих значений, то есть раньше они обращались к какому то конкретному компоненту для получения их, а теперь они просто хранятся в общем пространстве и любой компонент имеет к ним доступ.
После запуска игры видно что компоненты получают нужные значения:
Теперь когда известно как это работает, можно вывести основную инфраструктуру для доступа к общему состоянию в базовый класс, чтобы не делать это все в каждом компоненте отдельно:
И сделаю его абстрактным, чтобы случайно не создать его экземпляр… А так же желательно добавить атрибут, указывающий, что данный базовый компонент требует наличия компонента SharedState:
Теперь нужно изменить компоненты FirstComponent и SecondComponent, чтобы они наследовались от SharedStateComponent и убрать все лишнее:
Ок. А как насчет вызова методов? Это предлагается делать так же не напрямую, а через паттерн Publisher-Subscriber. Упрощенный.
Для реализации этого нужно добавить еще один общий компонент, по аналогии с тем, который содержит данные, за тем исключением что этот будет содержать только подписки и будет называться SharedEvents:
Принцип следующий. Компонент, который Хочет вызвать какой то метод у другого компонента, будет это делать не напрямую, а вызовом события, так же по названию, как мы получаем данные из общего состояния.
Каждый компонент, подписывается на некоторые события, которые он готов отслеживать. И если он отлавливает это событие он выполняет обработчик, который определен в самом компоненте.
Создадим компонент SharedEvents:
И определим структуру, необходимую для управления подписками и публикациями
Для обмена данными между подписками-публикациями определен базовый класс, конкретный будет определяться для автора каждого события самостоятельно, далее будут определены несколько для примера:
Теперь нужно добавить новый компонент в игровой объект:
и немного расширить базовый класс SharedStateComponent и добавить требование того чтобы объект содержал и SharedEvents
Так же как и объект общего состояния объект общих подписок нужно получить из игрового объекта:
Теперь определим подписку на событие, которое обработаем в FisrtComponent и класс для передачи данных через этот тип события, а также изменим SecondComponent чтобы событие по этой подписке было опубликовано:
Теперь мы подписались на любое событие в названием «writesomedata» в компоненте FirstComponent и просто выводим сообщение в консоль при его возникновении. А возникает оно в данном примере путем вызова публикации события с именем «writesomedata» в компоненте SecondComponent и передачи некоторой информации, которая может быть использована в компоненте, который отлавливает события по такому названию.
После запуска игры через 5 секунд мы увидим результат обработки события в FirstComponent:
Итог
Теперь если нужно расширить компоненты данного игрового объекта, которые тоже будут использовать общее состояние и общие события нужно добавить класс и просто унаследоваться от SharedStateComponent:
Автор: Денис Козлов