Table of Contents

LocalMessageBus

LocalMessageBus is a typed publish/subscribe channel living beside the world as a local singleton: entities announce that something happened, and unrelated code — UI, audio, analytics — reacts without either side knowing about the other. Nothing here crosses the network.

When to use this

  • Decoupling presentation from simulation: an entity publishes "took damage", the HUD and the audio system subscribe independently.
  • Cross-cutting reactions where a direct reference would be awkward — the publisher must not need to know its listeners.

When not to

  • Anything another machine must learn about — that is an RPC or synchronized state; the bus is process-local.
  • Communication inside one entity, or between an entity and its own view — a direct call or a change callback is simpler and traceable.
  • Gameplay state — a message is an event that vanishes after delivery; a joiner or a rollback sees nothing of it.

Minimal example

DamageMessage.cs

using LiteEntitySystem.Extensions;

// TSender declares who is allowed to publish this message
public struct DamageMessage : IBusMessage<Fighter>
{
    public byte Amount;
    public byte RemainingHealth;
}

Publishing, from the entity

public void ApplyDamage(byte amount)
{
    Health.Value = amount >= Health.Value ? (byte)0 : (byte)(Health.Value - amount);

    if (IsClient && EntityManager.InNormalState)
        this.GetOrCreateLocalMessageBus()
            .Send(this, new DamageMessage { Amount = amount, RemainingHealth = Health.Value });
}

Subscribing, from a view or system

public class DamageHud : IDisposable
{
    private readonly BusSubscription _subscription;

    public DamageHud(LocalMessageBus bus) =>
        _subscription = bus.Subscribe<Fighter, DamageMessage>(OnDamage);

    private void OnDamage(Fighter sender, DamageMessage msg) { /* flash the health bar */ }

    public void Dispose() => _subscription.Dispose();
}

How it works

Messages are typed by sender

IBusMessage<TSender> binds a message type to the kind of thing that publishes it, so Subscribe<Fighter, DamageMessage> gives handlers a correctly typed sender with no casting. Messages are structs — a published message allocates nothing.

Getting the bus

The bus is an ILocalSingleton, so it lives on the manager: register one with AddLocalSingleton, or let entity.GetOrCreateLocalMessageBus() create it on first use. Since it is per manager, a listen-server process has two independent buses — the server's and the client's — which is what you want: a client-side effect message never reaches server logic.

Subscriptions are disposable

Subscribe returns a BusSubscription; disposing it detaches the handler. Tie that to the lifetime of the subscriber — a view's destruction, a scene teardown — or the handler outlives its object and fires into a dead reference. Destroy() on the bus itself drops every channel, and a manager reset destroys local singletons for you.

Publishing during prediction

The bus is not part of the simulation: it has no rollback, no history, no replay. A message published from predicted code fires again on every re-simulation, so gate publications with EntityManager.InNormalState, exactly as with any other one-shot effect.

Setting DebugToLog = true logs every published message with its sender and handler — useful when tracing who reacts to what.

Warning

Common mistakes

  • Publishing from predicted code without an InNormalState gate — every correction replays the message and the effects behind it.
  • Never disposing subscriptions — handlers accumulate across sessions and keep dead objects alive.
  • Expecting messages to reach other machines — the bus is in-process; use RPCs for that.
  • Carrying state in messages — a message that nobody was subscribed for is simply gone; state belongs in SyncVars.