Events¶
Learn how to handle window events and create custom event systems in your Brine2D games.
Quick Start¶
using Brine2D.Events;
public class GameScene : Scene
{
private readonly EventBus _eventBus;
public GameScene(EventBus eventBus)
{
_eventBus = eventBus;
}
protected override Task OnInitializeAsync(CancellationToken ct)
{
// Subscribe to window resize event
_eventBus.Subscribe<WindowResizedEvent>(OnWindowResized);
return Task.CompletedTask;
}
private void OnWindowResized(WindowResizedEvent e)
{
Logger.LogInformation("Window resized to {Width}x{Height}",
e.Width, e.Height);
// Adjust camera, UI, etc.
UpdateCameraAspectRatio(e.Width, e.Height);
}
protected override Task OnUnloadAsync(CancellationToken ct)
{
// Unsubscribe to prevent memory leaks
_eventBus.Unsubscribe<WindowResizedEvent>(OnWindowResized);
return Task.CompletedTask;
}
}
Topics¶
| Guide | Description | Difficulty |
|---|---|---|
| Window Events | Handle window resize, focus, minimize, etc. | ⭐ Beginner |
Key Concepts¶
EventBus¶
The EventBus provides pub/sub event system:
public class EventBus
{
// Subscribe to events
void Subscribe<TEvent>(Action<TEvent> handler);
// Unsubscribe
void Unsubscribe<TEvent>(Action<TEvent> handler);
// Publish events
void Publish<TEvent>(TEvent eventData);
}
Register as singleton (shared across scenes):
builder.Services.AddSingleton<EventBus>();
Built-In Events¶
Brine2D provides window events out of the box:
| Event | When Fired |
|---|---|
| WindowResizedEvent | Window size changed |
| WindowFocusedEvent | Window gained focus |
| WindowUnfocusedEvent | Window lost focus |
| WindowMinimizedEvent | Window minimized |
| WindowRestoredEvent | Window restored from minimize |
| WindowClosedEvent | Window close requested |
Common Tasks¶
Handle Window Resize¶
protected override Task OnInitializeAsync(CancellationToken ct)
{
_eventBus.Subscribe<WindowResizedEvent>(e =>
{
// Update camera aspect ratio
var aspectRatio = (float)e.Width / e.Height;
_camera.AspectRatio = aspectRatio;
// Update UI layout
_uiCanvas.UpdateLayout(e.Width, e.Height);
});
return Task.CompletedTask;
}
Create Custom Event¶
// Define event
public class PlayerDiedEvent
{
public int Score { get; set; }
public Vector2 Position { get; set; }
}
// Publish event
_eventBus.Publish(new PlayerDiedEvent
{
Score = _playerScore,
Position = _playerPosition
});
// Subscribe to event
_eventBus.Subscribe<PlayerDiedEvent>(e =>
{
Logger.LogInformation("Player died at {Pos} with score {Score}",
e.Position, e.Score);
ShowGameOverScreen(e.Score);
});
Decouple Game Logic¶
// Instead of direct coupling
public class Player
{
private readonly GameManager _gameManager;
public void Die()
{
_gameManager.OnPlayerDied(this); // ❌ Tight coupling
}
}
// Use events for loose coupling
public class Player
{
private readonly EventBus _eventBus;
public void Die()
{
_eventBus.Publish(new PlayerDiedEvent { ... }); // ✅ Decoupled
}
}
// Multiple listeners can react
_eventBus.Subscribe<PlayerDiedEvent>(e => UpdateUI());
_eventBus.Subscribe<PlayerDiedEvent>(e => PlaySound());
_eventBus.Subscribe<PlayerDiedEvent>(e => ShowGameOver());
Event-Driven AI¶
// Player shoots - publish event
_eventBus.Publish(new WeaponFiredEvent
{
Position = _playerPosition,
Loudness = 100f
});
// Enemies react to sound
_eventBus.Subscribe<WeaponFiredEvent>(e =>
{
foreach (var enemy in _enemies)
{
var distance = Vector2.Distance(enemy.Position, e.Position);
if (distance < e.Loudness)
{
enemy.Investigate(e.Position);
}
}
});
Best Practices¶
✅ DO¶
- Unsubscribe in OnUnloadAsync() - Prevent memory leaks
- Use EventBus for decoupling - Loose coupling between systems
- Create typed events - Clear event data structure
- Subscribe in OnInitializeAsync() - Early setup
- Use singleton EventBus - Shared across scenes
// ✅ Good pattern
protected override Task OnInitializeAsync(CancellationToken ct)
{
_eventBus.Subscribe<WindowResizedEvent>(OnWindowResized);
return Task.CompletedTask;
}
protected override Task OnUnloadAsync(CancellationToken ct)
{
_eventBus.Unsubscribe<WindowResizedEvent>(OnWindowResized);
return Task.CompletedTask;
}
private void OnWindowResized(WindowResizedEvent e)
{
// Handle event
}
❌ DON'T¶
- Don't forget to unsubscribe - Memory leaks
- Don't use for high-frequency events - Performance overhead
- Don't mutate event data - Events should be immutable
- Don't use generic object events - Use typed events
- Don't create circular event chains - Stack overflow
// ❌ Bad - forgot to unsubscribe
protected override Task OnInitializeAsync(CancellationToken ct)
{
_eventBus.Subscribe<WindowResizedEvent>(OnWindowResized);
return Task.CompletedTask;
}
// OnUnloadAsync missing - memory leak!
// ❌ Bad - high frequency
protected override void OnUpdate(GameTime gameTime)
{
_eventBus.Publish(new FrameUpdateEvent()); // 60 times per second - slow!
}
// ❌ Bad - circular events
_eventBus.Subscribe<EventA>(e => _eventBus.Publish(new EventB()));
_eventBus.Subscribe<EventB>(e => _eventBus.Publish(new EventA())); // Stack overflow!
Event Patterns¶
Observer Pattern¶
// Classic observer pattern via EventBus
public class HealthChangedEvent
{
public Entity Entity { get; set; }
public int OldHealth { get; set; }
public int NewHealth { get; set; }
}
// Publisher
public class HealthComponent : Component
{
private readonly EventBus _eventBus;
private int _health;
public int Health
{
get => _health;
set
{
var old = _health;
_health = value;
_eventBus.Publish(new HealthChangedEvent
{
Entity = Entity,
OldHealth = old,
NewHealth = value
});
}
}
}
// Observers
_eventBus.Subscribe<HealthChangedEvent>(e => UpdateHealthBar(e));
_eventBus.Subscribe<HealthChangedEvent>(e => CheckDeath(e));
_eventBus.Subscribe<HealthChangedEvent>(e => PlayDamageSound(e));
Command Pattern¶
// Commands as events
public interface ICommand
{
void Execute();
}
public class MoveCommand : ICommand
{
public Vector2 Direction { get; set; }
public void Execute() { /* move logic */ }
}
// Publish command
_eventBus.Publish<ICommand>(new MoveCommand { Direction = Vector2.Up });
// Execute commands
_eventBus.Subscribe<ICommand>(cmd => cmd.Execute());
Troubleshooting¶
Memory Leak¶
Symptom: Memory usage grows after scene changes
Cause: Event handlers not unsubscribed
Solution:
// Track handlers to unsubscribe
private Action<WindowResizedEvent> _resizeHandler;
protected override Task OnInitializeAsync(CancellationToken ct)
{
_resizeHandler = OnWindowResized;
_eventBus.Subscribe(_resizeHandler);
return Task.CompletedTask;
}
protected override Task OnUnloadAsync(CancellationToken ct)
{
_eventBus.Unsubscribe(_resizeHandler); // ✅ Prevents leak
return Task.CompletedTask;
}
Events Not Firing¶
Symptom: Subscribed but handler never called
Solutions:
- Check event type matches:
// ❌ Different types won't match
_eventBus.Publish(new PlayerDiedEvent());
_eventBus.Subscribe<EnemyDiedEvent>(e => { }); // Won't fire
// ✅ Same type
_eventBus.Publish(new PlayerDiedEvent());
_eventBus.Subscribe<PlayerDiedEvent>(e => { }); // Will fire
- Verify subscription happened:
Logger.LogInformation("Subscribed to PlayerDiedEvent");
_eventBus.Subscribe<PlayerDiedEvent>(e =>
{
Logger.LogInformation("Event received!"); // Debug
});
Exception in Handler¶
Symptom: Exception in one handler breaks others
Solution: EventBus should catch and log exceptions
// EventBus implementation should handle exceptions
public void Publish<TEvent>(TEvent eventData)
{
foreach (var handler in _handlers)
{
try
{
handler(eventData);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in event handler");
// Continue to next handler
}
}
}
Performance Considerations¶
Event Bus Overhead¶
Cost per event: - Lookup: O(1) - Fast dictionary lookup - Notify: O(n) - Iterate all subscribers
Recommendation: - ✅ Use for infrequent events (player died, level complete) - ❌ Avoid for high-frequency events (every frame update)
// ✅ Good - infrequent
_eventBus.Publish(new LevelCompleteEvent());
_eventBus.Publish(new PlayerDiedEvent());
// ❌ Bad - every frame
protected override void OnUpdate(GameTime gameTime)
{
_eventBus.Publish(new FrameUpdateEvent()); // 60 times per second!
}
Related Topics¶
- Window Events - Handle window events
- Dependency Injection - Inject EventBus
- Architecture - Event-driven architecture
Ready to use events? Start with Window Events!