Управление состоянием и событиями между компонентами в GameObject

в 14:07, , рубрики: Unity 3D, unity туториал, unity уроки, unity3d, unity3d уроки

Управление состоянием и событиями между компонентами в GameObject

Ссылка на проект

Как известно всем, более или менее знакомых с платформой Unity, каждый игровой объект GameObject состоит из компонентов (встроенных или пользовательских, который обычно называют «скрипт»). Компоненты наследуются от базового класса MonoBehavior.

Управление состоянием и событиями между компонентами в GameObject - 1

И обычно, ну или часто, для связывания компонентов осуществляется прямая связь.

Управление состоянием и событиями между компонентами в GameObject - 2

Т.е. в одном компоненте, для получения данных другого компонента, мы получаем последний с помощью метода GetComponent<…>(), например так:

Управление состоянием и событиями между компонентами в GameObject - 3

В данном примере в переменную someComponent будет помещена ссылка на компонент типа SomeComponent.

При таком «сильно связанном» подходе, особенно, при наличии большого количества компонентов, довольно просто запутаться и поддерживать целостность такой связи. К примеру, если изменится название свойства или метода в одном компоненте, то придется исправлять во всех компонентах, использующих этот. И это гемор.

Под катом много картинок

Создание решения на базе «сильной связанности» компонентов

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

Управление состоянием и событиями между компонентами в GameObject - 4

Я добавил два скрипта FirstComponent и SecondComponent, которые будут использованы как компоненты в игровом объекте:

Управление состоянием и событиями между компонентами в GameObject - 5

Теперь я определю простую структуру для каждого из компонентов, необходимую для экспериментов.

Управление состоянием и событиями между компонентами в GameObject - 6

Управление состоянием и событиями между компонентами в GameObject - 7

Теперь представим ситуацию, при которой нам бы понадобилось, получить значения полей state1 из компонента FirstComponent и вызвать его метод ChangeState(…) в компоненте SecondComponent. Для этого нужно получить ссылку на компонент и запросить нужные данные в компоненте SecondComponent:

Управление состоянием и событиями между компонентами в GameObject - 8

После того как мы запустим игру в консоли будет видно, что мы получили данные из FisrtComponent из SecondComponent и изменили состояние первого

Управление состоянием и событиями между компонентами в GameObject - 9

Теперь точно также мы можем получить данные и в обратном направлении из компонента FirstComponent получить данные компонента SecondComponent.

Управление состоянием и событиями между компонентами в GameObject - 10

После запуска игры также будет видно что данные получаем и можем управлять компонентом SecondComponent из FirstComponent.

Управление состоянием и событиями между компонентами в GameObject - 11

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

Управление состоянием и событиями между компонентами в GameObject - 12

Управление состоянием и событиями между компонентами в GameObject - 13

Расширять даже один игровой объект новыми компонентами, если им нужно будет взаимодействовать с уже существующими будет довольно рутинно. А особенно, если, например, название поля state1 в компоненте FirstComponent изменится, например, на state_1 и придется во всех компонентах менять название, где это используется. Или когда полей у компонента становится слишком много, то тогда довольно сложно становится по ним ориентироваться.

Создание решения на базе «Общего состояния» между компонентами

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

Управление состоянием и событиями между компонентами в GameObject - 14

Общее состояние или Объект общего состояния (SharedState) это тоже компонент, который будет играть роль служебного компонента и хранить состояния всех компонентов игрового объекта.

Я создам новый компонент и назову его SharedState:

Управление состоянием и событиями между компонентами в GameObject - 15

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

Управление состоянием и событиями между компонентами в GameObject - 16

Теперь этот компонент нужно разместить на игровом объекте, чтобы остальные компоненты могли получить к нему доступ:

Управление состоянием и событиями между компонентами в GameObject - 17

Далее нужно внести некоторые правки в компоненты FirstComponent и SecondComponent, чтобы они использовали компонент SharedState для хранения своего состояния или данных:

Управление состоянием и событиями между компонентами в GameObject - 18

Управление состоянием и событиями между компонентами в GameObject - 19

Как видно по коду компонентов, мы больше не храним поля, вместо этого мы используем общее состояние и имеем доступ к его данным по ключу «state1» или «counter». Теперь эти данные не привязаны ни к одному компоненту, и если появится третий компонент, то получив доступ к SharedState он сможет иметь доступ ко всем этим данным.

Теперь для демонстрации работы этой схемы, нужно изменить методы Update в обоих компонентах. В FisrtComponent:

Управление состоянием и событиями между компонентами в GameObject - 20

И в компоненте SecondComponent:

Управление состоянием и событиями между компонентами в GameObject - 21

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

После запуска игры видно что компоненты получают нужные значения:

Управление состоянием и событиями между компонентами в GameObject - 22

Теперь когда известно как это работает, можно вывести основную инфраструктуру для доступа к общему состоянию в базовый класс, чтобы не делать это все в каждом компоненте отдельно:

Управление состоянием и событиями между компонентами в GameObject - 23

И сделаю его абстрактным, чтобы случайно не создать его экземпляр… А так же желательно добавить атрибут, указывающий, что данный базовый компонент требует наличия компонента SharedState:

Управление состоянием и событиями между компонентами в GameObject - 24

Теперь нужно изменить компоненты FirstComponent и SecondComponent, чтобы они наследовались от SharedStateComponent и убрать все лишнее:

Управление состоянием и событиями между компонентами в GameObject - 25

Управление состоянием и событиями между компонентами в GameObject - 26

Ок. А как насчет вызова методов? Это предлагается делать так же не напрямую, а через паттерн Publisher-Subscriber. Упрощенный.

Для реализации этого нужно добавить еще один общий компонент, по аналогии с тем, который содержит данные, за тем исключением что этот будет содержать только подписки и будет называться SharedEvents:

Управление состоянием и событиями между компонентами в GameObject - 27

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

Каждый компонент, подписывается на некоторые события, которые он готов отслеживать. И если он отлавливает это событие он выполняет обработчик, который определен в самом компоненте.
Создадим компонент SharedEvents:

Управление состоянием и событиями между компонентами в GameObject - 28

И определим структуру, необходимую для управления подписками и публикациями

Управление состоянием и событиями между компонентами в GameObject - 29

Для обмена данными между подписками-публикациями определен базовый класс, конкретный будет определяться для автора каждого события самостоятельно, далее будут определены несколько для примера:

Управление состоянием и событиями между компонентами в GameObject - 30

Теперь нужно добавить новый компонент в игровой объект:

Управление состоянием и событиями между компонентами в GameObject - 31

и немного расширить базовый класс SharedStateComponent и добавить требование того чтобы объект содержал и SharedEvents

Управление состоянием и событиями между компонентами в GameObject - 32

Так же как и объект общего состояния объект общих подписок нужно получить из игрового объекта:

Управление состоянием и событиями между компонентами в GameObject - 33

Управление состоянием и событиями между компонентами в GameObject - 34

Теперь определим подписку на событие, которое обработаем в FisrtComponent и класс для передачи данных через этот тип события, а также изменим SecondComponent чтобы событие по этой подписке было опубликовано:

Управление состоянием и событиями между компонентами в GameObject - 35

Управление состоянием и событиями между компонентами в GameObject - 36

Теперь мы подписались на любое событие в названием «writesomedata» в компоненте FirstComponent и просто выводим сообщение в консоль при его возникновении. А возникает оно в данном примере путем вызова публикации события с именем «writesomedata» в компоненте SecondComponent и передачи некоторой информации, которая может быть использована в компоненте, который отлавливает события по такому названию.

После запуска игры через 5 секунд мы увидим результат обработки события в FirstComponent:

Управление состоянием и событиями между компонентами в GameObject - 37

Итог

Теперь если нужно расширить компоненты данного игрового объекта, которые тоже будут использовать общее состояние и общие события нужно добавить класс и просто унаследоваться от SharedStateComponent:

Управление состоянием и событиями между компонентами в GameObject - 38

Автор: Денис Козлов

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js