EventBus - Sistema de Eventos para Unity

Neste artigo, explicarei como é o sistema de eventos em relação ao Unity. Vamos estudar métodos populares e analisar em detalhes a implementação em interfaces, que conheci enquanto trabalhava na Owlcat Games.





Conteúdo



  1. O que é um sistema de eventos?
  2. Implementações existentes

    2.1. Assinatura de chave

    2.2. Inscrição por Tipo de Evento

    2.3. Assinatura por tipo de assinante


  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.



. . .



Nossa solução se diferencia pelo uso de interfaces. Se você pensar um pouco, o uso de interfaces no sistema de eventos é muito lógico. Afinal, as interfaces foram originalmente inventadas para definir as capacidades de um objeto. No nosso caso, estamos falando sobre a capacidade de reagir a certos eventos do jogo.



No futuro, o sistema pode ser desenvolvido para um projeto específico. Por exemplo, em nosso jogo existem inscrições para os eventos de uma unidade específica. Outra chamada e conclusão de algum evento mecânico.



O link não é um repositório.




All Articles