com.wallstop-studios.dxmessaging 2.0.0-rc22 → 2.0.0-rc23

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/README.md CHANGED
@@ -39,12 +39,15 @@ For UntargetedMessages, DxMessaging is significantly faster (roughly 2x) than Un
39
39
 
40
40
  | Message Tech | Operations / Second | Allocations? |
41
41
  | ------------ | ------------------- | ------------ |
42
- | Unity | 2,566,400 | Yes |
43
- | DxMessaging (GameObject) - Normal | 2,726,400 | No |
44
- | DxMessaging (Component) - Normal | 2,749,000 | No |
45
- | DxMessaging (GameObject) - No-Copy | 2,883,800 | No |
46
- | DxMessaging (Component) - No-Copy | 2,855,200 | No |
47
- | DxMessaging (Untargeted) - No-Copy | 4,499,000 | No |
42
+ | Unity | 2,670,600 | Yes |
43
+ | DxMessaging (GameObject) - Normal | 2,722,400 | No |
44
+ | DxMessaging (Component) - Normal | 2,738,800 | No |
45
+ | DxMessaging (GameObject) - No-Copy | 2,871,800 | No |
46
+ | DxMessaging (Component) - No-Copy | 2,876,600 | No |
47
+ | DxMessaging (Untargeted) - No-Copy | 4,480,200 | No |
48
+ | Reflexive (One Argument) | 2,287,800 | No |
49
+ | Reflexive (Two Arguments) | 1,009,000 | No |
50
+ | Reflexive (Three Arguments) | 1,000,400 | No |
48
51
 
49
52
  # Functionality
50
53
  While not as fast, DxMessaging offers *additional functionality* as compared to Unity's messaging solution.
@@ -0,0 +1,37 @@
1
+ namespace DxMessaging.Core.Extensions
2
+ {
3
+ using System;
4
+ using System.Runtime.CompilerServices;
5
+
6
+ public static class EnumExtensions
7
+ {
8
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
9
+ public static bool HasFlagNoAlloc<T>(this T value, T flag)
10
+ where T : unmanaged, Enum
11
+ {
12
+ ulong valueUnderlying = GetUInt64(value);
13
+ ulong flagUnderlying = GetUInt64(flag);
14
+ return (valueUnderlying & flagUnderlying) == flagUnderlying;
15
+ }
16
+
17
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
18
+ private static unsafe ulong GetUInt64<T>(T value)
19
+ where T : unmanaged
20
+ {
21
+ /*
22
+ Works because T is constrained to unmanaged, so it's safe to reinterpret
23
+ All enums are value types and have a fixed size
24
+ */
25
+ return sizeof(T) switch
26
+ {
27
+ 1 => *(byte*)&value,
28
+ 2 => *(ushort*)&value,
29
+ 4 => *(uint*)&value,
30
+ 8 => *(ulong*)&value,
31
+ _ => throw new ArgumentException(
32
+ $"Unsupported enum size: {sizeof(T)} for type {typeof(T)}"
33
+ ),
34
+ };
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: f01ee914d7634a69a85f86bb02f2f07e
3
+ timeCreated: 1745162639
@@ -26,9 +26,7 @@
26
26
  public InstanceId(int id)
27
27
  {
28
28
  _id = id;
29
- #if UNITY_2017_1_OR_NEWER
30
29
  Object = null;
31
- #endif
32
30
  }
33
31
 
34
32
  #if UNITY_2017_1_OR_NEWER
@@ -1,6 +1,7 @@
1
1
  namespace DxMessaging.Core.MessageBus
2
2
  {
3
3
  using System;
4
+ using System.Threading;
4
5
  using Core;
5
6
  using Messages;
6
7
 
@@ -9,7 +10,10 @@
9
10
  /// </summary>
10
11
  public interface IMessageBus
11
12
  {
12
- public static int GlobalSequentialIndex = -1;
13
+ internal static int GlobalSequentialIndex = -1;
14
+
15
+ protected static int GenerateNewGlobalSequentialIndex() =>
16
+ Interlocked.Increment(ref GlobalSequentialIndex);
13
17
 
14
18
  public int RegisteredGlobalSequentialIndex { get; }
15
19
  public int RegisteredBroadcast { get; }
@@ -19,7 +23,7 @@
19
23
  public int RegisteredUntargeted { get; }
20
24
 
21
25
  /// <summary>
22
- /// Given an Untargeted message, determines whether or not it should be processed or skipped
26
+ /// Given an Untargeted message, determines whether it should be processed or skipped
23
27
  /// </summary>
24
28
  /// <typeparam name="TMessage">Specific type of message.</typeparam>
25
29
  /// <param name="message">Message to consider.</param>
@@ -28,7 +32,7 @@
28
32
  where TMessage : IUntargetedMessage;
29
33
 
30
34
  /// <summary>
31
- /// Given a Targeted message and its target, determines whether or not it should be processed or skipped.
35
+ /// Given a Targeted message and its target, determines whether it should be processed or skipped.
32
36
  /// </summary>
33
37
  /// <typeparam name="TMessage">Specific type of message.</typeparam>
34
38
  /// <param name="target">Target of the message.</param>
@@ -41,7 +45,7 @@
41
45
  where TMessage : ITargetedMessage;
42
46
 
43
47
  /// <summary>
44
- /// Given a Broadcast message and its source, determines whether or not it should be processed or skipped.
48
+ /// Given a Broadcast message and its source, determines whether it should be processed or skipped.
45
49
  /// </summary>
46
50
  /// <typeparam name="TMessage">Specific type of message.</typeparam>
47
51
  /// <param name="source">Source of the message.</param>
@@ -2,11 +2,16 @@
2
2
  {
3
3
  using System;
4
4
  using System.Collections.Generic;
5
+ using System.Linq.Expressions;
5
6
  using System.Reflection;
6
- using System.Threading;
7
+ using System.Runtime.CompilerServices;
8
+ using Extensions;
7
9
  using Helper;
8
10
  using Messages;
9
11
  using static IMessageBus;
12
+ #if UNITY_2017_1_OR_NEWER
13
+ using UnityEngine;
14
+ #endif
10
15
 
11
16
  /// <summary>
12
17
  /// Instanced MessageBus for use cases where you want distinct islands of MessageBuses.
@@ -43,7 +48,7 @@
43
48
  }
44
49
  }
45
50
 
46
- public int RegisteredGlobalSequentialIndex { get; private set; }
51
+ public int RegisteredGlobalSequentialIndex { get; } = GenerateNewGlobalSequentialIndex();
47
52
 
48
53
  public int RegisteredBroadcast
49
54
  {
@@ -77,9 +82,12 @@
77
82
 
78
83
  // For use with re-broadcasting to generic methods
79
84
  private static readonly object[] ReflectionMethodArgumentsCache = new object[2];
85
+ private static readonly List<Expression> ArgumentExpressionsCache = new();
80
86
 
81
87
  private const BindingFlags ReflectionHelperBindingFlags =
82
88
  BindingFlags.Static | BindingFlags.NonPublic;
89
+ private const BindingFlags ReflexiveMethodBindingFlags =
90
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
83
91
 
84
92
  private delegate void FastUntargetedBroadcast<T>(ref T message)
85
93
  where T : IUntargetedMessage;
@@ -118,6 +126,16 @@
118
126
  private readonly Dictionary<Type, object> _broadcastMethodsByType = new();
119
127
  private readonly Stack<List<object>> _innerInterceptorsStack = new();
120
128
 
129
+ private readonly Dictionary<
130
+ Type,
131
+ Dictionary<MethodSignatureKey, Action<MonoBehaviour, object[]>>
132
+ > _methodCache = new();
133
+
134
+ #if UNITY_2017_1_OR_NEWER
135
+ private readonly HashSet<MonoBehaviour> _recipientCache = new();
136
+ private readonly List<MonoBehaviour> _componentCache = new();
137
+ #endif
138
+
121
139
  private readonly RegistrationLog _log = new();
122
140
 
123
141
  static MessageBus()
@@ -128,11 +146,6 @@
128
146
  }
129
147
  }
130
148
 
131
- public MessageBus()
132
- {
133
- RegisteredGlobalSequentialIndex = Interlocked.Increment(ref GlobalSequentialIndex);
134
- }
135
-
136
149
  public Action RegisterUntargeted<T>(MessageHandler messageHandler, int priority = 0)
137
150
  where T : IUntargetedMessage
138
151
  {
@@ -708,14 +721,191 @@
708
721
  }
709
722
 
710
723
  bool foundAnyHandlers = false;
724
+ Dictionary<InstanceId, HandlerCache<int, HandlerCache>> targetedHandlers;
725
+ HandlerCache<int, HandlerCache> sortedHandlers;
726
+
727
+ if (typeof(TMessage) == typeof(DxReflexiveMessage))
728
+ {
729
+ #if UNITY_2017_1_OR_NEWER
730
+ ref DxReflexiveMessage reflexiveMessage = ref Unsafe.As<
731
+ TMessage,
732
+ DxReflexiveMessage
733
+ >(ref typedMessage);
734
+
735
+ GameObject go;
736
+ bool found;
737
+ UnityEngine.Object targetObject = target.Object;
738
+ switch (targetObject)
739
+ {
740
+ case GameObject gameObject:
741
+ {
742
+ found = true;
743
+ go = gameObject;
744
+ break;
745
+ }
746
+ case Component component:
747
+ {
748
+ found = true;
749
+ go = component.gameObject;
750
+ break;
751
+ }
752
+ default:
753
+ {
754
+ go = null;
755
+ found = false;
756
+ break;
757
+ }
758
+ }
759
+
760
+ if (found)
761
+ {
762
+ _recipientCache.Clear();
763
+ bool sentInADirection = false;
764
+ if (reflexiveMessage.sendMode.HasFlagNoAlloc(ReflexiveSendMode.Upwards))
765
+ {
766
+ sentInADirection = true;
767
+ if (
768
+ !reflexiveMessage.sendMode.HasFlagNoAlloc(ReflexiveSendMode.Downwards)
769
+ && !reflexiveMessage.sendMode.HasFlagNoAlloc(ReflexiveSendMode.Flat)
770
+ )
771
+ {
772
+ switch (reflexiveMessage.parameters.Length)
773
+ {
774
+ case 0:
775
+ {
776
+ go.SendMessageUpwards(reflexiveMessage.method);
777
+ break;
778
+ }
779
+ case 1:
780
+ {
781
+ go.SendMessageUpwards(
782
+ reflexiveMessage.method,
783
+ reflexiveMessage.parameters[0]
784
+ );
785
+ break;
786
+ }
787
+ default:
788
+ {
789
+ Component[] parentComponents = go.GetComponentsInParent(
790
+ typeof(MonoBehaviour),
791
+ true
792
+ );
793
+ foreach (Component component in parentComponents)
794
+ {
795
+ if (component is MonoBehaviour script)
796
+ {
797
+ SendMessage(script, ref reflexiveMessage);
798
+ }
799
+ }
800
+
801
+ break;
802
+ }
803
+ }
804
+ }
805
+ else
806
+ {
807
+ Component[] parentComponents = go.GetComponentsInParent(
808
+ typeof(MonoBehaviour)
809
+ );
810
+ foreach (Component component in parentComponents)
811
+ {
812
+ if (component is MonoBehaviour script)
813
+ {
814
+ SendMessage(script, ref reflexiveMessage);
815
+ }
816
+ }
817
+ }
818
+ }
819
+ if (reflexiveMessage.sendMode.HasFlagNoAlloc(ReflexiveSendMode.Downwards))
820
+ {
821
+ sentInADirection = true;
822
+ if (
823
+ !reflexiveMessage.sendMode.HasFlagNoAlloc(ReflexiveSendMode.Upwards)
824
+ && !reflexiveMessage.sendMode.HasFlagNoAlloc(ReflexiveSendMode.Flat)
825
+ )
826
+ {
827
+ switch (reflexiveMessage.parameters.Length)
828
+ {
829
+ case 0:
830
+ {
831
+ go.BroadcastMessage(reflexiveMessage.method);
832
+ break;
833
+ }
834
+ case 1:
835
+ {
836
+ go.BroadcastMessage(
837
+ reflexiveMessage.method,
838
+ reflexiveMessage.parameters[0]
839
+ );
840
+ break;
841
+ }
842
+ default:
843
+ {
844
+ _componentCache.Clear();
845
+ go.GetComponentsInChildren(true, _componentCache);
846
+ foreach (MonoBehaviour parentComponent in _componentCache)
847
+ {
848
+ SendMessage(parentComponent, ref reflexiveMessage);
849
+ }
850
+
851
+ break;
852
+ }
853
+ }
854
+ }
855
+ else
856
+ {
857
+ _componentCache.Clear();
858
+ go.GetComponentsInChildren(_componentCache);
859
+ foreach (MonoBehaviour parentComponent in _componentCache)
860
+ {
861
+ SendMessage(parentComponent, ref reflexiveMessage);
862
+ }
863
+ }
864
+ }
865
+ else if (
866
+ !sentInADirection
867
+ && reflexiveMessage.sendMode.HasFlagNoAlloc(ReflexiveSendMode.Flat)
868
+ )
869
+ {
870
+ switch (reflexiveMessage.parameters.Length)
871
+ {
872
+ case 0:
873
+ {
874
+ go.SendMessage(reflexiveMessage.method);
875
+ break;
876
+ }
877
+ case 1:
878
+ {
879
+ go.SendMessage(
880
+ reflexiveMessage.method,
881
+ reflexiveMessage.parameters[0]
882
+ );
883
+ break;
884
+ }
885
+ default:
886
+ {
887
+ _componentCache.Clear();
888
+ go.GetComponents(_componentCache);
889
+ foreach (MonoBehaviour component in _componentCache)
890
+ {
891
+ SendMessage(component, ref reflexiveMessage);
892
+ }
893
+ break;
894
+ }
895
+ }
896
+ }
897
+ }
898
+ #else
899
+ MessagingDebug.Log(
900
+ LogLevel.Error,
901
+ "Reflexive messages are not supported in this build."
902
+ );
903
+ #endif
904
+ }
905
+
711
906
  if (
712
- _targetedSinks.TryGetValue<TMessage>(
713
- out Dictionary<InstanceId, HandlerCache<int, HandlerCache>> targetedHandlers
714
- )
715
- && targetedHandlers.TryGetValue(
716
- target,
717
- out HandlerCache<int, HandlerCache> sortedHandlers
718
- )
907
+ _targetedSinks.TryGetValue<TMessage>(out targetedHandlers)
908
+ && targetedHandlers.TryGetValue(target, out sortedHandlers)
719
909
  && 0 < sortedHandlers.handlers.Count
720
910
  )
721
911
  {
@@ -3160,5 +3350,137 @@
3160
3350
  sourcedBroadcast(ref target, ref typedMessage);
3161
3351
  }
3162
3352
  }
3353
+
3354
+ #if UNITY_2017_1_OR_NEWER
3355
+ private static Action<MonoBehaviour, object[]> CompileMethodAction(MethodInfo methodInfo)
3356
+ {
3357
+ ParameterExpression componentParameter = Expression.Parameter(
3358
+ typeof(MonoBehaviour),
3359
+ "targetComponent"
3360
+ );
3361
+ ParameterExpression argsParameter = Expression.Parameter(typeof(object[]), "args");
3362
+ ParameterInfo[] methodParams = methodInfo.GetParameters();
3363
+
3364
+ ArgumentExpressionsCache.Clear();
3365
+ for (int i = 0; i < methodParams.Length; ++i)
3366
+ {
3367
+ Expression indexAccess = Expression.ArrayIndex(
3368
+ argsParameter,
3369
+ Expression.Constant(i)
3370
+ );
3371
+ Expression convertedArg = Expression.Convert(
3372
+ indexAccess,
3373
+ methodParams[i].ParameterType
3374
+ );
3375
+ ArgumentExpressionsCache.Add(convertedArg);
3376
+ }
3377
+
3378
+ // ReSharper disable once AssignNullToNotNullAttribute
3379
+ Expression instanceExpression = methodInfo.IsStatic
3380
+ ? null
3381
+ : Expression.Convert(componentParameter, methodInfo.DeclaringType);
3382
+ MethodCallExpression callExpression = Expression.Call(
3383
+ instanceExpression,
3384
+ methodInfo,
3385
+ ArgumentExpressionsCache
3386
+ );
3387
+ Expression<Action<MonoBehaviour, object[]>> lambda = Expression.Lambda<
3388
+ Action<MonoBehaviour, object[]>
3389
+ >(callExpression, componentParameter, argsParameter);
3390
+
3391
+ return lambda.Compile();
3392
+ }
3393
+ #endif
3394
+
3395
+ private void SendMessage(MonoBehaviour recipient, ref DxReflexiveMessage message)
3396
+ {
3397
+ if (!_recipientCache.Add(recipient))
3398
+ {
3399
+ return;
3400
+ }
3401
+
3402
+ Type componentType = recipient.GetType();
3403
+ if (
3404
+ !_methodCache.TryGetValue(
3405
+ componentType,
3406
+ out Dictionary<MethodSignatureKey, Action<MonoBehaviour, object[]>> methodCache
3407
+ )
3408
+ )
3409
+ {
3410
+ _methodCache[componentType] = methodCache =
3411
+ new Dictionary<MethodSignatureKey, Action<MonoBehaviour, object[]>>();
3412
+ }
3413
+
3414
+ MethodSignatureKey lookupKey = message.signatureKey;
3415
+ if (!methodCache.TryGetValue(lookupKey, out Action<MonoBehaviour, object[]> method))
3416
+ {
3417
+ MethodInfo methodInfo = null;
3418
+ try
3419
+ {
3420
+ methodInfo = componentType.GetMethod(
3421
+ message.method,
3422
+ ReflexiveMethodBindingFlags,
3423
+ null,
3424
+ message.parameterTypes,
3425
+ null
3426
+ );
3427
+ }
3428
+ catch (AmbiguousMatchException)
3429
+ {
3430
+ MethodInfo[] matchingMethods = componentType.GetMethods(
3431
+ ReflexiveMethodBindingFlags
3432
+ );
3433
+ foreach (MethodInfo matchingMethod in matchingMethods)
3434
+ {
3435
+ if (
3436
+ !string.Equals(
3437
+ matchingMethod.Name,
3438
+ message.method,
3439
+ StringComparison.Ordinal
3440
+ )
3441
+ || !ParameterTypesMatch(
3442
+ matchingMethod.GetParameters(),
3443
+ message.parameterTypes
3444
+ )
3445
+ )
3446
+ {
3447
+ continue;
3448
+ }
3449
+
3450
+ methodInfo = matchingMethod;
3451
+ break;
3452
+ }
3453
+ }
3454
+ catch
3455
+ {
3456
+ methodInfo = null;
3457
+ }
3458
+
3459
+ if (methodInfo != null)
3460
+ {
3461
+ method = CompileMethodAction(methodInfo);
3462
+ }
3463
+ methodCache[lookupKey] = method;
3464
+ }
3465
+
3466
+ method?.Invoke(recipient, message.parameters);
3467
+ }
3468
+
3469
+ private static bool ParameterTypesMatch(ParameterInfo[] methodParams, Type[] expectedTypes)
3470
+ {
3471
+ if (methodParams.Length != expectedTypes.Length)
3472
+ {
3473
+ return false;
3474
+ }
3475
+
3476
+ for (int i = 0; i < methodParams.Length; ++i)
3477
+ {
3478
+ if (methodParams[i].ParameterType != expectedTypes[i])
3479
+ {
3480
+ return false;
3481
+ }
3482
+ }
3483
+ return true;
3484
+ }
3163
3485
  }
3164
3486
  }
@@ -0,0 +1,132 @@
1
+ namespace DxMessaging.Core.Messages
2
+ {
3
+ using System;
4
+ using Attributes;
5
+
6
+ [Flags]
7
+ public enum ReflexiveSendMode
8
+ {
9
+ [Obsolete("Please use a valid Send Mode")]
10
+ None = 0,
11
+ Flat = 1 << 0,
12
+ Downwards = 1 << 1,
13
+ Upwards = 1 << 2,
14
+ }
15
+
16
+ public readonly struct MethodSignatureKey : IEquatable<MethodSignatureKey>
17
+ {
18
+ private const int HashBase = 5556137;
19
+ private const int HashMultiplier = 95785853;
20
+
21
+ private readonly string _methodName;
22
+ private readonly Type[] _parameterTypes;
23
+
24
+ private readonly int _hashCode;
25
+
26
+ public MethodSignatureKey(string methodName, Type[] parameterTypes)
27
+ : this()
28
+ {
29
+ _methodName = methodName ?? throw new ArgumentNullException(nameof(methodName));
30
+ _parameterTypes = parameterTypes;
31
+ _hashCode = CalculateHashCode();
32
+ }
33
+
34
+ private int CalculateHashCode()
35
+ {
36
+ int hashCode = HashBase + _methodName.GetHashCode();
37
+ foreach (Type type in _parameterTypes)
38
+ {
39
+ hashCode = hashCode * HashMultiplier + type.GetHashCode();
40
+ }
41
+
42
+ return hashCode;
43
+ }
44
+
45
+ public override int GetHashCode()
46
+ {
47
+ return _hashCode;
48
+ }
49
+
50
+ public override bool Equals(object obj)
51
+ {
52
+ return obj is MethodSignatureKey other && Equals(other);
53
+ }
54
+
55
+ public bool Equals(MethodSignatureKey other)
56
+ {
57
+ if (
58
+ _parameterTypes.Length != other._parameterTypes.Length
59
+ || _methodName != other._methodName
60
+ )
61
+ {
62
+ return false;
63
+ }
64
+
65
+ // ReSharper disable once LoopCanBeConvertedToQuery
66
+ for (int i = 0; i < _parameterTypes.Length; ++i)
67
+ {
68
+ if (_parameterTypes[i] != other._parameterTypes[i])
69
+ {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ return true;
75
+ }
76
+
77
+ public static bool operator ==(MethodSignatureKey left, MethodSignatureKey right)
78
+ {
79
+ return left.Equals(right);
80
+ }
81
+
82
+ public static bool operator !=(MethodSignatureKey left, MethodSignatureKey right)
83
+ {
84
+ return !(left == right);
85
+ }
86
+ }
87
+
88
+ [DxTargetedMessage]
89
+ public readonly partial struct DxReflexiveMessage
90
+ {
91
+ public readonly string method;
92
+ public readonly ReflexiveSendMode sendMode;
93
+ public readonly object[] parameters;
94
+
95
+ public readonly Type[] parameterTypes;
96
+
97
+ public readonly MethodSignatureKey signatureKey;
98
+
99
+ public DxReflexiveMessage(
100
+ string method,
101
+ ReflexiveSendMode sendMode,
102
+ params object[] parameters
103
+ )
104
+ {
105
+ this.method = method;
106
+ this.sendMode = sendMode;
107
+ this.parameters = parameters;
108
+
109
+ if (0 < parameters.Length)
110
+ {
111
+ int parameterCount = parameters.Length;
112
+ parameterTypes = new Type[parameterCount];
113
+ for (int i = 0; i < parameterCount; i++)
114
+ {
115
+ object parameter = parameters[i];
116
+ if (parameter == null)
117
+ {
118
+ throw new ArgumentNullException(
119
+ $"Parameter at index {i} is null, cannot resolve type!"
120
+ );
121
+ }
122
+ parameterTypes[i] = parameter.GetType();
123
+ }
124
+ }
125
+ else
126
+ {
127
+ parameterTypes = Array.Empty<Type>();
128
+ }
129
+ signatureKey = new MethodSignatureKey(method, parameterTypes);
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: f9a83f6bf3c0487f9e5cff498dbb6932
3
+ timeCreated: 1745161955
@@ -46,7 +46,7 @@
46
46
  /// </note>
47
47
  /// <param name="logLevel">Severity of the message.</param>
48
48
  /// <param name="message">Format string.</param>
49
- /// <param name="args">Args to populate format string with.</param>
49
+ /// <param name="args">Args to populate the format string with.</param>
50
50
  public static void Log(LogLevel logLevel, string message, params object[] args)
51
51
  {
52
52
  if (!enabled)
@@ -1,39 +1,45 @@
1
1
  namespace DxMessaging.Unity
2
2
  {
3
3
  using Core;
4
+ using Messages;
4
5
  using UnityEngine;
5
6
 
6
7
  [RequireComponent(typeof(MessagingComponent))]
7
8
  public abstract class MessageAwareComponent : MonoBehaviour
8
9
  {
10
+ public virtual MessageRegistrationToken Token => _messageRegistrationToken;
11
+
9
12
  protected MessageRegistrationToken _messageRegistrationToken;
10
13
 
11
14
  /// <summary>
12
- /// If true, will register/un-register handles when the component is enabled or disabled.
15
+ /// If true, will register/unregister handles when the component is enabled or disabled.
13
16
  /// </summary>
14
17
  protected virtual bool MessageRegistrationTiedToEnableStatus => true;
15
18
 
16
19
  protected bool _isQuitting;
17
20
 
18
- protected virtual void Awake()
19
- {
20
- SetupMessageHandlers();
21
- }
21
+ protected MessagingComponent _messagingComponent;
22
22
 
23
- protected void SetupMessageHandlers()
23
+ protected virtual void Awake()
24
24
  {
25
- if (_messageRegistrationToken == null)
26
- {
27
- MessagingComponent messenger = GetComponent<MessagingComponent>();
28
- _messageRegistrationToken = messenger.Create(this);
29
- }
30
-
25
+ _messagingComponent = GetComponent<MessagingComponent>();
26
+ _messageRegistrationToken = _messagingComponent.Create(this);
31
27
  RegisterMessageHandlers();
32
28
  }
33
29
 
34
30
  protected virtual void RegisterMessageHandlers()
35
31
  {
36
- // No-op, expectation is that implementations implement their own logic here
32
+ _ = _messageRegistrationToken.RegisterGameObjectTargeted<StringMessage>(
33
+ gameObject,
34
+ HandleStringGameObjectMessage
35
+ );
36
+ _ = _messageRegistrationToken.RegisterComponentTargeted<StringMessage>(
37
+ this,
38
+ HandleStringComponentMessage
39
+ );
40
+ _ = _messageRegistrationToken.RegisterUntargeted<GlobalStringMessage>(
41
+ HandleGlobalStringMessage
42
+ );
37
43
  }
38
44
 
39
45
  protected virtual void OnEnable()
@@ -72,5 +78,20 @@
72
78
  {
73
79
  _isQuitting = true;
74
80
  }
81
+
82
+ protected virtual void HandleStringGameObjectMessage(ref StringMessage message)
83
+ {
84
+ // No-op by default
85
+ }
86
+
87
+ protected virtual void HandleStringComponentMessage(ref StringMessage message)
88
+ {
89
+ // No-op by default
90
+ }
91
+
92
+ protected virtual void HandleGlobalStringMessage(ref GlobalStringMessage message)
93
+ {
94
+ // No-op by default
95
+ }
75
96
  }
76
97
  }
@@ -0,0 +1,15 @@
1
+ namespace DxMessaging.Unity.Messages
2
+ {
3
+ using Core.Attributes;
4
+
5
+ [DxUntargetedMessage]
6
+ public readonly partial struct GlobalStringMessage
7
+ {
8
+ public readonly string message;
9
+
10
+ public GlobalStringMessage(string message)
11
+ {
12
+ this.message = message;
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 58b2b780f9844aeb8929eeca855ba3be
3
+ timeCreated: 1745161506
@@ -0,0 +1,15 @@
1
+ namespace DxMessaging.Unity.Messages
2
+ {
3
+ using Core.Attributes;
4
+
5
+ [DxTargetedMessage]
6
+ public readonly partial struct StringMessage
7
+ {
8
+ public readonly string message;
9
+
10
+ public StringMessage(string message)
11
+ {
12
+ this.message = message;
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: cf8206ca4c8449a8b8e112ad25554f1f
3
+ timeCreated: 1745161457
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: f1a132a737bb42a79205b2269554e818
3
+ timeCreated: 1745160607
@@ -4,7 +4,7 @@
4
4
  "references": [],
5
5
  "includePlatforms": [],
6
6
  "excludePlatforms": [],
7
- "allowUnsafeCode": false,
7
+ "allowUnsafeCode": true,
8
8
  "overrideReferences": false,
9
9
  "precompiledReferences": [
10
10
  "System.Runtime.CompilerServices.Unsafe.dll"
@@ -5,6 +5,7 @@
5
5
  using Core;
6
6
  using DxMessaging.Core;
7
7
  using DxMessaging.Core.Extensions;
8
+ using DxMessaging.Core.Messages;
8
9
  using global::Unity.PerformanceTesting;
9
10
  using NUnit.Framework;
10
11
  using Scripts.Components;
@@ -30,9 +31,16 @@
30
31
  Debug.Log("| ------------ | ------------------- | ------------ | ");
31
32
 
32
33
  ComplexTargetedMessage message = new(Guid.NewGuid());
34
+ DxReflexiveMessage reflexiveMessage = new(
35
+ nameof(SimpleMessageAwareComponent.HandleSlowComplexTargetedMessage),
36
+ ReflexiveSendMode.Flat,
37
+ message
38
+ );
39
+
33
40
  Stopwatch timer = Stopwatch.StartNew();
34
41
 
35
42
  RunTest(component => Unity(timer, timeout, component.gameObject, message));
43
+
36
44
  RunTest(component => NormalGameObject(timer, timeout, component, message));
37
45
  RunTest(component => NormalComponent(timer, timeout, component, message));
38
46
  RunTest(component => NoCopyGameObject(timer, timeout, component, message));
@@ -40,6 +48,11 @@
40
48
 
41
49
  SimpleUntargetedMessage untargetedMessage = new();
42
50
  RunTest(component => NoCopyUntargeted(timer, timeout, component, untargetedMessage));
51
+ RunTest(component =>
52
+ ReflexiveOneArgument(timer, timeout, component.gameObject, reflexiveMessage)
53
+ );
54
+ RunTest(component => ReflexiveTwoArguments(timer, timeout, component.gameObject));
55
+ RunTest(component => ReflexiveThreeArguments(timer, timeout, component.gameObject));
43
56
  }
44
57
 
45
58
  [Test]
@@ -181,7 +194,10 @@
181
194
  )
182
195
  {
183
196
  int count = 0;
184
- var component = target.AddComponent<SimpleMessageAwareComponent>();
197
+ if (!target.TryGetComponent(out SimpleMessageAwareComponent component))
198
+ {
199
+ component = target.AddComponent<SimpleMessageAwareComponent>();
200
+ }
185
201
  component.slowComplexTargetedHandler = () => ++count;
186
202
  // Pre-warm
187
203
  target.SendMessage(
@@ -221,6 +237,130 @@
221
237
  DisplayCount("Unity", count, timeout, allocating);
222
238
  }
223
239
 
240
+ private static void ReflexiveThreeArguments(
241
+ Stopwatch timer,
242
+ TimeSpan timeout,
243
+ GameObject go
244
+ )
245
+ {
246
+ int count = 0;
247
+ if (!go.TryGetComponent(out SimpleMessageAwareComponent component))
248
+ {
249
+ component = go.AddComponent<SimpleMessageAwareComponent>();
250
+ }
251
+ component.reflexiveThreeArgumentHandler = () => ++count;
252
+ DxReflexiveMessage message = new(
253
+ nameof(SimpleMessageAwareComponent.HandleReflexiveMessageThreeArguments),
254
+ ReflexiveSendMode.Flat,
255
+ 1,
256
+ 2,
257
+ 3
258
+ );
259
+ InstanceId target = go;
260
+ // Pre-warm
261
+ message.EmitTargeted(target);
262
+
263
+ timer.Restart();
264
+ do
265
+ {
266
+ for (int i = 0; i < NumInvocationsPerIteration; ++i)
267
+ {
268
+ message.EmitTargeted(target);
269
+ }
270
+ } while (timer.Elapsed < timeout);
271
+ bool allocating;
272
+ try
273
+ {
274
+ Assert.That(() => message.EmitTargeted(target), Is.Not.AllocatingGCMemory());
275
+ allocating = false;
276
+ }
277
+ catch
278
+ {
279
+ allocating = true;
280
+ }
281
+
282
+ DisplayCount("Reflexive (Three Arguments)", count, timeout, allocating);
283
+ }
284
+
285
+ private static void ReflexiveTwoArguments(Stopwatch timer, TimeSpan timeout, GameObject go)
286
+ {
287
+ int count = 0;
288
+ if (!go.TryGetComponent(out SimpleMessageAwareComponent component))
289
+ {
290
+ component = go.AddComponent<SimpleMessageAwareComponent>();
291
+ }
292
+ component.reflexiveTwoArgumentHandler = () => ++count;
293
+ DxReflexiveMessage message = new(
294
+ nameof(SimpleMessageAwareComponent.HandleReflexiveMessageTwoArguments),
295
+ ReflexiveSendMode.Flat,
296
+ 1,
297
+ 2
298
+ );
299
+ InstanceId target = go;
300
+ // Pre-warm
301
+ message.EmitTargeted(target);
302
+
303
+ timer.Restart();
304
+ do
305
+ {
306
+ for (int i = 0; i < NumInvocationsPerIteration; ++i)
307
+ {
308
+ message.EmitTargeted(target);
309
+ }
310
+ } while (timer.Elapsed < timeout);
311
+ bool allocating;
312
+ try
313
+ {
314
+ Assert.That(() => message.EmitTargeted(target), Is.Not.AllocatingGCMemory());
315
+ allocating = false;
316
+ }
317
+ catch
318
+ {
319
+ allocating = true;
320
+ }
321
+
322
+ DisplayCount("Reflexive (Two Arguments)", count, timeout, allocating);
323
+ }
324
+
325
+ private static void ReflexiveOneArgument(
326
+ Stopwatch timer,
327
+ TimeSpan timeout,
328
+ GameObject go,
329
+ DxReflexiveMessage message
330
+ )
331
+ {
332
+ int count = 0;
333
+ if (!go.TryGetComponent(out SimpleMessageAwareComponent component))
334
+ {
335
+ component = go.AddComponent<SimpleMessageAwareComponent>();
336
+ }
337
+ component.slowComplexTargetedHandler = () => ++count;
338
+ InstanceId target = go;
339
+ // Pre-warm
340
+ message.EmitTargeted(target);
341
+
342
+ timer.Restart();
343
+ do
344
+ {
345
+ for (int i = 0; i < NumInvocationsPerIteration; ++i)
346
+ {
347
+ message.EmitTargeted(target);
348
+ }
349
+ } while (timer.Elapsed < timeout);
350
+ bool allocating;
351
+ try
352
+ {
353
+ Assert.That(() => message.EmitTargeted(target), Is.Not.AllocatingGCMemory());
354
+ allocating = false;
355
+ }
356
+ catch
357
+ {
358
+ allocating = true;
359
+ }
360
+
361
+ DisplayCount("Reflexive (One Argument)", count, timeout, allocating);
362
+ }
363
+
224
364
  private void NormalGameObject(
225
365
  Stopwatch timer,
226
366
  TimeSpan timeout,
@@ -154,20 +154,12 @@
154
154
  }
155
155
  }
156
156
 
157
- protected MessageRegistrationToken GetToken(MessageAwareComponent component)
157
+ protected static MessageRegistrationToken GetToken(MessageAwareComponent component)
158
158
  {
159
- // Reach inside and grab the token
160
- FieldInfo field = typeof(MessageAwareComponent).GetField(
161
- "_messageRegistrationToken",
162
- BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
163
- );
164
- Assert.IsNotNull(field);
165
- MessageRegistrationToken token = field.GetValue(component) as MessageRegistrationToken;
166
- Assert.IsNotNull(token);
167
- return token;
159
+ return component.Token;
168
160
  }
169
161
 
170
- protected IEnumerator WaitUntilMessageHandlerIsFresh()
162
+ protected static IEnumerator WaitUntilMessageHandlerIsFresh()
171
163
  {
172
164
  MessageBus messageBus = MessageHandler.MessageBus;
173
165
  Assert.IsNotNull(messageBus);
@@ -37,6 +37,8 @@
37
37
  public Action componentTargetedHandler;
38
38
  public Action complexComponentTargetedHandler;
39
39
  public Action componentBroadcastHandler;
40
+ public Action reflexiveTwoArgumentHandler;
41
+ public Action reflexiveThreeArgumentHandler;
40
42
 
41
43
  private bool _slowComplexTargetingEnabled = true;
42
44
  private bool _fastComplexTargetingEnabled = true;
@@ -122,6 +124,16 @@
122
124
  }
123
125
  }
124
126
 
127
+ public void HandleReflexiveMessageTwoArguments(int a, int b)
128
+ {
129
+ reflexiveTwoArgumentHandler?.Invoke();
130
+ }
131
+
132
+ public void HandleReflexiveMessageThreeArguments(int a, int b, int c)
133
+ {
134
+ reflexiveThreeArgumentHandler?.Invoke();
135
+ }
136
+
125
137
  public void HandleSlowComplexTargetedMessage(ComplexTargetedMessage message)
126
138
  {
127
139
  slowComplexTargetedHandler?.Invoke();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.wallstop-studios.dxmessaging",
3
- "version": "2.0.0-rc22",
3
+ "version": "2.0.0-rc23",
4
4
  "displayName": "DxMessaging",
5
5
  "description": "Synchronous Event Bus for Unity",
6
6
  "unity": "2021.3",