ZOA Messaging
Resolve services and publish events between systems.
Messaging provides FoundryServiceRegistry, a type-keyed service locator, and the synchronous in-process event bus. Callers resolve a service when they need to perform an operation and receive its result.
Publish an event to report something that happened, such as a weapon reloading. Listeners can respond without the publisher holding their references. Subscribe and unsubscribe with the listener’s lifetime.
Declared ZOA dependencies (0)
Referenced by ZOA manifests (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.
Reference
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);Integration
Behavior to account for
See also