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
- Qu'est-ce qu'un système d'événements?
- Implémentations existantes
2.1. Abonnement clé
2.2. Abonnement par type d'événement
2.3. Abonnement par type d'abonné -
3.1.
3.2.
3.3. -
4.1.
4.2.
4.3.
1. ?
: UI, , , . :
- . .
- . .
- . .
, . . , . , .
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.