Listening Patterns¶
Targeted across all targets¶
- Accept every targeted message of a given type regardless of who it's for.
using DxMessaging.Core; // InstanceId
using DxMessaging.Core.Messages;
// Update the combat feed for every requested heal.
_ = token.RegisterTargetedWithoutTargeting<Heal>(ShowRequestedHeal);
void ShowRequestedHeal(InstanceId target, Heal m) => combatFeed.ShowHeal(target, m.amount);
// Record the request only after message dispatch reaches post-processing.
_ = token.RegisterTargetedWithoutTargetingPostProcessor<Heal>(RecordProcessedHealRequest);
void RecordProcessedHealRequest(InstanceId target, Heal m) =>
metrics.RecordProcessedHealRequest(target, m.amount);
Broadcast across all sources¶
- Accept every broadcast message of a given type regardless of who emitted it.
using DxMessaging.Core; // InstanceId
using DxMessaging.Core.Messages;
// Spawn presentation feedback for every damage source.
_ = token.RegisterBroadcastWithoutSource<TookDamage>(ShowDamageNumber);
void ShowDamageNumber(InstanceId source, TookDamage m) => damageNumbers.Show(source, m.amount);
// Record the processed message for replay after every gameplay handler has run.
_ = token.RegisterBroadcastWithoutSourcePostProcessor<TookDamage>(RecordProcessedDamage);
void RecordProcessedDamage(InstanceId source, TookDamage m) =>
replay.RecordProcessedDamageMessage(source, m.amount);
The handler owns presentation state by spawning a damage number. The post-processor records that dispatch completed. It cannot infer the target's final health from the message payload.
Global accept-all (debug/inspection)¶
- Receive every message of every type on a handler; useful for tooling.
using DxMessaging.Core;
using DxMessaging.Core.Messages;
using DxMessaging.Core.MessageBus;
IMessageBus bus = MessageHandler.MessageBus;
MessageHandler handler = new(new InstanceId(1)) { active = true };
using MessageRegistrationToken token = MessageRegistrationToken.Create(handler, bus);
_ = token.RegisterGlobalAcceptAll(OnUntargeted, OnTargeted, OnBroadcast);
token.Enable();
void OnUntargeted(in IUntargetedMessage message) { /* inspect message */ }
void OnTargeted(in InstanceId target, in ITargetedMessage message) { /* inspect message */ }
void OnBroadcast(in InstanceId source, in IBroadcastMessage message) { /* inspect message */ }
Real-World Use Cases¶
Development Debug Dump¶
Use Message Monitor for recent emissions and Flow Graph for loaded-scene MessagingComponent topology in the Editor. Add a global observer when a runtime build, direct bus registrations, automated export, or custom dashboard needs the same stream in code:
using System;
using DxMessaging.Core;
using DxMessaging.Core.Messages;
using DxMessaging.Core.MessageBus;
using UnityEngine;
public sealed class DebugMessageLogger : IDisposable
{
private readonly MessageHandler _handler;
private readonly MessageRegistrationToken _token;
public DebugMessageLogger(IMessageBus bus)
{
_handler = new MessageHandler(new InstanceId(999), bus) { active = true };
_token = MessageRegistrationToken.Create(_handler, bus);
_ = _token.RegisterGlobalAcceptAll(OnUntargeted, OnTargeted, OnBroadcast);
_token.Enable();
}
private void OnUntargeted(in IUntargetedMessage message)
{
Debug.Log($"[Untargeted] {message.GetType().Name}: {message}");
}
private void OnTargeted(in InstanceId target, in ITargetedMessage message)
{
Debug.Log($"[Targeted -> {target}] {message.GetType().Name}: {message}");
}
private void OnBroadcast(in InstanceId source, in IBroadcastMessage message)
{
Debug.Log($"[Broadcast <- {source}] {message.GetType().Name}: {message}");
}
public void Dispose() => _token.Dispose();
}
Keep the owner alive for the intended observation scope:
#if DEVELOPMENT_BUILD || UNITY_EDITOR
using DebugMessageLogger logger = new(MessageHandler.MessageBus);
#endif
Attribute-Based Network Replication¶
Automatically replicate messages marked with custom attributes across the network:
using System;
using System.Reflection;
using System.Collections.Generic;
using DxMessaging.Core;
using DxMessaging.Core.Attributes;
using DxMessaging.Core.Messages;
using DxMessaging.Core.MessageBus;
using UnityEngine;
// Mark messages that should be replicated
[AttributeUsage(AttributeTargets.Struct)]
public class NetworkedAttribute : Attribute { }
[Networked]
[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct PlayerMoved
{
public readonly Vector3 position;
}
[Networked]
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct DealDamage
{
public readonly float amount;
}
// Network replication handler
public sealed class NetworkReplicator : IDisposable
{
private readonly INetworkManager _network;
private readonly MessageHandler _handler;
private readonly MessageRegistrationToken _token;
private readonly HashSet<Type> _networkedTypes = new();
public NetworkReplicator(INetworkManager network, IMessageBus bus)
{
_network = network;
_handler = new MessageHandler(new InstanceId(1000), bus) { active = true };
_token = MessageRegistrationToken.Create(_handler, bus);
CacheNetworkedTypes();
_ = _token.RegisterGlobalAcceptAll(OnUntargeted, OnTargeted, OnBroadcast);
_token.Enable();
}
private void CacheNetworkedTypes()
{
// Find all message types with [Networked] attribute
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute<NetworkedAttribute>() != null)
{
_networkedTypes.Add(type);
}
}
}
}
private void OnUntargeted(in IUntargetedMessage message)
{
if (_networkedTypes.Contains(message.GetType()))
{
_network.Send(message); // Serialize and send
}
}
private void OnTargeted(in InstanceId target, in ITargetedMessage message)
{
if (_networkedTypes.Contains(message.GetType()))
{
_network.Send(target, message);
}
}
private void OnBroadcast(in InstanceId source, in IBroadcastMessage message)
{
if (_networkedTypes.Contains(message.GetType()))
{
_network.Send(source, message);
}
}
public void Dispose() => _token.Dispose();
}
Use the owner for the full replication scope. Messages marked with [Networked] then replicate without explicit per-type registration:
using NetworkReplicator replicator = new(networkManager, MessageHandler.MessageBus);
var playerMoved = new PlayerMoved(playerPos);
playerMoved.EmitFrom(gameObject);
var dealDamage = new DealDamage(50f);
dealDamage.EmitTargeted(enemyId);
Message Analytics and Metrics¶
Track message frequency and performance across your entire game:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using DxMessaging.Core;
using DxMessaging.Core.Messages;
using DxMessaging.Core.MessageBus;
public sealed class MessageAnalytics : IDisposable
{
private readonly Dictionary<Type, (int count, long totalMs)> _stats = new();
private readonly Stopwatch _stopwatch = new();
private readonly MessageHandler _handler;
private readonly MessageRegistrationToken _token;
public MessageAnalytics(IMessageBus bus)
{
_handler = new MessageHandler(new InstanceId(1001), bus) { active = true };
_token = MessageRegistrationToken.Create(_handler, bus);
_ = _token.RegisterGlobalAcceptAll(OnUntargeted, OnTargeted, OnBroadcast);
_token.Enable();
}
private void OnUntargeted(in IUntargetedMessage message)
{
TrackMessage(message.GetType());
}
private void OnTargeted(in InstanceId target, in ITargetedMessage message)
{
TrackMessage(message.GetType());
}
private void OnBroadcast(in InstanceId source, in IBroadcastMessage message)
{
TrackMessage(message.GetType());
}
private void TrackMessage(Type messageType)
{
_stopwatch.Restart();
// Message processing happens here
_stopwatch.Stop();
if (!_stats.TryGetValue(messageType, out var stat))
{
stat = (0, 0);
}
_stats[messageType] = (stat.count + 1, stat.totalMs + _stopwatch.ElapsedMilliseconds);
}
public void PrintStats()
{
foreach (var kvp in _stats)
{
var avg = kvp.Value.totalMs / (double)kvp.Value.count;
UnityEngine.Debug.Log($"{kvp.Key.Name}: {kvp.Value.count} messages, avg {avg:F2}ms");
}
}
public void Dispose() => _token.Dispose();
}
When to Use Global Accept-All
Yes Good use cases:
- Development-time debugging and logging
- Cross-cutting concerns (analytics, telemetry, metrics)
- Attribute-based systems (networking, serialization, persistence)
- Testing and diagnostics tools
- Message replay/recording systems
Warning: Performance consideration: Global Accept-All handlers are invoked for every message of every type. For performance-sensitive gameplay logic, prefer type-specific registrations which use O(1) lookup instead of O(N) iteration.
No Avoid for:
- Core gameplay logic that only needs specific message types
- Hot paths with thousands of messages per frame
- Production code that can use specific type registrations instead
Tips
- Use across-all listeners for diagnostics, analytics, or cross-cutting observers.
- Prefer specific (target/source) registrations for gameplay logic.
Related