Skip to content
DxMessaging
Unity 2021.3+  ·  MIT  ·  zero allocation

Decoupled, simple systems.

Send a typed message and any system reacts, with no reference back to the sender. Three message types, about 10 ns per handler, zero GC.

openupm add com.wallstop-studios.dxmessaging
MessageSamples.cs
using DxMessaging.Core.Attributes;
using DxMessaging.Core.Extensions;
using DxMessaging.Unity;

[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct WaveStarted
{
    public readonly int Index;
}

// Register once; token cleanup follows the owner.
Token.RegisterUntargeted<WaveStarted>(OnWaveStarted);

// Send from any system.
WaveStarted waveStarted = new WaveStarted(3);
waveStarted.EmitUntargeted();
using DxMessaging.Core.Attributes;
using DxMessaging.Core.Extensions;
using DxMessaging.Unity;

[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct Heal
{
    public readonly int Amount;
}

// Register on the player that can receive heals.
Token.RegisterGameObjectTargeted<Heal>(gameObject, OnHeal);

// Send from any system.
Heal heal = new Heal(50);
heal.EmitGameObjectTargeted(player);
using DxMessaging.Core;
using DxMessaging.Core.Attributes;
using DxMessaging.Core.Extensions;
using DxMessaging.Unity;
using UnityEngine;

[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct TookDamage
{
    public readonly int Amount;
}

public sealed class DamageFeed : MessageAwareComponent
{
    protected override void RegisterMessageHandlers()
    {
        base.RegisterMessageHandlers();

        // Token belongs to this listener, not the object taking damage.
        _ = Token.RegisterBroadcastWithoutSource<TookDamage>(OnAnyDamage);
    }

    private void OnAnyDamage(InstanceId source, TookDamage message)
    {
        // React to damage from any source.
    }
}

public sealed class EnemyHealth : MonoBehaviour
{
    public void ApplyHit(int amount)
    {
        TookDamage tookDamage = new TookDamage(amount);
        tookDamage.EmitGameObjectBroadcast(gameObject);
    }
}
MIT License zero dependencies O(1) routing zero allocation ~10ns / handler sources OpenUPM · npm · Git

The three message types

01 · UNTARGETED

Announce to all

A global message with no sender and no target. Anything listening hears it. The PA system.

[DxUntargetedMessage]
02 · TARGETED

Command one

A message to one specific GameObject. Only that object receives it. The addressed letter.

[DxTargetedMessage]
03 · BROADCAST

Emit a fact

A fact from a known source. Listeners can filter by who sent it. The radio station.

[DxBroadcastMessage]

Start Here

  • Quick Start - Define, register, and emit your first message.
  • Mental Model - Choose between untargeted, targeted, and broadcast messages.
  • Inspector Tools - Use diagnostics and base-call warnings inside Unity.
  • Message Monitor - Inspect emissions, trace paths, and registration topology.
  • Performance - Read the current published benchmark tables.

Install

OpenUPM

Bash
openupm add com.wallstop-studios.dxmessaging

Git URL

Text Only
https://github.com/Ambiguous-Interactive/DxMessaging.git

See the Install Guide for scoped registry, Git URL, and local tarball options.

First Message

C#
using DxMessaging.Core.Attributes;
using DxMessaging.Core.Extensions;
using DxMessaging.Unity;
using UnityEngine;

[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct DamageRequested
{
    public readonly int Amount;
}

public sealed class DamageReceiver : MessageAwareComponent
{
    public int Health { get; private set; } = 100;

    protected override void RegisterMessageHandlers()
    {
        base.RegisterMessageHandlers();
        _ = Token.RegisterGameObjectTargeted<DamageRequested>(gameObject, OnDamageRequested);
    }

    private void OnDamageRequested(ref DamageRequested message)
    {
        Health = Mathf.Max(0, Health - Mathf.Max(0, message.Amount));
    }
}

public sealed class Hazard : MonoBehaviour
{
    public int Damage = 25;

    private void OnTriggerEnter(Collider other)
    {
        DamageRequested request = new DamageRequested(Damage);
        request.EmitGameObjectTargeted(other.gameObject);
    }
}

On the hazard GameObject, enable Is Trigger on its collider and add a kinematic Rigidbody with Use Gravity disabled. Put DamageReceiver and the entering collider on the same target GameObject. The hazard targets that exact object without knowing whether it is a player, enemy, crate, or future gameplay type. Neither component holds a reference to the other.

Why Teams Use It

Simple primitives

Three message shapes - untargeted, targeted, broadcast - and nothing else to learn. Each contract is an explicit typed struct, and no system holds a reference to any other.

Easy to use

Define a struct, register a handler, emit. Registration tokens follow their owner's lifecycle, so handlers remove themselves - no manual unsubscribe, no leaked listeners.

Small edits, big impact

The same simple primitives decouple entire systems. Wiring a feature in is one registration; removing it is deleting that line. Interceptors, handler priorities, and global observers layer on without touching existing code.

High performance

Struct messages and by-ref handlers keep steady-state dispatch at zero allocation. Type-indexed routing stays O(1), with published results around 10 ns per handler.

Next