Pre-alphaFoundry is in development. Tools, APIs, and sample content may change.

Browse documentation
Shared infrastructurecom.zoa.messaging · v0.4.0

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)

No ZOA dependencies declared in the package manifest.

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

Publishing an eventcsharp
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));
Events are plain structs. Passing by in avoids a copy and signals that the message is read-only.
Subscribing, and unsubscribing properlycsharp
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) { /* ... */ }
}
Registering a servicecsharp
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);
Prefer TryResolve where a missing service is survivable. Get throws, and during bootstrap a missing service should be loud.

Integration

Behavior to account for

See also