[Из песочницы] Управление состоянием и событиями между компонентами в GameObject


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

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

57c0013e6c716c9e8bae072510a1f6e9.jpg

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

86e3be0117dbb34634c6b6e6b195acaa.jpg

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

27ff8a466f7b514a44c259a8ec662798.jpg

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

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

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

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


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

580a1d24db1610425d786be6ff6755d1.jpg

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

82917fc7fead431a659b4198d8be26d4.jpg

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

d6827b6af2050750fc6ebd6015676f98.jpg

bcd00bf6b5dd87bf25c64084cc80d853.jpg

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

2e3f71c7018f3f65c930c82c699f2b9b.jpg

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

7918fb5f55a0585191e16e7dfbf3a428.jpg

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

ad6f4faf555d6c7af5e14ad1baa68d82.jpg

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

52e4f19383874c56b4d715cb6d1b6dc3.jpg

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

5dd0472753beeaf2811e569ca49ae240.jpg

8590f6b82421662793ba28df06e14de1.jpg

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

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


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

2b93bbffbbeaa2c98e64ad75089792ee.jpg

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

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

894b7fcfdbb7ff00f8f41f1b647b0f4b.jpg

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

6620d0ee9590f92ac838fc91a6ddfaaf.jpg

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

56e9a683243778dd34dda30d7a3a05f3.jpg

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

81da429a637ef32290ce7e5d61d7058f.jpg

b69e92cd3882471ed64b342cc833c150.jpg

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

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

013a2ade149a42ac361f604b3debb482.jpg

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

082fa5a618434e47147ff93bcf03fe2b.jpg

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

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

2085adebb94c05a2d0dd1d906441d375.jpg

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

8a1aeeb124f5fb7d550b2f7eb25280b4.jpg

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

042f97ca3a4a8ac31c9173634e6fd406.jpg

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

6ac51bd0c64736c30e3d42944eee2762.jpg

ec89b0f533fd773de44b572d110dbb31.jpg

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

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

f0c9abe69b2749783965064a27c9d207.jpg

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

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

f71a53545623eff24411b88a65dd17f6.jpg

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

cd674e57ea97a1f1a069edf0e0ee9525.jpg

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

0af703e5feab02ef1f7c5b4d10837188.jpg

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

e570b15d01e04fd801519515aa1baa69.jpg

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

c139cfb76f4ad397ea7bd0929bd33375.jpg

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

edb50ecf6435a6f9c7711d25cc9e6914.jpg

c202ff7e58ad1695473cfe3cf22a54a2.jpg

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

c06ed134d85eb3a9444e53d50cfe909c.jpg

fef8defe3c3366838a4300243475a6bb.jpg

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

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

1e18801db271b9e651c5e4dfb4cc659b.jpg

Итог


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

b6e30c1040c6bcba3eb3ca788ee499a2.jpg

© Habrahabr.ru