Skip to content

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

  1. Service Access
  2. Use service locator or dependency injection
  3. Prefer using ServiceLocator or dependency injection to get IEventManager. For simple event triggering, static helpers like GameEvent.Trigger() are provided for convenience.
  4. Use interfaces for event manager

  5. Event Definition

  6. Keep events immutable when possible
  7. Include only necessary data
  8. Use clear, descriptive names

  9. Listener Management

  10. Always unsubscribe in OnDisable/OnDestroy (The EventRegister extensions handle this automatically for MonoBehaviours)
  11. Avoid anonymous listeners
  12. Use weak references for long-lived subscriptions

  13. Event Creation

  14. Utilize event pooling by getting events via GameEvent.GetEvent() to reduce allocations

  15. 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 EventService as the core manager with thread-safe listener management.
  • Introduced event object pooling via GameEvent.GetEvent<T>() and GameEvent.ReleaseEvent<T>().
  • Added convenient static GameEvent.Trigger<T>(T eventInstance) for simplified event dispatching.
  • Provided EventRegister extension methods for easier MonoBehaviour listener 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.