-
Notifications
You must be signed in to change notification settings - Fork 0
Beacon Event System
Void's Beacon system provides a publish/subscribe (pub/sub) system for decoupled communication. Systems can send messages without knowing who is listening, and subscribers can listen for messages without knowing who is sending them.
Without a pub/sub system, code becomes tightly coupled. When a player dies, the player object directly calls methods on the UI, the audio system, and the score system. The player code has to know about every system that cares about player death. Adding new systems means modifying the player code.
// Tight coupling
player.OnDeath(() =>
{
ui.ShowGameOver();
audio.PlayDeathSound();
score.Reset();
// Every new system requires modifying this code
});
With beacons, the player just says "PlayerDied" and anyone who cares can react. The player doesn't know or care who is listening.
// Loose coupling
BeaconManager.Instance.Publish("PlayerDied", playerPosition, playerHealth);
// UI reacts
BeaconManager.Instance.Subscribe("PlayerDied", handle =>
{
var pos = handle.Get<Vect2>(0);
ui.ShowGameOver(pos);
});
// Audio reacts
BeaconManager.Instance.Subscribe("PlayerDied", handle =>
{
audio.PlayDeathSound();
});
// Score reacts
BeaconManager.Instance.Subscribe("PlayerDied", handle =>
{
score.Reset();
});
Subscribe to a topic
BeaconManager.Instance.Subscribe("PlayerDied", handle =>
{
Console.WriteLine($"Player died!");
});
Subscribe with an enum
public enum GameBeacons { PlayerDied, LevelStarted, GameOver }
BeaconManager.Instance.Subscribe(GameBeacons.PlayerDied, handle =>
{
Console.WriteLine("Player died!");
});
Publish a beacon
BeaconManager.Instance.Publish("PlayerDied");
Publish with data
BeaconManager.Instance.Publish("PlayerDied", playerPosition, playerHealth, playerName);
Beacons can carry data payloads. Access them by index:
BeaconManager.Instance.Subscribe("PlayerDied", handle =>
{
var position = handle.Get<Vect2>(0);
var health = handle.Get<float>(1);
var name = handle.Get<string>(2);
});
TryGet for safe access
if (handle.TryGet<Player>(0, out var player))
{
// player is valid and can be used safely
}
Store the handler reference and unsubscribe when no longer needed:
Action<BeaconHandle> handler = handle =>
{
Console.WriteLine("Received beacon!");
};
BeaconManager.Instance.Subscribe("PlayerDied", handler);
// Later, unsubscribe
BeaconManager.Instance.Unsubscribe("PlayerDied", handler);
BeaconManager.Instance.Clear();
The Beacon system is designed for performance:
- Topics are hashed using FNV-1a for fast lookups
- Subscribers are stored in a concurrent dictionary
- Publish is O(1) hash lookup
- Zero allocations for subscribers (delegates are cached)
Use beacons for cross-system communication where the sender doesn't need to know who is listening:
- Player events (death, level up, damage)
- Game state changes (started, paused, game over)
- UI events (menu opened, button clicked)
- System events (asset loaded, save started)
- Custom events (quest completed, achievement unlocked)
Don't use beacons for:
- Performance-critical code paths (each publish has overhead)
- Direct one-to-one communication (use direct method calls)
- Situations where you need a return value (beacons are one-way)
Use strings for dynamic topics:
BeaconManager.Instance.Publish($"LevelCompleted_{levelId}");
Use enums for fixed topics:
public enum GameBeacons { PlayerDied, LevelStarted, GameOver }
BeaconManager.Instance.Publish(GameBeacons.PlayerDied);
Enums are type-safe and prevent typos. They are recommended for most use cases.
Always unsubscribe when an object is destroyed to prevent memory leaks:
public class MySystem : IDisposable
{
public MySystem()
{
BeaconManager.Instance.Subscribe(GameBeacons.PlayerDied, OnPlayerDied);
}
public void Dispose()
{
BeaconManager.Instance.Unsubscribe(GameBeacons.PlayerDied, OnPlayerDied);
}
private void OnPlayerDied(BeaconHandle handle)
{
// Handle event
}
}