Broadcast Event Messaging в Unity3D

    При разработке игр, довольно часто возникает необходимость в построении системы широковещательной рассылки сообщений. Предположим, Вы хотите сделать так, чтобы в тот момент когда персонаж, управляемый игроком, вошел в определенную зону, или выполнил определенное действие, все заинтересованные в этом объекты получили уведомление. По возможности это уведомление должно нести в себе информацию о произошедшем событии. В данной статье, я предлагаю Вашему вниманию один из возможных способов построения подобной системы. Приведенная система построена на базе Unity3D EventSystem.

    Начиная с версии 4.6 в состав Unity3D включена UI System, значительно упрощающая процесс создания UI. К тому же она является Open Source проектом. В основе этой системы лежат два очень важных компонента — EventSystem и InputModules, позволяющие принимать и обрабатывать события. По сути, InputModules — это обычные компоненты. Они являются наследниками UIBehaviour который в свою очередь наследует MonoBehaviour и содержат в себе логику обработки событий, поступающих от EventSystem.
    Отправка события определенному GameObject осуществляется посредством вызова метода ExecuteEvents.Execute(). Определение метода имеет следующий вид:

    public static bool Execute<T>(GameObject target, BaseEventData data, EventFunction<T> functor) where T : IEventSystemHandler;
    

    Вызывая этот метод, в качестве параметров Вы должны передать ссылку на GameObject в списке ассоциированных компонентов которого, должен присутствовать InpuModule реализующий интерфейс T либо интерфейс-наследник T. Если таких компонентов окажется несколько, все они будут вызваны по очереди.
    Как видно, для отправки события необходимо иметь ссылку на целевой GameObject, что не подходит для широковещательной рассылки.
    Решением этой проблемы может послужить коллекция содержащая список GameObjects с прикрепленными InputModules способными обрабатывать широковещательные события.

    public abstract class BroadcastInputModule<TEventType> : BaseInputModule
        where TEventType : IEventSystemHandler
    {
        protected override void Awake()
        {
            base.Awake();
            BroadcastReceivers.RegisterBroadcastReceiver<TEventType>(gameObject);
        }
    
        protected override void OnDestroy()
        {
            base.OnDestroy();
            BroadcastReceivers.UnregisterBroadcastReceiver<TEventType>(gameObject);
        }
    }
    

    Класс BroadcastInputModule служит базовым для модулей обработчиков широковещательных событий. Его основной задачей является регистрация модуля в этой коллекции.
    Давайте перейдем к созданию модуля, который будет реагировать на глобальное событие, которое условно назовём "SomethingHappened".

    [AddComponentMenu("Event/Something Happened Input Module")]
    public class SomethingHappenedInputModule
        : BroadcastInputModule<ISomethingHappenedEventHandler>, ISomethingHappenedEventHandler
    {
        public void OnSomethigHappened(SomethingHappenedEventData data)
        {
            Debug.Log("SomethingHappenedInputModule::OnSomethigHappened()");
        }
    
        public override void Process()
        {
        }
    }
    

    Этот компонент должен быть добавлен ко всем GameObjects заинтересованным в получении события SomethingHappened. Интерфейс ISomethingHappenedEventHandler выглядит следующим образом:

    public interface ISomethingHappenedEventHandler : IEventSystemHandler
    {
        void OnSomethigHappened(SomethingHappenedEventData data);
    }
    

    Коллекция хранящая обработчики может быть довольно простой, например такой:

    public static class BroadcastReceivers
    {
        private static readonly IDictionary<Type, IList<GameObject>>
            BroadcstReceivers = new Dictionary<Type, IList<GameObject>>();
    
        public static IList<GameObject> GetHandlersForEvent<TEventType>()
            where TEventType : IEventSystemHandler
        {
            if (!BroadcstReceivers.ContainsKey(typeof (TEventType)))
            {
                return null;
            }
            return BroadcstReceivers[typeof (TEventType)];
        }
    
        public static void RegisterBroadcastReceiver<TEventType>(GameObject go)
            where TEventType : IEventSystemHandler
        {
            if (BroadcstReceivers.ContainsKey(typeof(TEventType)))
            {
                BroadcstReceivers[typeof(TEventType)].Add(go);
            }
            else
            {
                BroadcstReceivers.Add(typeof(TEventType), new List<GameObject>());
                BroadcstReceivers[typeof(TEventType)].Add(go);
            }
        }
    
        public static void UnregisterBroadcastReceiver<TEventType>(GameObject go)
        { . . . }    
    }
    

    В реальной игре, возможно, эта коллекция должна содержать WeakReferences вместо прямых ссылок на GameObjects, это уже зависит от требований.
    Последним элементом является класс BroadcastExecuteEvents:

    public static class BroadcastExecuteEvents
    {
        public static void Execute<T>(BaseEventData eventData, ExecuteEvents.EventFunction<T> functor) 
            where T : IEventSystemHandler
        {
            var handlers = BroadcastReceivers.GetHandlersForEvent<T>();
            if (handlers == null) return;
            foreach (var handler in handlers)
            {
                ExecuteEvents.Execute<T>(handler, eventData, functor);
            }
        }
    }
    

    Как видно из его определения, он является всего лишь обёрткой над ExecuteEvents и выполняет всего одну задачу — выбор подходящих обработчиков для указанного события и их вызов.

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

    BroadcastExecuteEvents.Execute<ISomethingHappenedEventHandler>(null, (i, d) => i.OnSomethigHappened(new SomethingHappenedEventData()));
    


    Исходный код к статье можно забрать с github.com/rumyancevpavel/BroadcastMessaging
    • +9
    • 16.2k
    • 4
    Share post

    Comments 4

      +5
      А чем это лучше старых добрых делегатов?
        +1
        Это не лучше и не хуже. Это просто немного другой подход, дающий возможность отказаться от использования стандартных .NET-событий, которые могут привести к серьёзным проблемам с утечкой памяти. А так же, это более консистентно вписывается в EventSystem поставляемую с Unity UI. К дополнительным преимуществам этого способа можно отнести возможность работы с обработчиками событий как с обычными компонентами Unity(например, всегда можно получить ссылку на обработчик через GetComponent<BroadcastInputModule>()).
        +1
        1. Если я правильно понимаю lifecycle объектов в Unity, то вызов Unregister... в методе OnDestroy позволяет не беспокоиться об утечках памяти на подписках на события и не использовать WeakReference.

        2. Хорошей практикой считается использование в игре пулла игровых объектов. Т.е. в игре с пуллом OnDestroy не будет вызываться для большинства объектов. Соответственно, нужен другой механизм регистрации и отписки. Например, вызывать Unregister... перед возвратом объекта в пулл.
          0
          1. Все верно.
          2. UI System и, в частности, ExecuteEvents.Execute устроен так, что не обрабатывает неактивные объекты (которые отсутствуют в иерархии сцены, свойство enabled=false и те для которых был вызван OnDisable). Недостаток приведенной реализации в том что при вызове BroadcastExecuteEvents.Execute в цикле foreach ExecuteEvents.Execute будет вызван даже для неактивных объектов, но сам ExecuteEvents.Execute такие объекты проигнорирует.
          Это можно решить изменив коллекцию с подписками. Регистрировать не GameObjects, а UIBehaviours, ибо в классе UIBehaviour определен метод IsActive() который можно использовать для избежания лишних вызовов.

        Only users with full accounts can post comments. Log in, please.