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

Decoupled, simple systems.

Connect gameplay systems without direct references between senders and receivers. Untargeted, targeted, and broadcast messages keep those systems independent, testable, and observable, while struct messages keep steady-state dispatch allocation-free.

Free forever. No paid editions, paid upgrades, or feature charges. Read the AI assistance disclosure.

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 GamePaused
{
    public readonly bool IsPaused;
}

// PauseController announces the fact after owning the state change.
GamePaused gamePaused = new GamePaused(isPaused);
gamePaused.EmitUntargeted();

// PauseOverlay owns this UI registration.
Token.RegisterUntargeted<GamePaused>(OnPauseOverlayChanged);

// MusicDirector owns a separate audio registration.
Token.RegisterUntargeted<GamePaused>(OnMusicPauseChanged);
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.Attributes;
using DxMessaging.Core.Extensions;
using DxMessaging.Unity;

[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct CollisionOccurred
{
    public readonly float Speed;
}

// CollisionReporter emits from the GameObject that collided.
CollisionOccurred collisionOccurred = new CollisionOccurred(speed);
collisionOccurred.EmitGameObjectBroadcast(gameObject);

// ImpactSound on the same GameObject owns this registration.
Token.RegisterGameObjectBroadcast<CollisionOccurred>(gameObject, OnImpactSound);

// BreakableObject on the same GameObject owns a separate registration.
Token.RegisterGameObjectBroadcast<CollisionOccurred>(gameObject, OnBreakableCollision);
10+ years in production MIT License zero dependencies O(1) routing zero allocations on steady-state dispatch 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]

First Message

The same three steps in each shape: declare the contract, register a handler, emit. Pick the one that matches what you are wiring up.

FirstMessage.cs
using DxMessaging.Core.Attributes;
using DxMessaging.Core.Extensions;
using DxMessaging.Unity;
using UnityEngine;

[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct GamePaused
{
    public readonly bool IsPaused;
}

public sealed class PauseController : MonoBehaviour
{
    private bool _paused;

    public void TogglePause()
    {
        _paused = !_paused;

        GamePaused gamePaused = new GamePaused(_paused);
        gamePaused.EmitUntargeted();
    }
}

public sealed class TimeScaleDriver : MessageAwareComponent
{
    protected override void RegisterMessageHandlers()
    {
        base.RegisterMessageHandlers();
        _ = Token.RegisterUntargeted<GamePaused>(OnGamePaused);
    }

    private void OnGamePaused(in GamePaused message)
    {
        Time.timeScale = message.IsPaused ? 0f : 1f;
    }
}

public sealed class AudioPauseDriver : MessageAwareComponent
{
    protected override void RegisterMessageHandlers()
    {
        base.RegisterMessageHandlers();
        _ = Token.RegisterUntargeted<GamePaused>(OnGamePaused);
    }

    private void OnGamePaused(in GamePaused message)
    {
        AudioListener.pause = message.IsPaused;
    }
}

One announcement, two unrelated systems. The overlay and the music both react, and PauseController holds a reference to neither. Adding a third listener is one more registration.

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(in 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, 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 damages that exact object without knowing whether it is a player, an enemy, a crate, or a type that does not exist yet.

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

[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct CollisionOccurred
{
    public readonly float Speed;
}

public sealed class CollisionReporter : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        CollisionOccurred collisionOccurred = new CollisionOccurred(
            collision.relativeVelocity.magnitude
        );
        collisionOccurred.EmitGameObjectBroadcast(gameObject);
    }
}

public sealed class ImpactSound : MessageAwareComponent
{
    public AudioSource Source;

    protected override void RegisterMessageHandlers()
    {
        base.RegisterMessageHandlers();
        _ = Token.RegisterGameObjectBroadcast<CollisionOccurred>(gameObject, OnCollisionOccurred);
    }

    private void OnCollisionOccurred(in CollisionOccurred message)
    {
        Source.volume = Mathf.Clamp01(message.Speed / 10f);
        Source.Play();
    }
}

public sealed class BreakableObject : MessageAwareComponent
{
    public float BreakSpeed = 6f;

    protected override void RegisterMessageHandlers()
    {
        base.RegisterMessageHandlers();
        _ = Token.RegisterGameObjectBroadcast<CollisionOccurred>(gameObject, OnCollisionOccurred);
    }

    private void OnCollisionOccurred(in CollisionOccurred message)
    {
        if (BreakSpeed <= message.Speed)
        {
            gameObject.SetActive(false);
        }
    }
}

The fact carries its source, so listeners on the same GameObject hear their own object's collisions and nothing else. Sound and breakage are separate components that never learn about each other.

AI Assistance Disclosure

DxMessaging began as an original project around 2014 and remains actively developed and used today. Over the past two years, I (wallstop) have been using LLMs to help improve the library's quality and performance, expand its tests, and create user-facing documentation. There is always a human in the loop: I actively review all changes and read through all user-facing copy. I share this so you can make an informed decision about using the project.

DxMessaging is free and will remain free forever. No paid editions, paid upgrades, or feature charges.

Start Here

  • Quick Start - Define, register, and emit your first message.
  • Mental Model - Choose between untargeted, targeted, and broadcast messages.
  • Message Monitor - Inspect emissions, filters, contexts, and captured call sites.
  • Flow Graph - Inspect loaded-scene MessagingComponent routes and delivery evidence.
  • Inspector Tools - Catch base-call mistakes and inspect component-local diagnostics.
  • 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.

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 readonly by-reference handlers keep steady-state dispatch at zero allocation. Type-indexed routing stays O(1); see the current CI table for measured scenario costs.

Next