A flexible, high-performance event system for Unity with support for synchronous and asynchronous event handling, priority-based execution, and polymorphic event dispatch.
- Multiple Handler Types: Action delegates, no-args delegates, and UniTask async handlers
- Priority System: Control execution order with priority values (higher priority = earlier execution)
- Polymorphic Events: Events can trigger handlers for base types and interfaces
- Async Support: Sequential and concurrent async event execution (requires UniTask)
- Stoppable Events: Events can halt propagation mid-execution
- Thread-Safe Modifications: Safe registration/deregistration during event raising
- Multiple Usage Patterns: Static EventBus, typed EventBus, or instanced EventContainer
public struct PlayerDiedEvent : IEvent
{
public string PlayerName;
public float Score;
}
public struct GamePausedEvent : IEvent, IStoppableEvent
{
public bool StopPropagation { get; set; }
}// Typed handlers
EventBus<PlayerDiedEvent>.Register(OnPlayerDied);
EventBus<PlayerDiedEvent>.Register(OnPlayerDiedAsync, priority: 10);
// No-args handlers
EventBus<GamePausedEvent>.Register(OnAnyGameEvent);
void OnPlayerDied(PlayerDiedEvent evt)
{
Debug.Log($"{evt.PlayerName} died with score {evt.Score}");
}
async UniTask OnPlayerDiedAsync(PlayerDiedEvent evt)
{
await ShowDeathAnimation();
}
void OnAnyGameEvent()
{
Debug.Log("Some game event occurred");
}// Simple raise
EventBus<PlayerDiedEvent>.Raise(new PlayerDiedEvent
{
PlayerName = "Alice",
Score = 1500
});
// Async sequential
await EventBus<PlayerDiedEvent>.RaiseSequentialAsync(playerEvent);
// Async concurrent
await EventBus<PlayerDiedEvent>.RaiseConcurrentAsync(playerEvent);Best for global game events with compile-time type safety:
// Registration
var binding = EventBus<PlayerLevelUpEvent>.Register(OnPlayerLevelUp, priority: 5);
// Deregistration
EventBus<PlayerLevelUpEvent>.Deregister(OnPlayerLevelUp);
// or
EventBus<PlayerLevelUpEvent>.Deregister(binding);
// Raising
EventBus<PlayerLevelUpEvent>.Raise(new PlayerLevelUpEvent { NewLevel = 5 });Best for scoped events (UI systems, game modes, etc.):
public class GameModeManager
{
private readonly EventContainer _events = new();
public void Initialize()
{
_events.Register<RoundStartEvent>(OnRoundStart);
_events.Register<RoundEndEvent>(OnRoundEnd);
}
public void StartRound()
{
_events.Raise(new RoundStartEvent { RoundNumber = currentRound });
}
}For conditional or configurable event raising:
// Basic usage
RaiseEvent.Event(new PlayerDiedEvent { PlayerName = "Bob" })
.RaiseSync();
// With conditions
RaiseEvent.Event(new ScoreUpdateEvent { Score = newScore })
.When(() => gameActive && !paused)
.RaiseSync();
// With specific container
RaiseEvent.Event(new UIEvent())
.WithContainer(uiEventContainer)
.WithPolymorphic(false)
.RaiseSync();
// Async variants
await RaiseEvent.Event(new SaveGameEvent())
.RaiseSequentialAsync();Higher priority handlers execute first:
EventBus<DamageEvent>.Register(ApplyArmor, priority: 10); // Runs first
EventBus<DamageEvent>.Register(ApplyDamage, priority: 5); // Runs second
EventBus<DamageEvent>.Register(UpdateUI, priority: 0); // Runs lastHandlers for base types and interfaces are automatically triggered:
public interface IGameEvent : IEvent { }
public struct SpecificEvent : IGameEvent { }
// This handler will receive ALL IGameEvent implementations
EventBus<IGameEvent>.Register(OnAnyGameEvent);
// This raises to both IGameEvent and SpecificEvent handlers
EventBus<SpecificEvent>.Raise(new SpecificEvent());Events can halt propagation to remaining handlers:
public struct InputEvent : IEvent, IStoppableEvent
{
public bool StopPropagation { get; set; }
public KeyCode Key;
}
void OnInputHandler(InputEvent evt)
{
if (evt.Key == KeyCode.Escape && menuOpen)
{
evt.StopPropagation = true; // Stop other handlers
CloseMenu();
}
}// Sequential: Handlers execute one after another
await EventBus<SaveEvent>.RaiseSequentialAsync(saveEvent);
// Concurrent: All handlers start simultaneously
await EventBus<EffectEvent>.RaiseConcurrentAsync(effectEvent);- Use structs for events - Better performance and prevents unintended mutations
- Prefer EventBus - Type-safe and performant for global events
- Use EventContainer - For scoped systems that need isolation
- Set priorities wisely - Critical handlers (like damage armor) should run first
- Implement IStoppableEvent - For events that might need to halt propagation
- Clean up bindings - Always deregister handlers when objects are destroyed
- Unity 2021.3 or higher
- UniTask package (optional, for async support)
To enable UniTask support, add UNITASK_SUPPORT to your project's Scripting Define Symbols.