-
Notifications
You must be signed in to change notification settings - Fork 0
Guides Migration Guide
This guide helps you introduce DxMessaging into an existing Unity project gradually and pragmatically. You don't need to rewrite everything at once.
MessageAwareComponent no longer registers two targeted StringMessage handlers and one
untargeted GlobalStringMessage handler by default. Most components need no migration. If a
component overrides the inherited string handler methods, add
protected override bool RegisterForStringMessages => true;.
Keep calling base.RegisterMessageHandlers() so registrations declared by parent components run.
DxMessaging 4.0 makes fast handlers and post-processors readonly. Change explicit handler
parameters from ref to in:
// 3.x
void OnDamageV3(ref ApplyDamage message) { }
// 4.0
void OnDamageV4(in ApplyDamage message) { }To update project scripts automatically, choose Tools / Wallstop Studios / DxMessaging /
Upgrade 3.x Fast Handlers to 4.0. The command previews the number of changes before writing. It
scans C# scripts under Assets, updates callbacks used by recognized DxMessaging registrations,
explicit FastHandler delegates, and the changed MessageAwareComponent overrides. When it
updates an inherited string-handler override, it also adds
protected override bool RegisterForStringMessages => true; to preserve the 3.x behavior. It
keeps an existing opt-in, opt-out, or computed property unchanged. It does not change package or
generated sources, interceptors, emission calls, comments, or strings.
The upgrade tool preserves UTF-8, including an optional byte-order mark, and BOM-marked UTF-16 or
UTF-32 encoding. It also keeps line endings unchanged and checks for edits immediately before each
replacement. It attempts to restore every earlier write if a later write fails. If a script changes
again before rollback, the tool preserves the newer edit and its backup instead of overwriting it. It
leaves unsupported encodings, overloads, qualified or cross-file callback declarations, and
ambiguous receivers unchanged. It also leaves a callback unchanged when its newly readonly
parameter is passed as a ref or out argument, except for the matching base call in a changed
MessageAwareComponent override, which it can update safely. The tool detects callbacks passed
directly to both a handler and an interceptor in the same script. It lists these cases in the
Console for manual review. Partial MessageAwareComponent declarations also require manual review
before adding the string-handler opt-in because another part can already declare the property.
Review project-wide callback uses, inspect the diff, and let Unity compile before committing the
migration.
Apply the same change to both parameters of callbacks that receive a target or source:
void OnDamage(in InstanceId source, in TookDamage message) { }Overrides of MessageAwareComponent.HandleStringGameObjectMessage,
HandleStringComponentMessage, and HandleGlobalStringMessage must also change their message
parameter from ref to in.
Keep interceptor parameters and emitted message variables as ref. Interceptors remain the
supported place to mutate a payload or its routing context before handlers observe it.
Rebuild precompiled assemblies and plugins that reference MessageHandler.FastHandler<T>,
MessageHandler.FastHandlerWithContext<T>, or the changed MessageAwareComponent virtual methods
because their metadata changed. Prefer readonly struct messages and readonly instance members;
calling a non-readonly member through an in parameter can make the compiler create a defensive
copy. Roslynator rule RCS1242 also reports an in parameter whose concrete struct type is not
declared readonly. Mark the message struct readonly when its fields and behavior are already
immutable. Keep intentionally mutable messages mutable for interceptor use and configure RCS1242
for those handler declarations instead. For class messages, in protects the reference itself but
does not make the referenced object's fields immutable.
- Rip out all C# events and rewrite everything
- Force the whole team to learn it before trying it
- Commit to full adoption before seeing benefits
- Pick ONE system to migrate (low risk, high visibility)
- Let old and new approaches coexist
- Expand usage as team comfort grows
- Evaluate after each migration step
-
Install DxMessaging via Package Manager
-
Read the Getting-Started-Visual-Guide (5 minutes)
-
Import the Mini Combat sample from Package Manager
-
Create a throwaway test scene and try:
[DxUntargetedMessage] [DxAutoConstructor] public readonly partial struct TestMessage { public readonly int value; } public class TestListener : MessageAwareComponent { protected override void RegisterMessageHandlers() { base.RegisterMessageHandlers(); _ = Token.RegisterUntargeted<TestMessage>(OnTest); } void OnTest(in TestMessage m) => Debug.Log($"Got {m.value}"); } // In another script: var msg = new TestMessage(42); msg.Emit();
Success criteria: You understand the basic flow and have no build errors.
- New UI system - Add a new settings menu that reacts to game state
- Achievement/analytics system - Listen to existing events without coupling
- New game mode - Implement it with DxMessaging from scratch
// 1. Define messages for interesting events (don't touch existing code yet)
[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct EnemyKilled {
public readonly string enemyType;
public readonly int playerLevel;
}
// 2. Make your NEW achievement system listen
public class AchievementSystem : MessageAwareComponent {
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterBroadcastWithoutSource<EnemyKilled>(OnEnemyKilled);
}
void OnEnemyKilled(InstanceId source, EnemyKilled msg) {
// Track kills, unlock achievements
if (msg.enemyType == "Boss") UnlockAchievement("BossSlayer");
}
}
// 3. Bridge from existing code (minimal change)
public class Enemy : MonoBehaviour {
public event Action OnDied; // OLD - keep for now
void Die() {
OnDied?.Invoke(); // OLD code still works
// NEW: Emit DxMessage too
var msg = new EnemyKilled(enemyType, PlayerStats.Level);
msg.EmitGameObjectBroadcast(gameObject);
}
}- Old code still works (zero risk)
- New system is decoupled
- Team sees immediate value (achievements without wiring)
- Easy to roll back if needed
- UI that references too many systems - Replace with message listeners
- Global static event buses - Convert to DxMessaging
- Memory-leak prone event chains - Eliminate manual unsubscribe
// Old event (keep for now)
public event Action<int> OnHealthChanged;
// New message
[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct HealthChanged { public readonly int newHealth; }
void TakeDamage(int amount) {
health -= amount;
// Fire both during migration
OnHealthChanged?.Invoke(health); // OLD
var msg = new HealthChanged(health);
msg.EmitGameObjectBroadcast(gameObject); // NEW
}// OLD listener (comment out when ready)
// void Awake() { player.OnHealthChanged += UpdateBar; }
// void OnDestroy() { player.OnHealthChanged -= UpdateBar; }
// NEW listener
public class HealthBar : MessageAwareComponent {
[SerializeField] private GameObject playerObject;
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterGameObjectBroadcast<HealthChanged>(playerObject, OnHealthChanged);
}
void OnHealthChanged(in HealthChanged msg) => UpdateBar(msg.newHealth);
}// Delete after confirming no one uses it:
// public event Action<int> OnHealthChanged; L
void TakeDamage(int amount) {
health -= amount;
var msg = new HealthChanged(health);
msg.EmitGameObjectBroadcast(gameObject); // Only this now
}Use this for each system you migrate:
System: _________________
[ ] Identified all listeners to migrate
[ ] Defined DxMessages for all events
[ ] Added DxMessage emissions (parallel with old events)
[ ] Migrated listeners one-by-one
[ ] Tested thoroughly
[ ] Removed old event declarations
[ ] Updated documentation/comments
- All new cross-system communication uses DxMessaging
- Old code migrates opportunistically (when touched)
- Code reviews check for messaging best practices
When to use DxMessaging (for new code):
- Any UI listening to game state -> DxMessaging
- Any analytics/logging -> DxMessaging
- Any cross-scene communication -> DxMessaging
- Any event with 2+ listeners -> DxMessaging
When to use direct references/events:
- Simple UI button -> method call (use UnityEvents)
- Single listener, same GameObject -> direct reference
- Private implementation details -> keep internal
DxMessaging 3.0 adds a memory-reclamation subsystem that resets empty
per-message-type and per-context slots so long-running sessions do not
retain a slot for every message type or InstanceId ever touched. The
2.x dispatch surface is unchanged; the new pieces are opt-in tuning and
diagnostics.
-
Optional
DxMessagingRuntimeSettingsasset. A ScriptableObject loaded viaResources.Load<DxMessagingRuntimeSettings>("DxMessagingRuntimeSettings"). UseAssets > Create > Wallstop Studios > DxMessaging > Runtime Settings (in Resources)to drop the asset underAssets/Resources/. Without an asset the runtime hands out a defaulted instance, so the 2.x out-of-the-box behavior is preserved. -
New
TrimandTrimAllAPI.IMessageBus.Trim(bool force = false)and the convenience wrapperMessageHandler.TrimAll(bool force = false)reclaim empty slots and pooled collections synchronously. Both return aTrimResultreporting how much was reclaimed. -
New
OccupiedTypeSlotsandOccupiedTargetSlotscounters onIMessageBus. Aggregated read-only counters for use in diagnostics and leak-watching tests.
- The defaults match 2.x behavior with no asset present: idle eviction is on with a 30 second threshold, the explicit Trim API is on, pool retention is LRU at 512 entries.
- No existing dispatch, registration, or interceptor code changes. Active registrations are never reclaimed; the sweep only resets empty slots.
- The settings asset hot-reloads through
DxMessagingRuntimeSettings.SettingsChanged. Existing buses re-apply caps without recreation, so editing the asset during Play mode does not invalidate registrations.
-
Shipped titles or dedicated servers running for hours. Drop the asset
in to bound retained slot memory; tune
IdleEvictionSecondsandBufferMaxDistinctEntriesfor the workload. -
Editor sessions across many scene loads. Call
MessageHandler.TrimAll(force: true)at scene unload to keep the occupancy counters honest. -
Leak diagnosis. Snapshot
OccupiedTypeSlots/OccupiedTargetSlots, run the operation, force a trim, then compare. Surviving slots correspond to active registrations.
For tuning, scenario tables, and worked examples, see the Guides-Memory-Reclamation. For asset parameters, defaults, and the full diagnostic API, see the Reference-Runtime-Settings.
public class LegacyBridge : MonoBehaviour {
[SerializeField] private LegacySystem legacySystem;
void Awake() {
// Old system fires event, we convert to message
legacySystem.OnSomethingHappened += (args) => {
var msg = new SomethingHappened(args);
msg.Emit();
};
}
}public class ModernBridge : MessageAwareComponent {
public event Action<int> LegacyEvent; // For old code that needs events
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterUntargeted<NewMessage>(OnMessage);
}
void OnMessage(in NewMessage msg) {
LegacyEvent?.Invoke(msg.value); // Fire old event
}
}// Phase 1: Keep old inspector references, emit messages
public class Player : MonoBehaviour {
[SerializeField] private HealthBar healthBar; // OLD - will remove later
void TakeDamage(int amount) {
health -= amount;
healthBar.UpdateHealth(health); // OLD direct call
var msg = new HealthChanged(health);
msg.EmitGameObjectBroadcast(gameObject); // NEW message
}
}// Phase 2: Remove direct references
public class Player : MonoBehaviour {
// [SerializeField] private HealthBar healthBar; -> DELETED
void TakeDamage(int amount) {
health -= amount;
var msg = new HealthChanged(health);
msg.EmitGameObjectBroadcast(gameObject); // Only this
}
}- New systems - No refactor needed, immediate win
- Analytics/logging - Decoupled observers, zero disruption
- UI that needs to listen to many systems - Eliminate reference spaghetti
- Global event buses - Direct replacement, clear improvement
- Stable, working code - If it ain't broke, don't rush
- Performance-critical paths - Validate overhead first
- Code that rarely changes - Low ROI for migration
- Third-party integrations - Keep adapters simple
- Simple button onClick -> method - UnityEvents are fine
- Private implementation details - Internal events are okay
- Single-listener, same-GameObject - Direct references are clearer
- Legacy systems about to be deleted - Why bother?
Problem: "Let's rewrite the entire codebase!"
Solution: Migrate incrementally. Set a rule: "One system per sprint" or "New features only."
Problem: Full commit before proving value.
Solution: Keep old code commented for 1-2 sprints:
// OLD (keep until 2024-02-01)
// player.OnHealthChanged += UpdateBar;
// NEW
_ = Token.RegisterGameObjectBroadcast<HealthChanged>(playerObject, OnHealthChanged);Problem: Using Untargeted for everything because it's "simpler."
Solution: Follow message type guidelines:
- Global state? Untargeted
- Command to one? Targeted
- Event from one? Broadcast
Problem: Converting every method call to a message.
Solution: Keep simple things simple:
// L OVERKILL - Just call the method!
var msg = new CloseDoorMessage(doorId);
msg.Emit();
// BETTER - Direct reference is fine
door.Close();Problem: Team doesn't understand when/how to use it.
- Schedule a 30-minute walkthrough
- Share the Getting-Started-Visual-Guide
- Pair program on first migrations
- Document team conventions in your wiki
Track these to validate migration is worthwhile:
- Lines of event subscribe/unsubscribe code removed
- Number of SerializedField references eliminated
- Memory leaks fixed (profiler)
- Time to add new observers (before/after)
- Ease of debugging message flow
- Team satisfaction (survey)
to "Before: Adding achievement tracking required touching 12 files. to After: Added achievement system with zero changes to existing code."
- Week 1: Experiment + add to one new feature
- Week 2-3: Migrate high-pain UI systems
- Week 4+: New code uses DxMessaging
- Month 1: Pilot with 2-3 systems
- Month 2-4: Gradual migration of problem areas
- Month 5+: Standard practice for new code
- Quarter 1: Pilot + evangelize
- Quarter 2-3: Migrate critical systems
- Quarter 4+: Opportunistic refactors
- "Reduces memory leaks and hard-to-debug issues"
- "Faster feature development (decoupled systems)"
- "Easier onboarding (clear message contracts)"
- "No more manual unsubscribe hell"
- "Built-in debugging (Message Monitor shows emissions and Flow Graph shows loaded-scene component routes)"
- "Add features without touching existing code"
- "Easier to reproduce bugs (message logs)"
- "Fewer null reference errors"
- "Clear system boundaries"
No! DxMessaging coexists happily with C# events, UnityEvents, and direct references. Migrate what benefits, leave what works.
Keep old events during migration. If you hate it, delete the DxMessaging parts and uncomment the old code.
Phase them out gradually:
- Keep references during transition
- Emit messages alongside old calls
- Migrate listeners
- Remove references in next refactor pass
Once the bus/provider abstractions are in place, wire listeners through the registration builder instead of hand-rolling handler lifecycles. Benefits:
- Container-managed lifetimes (
IDisposable,IInitializable,IStartable, etc.) automatically enable/disable registrations. - Centralises diagnostics toggles and message bus selection.
- Keeps MonoBehaviours and pure C# services on the same path.
public sealed class InventoryService : IStartable, IDisposable
{
private readonly MessageRegistrationLease lease;
public InventoryService(IMessageRegistrationBuilder registrationBuilder)
{
lease = registrationBuilder.Build(new MessageRegistrationBuildOptions
{
Configure = token =>
{
_ = token.RegisterUntargeted<InventoryChanged>(OnInventoryChanged);
}
});
}
public void Start()
{
lease.Activate();
}
public void Dispose()
{
lease.Dispose();
}
private static void OnInventoryChanged(in InventoryChanged message)
{
// respond to updates
}
}-
Unity scene code: Call
MessagingComponent.CreateRegistrationBuilder()during dependency injection and share the lease across helper services or pooled objects. -
Container shims: Define
ZENJECT_PRESENT,VCONTAINER_PRESENT, orREFLEX_PRESENTto enable the built-in installers/extensions that register the builder automatically when those frameworks are present. - Tests: Prefer the builder to create isolated tokens tied to the test fixture lifecycle.
Yes! Tests benefit from isolated message buses:
var testBus = new MessageBus();
var token = MessageRegistrationToken.Create(handler, testBus);
// Test in isolationEnable diagnostics only in Editor:
// DiagnosticsTarget.Editor scopes diagnostics to the Unity Editor automatically;
// player builds leave them off, so no #if UNITY_EDITOR guard is needed.
IMessageBus.GlobalDiagnosticsTargets = DiagnosticsTarget.Editor;Profile early, measure impact.
- Try Phase 0 - Install and experiment (today)
- Pick one system - Choose a low-risk, high-value target (this week)
- Timebox it - Give yourself 2 weeks to evaluate
- Measure results - Did it make life better?
- Expand or abort - Based on evidence, not hope
Remember: Migration is a journey, not a destination. Go at your own pace.
Questions? See Reference-Faq | Need patterns? See Guides-Patterns
- Getting-Started-Overview
- Getting-Started-Getting-Started
- Getting-Started-Install
- Getting-Started-Quick-Start
- Getting-Started-Visual-Guide
- Concepts-Message-Types
- Concepts-Listening-Patterns
- Concepts-Targeting-And-Context
- Concepts-Interceptors-And-Ordering
- Guides-Patterns
- Guides-Unity-Integration
- Guides-Testing
- Guides-Diagnostics
- Guides-Advanced
- Guides-Migration-Guide
- Advanced-Emit-Shorthands
- Advanced-Message-Bus-Providers
- Advanced-Runtime-Configuration
- Advanced-String-Messages
- Reference-Reference
- Reference-Quick-Reference
- Reference-Helpers
- Reference-Faq
- Reference-Glossary
- Reference-Troubleshooting
- Reference-Compatibility
Links