com.wallstop-studios.dxmessaging 2.0.0-rc27.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Docs/Comparisons.md +194 -66
- package/Docs/GettingStarted.md +84 -17
- package/Docs/Overview.md +114 -20
- package/Docs/Patterns.md +41 -2
- package/Docs/Performance.md +9 -9
- package/Docs/VisualGuide.md +75 -10
- package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
- package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +3 -3
- package/Editor/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
- package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +2 -2
- package/Editor/Analyzers/System.Collections.Immutable.dll +0 -0
- package/Editor/Analyzers/System.Reflection.Metadata.dll +0 -0
- package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
- package/README.md +262 -63
- package/Runtime/Core/MessageBus/MessageBus.cs +13 -0
- package/Runtime/Core/MessageHandler.cs +27 -33
- package/Runtime/Core/MessageRegistrationToken.cs +12 -21
- package/Runtime/Unity/MessageAwareComponent.cs +6 -0
- package/Runtime/Unity/MessagingComponent.cs +24 -0
- package/Samples~/Mini Combat/README.md +81 -21
- package/Samples~/Mini Combat/Walkthrough.md +23 -1
- package/Samples~/UI Buttons + Inspector/README.md +55 -12
- package/Tests/Runtime/Core/MessagingComponentLifecycleTests.cs +89 -0
- package/Tests/Runtime/Core/MessagingComponentLifecycleTests.cs.meta +11 -0
- package/Tests/Runtime/Core/ReflexiveMessageWarningTests.cs +86 -0
- package/Tests/Runtime/Core/ReflexiveMessageWarningTests.cs.meta +11 -0
- package/Tests/Runtime/Core/SourceGeneratorNestedTests.cs +1 -1
- package/Tests/Runtime/Core/UntargetedPrefreezeTests.cs +116 -0
- package/Tests/Runtime/Core/UntargetedPrefreezeTests.cs.meta +11 -0
- package/Tests/Runtime/Scripts/Components/ReflexiveReceiverComponent.cs +14 -0
- package/Tests/Runtime/Scripts/Components/ReflexiveReceiverComponent.cs.meta +11 -0
- package/package.json +1 -1
package/Docs/Performance.md
CHANGED
|
@@ -14,15 +14,15 @@ See also: `Docs/DesignAndArchitecture.md#performance-optimizations` for design d
|
|
|
14
14
|
|
|
15
15
|
| Message Tech | Operations / Second | Allocations? |
|
|
16
16
|
| ---------------------------------- | ------------------- | ------------ |
|
|
17
|
-
| Unity | 2,
|
|
18
|
-
| DxMessaging (GameObject) - Normal | 8,
|
|
19
|
-
| DxMessaging (Component) - Normal | 8,
|
|
20
|
-
| DxMessaging (GameObject) - No-Copy | 9,
|
|
21
|
-
| DxMessaging (Component) - No-Copy | 9,
|
|
22
|
-
| DxMessaging (Untargeted) - No-Copy | 14,
|
|
23
|
-
| Reflexive (One Argument) | 2,
|
|
24
|
-
| Reflexive (Two Arguments) | 2,
|
|
25
|
-
| Reflexive (Three Arguments) | 2,
|
|
17
|
+
| Unity | 2,747,000 | Yes |
|
|
18
|
+
| DxMessaging (GameObject) - Normal | 8,412,800 | No |
|
|
19
|
+
| DxMessaging (Component) - Normal | 8,406,200 | No |
|
|
20
|
+
| DxMessaging (GameObject) - No-Copy | 9,358,200 | No |
|
|
21
|
+
| DxMessaging (Component) - No-Copy | 9,177,800 | No |
|
|
22
|
+
| DxMessaging (Untargeted) - No-Copy | 14,922,800 | No |
|
|
23
|
+
| Reflexive (One Argument) | 2,832,800 | No |
|
|
24
|
+
| Reflexive (Two Arguments) | 2,371,200 | No |
|
|
25
|
+
| Reflexive (Three Arguments) | 2,350,600 | No |
|
|
26
26
|
|
|
27
27
|
## macOS
|
|
28
28
|
|
package/Docs/VisualGuide.md
CHANGED
|
@@ -434,35 +434,100 @@ START HERE
|
|
|
434
434
|
|
|
435
435
|
### "Do I always need MessageAwareComponent?"
|
|
436
436
|
|
|
437
|
-
**For Unity:** Yes
|
|
437
|
+
**For Unity:** Yes! It's the easiest way. Think of it like `MonoBehaviour` - you inherit from it and it handles all the messy lifecycle stuff automatically.
|
|
438
438
|
|
|
439
|
-
**For pure C#:** No, you can use `MessageRegistrationToken` directly.
|
|
439
|
+
**For pure C#:** No, you can use `MessageRegistrationToken` directly if you're not in Unity.
|
|
440
|
+
|
|
441
|
+
**Bottom line:** If you're in Unity, just use `MessageAwareComponent`. It'll save you hours of debugging.
|
|
440
442
|
|
|
441
443
|
### "Can I send a message to multiple targets?"
|
|
442
444
|
|
|
443
|
-
**No** - Targeted
|
|
445
|
+
**No** - Targeted messages go to ONE specific entity (like mailing a letter to one address).
|
|
446
|
+
|
|
447
|
+
#### Instead, use
|
|
448
|
+
|
|
449
|
+
- **Untargeted** if literally everyone should hear it (like a megaphone announcement)
|
|
450
|
+
- **Broadcast** if it's from one source and many can observe (like a news broadcast)
|
|
451
|
+
|
|
452
|
+
##### Example
|
|
444
453
|
|
|
445
|
-
|
|
446
|
-
|
|
454
|
+
```csharp
|
|
455
|
+
// ❌ DON'T: Try to target multiple entities
|
|
456
|
+
msg.EmitComponentTargeted(player1);
|
|
457
|
+
msg.EmitComponentTargeted(player2); // Feels wrong, right?
|
|
458
|
+
|
|
459
|
+
// ✅ DO: Use broadcast so everyone can listen
|
|
460
|
+
msg.EmitGameObjectBroadcast(enemy); // Now anyone can observe this enemy
|
|
461
|
+
```
|
|
447
462
|
|
|
448
463
|
### "What if I forget to unsubscribe?"
|
|
449
464
|
|
|
450
|
-
**You can't!**
|
|
465
|
+
**You literally can't forget!** 🎉
|
|
466
|
+
|
|
467
|
+
When your component is destroyed, DxMessaging automatically cleans up. No `OnDestroy()` needed. No memory leaks possible.
|
|
468
|
+
|
|
469
|
+
#### Old way (easy to forget)
|
|
470
|
+
|
|
471
|
+
```csharp
|
|
472
|
+
void OnEnable() { GameManager.OnScoreChanged += Update; }
|
|
473
|
+
void OnDisable() { GameManager.OnScoreChanged -= Update; } // Forgot this? LEAK!
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
##### DxMessaging way (impossible to forget)
|
|
477
|
+
|
|
478
|
+
```csharp
|
|
479
|
+
protected override void RegisterMessageHandlers() {
|
|
480
|
+
_ = Token.RegisterUntargeted<ScoreChanged>(Update);
|
|
481
|
+
}
|
|
482
|
+
// That's it! Automatic cleanup when component dies.
|
|
483
|
+
```
|
|
451
484
|
|
|
452
485
|
### "Is it slower than regular events?"
|
|
453
486
|
|
|
454
|
-
**Barely** (~10ns per handler
|
|
487
|
+
**Barely** (~10ns per handler = 0.00001 milliseconds).
|
|
488
|
+
|
|
489
|
+
#### Put it this way
|
|
490
|
+
|
|
491
|
+
- Regular C# event: ~50ns
|
|
492
|
+
- DxMessaging: ~60ns
|
|
493
|
+
- The difference: Drinking a coffee takes 3 billion nanoseconds
|
|
494
|
+
|
|
495
|
+
You get automatic lifecycle, zero leaks, full observability, and predictable ordering for a 20% overhead that's **completely negligible** in any real game.
|
|
455
496
|
|
|
456
497
|
### "Can I cancel a message?"
|
|
457
498
|
|
|
458
|
-
|
|
499
|
+
#### Yes! That's what interceptors are for
|
|
459
500
|
|
|
460
501
|
```csharp
|
|
461
|
-
|
|
462
|
-
|
|
502
|
+
// Cancel invalid damage
|
|
503
|
+
_ = token.RegisterBroadcastInterceptor<TookDamage>(
|
|
504
|
+
(ref InstanceId source, ref TookDamage msg) => {
|
|
505
|
+
if (msg.amount <= 0) return false; // Cancel invalid damage
|
|
506
|
+
if (IsInvincible(source)) return false; // Cancel during invincibility
|
|
507
|
+
return true; // Allow
|
|
508
|
+
}
|
|
463
509
|
);
|
|
464
510
|
```
|
|
465
511
|
|
|
512
|
+
##### Real-world uses
|
|
513
|
+
|
|
514
|
+
- Block input during cutscenes
|
|
515
|
+
- Cancel damage when invincible
|
|
516
|
+
- Prevent cheating (clamp values)
|
|
517
|
+
- Enforce game rules globally
|
|
518
|
+
|
|
519
|
+
### "Can I see what messages are firing?"
|
|
520
|
+
|
|
521
|
+
#### Yes! Open any component in the Inspector and scroll down
|
|
522
|
+
|
|
523
|
+
You'll see:
|
|
524
|
+
|
|
525
|
+
- Message history (last 50 messages with timestamps)
|
|
526
|
+
- Active registrations (what you're listening to)
|
|
527
|
+
- Call counts (how many times each handler ran)
|
|
528
|
+
|
|
529
|
+
**No more guessing.** You can literally see your event flow in real-time.
|
|
530
|
+
|
|
466
531
|
## ✅ Quick Checklist: Am I Doing It Right
|
|
467
532
|
|
|
468
533
|
- [ ] Using `MessageAwareComponent` for Unity components? ✅
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
fileFormatVersion: 2
|
|
2
|
-
guid:
|
|
2
|
+
guid: ac94a51a66736f443b68aa222d32eb04
|
|
3
3
|
PluginImporter:
|
|
4
4
|
externalObjects: {}
|
|
5
5
|
serializedVersion: 2
|
|
@@ -14,12 +14,12 @@ PluginImporter:
|
|
|
14
14
|
- first:
|
|
15
15
|
Any:
|
|
16
16
|
second:
|
|
17
|
-
enabled:
|
|
17
|
+
enabled: 1
|
|
18
18
|
settings: {}
|
|
19
19
|
- first:
|
|
20
20
|
Editor: Editor
|
|
21
21
|
second:
|
|
22
|
-
enabled:
|
|
22
|
+
enabled: 0
|
|
23
23
|
settings:
|
|
24
24
|
DefaultValueInitialized: true
|
|
25
25
|
- first:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DxMessaging for Unity — The Modern Event System
|
|
2
2
|
|
|
3
|
-
[](https://unity.com/
|
|
3
|
+
[](https://unity.com/releases/editor)<br/>
|
|
4
4
|
[](LICENSE.md)<br/>
|
|
5
5
|
[](package.json)<br/>
|
|
6
6
|
[](Docs/Performance.md)<br/>
|
|
@@ -32,17 +32,27 @@ Think of it as **the event system Unity should have built-in** — one that actu
|
|
|
32
32
|
|
|
33
33
|
## 30-Second Elevator Pitch
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
### If you've ever
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
- Forgotten to unsubscribe from an event and spent hours debugging memory leaks
|
|
38
|
+
- Had UI code tangled with 15 different game systems
|
|
39
|
+
- Wondered "which event fired when?" with no way to see message flow
|
|
40
|
+
- Copy-pasted event boilerplate dozens of times
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
1. **Targeted** - "Hey YOU!" (commands to specific objects)
|
|
41
|
-
1. **Broadcast** - "I did something!" (events from sources)
|
|
42
|
+
#### Then DxMessaging solves your problems
|
|
42
43
|
|
|
43
|
-
**
|
|
44
|
+
- **Zero memory leaks** - automatic lifecycle management, no manual unsubscribe
|
|
45
|
+
- **Zero coupling** - systems communicate without knowing each other exist
|
|
46
|
+
- **Full visibility** - see every message in the Inspector with timestamps and payloads
|
|
47
|
+
- **Complete control** - priority-based ordering, validation, and interception
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
##### Three simple message types
|
|
50
|
+
|
|
51
|
+
1. **Untargeted** - "Everyone listen!" (pause game, settings changed)
|
|
52
|
+
1. **Targeted** - "Tell Player to heal" (commands to specific entities)
|
|
53
|
+
1. **Broadcast** - "Enemy took damage" (events others can observe)
|
|
54
|
+
|
|
55
|
+
**One line:** It's the event system Unity should have shipped with - type-safe, leak-proof, and actually debuggable. 🚀
|
|
46
56
|
|
|
47
57
|
---
|
|
48
58
|
|
|
@@ -152,95 +162,194 @@ Looking for hard numbers? See OS-specific [Performance Benchmarks](Docs/Performa
|
|
|
152
162
|
|
|
153
163
|
## Why DxMessaging
|
|
154
164
|
|
|
155
|
-
### The
|
|
165
|
+
### The Problems You've Probably Hit
|
|
166
|
+
|
|
167
|
+
#### Scenario 1: The Memory Leak Nightmare
|
|
168
|
+
|
|
169
|
+
You write this innocent-looking code:
|
|
170
|
+
|
|
171
|
+
```csharp
|
|
172
|
+
public class GameUI : MonoBehaviour {
|
|
173
|
+
void OnEnable() {
|
|
174
|
+
GameManager.Instance.OnScoreChanged += UpdateScore;
|
|
175
|
+
}
|
|
176
|
+
// Oops, forgot OnDisable... leak! 💀
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Months later: "Why is our game using 2GB of RAM after an hour?"
|
|
181
|
+
|
|
182
|
+
##### Scenario 2: The Spaghetti Mess
|
|
156
183
|
|
|
157
184
|
```csharp
|
|
158
|
-
// C# Events: Manual lifecycle hell
|
|
159
185
|
public class GameUI : MonoBehaviour {
|
|
160
186
|
[SerializeField] private Player player;
|
|
161
187
|
[SerializeField] private EnemySpawner spawner;
|
|
188
|
+
[SerializeField] private InventorySystem inventory;
|
|
189
|
+
[SerializeField] private QuestSystem quests;
|
|
190
|
+
[SerializeField] private AudioManager audio;
|
|
191
|
+
// ... 15 more SerializeFields ...
|
|
162
192
|
|
|
163
193
|
void Awake() {
|
|
164
194
|
player.OnHealthChanged += UpdateHealth;
|
|
165
195
|
spawner.OnWaveStart += ShowWave;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Easy to forget = memory leaks
|
|
170
|
-
player.OnHealthChanged -= UpdateHealth;
|
|
171
|
-
spawner.OnWaveStart -= ShowWave;
|
|
196
|
+
inventory.OnItemAdded += RefreshInventory;
|
|
197
|
+
quests.OnQuestCompleted += ShowQuestNotification;
|
|
198
|
+
// ... 20 more subscriptions ...
|
|
172
199
|
}
|
|
173
200
|
}
|
|
174
201
|
```
|
|
175
202
|
|
|
176
|
-
|
|
203
|
+
**Your UI now depends on EVERYTHING.** Good luck refactoring that.
|
|
204
|
+
|
|
205
|
+
###### Scenario 3: The Debugging Black Hole
|
|
206
|
+
|
|
207
|
+
Player reports: "My health bar didn't update!"
|
|
208
|
+
|
|
209
|
+
You think: "Okay, which of the 47 events touching health failed? And in what order?"
|
|
177
210
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
211
|
+
**30 minutes later:** Still setting breakpoints everywhere...
|
|
212
|
+
|
|
213
|
+
### Common Problems
|
|
214
|
+
|
|
215
|
+
- ❌ **Memory leaks** from forgotten unsubscribes (every Unity dev's nightmare)
|
|
216
|
+
- ❌ **Tight coupling** making refactoring terrifying ("change one thing, break five others")
|
|
217
|
+
- ❌ **No execution order control** ("why does the UI update before the player takes damage?")
|
|
218
|
+
- ❌ **Impossible to debug** ("what fired when?" has no answer)
|
|
219
|
+
- ❌ **Boilerplate overload** (write 50 lines for 3 events)
|
|
182
220
|
|
|
183
221
|
### The DxMessaging Solution
|
|
184
222
|
|
|
223
|
+
#### Same scenarios, zero pain
|
|
224
|
+
|
|
225
|
+
##### Scenario 1: No More Memory Leaks
|
|
226
|
+
|
|
185
227
|
```csharp
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
228
|
+
public class GameUI : MessageAwareComponent {
|
|
229
|
+
protected override void RegisterMessageHandlers() {
|
|
230
|
+
base.RegisterMessageHandlers();
|
|
231
|
+
_ = Token.RegisterUntargeted<ScoreChanged>(UpdateScore);
|
|
232
|
+
}
|
|
233
|
+
// That's it! No manual cleanup needed.
|
|
234
|
+
// Token automatically handles OnEnable/OnDisable/OnDestroy
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Automatic lifecycle = impossible to leak.** 🎉
|
|
239
|
+
|
|
240
|
+
###### Scenario 2: No More Coupling
|
|
241
|
+
|
|
242
|
+
```csharp
|
|
243
|
+
public class GameUI : MessageAwareComponent {
|
|
244
|
+
// Zero SerializeFields! Zero references!
|
|
245
|
+
|
|
246
|
+
protected override void RegisterMessageHandlers() {
|
|
247
|
+
base.RegisterMessageHandlers();
|
|
248
|
+
_ = Token.RegisterUntargeted<HealthChanged>(OnHealth);
|
|
249
|
+
_ = Token.RegisterUntargeted<WaveStarted>(OnWave);
|
|
250
|
+
_ = Token.RegisterUntargeted<ItemAdded>(OnItem);
|
|
251
|
+
// Listen to anything, from anywhere, no coupling
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Your UI is now independent.** Swap systems freely without breaking anything.
|
|
257
|
+
|
|
258
|
+
###### Scenario 3: Debugging is Built In
|
|
259
|
+
|
|
260
|
+
Open any `MessageAwareComponent` in the Inspector:
|
|
189
261
|
|
|
190
|
-
|
|
262
|
+
```text
|
|
263
|
+
Message History (last 50):
|
|
264
|
+
[12:34:56] HealthChanged (amount: 25) → Priority: 0
|
|
265
|
+
[12:34:55] ItemAdded (id: 42, count: 1) → Priority: 5
|
|
266
|
+
[12:34:54] WaveStarted (wave: 3) → Priority: 0
|
|
267
|
+
|
|
268
|
+
Active Registrations:
|
|
269
|
+
✓ HealthChanged (5 handlers)
|
|
270
|
+
✓ ItemAdded (2 handlers)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**See exactly what fired, when, and who handled it.** No guesswork.
|
|
274
|
+
|
|
275
|
+
### How It Transforms Your Code
|
|
276
|
+
|
|
277
|
+
```csharp
|
|
278
|
+
// 1. Define messages (clean, typed, discoverable)
|
|
191
279
|
[DxTargetedMessage]
|
|
192
280
|
[DxAutoConstructor]
|
|
193
281
|
public readonly partial struct Heal { public readonly int amount; }
|
|
194
282
|
|
|
195
283
|
// 2. Listen (automatic lifecycle - zero leaks)
|
|
196
|
-
public class
|
|
284
|
+
public class Player : MessageAwareComponent {
|
|
197
285
|
protected override void RegisterMessageHandlers() {
|
|
198
286
|
base.RegisterMessageHandlers();
|
|
199
287
|
_ = Token.RegisterComponentTargeted<Heal>(this, OnHeal);
|
|
200
288
|
}
|
|
201
289
|
|
|
202
|
-
void OnHeal(ref Heal m)
|
|
290
|
+
void OnHeal(ref Heal m) {
|
|
291
|
+
health += m.amount;
|
|
292
|
+
Debug.Log($"Healed {m.amount}!");
|
|
293
|
+
}
|
|
203
294
|
}
|
|
204
295
|
|
|
205
|
-
// 3. Send (
|
|
206
|
-
var heal = new Heal(
|
|
207
|
-
heal.
|
|
296
|
+
// 3. Send (from anywhere - zero coupling)
|
|
297
|
+
var heal = new Heal(50);
|
|
298
|
+
heal.EmitComponentTargeted(playerComponent);
|
|
208
299
|
```
|
|
209
300
|
|
|
210
|
-
|
|
301
|
+
#### What you get
|
|
211
302
|
|
|
212
|
-
- ✅ **Zero memory leaks** -
|
|
213
|
-
- ✅ **
|
|
214
|
-
- ✅ **
|
|
215
|
-
- ✅ **
|
|
216
|
-
- ✅ **
|
|
217
|
-
- ✅ **Intercept & validate** - enforce rules before handlers run
|
|
303
|
+
- ✅ **Zero memory leaks** - tokens clean up automatically when components are destroyed
|
|
304
|
+
- ✅ **Zero coupling** - no SerializeFields, no GetComponent, no direct references
|
|
305
|
+
- ✅ **Full visibility** - see message flow in Inspector with timestamps and payloads
|
|
306
|
+
- ✅ **Predictable order** - priority-based execution (no more mystery race conditions)
|
|
307
|
+
- ✅ **Type-safe** - compile-time guarantees, refactor with confidence
|
|
308
|
+
- ✅ **Intercept & validate** - enforce rules before handlers run (clamp damage, block invalid input)
|
|
309
|
+
- ✅ **Extension points everywhere** - interceptors, handlers, post-processors with priorities
|
|
218
310
|
|
|
219
311
|
## Killer Features
|
|
220
312
|
|
|
221
|
-
|
|
313
|
+
Why DxMessaging is different:
|
|
314
|
+
|
|
315
|
+
### 🚀 Performance: Zero-Allocation, Zero-Leak Design
|
|
222
316
|
|
|
223
|
-
|
|
317
|
+
**The problem with normal events:** Boxing allocations, GC spikes, memory leaks from forgotten unsubscribes.
|
|
318
|
+
|
|
319
|
+
#### DxMessaging solution
|
|
224
320
|
|
|
225
321
|
```csharp
|
|
226
|
-
void OnDamage(ref TookDamage msg) { //
|
|
227
|
-
health -= msg.amount;
|
|
322
|
+
void OnDamage(ref TookDamage msg) { // Pass by ref = zero allocations
|
|
323
|
+
health -= msg.amount; // No boxing, no GC pressure
|
|
228
324
|
}
|
|
325
|
+
// Automatic cleanup = zero leaks, guaranteed
|
|
229
326
|
```
|
|
230
327
|
|
|
231
|
-
|
|
328
|
+
**Real-world impact:** A game emitting 1000 messages/second uses **zero GC** with DxMessaging vs. 40KB/sec with boxed events.
|
|
329
|
+
|
|
330
|
+
### 🎯 Three Message Types That Actually Make Sense
|
|
331
|
+
|
|
332
|
+
Most event systems force you into one pattern. DxMessaging gives you the right tool for each job:
|
|
232
333
|
|
|
233
334
|
```csharp
|
|
234
|
-
// Untargeted:
|
|
235
|
-
[DxUntargetedMessage]
|
|
335
|
+
// Untargeted: "Everyone, listen up!" (global announcements)
|
|
336
|
+
[DxUntargetedMessage]
|
|
337
|
+
public struct GamePaused { }
|
|
338
|
+
// ↳ Perfect for: settings, scene transitions, global state
|
|
236
339
|
|
|
237
|
-
// Targeted:
|
|
238
|
-
[DxTargetedMessage]
|
|
340
|
+
// Targeted: "Hey Player, do this!" (commands to specific entities)
|
|
341
|
+
[DxTargetedMessage]
|
|
342
|
+
public struct Heal { public int amount; }
|
|
343
|
+
// ↳ Perfect for: UI actions, direct commands, player input
|
|
239
344
|
|
|
240
|
-
// Broadcast:
|
|
241
|
-
[DxBroadcastMessage]
|
|
345
|
+
// Broadcast: "I took damage!" (events others can observe)
|
|
346
|
+
[DxBroadcastMessage]
|
|
347
|
+
public struct TookDamage { public int amount; }
|
|
348
|
+
// ↳ Perfect for: achievements, analytics, UI updates from entities
|
|
242
349
|
```
|
|
243
350
|
|
|
351
|
+
**Why this matters:** You're not forcing everything through one generic "Event<T>" pattern. Each message type has clear semantics.
|
|
352
|
+
|
|
244
353
|
### 🔄 The Message Pipeline
|
|
245
354
|
|
|
246
355
|
Every message flows through 3 stages with priority control:
|
|
@@ -255,46 +364,136 @@ flowchart LR
|
|
|
255
364
|
style PP fill:#eef7ee,stroke:#52c41a
|
|
256
365
|
```
|
|
257
366
|
|
|
258
|
-
### 🎭 Listen to
|
|
367
|
+
### 🎭 Global Observers: Listen to EVERYTHING (Unique Feature!)
|
|
259
368
|
|
|
260
|
-
|
|
369
|
+
**The problem with normal events:** To track all player damage, enemy damage, and NPC damage, you need 3 separate event subscriptions.
|
|
370
|
+
|
|
371
|
+
**DxMessaging's superpower:** Subscribe ONCE to a message type, receive ALL instances with source information:
|
|
261
372
|
|
|
262
373
|
```csharp
|
|
263
|
-
// Track ALL damage,
|
|
374
|
+
// Track ALL damage from ANY source (players, enemies, NPCs, environment)
|
|
264
375
|
_ = token.RegisterBroadcastWithoutSource<TookDamage>(
|
|
265
376
|
(InstanceId source, TookDamage msg) => {
|
|
377
|
+
Debug.Log($"{source} took {msg.amount} damage!");
|
|
266
378
|
Analytics.LogDamage(source, msg.amount);
|
|
379
|
+
CheckAchievements(source, msg.amount);
|
|
267
380
|
}
|
|
268
381
|
);
|
|
269
382
|
```
|
|
270
383
|
|
|
271
|
-
|
|
384
|
+
#### Real-world use cases
|
|
385
|
+
|
|
386
|
+
- **Achievement system:** Track all kills, deaths, damage across the entire game
|
|
387
|
+
- **Combat log:** "Player took 25 damage, Enemy3 took 50 damage, Boss took 100 damage"
|
|
388
|
+
- **Analytics:** Aggregate stats from all entities without knowing about them upfront
|
|
389
|
+
- **Debug tools:** Watch ALL messages in the Inspector without instrumenting code
|
|
390
|
+
|
|
391
|
+
**Why this is revolutionary:** Traditional event buses require you to know entity types upfront. DxMessaging lets you observe dynamically.
|
|
392
|
+
|
|
393
|
+
### 🛡️ Interceptors: Validate Before Execution (Safety Built In)
|
|
394
|
+
|
|
395
|
+
**The problem with normal events:** Validation logic duplicated in every handler, or bugs when you forget.
|
|
396
|
+
|
|
397
|
+
**DxMessaging solution:** Validate ONCE before ANY handler runs:
|
|
272
398
|
|
|
273
399
|
```csharp
|
|
274
|
-
//
|
|
400
|
+
// ONE interceptor protects ALL handlers
|
|
275
401
|
_ = token.RegisterBroadcastInterceptor<TookDamage>(
|
|
276
402
|
(ref InstanceId src, ref TookDamage msg) => {
|
|
277
|
-
if (msg.amount <= 0) return false;
|
|
278
|
-
|
|
403
|
+
if (msg.amount <= 0) return false; // Cancel invalid
|
|
404
|
+
if (msg.amount > 999) {
|
|
405
|
+
msg = new TookDamage(999); // Clamp excessive
|
|
406
|
+
}
|
|
407
|
+
if (IsGodModeActive(src)) return false; // Block damage
|
|
279
408
|
return true;
|
|
280
|
-
}
|
|
409
|
+
},
|
|
410
|
+
priority: -100 // Run FIRST
|
|
281
411
|
);
|
|
412
|
+
|
|
413
|
+
// Now ALL handlers receive clean, validated data
|
|
414
|
+
_ = token.RegisterComponentTargeted<TookDamage>(player, OnDamage);
|
|
415
|
+
void OnDamage(ref TookDamage msg) {
|
|
416
|
+
// No validation needed - interceptor guarantees validity!
|
|
417
|
+
health -= msg.amount;
|
|
418
|
+
}
|
|
282
419
|
```
|
|
283
420
|
|
|
284
|
-
|
|
421
|
+
#### Real-world use cases
|
|
422
|
+
|
|
423
|
+
- Clamp/normalize values (damage, healing, speeds)
|
|
424
|
+
- Enforce game rules ("can't heal above max health")
|
|
425
|
+
- Block messages during cutscenes
|
|
426
|
+
- Log/audit sensitive actions
|
|
285
427
|
|
|
286
|
-
|
|
428
|
+
### 🔍 Built-in Inspector Diagnostics (Actually Debuggable!)
|
|
429
|
+
|
|
430
|
+
**The problem with normal events:** "Which event fired? When? Who handled it? In what order?" = 🤷
|
|
431
|
+
|
|
432
|
+
**DxMessaging solution:** Click any `MessageAwareComponent` in the Inspector:
|
|
433
|
+
|
|
434
|
+
```text
|
|
435
|
+
┌─────────────────────────────────────────────────────┐
|
|
436
|
+
│ Message History (last 50) │
|
|
437
|
+
├─────────────────────────────────────────────────────┤
|
|
438
|
+
│ [12:34:56.123] HealthChanged │
|
|
439
|
+
│ → amount: 25 │
|
|
440
|
+
│ → priority: 0 │
|
|
441
|
+
│ → handlers: 3 │
|
|
442
|
+
│ │
|
|
443
|
+
│ [12:34:55.987] ItemAdded │
|
|
444
|
+
│ → itemId: 42, count: 1 │
|
|
445
|
+
│ → priority: 5 │
|
|
446
|
+
│ → handlers: 2 │
|
|
447
|
+
├─────────────────────────────────────────────────────┤
|
|
448
|
+
│ Active Registrations │
|
|
449
|
+
├─────────────────────────────────────────────────────┤
|
|
450
|
+
│ ✓ HealthChanged (priority: 0, called: 847 times) │
|
|
451
|
+
│ ✓ ItemAdded (priority: 5, called: 23 times) │
|
|
452
|
+
│ ✓ TookDamage (priority: 10, called: 1,203 times) │
|
|
453
|
+
└─────────────────────────────────────────────────────┘
|
|
454
|
+
```
|
|
287
455
|
|
|
288
|
-
|
|
456
|
+
#### Real-world debugging scenarios
|
|
289
457
|
|
|
290
|
-
|
|
458
|
+
- "Did my message fire?" → Check history, see timestamp
|
|
459
|
+
- "Why didn't my handler run?" → Check registrations, see if it's active
|
|
460
|
+
- "What's firing too often?" → Sort by call count
|
|
461
|
+
- "What's the execution order?" → Sort by priority
|
|
462
|
+
|
|
463
|
+
**No more:** Setting 50 breakpoints and stepping through code for 30 minutes.
|
|
464
|
+
|
|
465
|
+
### 🏝️ Local Bus Islands for Testing (Actually Testable!)
|
|
466
|
+
|
|
467
|
+
**The problem with normal events:** Global static events contaminate tests. Mock hell. Flaky tests.
|
|
468
|
+
|
|
469
|
+
**DxMessaging solution:** Each test gets its own isolated message bus:
|
|
291
470
|
|
|
292
471
|
```csharp
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
//
|
|
472
|
+
[Test]
|
|
473
|
+
public void TestAchievementSystem() {
|
|
474
|
+
// Create isolated bus - zero global state
|
|
475
|
+
var testBus = new MessageBus();
|
|
476
|
+
var handler = new MessageHandler(new InstanceId(1)) { active = true };
|
|
477
|
+
var token = MessageRegistrationToken.Create(handler, testBus);
|
|
478
|
+
|
|
479
|
+
// Test in isolation
|
|
480
|
+
_ = token.RegisterBroadcastWithoutSource<EnemyKilled>(achievements.OnKill);
|
|
481
|
+
|
|
482
|
+
var msg = new EnemyKilled("Boss");
|
|
483
|
+
msg.EmitGameObjectBroadcast(enemy, testBus); // Only this test sees it
|
|
484
|
+
|
|
485
|
+
Assert.IsTrue(achievements.Unlocked("BossSlayer"));
|
|
486
|
+
}
|
|
487
|
+
// Bus destroyed, zero cleanup needed
|
|
296
488
|
```
|
|
297
489
|
|
|
490
|
+
#### Why this matters
|
|
491
|
+
|
|
492
|
+
- Tests don't interfere with each other
|
|
493
|
+
- No "arrange/act/cleanup" boilerplate
|
|
494
|
+
- No mocking frameworks needed
|
|
495
|
+
- Parallel test execution works perfectly
|
|
496
|
+
|
|
298
497
|
## Documentation
|
|
299
498
|
|
|
300
499
|
### 🎓 Learn
|
|
@@ -7,6 +7,7 @@ namespace DxMessaging.Core.MessageBus
|
|
|
7
7
|
using System.Runtime.CompilerServices;
|
|
8
8
|
using DataStructure;
|
|
9
9
|
using Diagnostics;
|
|
10
|
+
using DxMessaging.Core;
|
|
10
11
|
using Extensions;
|
|
11
12
|
using Helper;
|
|
12
13
|
using Messages;
|
|
@@ -177,6 +178,7 @@ namespace DxMessaging.Core.MessageBus
|
|
|
177
178
|
);
|
|
178
179
|
|
|
179
180
|
private bool _diagnosticsMode = GlobalDiagnosticsMode;
|
|
181
|
+
private bool _loggedReflexiveWarning;
|
|
180
182
|
|
|
181
183
|
public Action RegisterUntargeted<T>(MessageHandler messageHandler, int priority = 0)
|
|
182
184
|
where T : IUntargetedMessage
|
|
@@ -1131,6 +1133,17 @@ namespace DxMessaging.Core.MessageBus
|
|
|
1131
1133
|
|
|
1132
1134
|
if (typeof(TMessage) == typeof(ReflexiveMessage))
|
|
1133
1135
|
{
|
|
1136
|
+
if (!_loggedReflexiveWarning)
|
|
1137
|
+
{
|
|
1138
|
+
_loggedReflexiveWarning = true;
|
|
1139
|
+
if (MessagingDebug.enabled)
|
|
1140
|
+
{
|
|
1141
|
+
MessagingDebug.Log(
|
|
1142
|
+
LogLevel.Warn,
|
|
1143
|
+
"ReflexiveMessage dispatch traverses the Unity hierarchy and is significantly slower than typed messages. Prefer targeted or broadcast messages where possible."
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1134
1147
|
#if UNITY_2017_1_OR_NEWER
|
|
1135
1148
|
ref ReflexiveMessage reflexiveMessage = ref Unsafe.As<TMessage, ReflexiveMessage>(
|
|
1136
1149
|
ref typedMessage
|