Provides support for indirectly passing custom messages between objects, making event-driven architecture easy, safe, and decoupled. Minimal setup, similar in syntax to the builtin OnPointerClick
or OnCollisionEnter
.
- Create a data type for your custom message
- Receiver must inherit from
ScopedListener
, which will automatically manage registration in the MonoBehaviour lifecycle - Create a function in the receiver to receive the event
- Fire the event!
//Step 1
public class InteractEvent : Event
{
public Player player;
public Interactable interactedObject;
}
//Step 2
public class LightSwitch : ScopedListener
{
//Step 3
[EventHandler]
void HandleLightsToggle(InteractEvent e) //This function can be named anything
{
if(e.interactedObject == this) connectedLight.enabled = !connectedLight.enabled;
}
}
//Step 4 - Player.cs
InteractEvent e = new InteractEvent { player = this, interactedObject = objectUnderCursor };
EventBus.Main.Dispatch(e);
Event handling functions can be named anything, so long as the parameter type matches and the function is marked EventHandler
. This also means that multiple EventHandlers can be defined in the same script for the same type, allowing easy processing of events.
ScopedListener
is a convenience, but the IListener
interface allows for finer control over registration, or a listener that isn't a unity object. By default ScopedListener
registers the whole class using EventBus.Main.RegisterStaticHandlers
/ EventBus.Main.UnregisterAllHandlers
. Managing specifically callbacks dynamically (not on enable/disable) is also possible, and supports lambdas and local functions as well.
No matter where an event is fired from, it will reach all valid listeners. This makes it useful for decoupling. For example, an achievement for "Deal at least 100 damage in a single hit" or "Harvest 30 rutabaga" would listen for a DamageEvent or CropHarvestEvent. This also helps maintainability, as putting achievement code in the combat system or interaction controller would make reading difficult. Robert Nystrom explains this application in detail: https://gameprogrammingpatterns.com/event-queue.html
This implementation can also be used like a query, or like the Builder pattern, with the message object treated as an accumulator. To implement an equippable trinket that grants +25 fire damage on attacks and -12% resistance to water damage, the item can listen for a DamageEvent and modify the damage value before it is applied. Priority.Final is equivalent to Builder.Build().
The query approach includes consuming and cancelling events. Consuming can be useful for inputs that should only reach one target. Cancelling can be useful for status effects and abilities, such as blocking the first projectile from a direction and reducing all further damage from that direction by 10%.
However, events should NOT be used as a replacement for Update, since the indirection layers have an overhead. They are intended for relatively infrequent, unpredictable inversion of control.
Messaging use inspired by patterns such as Observer, Event Bus, and Pub/Sub. Query use with mutable events inspired by the event buses of MinecraftForge and Bukkit. Unintentionally similar to Guava's EventBus.