LoL Engine Event System¶
A flexible, type-safe event system for Unity games built with LoL Engine.
Features¶
- Type-safe event handling
- Generic event listeners
- Service-based architecture with EventService as the core implementation of IEventManager
- Automatic event cleanup when using the EventRegister extensions like OnEnable/OnDisable
- Runtime event debugging support
- Thread-safe event subscription and triggering.
- Event object pooling to reduce garbage collection.
- Convenient static methods for triggering common events (e.g.,
GameEvent.Trigger<T>()). - Editor-friendly static state reset for improved stability during script recompiles.
Directory Structure¶
Events/
├── Providers/ # EventRegister extensions, listener wrappers
├── GameEvents/ # Concrete event implementations
├── Interfaces/ # Core interfaces
├── Service/ # EventService implementation
Basic Usage¶
Service Registration¶
When Enable Event Manager is on in ServiceConfiguration, ConfigurableServiceInitializer creates EventService, registers IEventManager, and calls GameEvent.Initialize(eventManager) automatically. You normally do not register events manually.
// Only needed for custom bootstrap / tests:
var eventService = new EventService();
ServiceLocator.Instance.Register<IEventManager>(eventService);
eventService.Initialize();
GameEvent.Initialize(eventService);
1. Define an Event¶
public class PlayerDeathEvent : GameEvent
{
public Vector3 DeathPosition { get; set; }
public string CauseOfDeath { get; set; }
}
2. Create a Listener¶
public class DeathHandler : MonoBehaviour, IEventListener<PlayerDeathEvent>
{
// Using EventRegister extensions for automatic add/remove in OnEnable/OnDisable
private void OnEnable()
{
this.EventStartListening<PlayerDeathEvent>();
}
private void OnDisable()
{
this.EventStopListening<PlayerDeathEvent>();
}
public void OnGameEvent(PlayerDeathEvent eventData)
{
Debug.Log($"Player died at {eventData.DeathPosition} due to {eventData.CauseOfDeath}");
}
}
// Alternative: Manual registration (less common for MonoBehaviours)
/*
public class ManualDeathHandler : MonoBehaviour, IEventListener<PlayerDeathEvent>
{
private IEventManager _eventManager;
private void Start()
{
_eventManager = ServiceLocator.Instance.Get<IEventManager>();
if (_eventManager != null)
{
_eventManager.AddListener<PlayerDeathEvent>(this);
}
}
private void OnDestroy()
{
if (_eventManager != null)
{
_eventManager.RemoveListener<PlayerDeathEvent>(this);
}
}
public void OnGameEvent(PlayerDeathEvent eventData)
{
Debug.Log($"Player died at {eventData.DeathPosition}");
}
}
*/
3. Trigger Events¶
public class GameController : MonoBehaviour
{
// Option 1: Using static GameEvent.Trigger (recommended for simplicity)
private void TriggerDeathWithStaticHelper()
{
// Get a pooled event instance
var deathEvent = GameEvent.GetEvent<PlayerDeathEvent>();
deathEvent.DeathPosition = transform.position;
deathEvent.CauseOfDeath = "Fall damage";
GameEvent.Trigger(deathEvent);
// The event is automatically returned to the pool after triggering
}
// Option 2: Using the IEventManager instance (if preferred or for specific scenarios)
private IEventManager _eventManager;
private void Start()
{
_eventManager = ServiceLocator.Instance.Get<IEventManager>();
}
private void TriggerDeathWithEventManagerInstance()
{
if (_eventManager == null) return;
var deathEvent = GameEvent.GetEvent<PlayerDeathEvent>(); // Still use pooling
deathEvent.DeathPosition = transform.position;
deathEvent.CauseOfDeath = "Fall damage";
_eventManager.TriggerEvent(deathEvent);
}
}
Dependency Injection¶
public class MyClass
{
private readonly IEventManager _eventManager;
public MyClass(IEventManager eventManager)
{
_eventManager = eventManager;
}
}
When to Use Events vs Direct Calls¶
Use Events For:¶
- Cross-System Communication: When systems need to communicate without direct dependencies
- One-to-Many Broadcasting: When one action needs to notify multiple systems
- State Change Notifications: When system states need to be observable by many listeners
- Service Lifecycle Events: Broadcasting initialization, shutdown, or state transitions
- Decoupled Architectures: When you want loose coupling between game systems
Avoid Events For:¶
- Direct Parent-Child Communication: Use direct method calls for simple hierarchies
- High-Frequency Updates: Avoid events in Update() loops (use properties or observers)
- Synchronous Return Values: Events are fire-and-forget, can't return values
- Local Component Communication: Use UnityEvents or direct references within same GameObject
Common Usage Scenarios¶
Scenario 1: Enemy Death → Multiple Systems React¶
// Enemy triggers death event
public class Enemy : MonoBehaviour
{
public void Die()
{
CombatEvents.EnemyKilled(_enemyId, _enemyType);
Destroy(gameObject);
}
}
// Multiple systems listen independently
public class KillCounterUI : MonoBehaviour, IEventListener<EasyEnemyKilledEvent>
{
private void OnEnable() => this.EventStartListening<EasyEnemyKilledEvent>();
private void OnDisable() => this.EventStopListening<EasyEnemyKilledEvent>();
public void OnGameEvent(EasyEnemyKilledEvent e) => UpdateKillCount();
}
public class LootSpawner : MonoBehaviour, IEventListener<EasyEnemyKilledEvent>
{
private void OnEnable() => this.EventStartListening<EasyEnemyKilledEvent>();
private void OnDisable() => this.EventStopListening<EasyEnemyKilledEvent>();
public void OnGameEvent(EasyEnemyKilledEvent e) => SpawnLoot(e);
}
Scenario 2: Audio Events¶
// AudioService broadcasts playback lifecycle events.
public class MusicHud : MonoBehaviour, IEventListener<AudioEvents.SoundStarted>
{
private void OnEnable() => this.EventStartListening<AudioEvents.SoundStarted>();
private void OnDisable() => this.EventStopListening<AudioEvents.SoundStarted>();
public void OnGameEvent(AudioEvents.SoundStarted e)
{
if (e.Track != null && e.Track.Name == "Music")
{
ShowNowPlaying(e.AudioId);
}
}
}
Scenario 3: Level Conditions → Character Stats¶
// Level broadcasts environmental conditions
public class LevelManager : MonoBehaviour
{
private void Start()
{
var conditions = GameEvent.GetEvent<LevelConditionEvent>();
conditions.Weather = "Rainy";
conditions.DefenseModifier = 0.8f; // -20% defense
GameEvent.Trigger(conditions);
}
}
// All characters listen and adapt
public class CharacterStats : MonoBehaviour, IEventListener<LevelConditionEvent>
{
public void OnGameEvent(LevelConditionEvent e)
{
_currentDefense = _baseDefense * e.DefenseModifier;
}
}
Best Practices¶
- Service Access
- Use service locator or dependency injection
- Prefer using ServiceLocator or dependency injection to get IEventManager. For simple event triggering, static helpers like GameEvent.Trigger
() are provided for convenience. -
Use interfaces for event manager
-
Event Definition
- Keep events immutable when possible
- Include only necessary data
-
Use clear, descriptive names
-
Listener Management
- Always unsubscribe in OnDisable/OnDestroy (The EventRegister extensions handle this automatically for MonoBehaviours)
- Avoid anonymous listeners
-
Use weak references for long-lived subscriptions
-
Event Creation
-
Utilize event pooling by getting events via GameEvent.GetEvent
() to reduce allocations -
Error Handling
Get<IEventManager>() returns null if the service is missing (logs a warning). Use Require or null-check:
using LoLEngine.Core.ServiceManagement.Service;
var eventManager = ServiceLocator.Instance.Get<IEventManager>();
if (eventManager == null)
{
Debug.LogError("Event manager not registered — enable Enable Event Manager");
return;
}
Troubleshooting¶
Common issues:
- Get<IEventManager>() returns null: Enable Enable Event Manager in ServiceConfiguration. Ensure ImprovedGameInitializer has run. GameEvent.Initialize() is called automatically during bootstrap when using the standard initializer.
- Missing events: Verify event registration and listener setup
- Memory leaks: Check for proper unsubscription. Use EventRegister extensions for MonoBehaviours or ensure manual unsubscription
- Event ordering: Review execution order settings
Recent Key Improvements¶
See CHANGELOG.md for full version history.
- Implemented
EventServiceas the core manager with thread-safe listener management. - Introduced event object pooling via
GameEvent.GetEvent<T>()andGameEvent.ReleaseEvent<T>(). - Added convenient static
GameEvent.Trigger<T>(T eventInstance)for simplified event dispatching. - Provided
EventRegisterextension methods for easierMonoBehaviourlistener management. - Ensured static state (like subscriber lists) is reset on domain reloads in the Unity Editor for better stability.
License¶
Part of the LoL Engine framework.