EventBus - Système d'événements pour Unity

Dans cet article, je vais vous expliquer ce qu'est un système d'événements par rapport à Unity. Étudions les méthodes populaires et analysons en détail l'implémentation sur les interfaces, que j'ai rencontrée en travaillant chez Owlcat Games.





Contenu



  1. Qu'est-ce qu'un système d'événements?
  2. Implémentations existantes

    2.1. Abonnement clé

    2.2. Abonnement par type d'événement

    2.3. Abonnement par type d'abonné


  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 . -, . : IQiuckSaveHandler — SaveLoadManager .



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.



. . .



Notre solution se distingue par l'utilisation d'interfaces. Si vous y réfléchissez un peu, l'utilisation des interfaces dans le système d'événements est très logique. Après tout, les interfaces ont été inventées à l'origine pour définir les capacités d'un objet. Dans notre cas, nous parlons de la capacité de réagir à certains événements du jeu.



À l'avenir, le système pourra être développé pour un projet spécifique. Par exemple, dans notre jeu, il y a des abonnements aux événements d'une unité spécifique. Un autre appel et achèvement d'un événement mécanique.



Le lien n'est pas un référentiel.




All Articles