ZOA Messaging
The publish/subscribe bus and service registry every other package talks through.
Messaging is the root of the dependency graph. It depends on nothing, and almost everything depends on it. When a weapon needs to tell the HUD it reloaded, it publishes an event rather than holding a reference to the HUD, and the two packages never learn about each other.
There are two primitives here and they do different jobs. The event bus carries things that happened. The service registry answers the question "who is providing X right now". Reaching for the wrong one is the most common mistake. If a subscriber missing the message is fine, publish it; if the caller needs an answer, resolve a service.
Both are small. There is no DI container, no reflection scan, and no attribute magic. The registry is a static type-keyed locator; the bus dispatches synchronously, in process.
Depends on (0)
Depended on by (27)
How it works
Concepts
The bus is hosted for you
FoundryEventBusHost spawns itself before the first scene loads, creates an InProcessFoundryEventBus, registers it, and marks itself DontDestroyOnLoad. You never place it in a scene and there is no prefab to remember.
The one ordering constraint worth knowing: if your own code runs at RuntimeInitializeLoadType.BeforeSceneLoad and publishes immediately, the host may not have registered yet. Use AfterSceneLoad, or defer to Start.
Cross-cutting contracts live here
A handful of contracts sit in Messaging that look like they belong elsewhere. AudioBusId and IAudioBusService are the clearest case: both Nucleon (player audio) and the AI package need them, and putting them in either one would couple Nucleon to AI.
Messaging is the sanctioned home for a contract that more than one package needs and no package owns. Break that rule and the graph grows edges nobody intended.
Surface
Key types
IFoundryEventBus
interface
Publish/subscribe over plain CLR types. Publishing to nobody is legal and free.
- void Publish<T>(in T message)
- IDisposable Subscribe<T>(Action<T> handler)
FoundryServiceRegistry
service
Static, type-keyed service locator. Register an implementation, resolve it from anywhere, without a container.
- static void Register<T>(T instance)
- static bool TryResolve<T>(out T instance)
- static T Get<T>()
InProcessFoundryEventBus
class
The default in-memory bus implementation. Synchronous dispatch on the calling thread.
FoundryEventBusHost
component
Hidden bootstrapper that owns the scene-lifetime bus. Auto-spawned, DontDestroyOnLoad, execution order -10000. Not added by hand.
IPlayerContext
interface
Resolves the active player without a scene reference, so packages can ask 'who is the player' without depending on Nucleon.
IAudioBusService
interface
Resolves AudioBusId values to AudioMixerGroups and exposes per-bus volume. Linear 0-1 at the surface, converted to dB internally.
- AudioMixerGroup GetGroup(AudioBusId bus)
- void SetVolume(AudioBusId bus, float linear01)
- float GetVolume(AudioBusId bus)
- event Action<AudioBusId, float> VolumeChanged
AudioBusId
enum
Master, Music, SFX, Voice, Ambient. Gameplay code targets a sub-bus; Master is for settings sliders.
CursorLockBus
class
Arbitrates cursor lock between systems that each think they should own it, so opening inventory over a locked first-person camera behaves.
Usage
Examples
using ZOA.Messaging;
public readonly struct WeaponReloaded
{
public readonly int WeaponId;
public readonly int RoundsLoaded;
public WeaponReloaded(int weaponId, int roundsLoaded)
{
WeaponId = weaponId;
RoundsLoaded = roundsLoaded;
}
}
// Anywhere that finishes a reload:
var bus = FoundryServiceRegistry.Get<IFoundryEventBus>();
bus.Publish(new WeaponReloaded(weaponId, 30));using System;
using UnityEngine;
using ZOA.Messaging;
public sealed class AmmoCounter : MonoBehaviour
{
private IDisposable _subscription;
private void OnEnable()
{
var bus = FoundryServiceRegistry.Get<IFoundryEventBus>();
_subscription = bus.Subscribe<WeaponReloaded>(OnReloaded);
}
// Subscribe returns IDisposable rather than needing a matching
// Unsubscribe call, so the teardown cannot drift from the setup.
private void OnDisable() => _subscription?.Dispose();
private void OnReloaded(WeaponReloaded evt) => Redraw(evt.RoundsLoaded);
private void Redraw(int rounds) { /* ... */ }
}using ZOA.Messaging;
using ZOA.Messaging.Audio;
public sealed class ProjectAudioBusService : IAudioBusService
{
// ... implementation ...
}
// During bootstrap, before anything resolves it:
FoundryServiceRegistry.Register<IAudioBusService>(new ProjectAudioBusService());
// From anywhere afterwards:
if (FoundryServiceRegistry.TryResolve<IAudioBusService>(out var audio))
audio.SetVolume(AudioBusId.SFX, 0.8f);Read this
Notes and caveats
See also