-
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.
- 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(ref 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(ref 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 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(ref 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.RegisterBroadcast<HealthChanged>(...);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 (Inspector shows message history)"
- "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(ref 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:
#if UNITY_EDITOR
IMessageBus.GlobalDiagnosticsMode = true;
#endifProfile 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 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