[Из песочницы] Простая система событий в Unity
Что это такое, и для чего это нужно? Рано или поздно любой проект Unity разрастается большим количеством скриптов и становится трудно держать в голове, какой скрипт с каким связан. С такой проблемой столкнулся и я. Через некоторое время вышел на публикацию «Методы организации взаимодействия между скриптами в Unity3D ». Меня сразу заинтересовал третий подход «Мировой эфир» (настоятельно рекомендую почитать, отлично показано, зачем это нужно). Он идеально мне подходит, но в той статье указан сложный вариант и я, не обладая большими знаниями программирования, так и не смог понять его. В комментариях заметил упоминания встроенной в язык системы событий. Погуглив, нашел статью про события в C#. В этом посте я хочу рассказать, как подружить unity и систему событий C#, чтобы уберечь форумы и unity answers от похожих вопросов.Суть Пусть у нас есть игра — шутер. При нажатии на кнопку прицеливания игрок переходит на шаг, включается камера прицела, выполняется сотня других действий. А при нажатии кнопки стрельбы — создание пули, просчет полета и т.д. Все это выльется в огромную кашу с кучей связей и программировать это все станет очень тяжело (см. картинку в первой статье).Другой важный момент — для разных платформ управление будет сильно различаться и придется править кучу скриптов, содержащих в себе ввод.События предлагают куда более элегантный и удобный подход, см. картинку выше. Схема приобретет такой вид:
InputAggregator слушает ввод и как только игрок нажал клавишу/тапнул, говорит в эфир: «Игрок нажал кнопку Х!». Об этом сразу узнают скрипты, подписанные на это событие (а точнее методы в этих скриптах) и выполняются указанные методы. Причем эти методы могут вызываться разными событиями, например, метод «Умереть» вызывается как при получении критического урона, так и, к примеру, при выходу за границы уровня.
Обратите внимание, что нам не важно, какую клавишу нажал игрок, важно лишь то, что произошло событие. Т.е. для подгонки управления для другой платформы нам не надо лезть в 10 скриптов и все их править, достаточно будет модифицировать InputAggregator.
Зачем нужен EventController — смотрите ниже.
Как это реализовать? Забудем про шутер и пули, давайте поставим себе простую задачу: Есть две сферы:
Мы хотим при нажатии кнопки Space сдвинуть левую сферу вверх, а нижнюю — вниз. И если какая-то из них уходит слишком далеко, вернуть их обратно.
Но для начала сделаем только первую часть, без преодоления границ:
Перво — наперво создадим скрипты сфер, содержащие методы телепортов и повесим их на сферы:
public class Sphere1T: MonoBehaviour { public void TeleportUp () { transform.Translate (Vector3.up); } }
Аналогично для другой сферы.Создадим скрипт EventController и повесим его куда-нибудь (я повесил на камеру).В нем создадим делегат MethodContainer ():
public delegate void MethodContainer (); Если очень грубо, делегаты — это указатели на методы или контейнеры методов. Для событий делегат используется как тип и с ним самим ничего делать не надо, поэтому особо вникать в механику делегатов необязательно (но знания лишними не бывают). Важно только знать, что к делегату подходят только методы, соответствующие его сигнатуре. В нашем случае это методы, не возвращающие значений (void) и не имеющие параметров (). Соответственно и методы, вызываемые нашими событиями, должны иметь такой вид.Ну и создадим в нем линки на наши сферы, в редакторе не забываем указать, какой линк к чему относится (вы должны уже уметь это делать, если читаете эту статью), они нам понадобятся.
public Sphere1T s1t; public Sphere2T s2t; Теперь создадим скрипт InputAggregator. Создадим в нем событие типа MethodContainer, вызывающее телепорт сфер:
public static event EventController.MethodContainer OnTeleportEvent; Т.е. наш делегат мы указываем в качестве типа. Обратите внимание, что создавать линк на EventController не нужно.В методе Update () вызовем наше событие: void Update () { if (Input.GetKeyDown («space»)) OnTeleportEvent (); } Все, теперь если мы нажмем пробел, то активируется событие OnTeleportEvent. Осталось только указать, какие методы он должен вызывать.Вернемся в EventController и в методе Awake () (вызывается при старте игры, похож на Start ()) подпишем на наше событие методы TeleportUp () и TeleportDown (). Делается это при помощи операции инкремента, мы как бы прибавляем к событию методы:
void Awake () { InputAggregator.OnTeleportEvent += s1t.TeleportUp; //Обратите внимание, линк на класс (скрипт), содержащий событие, делать не нужно! InputAggregator.OnTeleportEvent += s2t.TeleportDown; } Готово! Теперь при нажатии пробела вызывается событие OnTeleportEvent, которое вызывает два метода в скриптах на сферах.Теперь попробуем сделать обработку границ.
В скрипты обеих сфер добавим событие выхода за границы (по одному на каждую):
public static event EventController.MethodContainer OnAbroadLeft; public static event EventController.MethodContainer OnAbroadRight; В методах телепортов вызываем событие: if (transform.position.y > 3) OnAbroadLeft (); //Для правой < -3 И добавим методы телепорта назад (можно назвать одними именами): public void ResetPosit() { transform.position = new Vector3(-2, 0, 0); //Для правой сферы (2,0,0) } А в Awake() EventController'a подписываемся:
Sphere1T.OnAbroadLeft += s1t.ResetPosit; Sphere2T.OnAbroadRight += s2t.ResetPosit; Все, теперь наши сферы исправно двигаются и не убегают далеко.Еще раз, полный код всех скриптов:
public class InputAggregator: MonoBehaviour { public static event EventController.MethodContainer OnTeleportEvent;
void Update () { if (Input.GetKeyDown («space»)) OnTeleportEvent (); } } public class EventController: MonoBehaviour { public delegate void MethodContainer ();
public Sphere1T s1t; public Sphere2T s2t; void Awake () { InputAggregator.OnTeleportEvent += s1t.TeleportUp; InputAggregator.OnTeleportEvent += s2t.TeleportDown;
Sphere1T.OnAbroadLeft += s1t.ResetPosit; Sphere2T.OnAbroadRight += s2t.ResetPosit; } } public class Sphere1T: MonoBehaviour { public static event EventController.MethodContainer OnAbroadLeft;
public void TeleportUp () { transform.Translate (Vector3.up);
if (transform.position.y > 3) OnAbroadLeft (); }
public void ResetPosit () { transform.position = new Vector3(-2, 0, 0); } } public class Sphere2T: MonoBehaviour { public static event EventController.MethodContainer OnAbroadRight;
public void TeleportDown () { transform.Translate (Vector3.down);
if (transform.position.y < -3) OnAbroadRight(); }
public void ResetPosit () { transform.position = new Vector3(2, 0, 0); } } ЗаключениеЭто был простейший пример. Разумеется, для двух сфер воротить события абсолютно не нужно, но когда у вас в проекте больше двадцати скриптов, события приходят на помощь. Вот как мне, например:
InputAggregator_script.OnTrajectoryCall_event += trajectoryScript.DrawTrajectory; InputAggregator_script.OnTrajectoryCall_event += destinationGUIScript.DrawDestinationText; InputAggregator_script.OnTrajectoryCall_event += playerMovScript.RestartMov;
InputAggregator_script.OnTrajectoryClean_event += trajectoryScript.CleanTrajectory; InputAggregator_script.OnTrajectoryClean_event += destinationGUIScript.DisableDestinationText;
InputAggregator_script.OnTurnSwitch_event += WorldState_class.ChangeDate; InputAggregator_script.OnTurnSwitch_event += playerMovScript.Mov; InputAggregator_script.OnTurnSwitch_event += dateGUIScript.UpdateDateBar; Это только начало, представляете, как было бы сложно делать подобное для десятков скриптов без событий? А тут все аккуратно, видно, что на что подписано.Надеюсь, статья была полезной, буду рад комментариям и замечаниям. Большое спасибо авторам двух указанных мною в начале статей, без них я сам бы не разобрался.
Удачи вам в ваших проектах!
P.S. Небольшое дополнениеУ вас наверняка возник вопрос, как вызвать событие из другого скрипта, ведь если попытаться сделать что-нибудь вроде
InputAggregator_script.OnTurnSwitch_event (); то компилятор выдаст ошибку. Это ограничение можно обойти, если в скрипте, в котором описано событие, создать метод, вызывающий это событие, например public void CallOnTurnSwitchEvent () { OnTurnSwitch_event (); } И тогда вызвать это событие из другого класса можно будет банальным public InputAggregator link;
void SomeMethod () { link.CallOnTurnSwitchEvent (); } Немного массивно по синтаксису, но неудобств почти не добавляет.