A simple implementation of an event bus system for Unity3D. A bus is a ScriptableObject, can be instantiated as an .asset inside the project hierarchy and then assigned to listeners and emitters. Listeners can be informed about their callers (emitters). Mainly intended for asynchronous events, listeners that return a value and decoupled non-critical game systems.
Last tested on Unity 6.4
The result datapath doesn't exist for non-{sync, async} events. Emitter-Bus-Listener: n-n-n
- UniTask for asynchronous events
- implement an event bus and give it some events
[CreateAssetMenu(menuName = "SimpleEventBus/Example/EventBus", fileName = "MyEventBus.asset")]
public class MyEventBus : EventBus {
/* C# events */
public event Action<string> OnAnonymousEvent;
public event Action<EventInfo, string> OnEvent; // listener knows emitter
/* custom events */
// two input params, no return value
public Event<ValueTuple<string, string>>.Anonymous OnAnonymousCustomEvent; // listener has no info on emitter
public Event<ValueTuple<string, string>> OnCustomEvent; // listener knows emitter
// string input, return value, listener knows emitter
public Event<string>.Synchronous<string> OnSyncEvent; // listener returns string
public Event<string>.Asynchronous<string> OnAsyncEvent; // listener returns UniTask<string>
// or implement invocation methods for validation
public void InvokeCustomEvent(EventEmitterInfo invokingFrom, ValueTuple<string, string> params) {
if (!OnCustomEvent.HasListeners()) throw new Exception("no listeners");
var eventInfo = GetEventInfo(invokingFrom, DateTime.UtcNow);
OnCustomEvent.Invoke(eventInfo, params);
}
}- listener
public class MyListener : MonoBehaviour, IEventListener {
public MyEventBus eventBus;
void OnEnable() => eventBus.OnCustomEvent.Subscribe(HandleCustomEvent);
void OnDisable() => eventBus.OnCustomEvent.Unsubscribe(HandleCustomEvent);
void HandleCustomEvent(EventInfo eventInfo, ValueTuple<string, string> data) =>
Debug.Log($"I got u, {((Component)eventInfo.emitter.SourceRef).gameObject.name}");
}- emitter
[EventEmitterInfo(emitterType: typeof(MyEmitter), withTypeInfo: false)]
public class MyEmitter : MonoBehaviour, IEventEmitter {
public MyEventBus eventBus;
EventEmitterInfo emitterInfo;
void Awake() {
// cache the metadata
emitterInfo = (EventEmitterInfo)typeof(MyEmitter)
.GetCustomAttribute(typeof(EventEmitterInfo))
emitterInfo.SourceRef = this;
// the cancel token to be passed to Async events
emitterInfo.CancellationToken = destroyCancellationToken;
}
void work() {
// invoke an event directly
var eventInfo = new EventInfo(emitterInfo, DateTime.UtcNow);
eventBus.OnCustomEvent.Invoke(eventInfo, ("some", "data"));
// or implement invocation methods for validation
eventBus.InvokeCustomEvent(emitterInfo, ("some", "data"));
// listeners return all results
var results = eventBus.OnSyncEvent.Invoke(eventInfo, ("123"))
}
async void workAsync(EventInfo eventInfo) {
var task = eventBus.OnAsyncEvent.Invoke(eventInfo, "123");
var results = await task;
}
}- events are not optimal for time-critical code
- not tested in Update, per-frame etc.
- a bus SO should only be instantiated once per system context
- this keeps members closely grouped instead of spread between bus implementations
- too generic event buses (e.g. IntEventBus) make the code harder to read and trace
- attributes seem annoying, cooler ways to define class metadata?
- for sync and async events, it's very possible to implement an equivalent of EventInfo so that emitters know what listeners returned which value; alternatively simply also return a
thisreference
- UI Canvas
- a bunch of buttons with handlers inside MySimpleEmitter
- (also possible to directly invoke anonymous events on the bus)
- participants
- 2 listeners
- 1 emitter
- 1 event bus
- Simple
- anonymous: listener has no info on emitter of event
- info: emitter passes on its metadata when invoking the bus
- interface: emitter passes on itself when invoking the bus
- Custom: emitter passes on its metadata when invoking the bus
- anonymous: listener has no info on emitter of event
- Sync (anonymous)
- listener returns a value
- listener has no info on emitter of event
- only the last listener registered on the event will have its value read by the emitter
- Sync
- listener returns a value or tuple
- emitter passes on its metadata when invoking the bus
- all listener return values will be read by the emitter
- Async
- listener returns a value
- emitter passes on its metadata when invoking the bus
- all listener return values will be awaited and read by the emitter