87

Как делать игры в Unity. Попытка в серию обучающих постов #3

Пост №3. Несколько способов связи разных компонентов

Дисклеймер:

Во-первых, эти посты я пишу для тех, кто хоть немного знаком с C#. Во-вторых, это далеко не все способы связи частей программы/игры. В-третьих, не стоит забывать, что приведенные ниже код написан в просветительских целях, ориентирован на низкий порог вхождения, поэтому "делать еще более правильнее и профессиональнее, используя больше абстракций, а не конкретные классы" я не стану. И еще - можно бесконечно долго спорить о паттернах и анти-паттернах =)

Всем привет!
Сегодня расскажу и покажу три способа связи разных компонентов.

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

Сферическая задача в вакууме будет такой:

Есть некий объект-счетчик и есть интерфейс с текстовым полем и кнопкой. Счетчик существует сам по себе, интерфейс - сам по себе. Объект не должен знать о существовании каких-либо частей UI и уж тем более не должен от них зависеть - это поможет в дальнейшем избежать массу проблем (самая очевидная - сложности в модификации при подобных не совсем уместных зависимостях). Но этот объект может (и должен) обладать неким инструментом, что бы можно было реагировать на его изменения в процессе игры.

Н-р, этим объектом-счетчиком может быть персонаж с набором очков жизней. А интерфейс должен отображать корректное значение здоровья. При этом обновлять текст каждый кадр - не самая хорошая затея. Поэтому мы должны как-то научить интерфейс реагировать на изменения счетчика.


Проект урока можно посмотреть в гите .

1. Паттерн Singleton

Данный паттерн (хотя в ряде случаев из-за недостатоков считается анти-паттерном) в "полной реализации" гарантирует наличие единственного экземпляра класса и предоставляет единую точку доступа к этому экземпляру.

В обучающем примере будет упрощенная реализация с наследованием от MonoBehaviour. Это нужно для того, что бы в примере просто разместить этот компонент в каком-нибудь объекте (для наглядности лучше в отдельный GameObject на "видном месте") на сцене и после запуска игры иметь простой доступ к этому компоненту. Директива #region использована исключительно для наглядности разделения блоков, т.к. в следующем пункте будем  дорабатывать этот класс.

Из-за такой условности, что упрощенная реализацию плюс  наследование от MonoBehaviour, у моего примера есть существенный недостаток - инициализация происходит в методе Awake. Следовательно, если на сцене изначально (при запуске игры) будут находиться объекты, которым потребуется доступ к этому классу, то делать это придется после Awake() - т.е. не раньше чем в Enable() или Start().

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

2. Паттерн ServiceLocator

Этот паттерн позволяет в едином месте хранить ссылки на "сервисы". И как правило - каждый "сервис" - это отдельный класс. В реалиях игры этими "сервисами" могут быть контроллер интерфейса (который, допустим, создает все окна и элементы UI), контроллер игрового режима (который, н-р, знает все правила текущей игры), какой-нибудь контроллер ботов (содержащий список всех ботов на сцене) и т.д.

На основе предыдущего пункта создадим такой Локатор. В моем примере он (локатор) наследуется от MonoBehaviour для наглядного размещения на сцене. Однако это вовсе не обязательно. Класс-локатор может быть статичным, а может быть и Singleton (но без наследования MonoBehaviour и без размещения на сцене его пришлось бы создавать в каком-нибудь месте игры, н-р, при инициализации).

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

"Локатор" готов. Но сам по себе он бесполезен. Теперь создадим основную сущность этого урока - Счетчик. Он должен будет регистрироваться в существующем локаторе и давать возможность корректно изменять свое состояние (помните про совет с минимумом общедоступных членов класса?) и выдавать по запросу свое актуальное состояние (значение счетчика).

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

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

3. События

События сообщают объектам-подписчикам о каком-то изменении. В данном примере Счетчик должен будет сообщать, что изменилось его значение. И что бы избежать лишней последовательности "Сработало событие -> Запрашиваем актуальное состояние -> Выводим результат" при вызове события сразу будет передаваться актуальное состояние счетчика.

События объявляются ключевым словом event с указанием типа делегата, которым будет представлено событие. Для примера подойдет "встроенный" в C# делегат Action<int> - он принимает параметр типа int - с его помощью будем передавать значение счетчика.

В написанный выше класс Счетчика внесем несколько изменений.

Подключим пространство имен и объявим событие

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост
Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

И в методе изменения счетчика добавим вызов события

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

Теперь быстренько создадим класс для работы кнопки и класс для текстового поля для "отлова" события изменения счетчика.

Кнопка должна будет через Локатор получить Счетчик и вызвать метод изменения счетчика.

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

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

[ВАЖНО] если вы подписываетесь на какое-то событие, не забывайте от него отписываться. Иначе это может привести не только к неожиданному поведению Вашей игры, но и критическим ошибкам.

Как делать игры в Unity. Попытка в серию обучающих постов #3 Unity, Csharp, Gamedev, Обучение, Длиннопост

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


В результате получается следующее:

Запускается игра, сингтон-локатор инициализируется. Затем Счетчик регистрируется в локаторе. Следом (по порядку вызова Unity-методов MonoBehaviour) текстовое поле подписывается на событие изменения счетчика и первый раз выводит текущее состояние этого счетчика. А при нажатие на кнопку происходит увеличение счетчика. В результате чего вызывается событие изменения, на которое реагирует текстовый компонент.


На этом пост подходит к концу.

Еще раз напомню, что код и сцену можно посмотреть в репозитории.

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

Успехов в геймдеве!

Правила сообщества

ОБЩИЕ ПРАВИЛА:

- Уважайте чужой труд и используйте конструктивную критику

- Не занимайтесь саморекламой, пишите качественные и интересные посты

- Никакой политики


СТОИТ ПУБЛИКОВАТЬ:

- Посты о Вашей игре с историей её разработки и описанием полученного опыта

- Обучающие материалы, туториалы

- Интервью с опытными разработчиками

- Анонсы бесплатных мероприятий для разработчиков и истории их посещения;
- Ваши работы, если Вы художник/композитор и хотите поделиться ими на безвозмездной основе

НЕ СТОИТ ПУБЛИКОВАТЬ:

- Посты, содержащие только вопрос или просьбу помочь
- Посты, содержащие только идею игры

- Посты, единственная цель которых - набор команды для разработки игры

- Посты, не относящиеся к тематике сообщества

Подобные посты по решению администрации могут быть перемещены из сообщества в общую ленту.

ЗАПРЕЩЕНО:

- Публиковать бессодержательные посты с рекламой Вашего проекта (см. следующий пункт), а также все прочие посты, содержащие рекламу/рекламные интеграции

- Выдавать чужой труд за свой

Подобные посты будут перемещены из сообщества в общую ленту, а их авторы по решению администрации могут быть внесены в игнор-лист сообщества.


О РАЗМЕЩЕНИИ ССЫЛОК:

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

- Пост должен быть содержательным и интересным для пользователей, нести пользу для сообщества

- Ссылка должна размещаться непосредственно в начале или конце поста и только один раз

- Cсылка размещается в формате: "Страница игры в Steam: URL"