From a86a0fd807b74b6ce7a7cd6253e9f5323c6bc76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9tur=20Darri?= <11541598+PeturDarri@users.noreply.github.com> Date: Sun, 17 Oct 2021 14:17:16 +0000 Subject: [PATCH] Updated XmlDoc comments and README. --- README.md | 166 ++++++++++++++++++++++++++++- Runtime/GenericEventBus.cs | 23 +++- Runtime/TargetedGenericEventBus.cs | 64 +++++++++++ 3 files changed, 246 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index afde9c5..41df011 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # Generic Event Bus -A synchronous event bus for Unity, using strictly typed events and generics to reduce runtime overhead. +A synchronous event bus for Unity written in C#, using strictly typed events and generics to reduce runtime overhead. ## Features * Events are defined as types, instead of as members in some class or as string IDs. -* Generics are used to move runtime overhead to compile time. There's no `Dictionary`. -* Listeners can include a `priority` number when subscribing to an event to control their order in the event execution, regardless of _when_ the listener subscribes. +* Generics are used to move runtime overhead to compile time. _(There's no `Dictionary`)_ +* Listeners can include a [priority](#priority) number when subscribing to an event to control their order in the event execution, regardless of _when_ the listener subscribes. +* Built-in support for [targeting events](#targeted-events) to specific objects, with an optional source object that raised the event. +* Event data can be [modified by listeners](#modifying-event-data), or completely [consumed](#consuming-events) to stop it. +* Events can be queued if other events are currently being raised. ## Usage To create an event bus, use the `GenericEventBus` type: @@ -72,6 +75,7 @@ private void OnGameStartedEvent(ref GameStartedEvent eventData) Debug.Log($"Game started with {eventData.NumberOfPlayers} player(s)"); } ``` +### Priority You can also include a `float priority` argument when calling `SubscribeTo`. Subscribing to an event with a high priority means you'll receive the event before other listeners that have a lower priority. This is great for defining the order of listeners without having to worry about _when_ each listener subscribes to the event. ```c# private void OnEnable() @@ -96,4 +100,158 @@ private void OnGameStartedEventPriority(ref GameStartedEvent eventData) Debug.Log("This will be invoked first, even though it was added last!"); } ``` -The default `priority` is `0` and listeners with the same priority will be invoked in the order they were added. \ No newline at end of file +The default `priority` is `0` and listeners with the same priority will be invoked in the order they were added. + +### Targeted events +Things get a lot more interesting when using targeted events. You can think of this more like a message bus, where objects can raise events that are meant to be heard by specific target object. + +To use targeted events, you must include a second generic type parameter in `GenericEventBus` to specify what type of object can be a target, like `GameObject`: +```c# +var eventBus = new GenericEventBus(); +``` + +You get all the same methods in this event bus as in the other one, so you can still raise non-targeted events, but now you can include a target and source object with raised events: +```c# +eventBus.Raise(new DamagedEvent { Damage = 10f }, targetGameObject, sourceGameObject); +``` +In this example, `DamagedEvent` is defined just like any other event: +```c# +public struct DamagedEvent : IEvent +{ + public float Damage; +} +``` +--- +To listen to this event, use the `SubscribeToTarget` method: +```c# +private float health = 100f; + +private void OnEnable() +{ + eventBus.SubscribeToTarget(gameObject, OnDamagedEvent); +} + +private void OnDisable() +{ + eventBus.UnsubscribeFromTargetgameObject, OnDamagedEvent); +} + +private void OnDamagedEvent(ref DamagedEvent eventData, GameObject target, GameObject source) +{ + health -= eventData.Damage; + + Debug.Log($"{target} received {eventData.Damage} damage from {source}"); +} +``` +--- +This pattern allows you to have objects communicate with each other in a very decoupled way. If no one is listening to the target object, the event is ignored. + +Another benefit from this pattern is that now you have an event of when objects are damaged, which any script can listen to. + +For example, if you wanted to have some UI showing damage numbers on anything the player damages, you could do that like this: +```c# +private void OnEnable() +{ + eventBus.SubscribeToSource(playerObject, OnPlayerInflictedDamageEvent); +} + +private void OnDisable() +{ + eventBus.UnsubscribeFromSource(playerObject, OnPlayerInflictedDamageEvent); +} + +private void OnPlayerInflictedDamageEvent(ref DamagedEvent eventData, GameObject target, GameObject source) +{ + SpawnDamageNumberOn(target, eventData.Damage); +} +``` +--- +And any listeners that don't specify a target or source will simply get all events, regardless of the target or source. Perfect for something like a kill feed UI: +```c# +public struct KilledEvent : IEvent +{ + public IWeapon Weapon; +} + +private void OnEnable() +{ + eventBus.SubscribeTo(OnKilledEvent); +} + +private void OnDisable() +{ + eventBus.UnsubscribeFrom(OnKilledEvent); +} + +private void OnKilledEvent(ref KilledEvent eventData, GameObject target, GameObject source) +{ + Debug.Log($"{source} killed {target} with {eventData.Weapon}!"); +} +``` +--- +### Modifying event data +Listeners can modify the event data they receive, so listeners afterwards will receive the modified data. This can be extremely useful for implementing features like damage type resistance/weakness: +```c# +public enum DamageType +{ + Bludgeoning, + Fire, + Cold +} + +public struct DamagedEvent : IEvent +{ + public DamageType Type; + public float Amount; +} +``` +```c# +[SerializeField] +private DamageType resistanceType; + +private void OnEnable() +{ + // Subscribe to the damage event targeting this game object with a higher priority than default. + eventBus.SubscribeToTarget(gameObject, OnDamagedEvent, 100f); +} + +private void OnDisable() +{ + eventBus.UnsubscribeFromTarget(gameObject, OnDamagedEvent); +} + +private void OnDamagedEvent(ref DamagedEvent eventData) +{ + // If we are resistant to this damage type, halve the damage. + if (eventData.Type == resistanceType) + { + eventData.Amount *= 0.5f; + } +} +``` +#### Consuming events +You can also stop the event completely using `ConsumeCurrentEvent()`. This can be used to implement a quick god mode script that's completely decoupled from the rest of the health/damage scripts: +```c# +[SerializeField] +private bool godMode; + +private void OnEnable() +{ + // Subscribe to the damage event targeting this game object with a higher priority than default. + eventBus.SubscribeToTarget(gameObject, OnDamagedEvent, 100f); +} + +private void OnDisable() +{ + eventBus.UnsubscribeFromTarget(gameObject, OnDamagedEvent); +} + +private void OnDamagedEvent(ref DamagedEvent eventData) +{ + // If we're in god mode, consume the event. + if (godMode) + { + eventBus.ConsumeCurrentEvent(); + } +} +``` \ No newline at end of file diff --git a/Runtime/GenericEventBus.cs b/Runtime/GenericEventBus.cs index 16a6d6e..8388185 100644 --- a/Runtime/GenericEventBus.cs +++ b/Runtime/GenericEventBus.cs @@ -26,9 +26,15 @@ static GenericEventBus() private uint _currentRaiseRecursionDepth; + /// + /// Has the current raised event been consumed? + /// public bool CurrentEventIsConsumed => _currentRaiseRecursionDepth > 0 && _raiseRecursionsConsumed.Contains(_currentRaiseRecursionDepth); + /// + /// Is an event currently being raised? + /// public bool IsEventBeingRaised => _currentRaiseRecursionDepth > 0; /// @@ -39,20 +45,22 @@ static GenericEventBus() public delegate void EventHandler(ref TEvent eventData) where TEvent : TBaseEvent; /// - /// Raises the given event immediately, regardless if another event is currently still being raised. + /// Raises the given event immediately, regardless if another event is currently still being raised. /// /// The event to raise. /// The type of event to raise. + /// Returns true if the event was consumed with . public bool RaiseImmediately(TEvent @event) where TEvent : TBaseEvent { return RaiseImmediately(ref @event); } /// - /// Raises the given event immediately, regardless if another event is currently still being raised. + /// Raises the given event immediately, regardless if another event is currently still being raised. /// /// The event to raise. /// The type of event to raise. + /// Returns true if the event was consumed with . public virtual bool RaiseImmediately(ref TEvent @event) where TEvent : TBaseEvent { var wasConsumed = false; @@ -94,10 +102,11 @@ public virtual bool RaiseImmediately(ref TEvent @event) where TEvent : T } /// - /// Raises the given event. If there are other events currently being raised, this event will be raised after those events finish. + /// Raises the given event. If there are other events currently being raised, this event will be raised after those events finish. /// /// The event to raise. /// The type of event to raise. + /// If the event was raised immediately, returns true if the event was consumed with . public virtual bool Raise(in TEvent @event) where TEvent : TBaseEvent { if (!IsEventBeingRaised) @@ -136,6 +145,9 @@ public virtual void UnsubscribeFrom(EventHandler handler) where listeners.RemoveListener(handler); } + /// + /// Consumes the current event being raised, which stops the propagation to other listeners. + /// public void ConsumeCurrentEvent() { if (_currentRaiseRecursionDepth == 0) return; @@ -146,6 +158,11 @@ public void ConsumeCurrentEvent() } } + /// + /// Removes all the listeners of the given event type. + /// + /// The event type. + /// Thrown if an event is currently being raised. public void ClearListeners() where TEvent : TBaseEvent { if (IsEventBeingRaised) diff --git a/Runtime/TargetedGenericEventBus.cs b/Runtime/TargetedGenericEventBus.cs index 29cfaee..2a8dce3 100644 --- a/Runtime/TargetedGenericEventBus.cs +++ b/Runtime/TargetedGenericEventBus.cs @@ -36,6 +36,14 @@ public override bool Raise(in TEvent @event) return Raise(@event, DefaultObject, DefaultObject); } + /// + /// Raises the given event. If there are other events currently being raised, this event will be raised after those events finish. + /// + /// The event to raise. + /// The target object for this event. + /// The source object for this event. + /// The type of event to raise. + /// If the event was raised immediately, returns true if the event was consumed with . public bool Raise(TEvent @event, TObject target, TObject source) where TEvent : TBaseEvent { if (!IsEventBeingRaised) @@ -54,11 +62,27 @@ public override bool RaiseImmediately(ref TEvent @event) return RaiseImmediately(ref @event, DefaultObject, DefaultObject); } + /// + /// Raises the given event immediately, regardless if another event is currently still being raised. + /// + /// The event to raise. + /// The target object for this event. + /// The source object for this event. + /// The type of event to raise. + /// Returns true if the event was consumed with . public bool RaiseImmediately(TEvent @event, TObject target, TObject source) where TEvent : TBaseEvent { return RaiseImmediately(ref @event, target, source); } + /// + /// Raises the given event immediately, regardless if another event is currently still being raised. + /// + /// The event to raise. + /// The target object for this event. + /// The source object for this event. + /// The type of event to raise. + /// Returns true if the event was consumed with . public bool RaiseImmediately(ref TEvent @event, TObject target, TObject source) where TEvent : TBaseEvent { @@ -106,6 +130,13 @@ public override void SubscribeTo(EventHandler handler, float pri listeners.AddListener(handler, priority); } + /// + /// Subscribe to a given event type. + /// + /// The method that should be invoked when the event is raised. + /// Higher priority means this listener will receive the event earlier than other listeners with lower priority. + /// If multiple listeners have the same priority, they will be invoked in the order they subscribed. + /// The event type to subscribe to. public void SubscribeTo(TargetedEventHandler handler, float priority = 0) where TEvent : TBaseEvent { @@ -119,12 +150,25 @@ public override void UnsubscribeFrom(EventHandler handler) listeners.RemoveListener(handler); } + /// + /// Unsubscribe from a given event type. + /// + /// The method that was previously given in SubscribeTo. + /// The event type to unsubscribe from. public void UnsubscribeFrom(TargetedEventHandler handler) where TEvent : TBaseEvent { var listeners = TargetedEventListeners.Get(this); listeners.RemoveListener(handler); } + /// + /// Subscribe to a given event type, but only if it targets the given object. + /// + /// The target object. + /// The method that should be invoked when the event is raised. + /// Higher priority means this listener will receive the event earlier than other listeners with lower priority. + /// If multiple listeners have the same priority, they will be invoked in the order they subscribed. + /// The event type to subscribe to. public void SubscribeToTarget(TObject target, TargetedEventHandler handler, float priority = 0) where TEvent : TBaseEvent { @@ -132,6 +176,12 @@ public void SubscribeToTarget(TObject target, TargetedEventHandler + /// Unsubscribe from a given event type, but only if it targets the given object. + /// + /// The target object. + /// The method that was previously given in SubscribeToTarget. + /// The event type to unsubscribe from. public void UnsubscribeFromTarget(TObject target, TargetedEventHandler handler) where TEvent : TBaseEvent { @@ -139,6 +189,14 @@ public void UnsubscribeFromTarget(TObject target, TargetedEventHandler + /// Subscribe to a given event type, but only if it comes from the given object. + /// + /// The source object. + /// The method that should be invoked when the event is raised. + /// Higher priority means this listener will receive the event earlier than other listeners with lower priority. + /// If multiple listeners have the same priority, they will be invoked in the order they subscribed. + /// The event type to subscribe to. public void SubscribeToSource(TObject source, TargetedEventHandler handler, float priority = 0) where TEvent : TBaseEvent { @@ -146,6 +204,12 @@ public void SubscribeToSource(TObject source, TargetedEventHandler + /// Unsubscribe from a given event type, but only if it comes from the given object. + /// + /// The source object. + /// The method that was previously given in SubscribeToSource. + /// The event type to unsubscribe from. public void UnsubscribeFromSource(TObject source, TargetedEventHandler handler) where TEvent : TBaseEvent {