Базовые компоненты надоели и мы приступаем к созданию своих скриптов. После успешной компиляции обнаруживается, что получить заветные поля другого компонента довольно сложно. Поначалу все не так однозначно, но с добавлением все новых и новых объектов становится неоднозначным решение такой задачи. Сегодня поговорим о передаче информации между объектами системы с точки зрения языка и игрового движка, а также вы узнаете(либо же повторите) о способах их "вязки". Сегодня только спицы, никаких крючков!
Ключевая причина, которая толкает на совершение ошибок в данной области - неверное понимание инкапсуляции как парадигмы ООП. Для начала определимся: инкапсуляция - это не про сеттеры и модификаторы доступа. Ключевая суть инкапсуляции, это предоставление клиенту ровно той информации, которой он может пользоваться в той мере что ему положена. Это касается и методов. В попытке сделать систему легко настраиваемой мы забываем об этом, оставляя излишнюю доступность клиентам класса. А как же тогда передавать данные между объектами? На этот случай Unity предоставляет следующий "арсенал":
- SerializeField;
- Поиск объекта на сцене.
Кроме того рассмотрим механизмы C# для передачи данных, которые тоже помогут нам соединить "заблудшие души":
- События;
- Singlton (крайняя мера извращения).
Используя эти способы мы построим в меру закрытую систему. Не забывайте, что мы рассматриваем сейчас контекст передачи и обмена данных между объектами внутри системы. Не внутри классов и объектов. Перейдем к примерам.
SerializeField
По умолчанию Unity сериализует все публичные поля. И да, привыкайте к этому термину. Но использование публичных полей без необходимости может привести к неконтролируемым изменениям - поле будет использоваться любым клиентом у которого есть деньги , имеющим доступ к компоненту. Поэтому мы заменим модификатор public на private и укажем перед ним атрибут [SerializeField]. Это заставит Unity сериализовать данное поле и позволить изменять его все так же из инспектора. Выглядит это так:
Отмечу, что Unity не захочет проводить данную операцию со статическими полями и свойствами. Еще одно условие - объект должен принадлежать одной из следующих групп:
- Все классы, унаследованные от UnityEngine.Object, такие как GameObject, Component, MonoBehaviour, Texture2D, AnimationClip;
- Базовые типы, вроде int, float, string, bool;
- Перечисления (enums);
- Структуры (structs);
- Списки и массивы, типы которых могут быть сериализуемы;
- Встроенные в Unity типы вроде Vector2, Vector3, Vector4, Quaternion, Matrix4x4, Color, Rect, LayerMask.
Нас в данном случае интересует пункт 1. Для передачи в инспекторе мы перетаскиваем нужный объект из иерархии в целевое поле. Движок автоматически извлечет нужный тип компонента с помощью метода GetComponent<T>(). Если указываемый объект не содержит компонент поля, задать этот объект не получится.
Это достаточно удобный способ создания прочных связей с объектом, который находится в одной сцене. Также это позволяет делать систему гибкой и настраиваемой для геймдизайнера. Но если объект, информацию которого мы хотим получить, передается из другой сцены, либо же создается в процессе игры, эффект от данной операции будет нулевым.
Поиск объекта на сцене
Класс GameObject имеет множество статических методов для поиска объектов. Объяснение принципа работы каждого будет по своей сути копипастом, оставляю задание по изучению Вам на дом. Здесь лишь перечислю их:
- public static GameObject[] FindGameObjectsWithTag(string tag);
Многие пользователи критикуют данный способ поиска объекта за высокую нагрузку и как следствие низкую производительность. Да, алгоритм проходит по всем объектам сцены, что действительно является расточительной операции. Но если речь идет о связывании объектов при инициализации (загрузке) сцены, о разовых случаях применения, я однозначно выберу его. Повторюсь, для разовых. Update'ы напрочь убьют fps, если там окажется один из этих методов.
А что с C# ?
Структура кода не всегда позволяет организовать простую передачу ссылки на объект или экземпляра объекта. Поэтому возвращаемся к старым и добрым стандартным инструментам языка.
События
Отличный способ оповестить других участников о совершении какого либо действия. Такой способ в частности хорошо показывает себя, когда имеется небольшой количество сенсоров (вплоть до одного), который должен при выполнении некоторого условия вызвать реакцию множества частиц, ожидающих от него некоторого поведения.
Пример из игры жанра Match 3. Нужен компонент, который при нахождении совпадения 3 клеток в ряд должен на некоторое время заблокировать игровое поле для действий пользователя. В это время должны срабатывать визуальные эффекты, а по окончании поле вновь должно быть открытым.
Компонент MatchCounter имеет два события OnMatch и OnMatchEnd, объявленных через стандартный делегат UnityAction. Для их использования нужно подключить пространство имен UnityEngine.Events. Этот делегат имеет следующее объявление: public delegate void UnityAction(). Собственно сами события:
Теперь займемся нашим компонентом FieldLocker. Он будет подписан на события MatchCounter, при вызове события OnMatch блокировать поле, OnMatchEnd - открывать снова.
Краткое пояснение. Panel - это обычный объект, на котором висит Image с прозрачностью цвета по каналу Альфа в 0f(полностью прозрачный). Размер задан по размеру игрового поля. Когда объект активен, он не позволяет нажимать на клетки игрового поля, тем самым блокируя его. Поэтому в обычном состоянии он выключен.
Таким образом мы получили данные о вызове события, не производя поиск по сцене. С другой стороны, нужно обеспечить подписку, а для этого объекты должны быть "знакомы". В таком случае можно было организовать инкапсуляцию поля FieldLocker в MatchCounter. Это один из вариантов решения. Тогда была опасность того, что обязанность по блокированию поля перейдет классу MatchCounter, что ставит независимость FieldLocker под угрозу и нарушает SRP(принцип единственной ответственности). Я предпочитаю разделить их на данном этапе, так как ожидаю, что и другому элементу системуы в будущем может понадобится доступ.
Singlton
Возможно в данный момент это решение будет для Вас единственно возможным. Это признак того, что допущена ошибка, исправлять которую не спешат. Имейте это ввиду. А пока опишу, как создать синглтон для компонента.
В Awake мы гарантируем, что объект будет создан в единственном экземпляре. Чтобы получить доступ к нему нужно обратиться к полю Instance. К примеру:
Score.Instance.AddPoints(3)
Это удобно лишь до некоторого времени, пока не зайдет речь о модификации и внедрении нового поведения в систему, связанного с этим классом. К тому же он становится слишком открытым, продавая свои услуги любому клиенту проекта. Я бы назвал это неоднозначным контрактом - когда класс не требует ничего в замен за предоставление информации. Советую использовать лишь для реализации прототипов и тестовых вещей.
Главным фактором, влияющим на прогресс проекта является как ни странно не качество кода и объектной модели. Осознанное создание будет вести вперед, к релизу. Не забывайте, для чего Вы читаете эту статью, берите только ту информацию, которая покажется полезной. Даже мысли, которые возникли, станут искрой для новых действий. Используйте все указанные методы таким образом, как посчитаете нужным и лишь тогда, когда Вам они покажутся не столько необходимыми, сколько понятными и привлекательными. Удачи в новых проектах!
#unity #unity3d #программирование #gamedev #программирование для начинающих