Пост №3. Несколько способов связи разных компонентов
Дисклеймер:
Во-первых, эти посты я пишу для тех, кто хоть немного знаком с C#. Во-вторых, это далеко не все способы связи частей программы/игры. В-третьих, не стоит забывать, что приведенные ниже код написан в просветительских целях, ориентирован на низкий порог вхождения, поэтому "делать еще более правильнее и профессиональнее, используя больше абстракций, а не конкретные классы" я не стану. И еще - можно бесконечно долго спорить о паттернах и анти-паттернах =)
Всем привет!
Сегодня расскажу и покажу три способа связи разных компонентов.
Сферическая задача в вакууме будет такой:
Есть некий объект-счетчик и есть интерфейс с текстовым полем и кнопкой. Счетчик существует сам по себе, интерфейс - сам по себе. Объект не должен знать о существовании каких-либо частей UI и уж тем более не должен от них зависеть - это поможет в дальнейшем избежать массу проблем (самая очевидная - сложности в модификации при подобных не совсем уместных зависимостях). Но этот объект может (и должен) обладать неким инструментом, что бы можно было реагировать на его изменения в процессе игры.
Н-р, этим объектом-счетчиком может быть персонаж с набором очков жизней. А интерфейс должен отображать корректное значение здоровья. При этом обновлять текст каждый кадр - не самая хорошая затея. Поэтому мы должны как-то научить интерфейс реагировать на изменения счетчика.
Проект урока можно посмотреть в гите .
1. Паттерн Singleton
Данный паттерн (хотя в ряде случаев из-за недостатоков считается анти-паттерном) в "полной реализации" гарантирует наличие единственного экземпляра класса и предоставляет единую точку доступа к этому экземпляру.
В обучающем примере будет упрощенная реализация с наследованием от MonoBehaviour. Это нужно для того, что бы в примере просто разместить этот компонент в каком-нибудь объекте (для наглядности лучше в отдельный GameObject на "видном месте") на сцене и после запуска игры иметь простой доступ к этому компоненту. Директива #region использована исключительно для наглядности разделения блоков, т.к. в следующем пункте будем дорабатывать этот класс.
Из-за такой условности, что упрощенная реализацию плюс наследование от MonoBehaviour, у моего примера есть существенный недостаток - инициализация происходит в методе Awake. Следовательно, если на сцене изначально (при запуске игры) будут находиться объекты, которым потребуется доступ к этому классу, то делать это придется после Awake() - т.е. не раньше чем в Enable() или Start().
2. Паттерн ServiceLocator
Этот паттерн позволяет в едином месте хранить ссылки на "сервисы". И как правило - каждый "сервис" - это отдельный класс. В реалиях игры этими "сервисами" могут быть контроллер интерфейса (который, допустим, создает все окна и элементы UI), контроллер игрового режима (который, н-р, знает все правила текущей игры), какой-нибудь контроллер ботов (содержащий список всех ботов на сцене) и т.д.
На основе предыдущего пункта создадим такой Локатор. В моем примере он (локатор) наследуется от MonoBehaviour для наглядного размещения на сцене. Однако это вовсе не обязательно. Класс-локатор может быть статичным, а может быть и Singleton (но без наследования MonoBehaviour и без размещения на сцене его пришлось бы создавать в каком-нибудь месте игры, н-р, при инициализации).
"Локатор" готов. Но сам по себе он бесполезен. Теперь создадим основную сущность этого урока - Счетчик. Он должен будет регистрироваться в существующем локаторе и давать возможность корректно изменять свое состояние (помните про совет с минимумом общедоступных членов класса?) и выдавать по запросу свое актуальное состояние (значение счетчика).
Но как же Счетчик будет сообщать об изменении своего состояния? Чтобы другие объекты в игре могли отслеживать изменения Счетчика без необходимости его регулярной проверки, будем использовать...
3. События
События сообщают объектам-подписчикам о каком-то изменении. В данном примере Счетчик должен будет сообщать, что изменилось его значение. И что бы избежать лишней последовательности "Сработало событие -> Запрашиваем актуальное состояние -> Выводим результат" при вызове события сразу будет передаваться актуальное состояние счетчика.
События объявляются ключевым словом event с указанием типа делегата, которым будет представлено событие. Для примера подойдет "встроенный" в C# делегат Action<int> - он принимает параметр типа int - с его помощью будем передавать значение счетчика.
В написанный выше класс Счетчика внесем несколько изменений.
Подключим пространство имен и объявим событие
И в методе изменения счетчика добавим вызов события
Теперь быстренько создадим класс для работы кнопки и класс для текстового поля для "отлова" события изменения счетчика.
Кнопка должна будет через Локатор получить Счетчик и вызвать метод изменения счетчика.
Компонент для текста должен будет при старте игры отобразить состояние счетчика, а в последствии обновлять информацию только по необходимости (т.е. при срабатывании события OnChange). Для этого создаваемый компонент должен будет подписаться на это событие.
[ВАЖНО] если вы подписываетесь на какое-то событие, не забывайте от него отписываться. Иначе это может привести не только к неожиданному поведению Вашей игры, но и критическим ошибкам.
Из текста я опущу то, что эти компоненты нужно разместить на сцене, в случае кнопки - еще необходимо задать обработчик нажатия в компоненте Button в инспекторе, а в случае компонента для текста - через инспектор задать значение текстового поля.
В результате получается следующее:
Запускается игра, сингтон-локатор инициализируется. Затем Счетчик регистрируется в локаторе. Следом (по порядку вызова Unity-методов MonoBehaviour) текстовое поле подписывается на событие изменения счетчика и первый раз выводит текущее состояние этого счетчика. А при нажатие на кнопку происходит увеличение счетчика. В результате чего вызывается событие изменения, на которое реагирует текстовый компонент.
На этом пост подходит к концу.
Еще раз напомню, что код и сцену можно посмотреть в репозитории.
Надеюсь, кому-нибудь пост окажется полезным. И буду рад, если встретив для себя какой-то незнакомый термин Вы почитаете об этом подробнее.
Успехов в геймдеве!