Skip to content

Beacon Event System

Shmellyorc edited this page Aug 27, 2026 · 1 revision

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.

The Problem

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
});

The Solution

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();
});

Basic Usage

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);

Accessing Data

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
}

Unsubscribing

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);

Clearing All Subscriptions

BeaconManager.Instance.Clear();

Performance

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)

When to Use Beacons

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)

Topics vs Enums

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.

Unsubscribe on Object Destruction

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
    }
}

Back to Home

Clone this wiki locally