EventBus: sistema de eventos para Unity

En este artículo, lo guiaré a través de lo que es el sistema de eventos en relación con Unity. Estudiemos métodos populares y analicemos en detalle la implementación en interfaces, que conocí mientras trabajaba en Owlcat Games.





Contenido



  1. ¿Qué es un sistema de eventos?
  2. Implementaciones existentes

    2.1. Suscripción clave

    2.2. Suscripción por tipo de evento

    2.3. Suscripción por tipo de suscriptor


  3. 3.1.

    3.2.

    3.3.


  4. 4.1.

    4.2.

    4.3.


1. ?



: UI, , , . :



  1. . .
  2. . .
  3. . .


, . . , . , .



public class InputManager : MonoBehavioiur
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            EventSystem.RaiseEvent("quick-save");
        }
    }
}

public class SaveLoadManager : Monobehaviour
{
    private void OnEnable()
    {
        EventSystem.Subscribe("quick-save", QuickSave);
    }

    private void OnDisable()
    {
        EventSystem.Unsubscribe("quick-save", QuickSave);
    }

    private void QuickSave()
    {
        //  
        ...
    }
}


SaveLoadManager.OnEnable() QuickSave "quick-save". , EventSystem.RaiseEvent("quick-save") SaveLoadManager.QuickSave() . , null reference exception .



. , .



— , . . — .



2.



:



// 
EventSystem.Subscribe(_, _);

// 
EventSystem.RaiseEvent(_, );


, .



2.1.



_ Enum. — IDE, . . params object[] args. IDE .



// 
EventSystem.Subscribe("get-damage", OnPlayerGotDamage);

// 
EventSystem.RaiseEvent("get-damage", player, 10);

//  
void OnPlayerGotDamage(params object[] args)
{
    Player player = args[0] as Player;
    int damage = args[1] as int;
    ...
}


2.2.



, .



// 
EventSystem.Subscribe<GetDamageEvent>(OnPlayerGotDamage);

// 
EventSystem.RaiseEvent<GetDamageEvent>(new GetDamageEvent(player, 10));

//  
void OnPlayerGotDamage(GetDamageEvent evt)
{
    Player player = evt.Player;
    int damage = evt.Damage;
    Debug.Log($"{Player} got damage {damage}");
}


2.3.



. , . , .



public class UILog : MonoBehaviour, IPlayerDamageHandler
{
    void Start()
    {
        // 
        EventSystem.Subscribe(this);
    }

    //  
    public void HandlePlayerDamage(Player player, int damage)
    {
        Debug.Log($"{Player} got damage {damage}");
    }
}

// 
EventSystem.RaiseEvent<IPlayerDamageHandler>(h =>
    h.HandlePlayerDamage(player, damage));


3.



. , . " ".



3.1.



, , .



. , :



public interface IQiuckSaveHandler : IGlobalSubscriber
{
    void HandleQuickSave();
}


, , IGlobalSubscriber. - , . IGlobalSubscriber , .



:



public class SaveLoadManager : Monobehaviour, IQiuckSaveHandler
{
    private void OnEnable()
    {
        EventBus.Subscribe(this);
    }

    private void OnDisable()
    {
        EventBus.Unsubscribe(this);
    }

    private void HandleQuickSave()
    {
        //  
        ...
    }
}


Subscribe.



public static class EventBus
{
    private static Dictionary<Type, List<IGlobalSubscriber>> s_Subscribers
        = new Dictionary<Type, List<IGlobalSubscriber>>();

    public static void Subscribe(IGlobalSubscriber subscriber)
    {
        List<Type> subscriberTypes = GetSubscriberTypes(subscriber.GetType());
        foreach (Type t in subscriberTypes)
        {
            if (!s_Subscribers.ContainsKey(t))
                s_Subscribers[t] = new List<IGlobalSubscriber>();
            s_Subscribers[t].Add(subcriber);
        }
    }
}


s_Subscribers. , .



GetSubscriberTypes . -, . : IQiuckSaveHandlerSaveLoadManager .



subscriberTypes. s_Subscribers .



GetSubscribersTypes:



public static List<Type> GetSubscribersTypes(IGlobalSubscriber globalSubscriber)
{
    Type type = globalSubscriber.GetType();
    List<Type> subscriberTypes = type
        .GetInterfaces()
        .Where(it =>
                it.Implements<IGlobalSubscriber>() &&
                it != typeof(IGlobalSubscriber))
        .ToList();
    return subscriberTypes;
}


, , IGlobalSubscriber. , .



, EventBus , .



3.2.



, . InputManager 'S', .



:



public class InputManager : MonoBehavioiur
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            EventBus.RaiseEvent<IQiuckSaveHandler>(
                IQiuckSaveHandler handler => handler.HandleQuickSave());
        }
    }
}


RaiseEvent:



public static class EventBus
{
    public static void RaiseEvent<TSubscriber>(Action<TSubscriber> action)
    where TSubscriber : IGlobalSubscriber
    {
        List<IGlobalSubscriber> subscribers = s_Subscribers[typeof(TSubscriber)];
        foreach (IGlobalSubscriber subscriber in subscribers)
        {
            action.Invoke(subscriber as TSubscriber);
        }
    }
}


TSubscriber IQiuckSaveHandler. IQiuckSaveHandler handler => handler.HandleQuickSave() action, IQiuckSaveHandler. action HandleQuickSave .



IQiuckSaveHandler handler => handler.HandleQuickSave() C# h => h.HandleQuickSave().



, .



3.3.



. :



public interface IQuickSaveLoadHandler : IGlobalSubscriber
{
    void HandleQuickSave();
    void HandleQuickLoad();
}


, , .



, - . 1 . .



public interface IUnitDeathHandler : IGlobalSubscriber
{
    void HandleUnitDeath(Unit deadUnit, Unit killer);
}

public class UILog : IUnitDeathHandler
{
    public void HandleUnitDeath(Unit deadUnit, Unit killer)
    {
        Debug.Log(killer.name + " killed " + deadUnit.name);
    }
}

public class Unit 
{
    private int m_Health

    public void GetDamage(Unit damageDealer, int damage)
    {
        m_Health -= damage;
        if (m_Health <= 0)
        {
            EventBus.RaiseEvent<IQiuckSaveHandler>(h =>
                h.HandleUnitDeath(this, damageDealer));
        }
    }
}


.



4.



, , .



4.1.



. , try catch:



public static void RaiseEvent<TSubscriber>(Action<TSubscriber> action)
where TSubscriber : IGlobalSubscriber
{
    List<IGlobalSubscriber> subscribers = s_Subscribers[typeof(TSubscriber)];
    foreach (IGlobalSubscriber subscriber in subscribers)
    {
        try
        {
            action.Invoke(subscriber as TSubscriber);
        }
        catch (Exception e)
        {
            Debug.LogError(e);
        }
    }
}


4.2.



GetSubscribersTypes , . , .



private static Dictionary<Type, List<Types>> s_CashedSubscriberTypes = 
    new Dictionary<Type, List<Types>>()

public static List<Type> GetSubscribersTypes(
    IGlobalSubscriber globalSubscriber)
{
    Type type = globalSubscriber.GetType();
    if (s_CashedSubscriberTypes.ContainsKey(type))
        return s_CashedSubscriberTypes[type];

    List<Type> subscriberTypes = type
        .GetInterfaces()
        .Where(it =>
                it.Implements<IGlobalSubsriber>() &&
                it != typeof(IGlobalSubsriber))
        .ToList();

    s_CashedSubscriberTypes[type] = subscriberTypes;
    return subscriberTypes;
}


4.3.



, - :



public static void Unsubscribe(IGlobalSubsriber subcriber)
{
    List<Types> subscriberTypes = GetSubscriberTypes(subscriber.GetType());
    foreach (Type t in subscriberTypes)
    {
        if (s_Subscribers.ContainsKey(t))
            s_Subscribers[t].Remove(subcriber);
    }
}


.



Collection was modified; enumeration operation might not execute.



, - foreach .



foreach (var a in collection)
{
    if (a.IsBad())
    {
        collection.Remove(a); //  
    }
}


, .



, . , , . , , null. .



public class SubscribersList<TSubscriber> where TSubscriber : class
{
    private bool m_NeedsCleanUp = false;

    public bool Executing;

    public readonly List<TSubscriber> List = new List<TSubscriber>();

    public void Add(TSubscriber subscriber)
    {
        List.Add(subscriber);
    }

    public void Remove(TSubscriber subscriber)
    {
        if (Executing)
        {
            var i = List.IndexOf(subscriber);
            if (i >= 0)
            {
                m_NeedsCleanUp = true;
                List[i] = null;
            }
        }
        else
        {
            List.Remove(subscriber);
        }
    }

    public void Cleanup()
    {
        if (!m_NeedsCleanUp)
        {
            return;
        }

        List.RemoveAll(s => s == null);
        m_NeedsCleanUp = false;
    }
}


EventBus:



public static class EventBus
{
    private static Dictionary<Type, SubscribersList<IGlobalSubcriber>> s_Subscribers
        = new Dictionary<Type, SubscribersList<IGlobalSubcriber>>();
}


RaiseEvent:



public static void RaiseEvent<TSubscriber>(Action<TSubscriber> action)
where TSubscriber : IGlobalSubscriber
{
    SubscribersList<IGlobalSubscriber> subscribers = s_Subscribers[typeof(TSubscriber)];

    subscribers.Executing = true;
    foreach (IGlobalSubscriber subscriber in subscribers.List)
    {
        try
        {
            action.Invoke(subscriber as TSubscriber);
        }
        catch (Exception e)
        {
            Debug.LogError(e);
        }
    }
    subscribers.Executing = false;
    subscribers.Cleanup();
}


, . , , . , . , .



5.



. . .



Nuestra solución se distingue por el uso de interfaces. Si lo piensa un poco, el uso de interfaces en el sistema de eventos es muy lógico. Después de todo, las interfaces se inventaron originalmente para definir las capacidades de un objeto. En nuestro caso, estamos hablando de la capacidad de reaccionar ante determinados eventos del juego.



En el futuro, el sistema se puede desarrollar para un proyecto específico. Por ejemplo, en nuestro juego, hay suscripciones a los eventos de una unidad en particular. Otra llamada y finalización de algún evento mecánico.



El enlace no es un repositorio.




All Articles