├── DispatchAction.cs ├── DispatchAction.cs.meta ├── Dispatcher.cs ├── Dispatcher.cs.meta ├── E7.EnumDispatcher.asmdef ├── E7.EnumDispatcher.asmdef.meta ├── Editor.meta ├── Editor ├── DispatchDebugger.cs ├── DispatchDebugger.cs.meta ├── E7.EnumDispatcher.Editor.asmdef └── E7.EnumDispatcher.Editor.asmdef.meta ├── FAttribute.cs ├── FAttribute.cs.meta ├── JobsSupport.meta ├── JobsSupport ├── ActionCategory.cs ├── ActionCategory.cs.meta ├── ActionExact.cs ├── ActionExact.cs.meta ├── ActionFlag.cs ├── ActionFlag.cs.meta ├── JobDispatchAction.cs └── JobDispatchAction.cs.meta ├── README.md ├── README.md.meta ├── Systems.meta ├── Systems ├── ActionHandlerSystem.cs ├── ActionHandlerSystem.cs.meta ├── DispatchingSystem.cs ├── DispatchingSystem.cs.meta ├── EnumTypeManager.cs └── EnumTypeManager.cs.meta ├── Tests.meta ├── Tests ├── DispatchActionTests.cs ├── DispatchActionTests.cs.meta ├── E7.EnumDispatcher.Tests.asmdef ├── E7.EnumDispatcher.Tests.asmdef.meta ├── EnumDispatcherTestBase.cs ├── EnumDispatcherTestBase.cs.meta ├── JobDispatchActionTests.cs ├── JobDispatchActionTests.cs.meta ├── MyECSTestsFixture.cs └── MyECSTestsFixture.cs.meta ├── package.json └── package.json.meta /DispatchAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Unity.Collections; 4 | using Unity.Entities; 5 | using Unity.Mathematics; 6 | using UnityEngine; 7 | 8 | namespace E7.EnumDispatcher 9 | { 10 | /// 11 | /// Special-purpose action. 12 | /// 13 | public enum SignalAction 14 | { 15 | /// 16 | /// Dispatch this action to ping all stores to update its data based on its own "external data". 17 | /// Normally system responds to store data change. Sometimes we can't put those data in ECS effectively. 18 | /// 19 | /// The solution is to have the store updates its own data from external data on this signal, then the system could 20 | /// responds to store data change as usual. The signal acts as a manual "changed" for external data, normally ECS is responsible 21 | /// to track those changes. 22 | /// 23 | ExternalDataChanged 24 | } 25 | 26 | /// 27 | /// Basically just in disguise, but serves different purpose. 28 | /// 29 | public class ChangedSignal : DispatchAction { } 30 | 31 | public partial class DispatchAction : Component 32 | { 33 | private EnumTypeManager ETM; 34 | 35 | //An int representing enum's type 36 | private int enumCategoryIndex; 37 | //The enum's actual int value 38 | private int enumActionTypeIndex; 39 | //Do not dispose! It is statically kept for everyone. 40 | private NativeArray flags; 41 | 42 | private Dictionary payload; 43 | 44 | //Just a running number unique to every action 45 | private int actionId; 46 | internal int ActionId => actionId; 47 | 48 | public override string ToString() => $"Action Category : {CategoryName} Type : {ActionTypeName}"; 49 | 50 | private static int globalActionId; 51 | 52 | /// 53 | /// Take the `int` representation of your action and flags with you in the job. 54 | /// 55 | public JobDispatchAction CastJob() => new JobDispatchAction(enumCategoryIndex, enumActionTypeIndex, flags); 56 | 57 | /// 58 | /// You can pre-create the action to be dispatched with this. 59 | /// Each action allocates a `Dictionary` for the payload so it make sense to keep them for reuse. 60 | /// 61 | public static DispatchAction Create( 62 | ENUM e, 63 | params (Enum key, object pl)[] payload 64 | ) 65 | where ENUM : struct, IConvertible 66 | => Create(EnumTypeManager.Singleton, e, payload); 67 | 68 | public static DispatchAction Create( 69 | EnumTypeManager etm, 70 | ENUM e, 71 | params (Enum key, object pl)[] payload 72 | ) 73 | where ENUM : struct, IConvertible 74 | { 75 | var payloadDict = new Dictionary(); 76 | foreach (var x in payload) 77 | { 78 | payloadDict.Add(x.key, x.pl); 79 | } 80 | 81 | globalActionId++; 82 | return new DispatchAction() 83 | { 84 | ETM = etm, 85 | enumCategoryIndex = etm.GetCategoryIndex(), 86 | enumActionTypeIndex = etm.Category().FastCastToActionType(e), 87 | flags = etm.Category().GetFlags(e), 88 | payload = payloadDict, 89 | actionId = globalActionId, 90 | }; 91 | } 92 | 93 | internal DispatchAction() { } 94 | 95 | /// 96 | /// Copy values from other DispatchAction without replacing the reference type. Copy even action ID. 97 | /// 98 | internal DispatchAction(DispatchAction da) 99 | { 100 | this.ETM = da.ETM; 101 | this.enumCategoryIndex = da.enumCategoryIndex; 102 | this.enumActionTypeIndex = da.enumActionTypeIndex; 103 | this.payload = da.payload; 104 | this.actionId = da.actionId; 105 | this.flags = da.flags; 106 | } 107 | 108 | public override int GetHashCode() => actionId; 109 | 110 | /// 111 | /// `if` on this when handling action, before switch-case on the returned `out` variable. 112 | /// You can skip the category check if you are handling only one type of enum that represent the action. In that case use `As`. 113 | /// 114 | /// It does not care about integer payload, the cast to enum is cached. 115 | /// 116 | /// If the check returns `false`, this value is default(`ENUM`) but you should not use it anyways. 117 | public bool Category(out ENUM actionType) where ENUM : struct, IConvertible 118 | { 119 | bool checkResult = ETM.GetCategoryIndex() == enumCategoryIndex; 120 | actionType = checkResult ? ETM.Category().FastCastFromActionType(enumActionTypeIndex) : default(ENUM); 121 | return checkResult; 122 | } 123 | 124 | /// 125 | /// `if` on this when handling action, an overload with discarded `out` you just want to check category. 126 | /// 127 | public bool Category() where ENUM : struct, IConvertible 128 | => Category(out _); 129 | 130 | /// 131 | /// If you want to skip the check on action's category, use `switch case` with value returned from this method. 132 | /// It might be useful in `StateReactSystem` where you know which category has been handled by the store for some performance. 133 | /// The cast to enum is cached. 134 | /// 135 | public ENUM As() where ENUM : struct, IConvertible 136 | => ETM.Category().FastCastFromActionType(enumActionTypeIndex); 137 | 138 | /// 139 | /// When you want to `if` on the action directly. This one also check for the correct category. 140 | /// 141 | public bool Is(ENUM actionEnum) where ENUM : struct, IConvertible 142 | { 143 | var actionCategory = ETM.GetCategoryIndex(); 144 | var actionType = ETM.Category().FastCastToActionType(actionEnum); 145 | //Debug.Log($"{this.enumCategoryIndex} == {actionCategory} && {this.enumActionTypeIndex} == {actionType}"); 146 | return this.enumCategoryIndex == actionCategory && this.enumActionTypeIndex == actionType; 147 | } 148 | 149 | /// 150 | /// When you want to `if` on the action directly. It does not care about category. 151 | /// It might be useful in `StateReactSystem` where you know which category has been handled by the store for some performance. 152 | /// If you want to care, use `.Is`. 153 | /// 154 | public bool Type(ENUM actionEnum) where ENUM : struct, IConvertible 155 | => ETM.Category().FastCastToActionType(actionEnum) == enumActionTypeIndex; 156 | 157 | /// 158 | /// Each action type can be attached with multiple flags which are comparable cross-categories. 159 | /// `if` on this to check for a flag. 160 | /// 161 | public bool Flagged(string value) 162 | { 163 | int intFlag = ETM.StringFlagToInt(value); 164 | return flags.Contains(intFlag); 165 | } 166 | 167 | 168 | /// 169 | /// Match the key then unboxing from `object` to specified type, throws when the cast fail 170 | /// If `optional`, get a default value when payload key does not match. If the key match but the cast fail while `optional` you will still get a throw. 171 | /// 172 | public T GetPayload(Enum payloadKey, bool optional = false) 173 | { 174 | if (payload.TryGetValue(payloadKey, out object grab)) 175 | { 176 | //Still can throw. 177 | return (T)grab; 178 | } 179 | else 180 | { 181 | return optional ? default(T) 182 | : throw new System.InvalidCastException($"There is no payload in {this} that match the key {payloadKey}."); 183 | } 184 | } 185 | 186 | public (T1, T2) GetPayload(Enum pk1, Enum pk2, (bool, bool) optionals = default) 187 | => ( 188 | GetPayload(pk1, optionals.Item1), 189 | GetPayload(pk2, optionals.Item2) 190 | ); 191 | 192 | public (T1, T2, T3) GetPayload(Enum pk1, Enum pk2, Enum pk3, (bool, bool, bool) optionals = default) 193 | => ( 194 | GetPayload(pk1, optionals.Item1), 195 | GetPayload(pk2, optionals.Item2), 196 | GetPayload(pk3, optionals.Item3) 197 | ); 198 | 199 | public (T1, T2, T3, T4) GetPayload(Enum pk1, Enum pk2, Enum pk3, Enum pk4, (bool, bool, bool, bool) optionals = default) 200 | => ( 201 | GetPayload(pk1, optionals.Item1), 202 | GetPayload(pk2, optionals.Item2), 203 | GetPayload(pk3, optionals.Item3), 204 | GetPayload(pk4, optionals.Item4) 205 | ); 206 | 207 | /// 208 | /// Unboxing from `object` to type T. Only returns true if payload is there and the cast to type T success. 209 | /// Useful when you want to check on the payload before other fields, or when it is possible to contains a payload or not. 210 | /// 211 | public bool HasPayload(Enum payloadKey, out T castedPayload) 212 | { 213 | if (payload.TryGetValue(payloadKey, out object grab)) 214 | { 215 | if (typeof(T).IsAssignableFrom(grab.GetType())) 216 | { 217 | castedPayload = (T)grab; 218 | return true; 219 | } 220 | } 221 | castedPayload = default(T); 222 | return false; 223 | } 224 | 225 | /// 226 | /// Use reflection on enum type to get a readable string. 227 | /// 228 | public string CategoryName => ETM.GetFullNameFromIndex(enumCategoryIndex); 229 | 230 | /// 231 | /// Use reflection on enum type to get a readable string. 232 | /// 233 | public string ActionTypeName => ETM.GetValueNameFromIndex(enumCategoryIndex, enumActionTypeIndex); 234 | } 235 | } -------------------------------------------------------------------------------- /DispatchAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: efaf392d6cfc54e3d96ddb7e1b42c0ba 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Dispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Unity.Entities; 3 | using UnityEngine; 4 | 5 | namespace E7.EnumDispatcher 6 | { 7 | public delegate void ActionHandlerDelegate(DispatchAction action); 8 | 9 | /// 10 | /// Dispatcher static class access a in your active world. 11 | /// System like is automatically a dispatch subscriber on its . 12 | /// You can also make a handle action with . 13 | /// 14 | public static class Dispatcher 15 | { 16 | /// 17 | /// Returns `null` when no active world. 18 | /// 19 | /// 20 | public static DispatchingSystem Active => World.DefaultGameObjectInjectionWorld == null ? null : Of(World.DefaultGameObjectInjectionWorld); 21 | 22 | public static DispatchingSystem Of(World w) => w.GetOrCreateSystem(); 23 | 24 | /// 25 | /// Dispatch to a dispatcher of the currently active world. 26 | /// 27 | public static void Dispatch(T e, 28 | params (Enum, object)[] payload) 29 | where T : struct, IConvertible 30 | { 31 | DispatchingSystem activeDs = Active; 32 | if (activeDs == null) 33 | { 34 | throw new Exception($"You cannot dispatch an action to an empty active world."); 35 | } 36 | activeDs.Dispatch(e, payload); 37 | } 38 | 39 | /// 40 | /// Signal that something changed to the currently active world. 41 | /// 42 | public static void SignalChanged(T e) 43 | where T : struct, IConvertible 44 | { 45 | DispatchingSystem activeDs = Active; 46 | if (activeDs == null) 47 | { 48 | throw new Exception($"You cannot dispatch an action to an empty active world."); 49 | } 50 | activeDs.SignalChanged(e); 51 | } 52 | 53 | /// 54 | /// Dispatch to a dispatcher of the currently active world with pre-created action. 55 | /// Use `DispatchAction.Create` method to create and cache an action. 56 | /// 57 | public static void Dispatch(DispatchAction da) 58 | { 59 | DispatchingSystem activeDs = Active; 60 | if (activeDs == null) 61 | { 62 | throw new Exception($"You cannot dispatch an action to an empty active world."); 63 | } 64 | activeDs.Dispatch(da); 65 | } 66 | 67 | /// 68 | /// You could subscribe with any out-of-ECS callback, but remember to as well. 69 | /// 70 | public static void Subscribe(ActionHandlerDelegate handler) 71 | { 72 | Active.Subscribe(handler); 73 | } 74 | 75 | /// 76 | /// Does nothing if is `null`. It is safe to use this in `OnDestroy` when the game quits. 77 | /// 78 | public static void Unsubscribe(ActionHandlerDelegate handler) => Active?.Unsubscribe(handler); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Dispatcher.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: adec5da851d634a00b27d8490d2bcfb5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /E7.EnumDispatcher.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "E7.EnumDispatcher", 3 | "references": [ 4 | "Unity.Entities", 5 | "Unity.Entities.Hybrid", 6 | "Unity.Collections", 7 | "Unity.Mathematics" 8 | ], 9 | "optionalUnityReferences": [], 10 | "includePlatforms": [], 11 | "excludePlatforms": [], 12 | "allowUnsafeCode": false, 13 | "overrideReferences": false, 14 | "precompiledReferences": [], 15 | "autoReferenced": true, 16 | "defineConstraints": [] 17 | } -------------------------------------------------------------------------------- /E7.EnumDispatcher.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e127d3ea2048f4700a2294383d68356a 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0795126000ebf43ef80b4077ef33f795 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/DispatchDebugger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using Unity.Entities; 4 | using UnityEditor; 5 | 6 | namespace E7.EnumDispatcher 7 | { 8 | public static class DispatchDebugger 9 | { 10 | /// 11 | /// Use `GUILayout` and `EditorGUILayout` to draw dispatcher debugger tools based on one category of action. 12 | /// 13 | public static void DrawDebuggerDropdown() where ENUM : struct, IConvertible 14 | { 15 | var ETM = EnumTypeManager.Singleton; 16 | bool unusable = ETM == null; 17 | // if(ETM == null) ETM = GetEditorWorld().GetOrCreateManager(); 18 | 19 | EditorGUI.BeginDisabledGroup(unusable); 20 | 21 | Type enumType = ETM.Category().GetTypeOfEnum(); 22 | 23 | var names = unusable ? new string[0] : ETM.Category().GetNames(); 24 | var values = unusable ? new ENUM[0] : (ENUM[])Enum.GetValues(enumType); 25 | var niceName = unusable ? "---" : ETM.Category().FullNiceName(); 26 | 27 | GUILayout.BeginHorizontal(); 28 | GUILayout.Label(niceName); 29 | EditorStruct.selectedEnum = EditorGUILayout.Popup(EditorStruct.selectedEnum, names); 30 | if (GUILayout.Button(nameof(Dispatcher.Dispatch))) 31 | { 32 | Dispatcher.Dispatch(values[EditorStruct.selectedEnum]); 33 | } 34 | GUILayout.EndHorizontal(); 35 | 36 | EditorGUI.EndDisabledGroup(); 37 | } 38 | 39 | private struct EditorStruct 40 | { 41 | internal static int selectedEnum; 42 | } 43 | 44 | /// 45 | /// Use `GUILayout` to draw dispatcher debugger tools based on one category of action. 46 | /// 47 | public static void DrawDebuggerMatrix(int column = 3, int buttonHeight = 13) where ENUM : struct, IConvertible 48 | { 49 | var ETM = EnumTypeManager.Singleton; 50 | bool unusable = ETM == null; 51 | // var ETM = World.Active?.GetOrCreateManager(); 52 | // bool unusable = ETM == null; 53 | // if(ETM == null) ETM = GetEditorWorld().GetOrCreateManager(); 54 | 55 | EditorGUI.BeginDisabledGroup(unusable); 56 | 57 | GUIStyle btStyle = new GUIStyle(GUI.skin.button); 58 | btStyle.fontSize = (int)(buttonHeight / 1.5f); 59 | 60 | var names = unusable ? new string[0] : ETM.Category().GetNames(); 61 | var values = unusable ? new ENUM[0] : (ENUM[])Enum.GetValues(ETM.Category().GetTypeOfEnum()); 62 | var niceName = unusable ? "---" : ETM.Category().FullNiceName(); 63 | 64 | GUILayout.BeginHorizontal(); 65 | GUILayout.FlexibleSpace(); 66 | GUILayout.Label(niceName); 67 | GUILayout.FlexibleSpace(); 68 | GUILayout.EndHorizontal(); 69 | Rect labelRect = GUILayoutUtility.GetLastRect(); 70 | 71 | GUI.Box(labelRect, ""); 72 | 73 | for (int i = 0; i < names.Length; i++) 74 | { 75 | GUILayout.BeginHorizontal(GUILayout.Height(buttonHeight)); 76 | GUILayout.FlexibleSpace(); 77 | GUILayout.EndHorizontal(); 78 | Rect r = GUILayoutUtility.GetLastRect(); 79 | r.width /= column; 80 | 81 | for (int j = 0; j < column && i < names.Length; j++) 82 | { 83 | if (GUI.Button(r, names[i], btStyle)) 84 | { 85 | Dispatcher.Dispatch(values[i]); 86 | } 87 | r.x += r.width; 88 | i++; 89 | } 90 | GUILayout.BeginHorizontal(GUILayout.Height(1)); 91 | GUILayout.FlexibleSpace(); 92 | GUILayout.EndHorizontal(); 93 | } 94 | EditorGUI.EndDisabledGroup(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Editor/DispatchDebugger.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8fcc40ed376564d94bc310fee31bd7f2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/E7.EnumDispatcher.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "E7.EnumDispatcher.Editor", 3 | "references": [ 4 | "Unity.Entities", 5 | "Unity.Entities.Hybrid", 6 | "Unity.Collections", 7 | "Unity.Mathematics", 8 | "E7.EnumDispatcher" 9 | ], 10 | "optionalUnityReferences": [], 11 | "includePlatforms": [ 12 | "Editor" 13 | ], 14 | "excludePlatforms": [], 15 | "allowUnsafeCode": false, 16 | "overrideReferences": false, 17 | "precompiledReferences": [], 18 | "autoReferenced": true, 19 | "defineConstraints": [] 20 | } -------------------------------------------------------------------------------- /Editor/E7.EnumDispatcher.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ad504919290a84e30a26b20ac02bd374 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /FAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace E7.EnumDispatcher 2 | { 3 | /// 4 | /// F is short for Flag. Each action belongs to one category, but can be attached with multiple flags. 5 | /// Use `[F(___, ___, ...)]` to define flags on your actions. Put it before your enum's value name. 6 | /// 7 | [System.AttributeUsage(System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)] 8 | public class FAttribute : System.Attribute 9 | { 10 | internal readonly string[] flags; 11 | public FAttribute(params string[] flags) 12 | { 13 | this.flags = flags; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /FAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 162314ebc8b8f44b1952cf4de13d0964 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JobsSupport.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 41bb6d6031b4647e0b493b4555da3740 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /JobsSupport/ActionCategory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Unity.Collections; 3 | 4 | namespace E7.EnumDispatcher 5 | { 6 | /// 7 | /// It make checking for an action's category works in a job. 8 | /// 9 | public struct ActionCategory : IDisposable 10 | where ENUM : struct 11 | { 12 | internal int categoryIndex; 13 | [ReadOnly] internal NativeHashMap fastConvert; 14 | 15 | public void Dispose() 16 | { 17 | fastConvert.Dispose(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /JobsSupport/ActionCategory.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b3b95f49d7bc4d5390ab435e8cdd19b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JobsSupport/ActionExact.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | 3 | namespace E7.EnumDispatcher 4 | { 5 | /// 6 | /// It make checking for a specific action works in a job. 7 | /// 8 | public struct ActionExact 9 | { 10 | internal int2 categoryAndTypeIndex; 11 | } 12 | } -------------------------------------------------------------------------------- /JobsSupport/ActionExact.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fa09db07ec9fc40bd9e53a912a04a7e2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JobsSupport/ActionFlag.cs: -------------------------------------------------------------------------------- 1 | namespace E7.EnumDispatcher 2 | { 3 | /// 4 | /// It make checking for a flag's existence of any action works in a job. 5 | /// 6 | public struct ActionFlag 7 | { 8 | internal int flagValue; 9 | public ActionFlag(string stringFlag, EnumTypeManager etm) => this.flagValue = etm.StringFlagToInt(stringFlag); 10 | public override string ToString() => $"Flag inner value : {flagValue.ToString()}"; 11 | } 12 | } -------------------------------------------------------------------------------- /JobsSupport/ActionFlag.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 86b31492c297642d1bf3563f164d5766 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JobsSupport/JobDispatchAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Unity.Collections; 3 | using Unity.Mathematics; 4 | using UnityEngine; 5 | 6 | namespace E7.EnumDispatcher 7 | { 8 | /// 9 | /// A which works in a job. 10 | /// 11 | public struct JobDispatchAction 12 | { 13 | public override string ToString() => $"JDA inner value : {categoryAndTypeIndex}"; 14 | 15 | internal int2 categoryAndTypeIndex; 16 | //Flags was a string, in the job we use int representation bookkeeped by Izumi out of a job. 17 | //Do not Dispose! Everyone is sharing this memory! 18 | [ReadOnly] internal NativeArray flags; 19 | 20 | internal JobDispatchAction(int category, int actionType, NativeArray flags) 21 | { 22 | this.categoryAndTypeIndex = new int2(category, actionType); 23 | this.flags = flags; 24 | } 25 | 26 | /// 27 | /// Works like `Category` of `DispatchAction` but requires baked `ActionCategory` from outside of the job. 28 | /// The baked `ActionCategory` includes `NativeHashMap` for fast casting to `out` variable from this method. 29 | /// 30 | public bool Category(ActionCategory ac, out ENUM outEnum) 31 | where ENUM : struct 32 | { 33 | if (categoryAndTypeIndex.x == ac.categoryIndex) 34 | { 35 | outEnum = As(ac); 36 | return true; 37 | } 38 | else 39 | { 40 | outEnum = default(ENUM); 41 | return false; 42 | } 43 | } 44 | 45 | /// 46 | /// Works like `Is` of `DispatchAction` but requires baked `ActionExact`, an enum representation from outside of the job. 47 | /// 48 | public bool Is(ActionExact ac) 49 | { 50 | if(ac.categoryAndTypeIndex.Equals(default(int2))) 51 | { 52 | throw new ArgumentException($"ActionExact's content is empty. Did you schedule a job without assigning the field's value?"); 53 | } 54 | return categoryAndTypeIndex.Equals(ac.categoryAndTypeIndex); 55 | } 56 | 57 | /// 58 | /// If you have checked the category outside the job, then you can `switch` on the `As` directly in-job. 59 | /// 60 | public ENUM As(ActionCategory ac) 61 | where ENUM : struct 62 | { 63 | if (ac.fastConvert.TryGetValue(categoryAndTypeIndex.y, out ENUM casted)) 64 | { 65 | return casted; 66 | } 67 | else 68 | { 69 | throw new System.Exception($"Type index {categoryAndTypeIndex.y} does not belong in category {typeof(ENUM).FullName}"); 70 | } 71 | } 72 | 73 | /// 74 | /// In a job you cannot use `string`, so you need to bake `ActionFlag` from outside for use in-job. 75 | /// 76 | public bool Flagged(ActionFlag flag) 77 | { 78 | if(flag.flagValue == default(int)) 79 | { 80 | throw new ArgumentException($"ActionFlag's content is empty. Did you schedule a job without assigning the field's value?"); 81 | } 82 | return flags.Contains(flag.flagValue); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /JobsSupport/JobDispatchAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2bf82c6adc90348ba930be22585ea5f1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enum Dispatcher 2 | 3 | Action dispatching is an important part in [Flux](https://facebook.github.io/flux/docs/dispatcher.html#content) and [Redux](https://redux.js.org/basics/actions), to ensure any "data store" could change its data without caring about anything else other than user's action. Unit testing paradise! Just dispatch actions and see what the data have become. 4 | 5 | I am bringing this workflow to Unity, but erasing the "string action label" pain point of actions in JavaScript by using C#'s `enum` instead. [Redux's designer said that](https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants), he advised using `string` as an action's key rather than JS `Symbol` because it is easily serializable and allows time travel. In Unity I care more about C# tooling provided by `enum`. 6 | 7 | ## Action principle of Redux / Flux 8 | 9 | - Every possible action by user must be represented by action. So in effect, an action cannot cause another action by itself. (So != public methods, which is kinda the "verb" of programming world.) 10 | - Data can only mutate in response to an action. Then that mutation will cause a presentation change. 11 | 12 | ### Benefits in Unity 13 | 14 | The "classic" way of handling action in Unity is something like `EventTrigger` will call a `public` method connected in the inspector, as the starting point. Then that object may further connected to other object via exposed variable, it ask the required objects to do something in chain. This **chain** is troubling when it goes on for about 2-3 layers, you started wondering who called this or change the data, and that "who" is not your user. 15 | 16 | It is equivalent to "cascade update problem" that Facebook mentioned in their [Flux documentation](https://facebook.github.io/flux/docs/in-depth-overview.html#content). 17 | 18 | By only do something in itself based on solely action, you rarely need to chain (public) method calls to tell others to be in sync because others are also handling the action in themselves. Receiving a broadcasted action thanks to callback magic may seems cheating at first, but Unity's connected fields are not that strong either. I think it even became missing and cause bigger problem than callbacks. 19 | 20 | ## Terms 21 | 22 | - **Action** : This is one `enum` number inside an `enum` type. `enum` with the same underlying `int` value but from different `enum` type is considered different. 23 | - **Category** : The `enum` type serves as action's category. You can check if an action is in a category or not. 24 | - **Payload** : One action can be attached with `object` payload. It is so that you can vary an action's detail instead of defining many more granular actions. 25 | - **Payload Key** : You can attach multiple payloads to each action. Because C# do not have dynamic dot notation like JavaScript and I don't want to mess with `dynamic`, you instead use payload key to get the correct payload from an action. Payload Key is also an `enum`. 26 | - **Flag** : Each action is strictly in one Category, however it could be added multiple Flags. An action from a different Category can be assigned the same Flag. For example you have the action `BackButtonPressed` in multiple Category describing pressing the top corner back button of each scene. You could assign `Back` flag to all of them, then have an action receiver do something whenever any `Back` was dispatched. (Like unloading things, etc.). Flags are instead based on `string` and not an another `enum`. You can define `const string` for them. 27 | 28 | ## Coding style 29 | 30 | ### Declaration and dispatching 31 | 32 | Your action declaration may looks like this, along with some simplified usages : 33 | 34 | ```cs 35 | public class MainMenu 36 | { 37 | public enum Action 38 | { 39 | QuitGame, 40 | ToModeSelect, 41 | ToCredits, 42 | TouchedEmptyArea, 43 | JoystickMoved, 44 | [F(Navigation)] LeftButton, //F attribute is short for flags. 45 | [F(Navigation)] RightButton, 46 | } 47 | 48 | public enum PayloadKey 49 | { 50 | TouchCoordinate, 51 | DirectionVector, 52 | Weight, 53 | } 54 | 55 | public const string Navigation = nameof(Navigation); 56 | 57 | public void ToModeSelectButtonOnClick() 58 | { 59 | //Without payload 60 | Dispatcher.Dispatch(MainMenu.Action.ToModeSelect); 61 | } 62 | 63 | public void EmptyAreaOnPointerDown() 64 | { 65 | //With payload (as a tuple of the key and `object`) 66 | Dispatcher.Dispatch(MainMenu.Action.TouchedEmptyArea, 67 | (MainMenu.PayloadKey.TouchCoordinate, new Vector2(100,150)) 68 | ); 69 | } 70 | 71 | public void JoystickOnMove() 72 | { 73 | //Recommended style to enhance readability with multiple payload is to explicitly type `payload:` parameter name. 74 | Dispatcher.Dispatch(MainMenu.Action.JobstickMoved, 75 | payload: 76 | (MainMenu.PayloadKey.DirectionVector, new Vector2(1,0)), 77 | (MainMenu.PayloadKey.Weight, 13) 78 | ); 79 | } 80 | } 81 | ``` 82 | 83 | ### Action handling 84 | 85 | How to directly check for that exact action with `if` : Use `.Is`. 86 | 87 | ```cs 88 | private void OnAction(DispatchAction action) 89 | { 90 | if (action.Is(MusicSelect.Action.SelectSong)) 91 | { 92 | ... 93 | } 94 | else if(action.Is(MusicSelect.Action.SelectDifficulty)) 95 | { 96 | ... 97 | } 98 | } 99 | ``` 100 | 101 | How to handle 2 categories at once with `if` and `switch case` : Use `.Category` then use the generic-typed `out` variable with `switch`. 102 | 103 | ```cs 104 | private void OnAction(DispatchAction action) 105 | { 106 | if (action.Category(out var actMs)) switch (actMs) 107 | { 108 | case MusicSelect.Action.SelectSong: 109 | ... 110 | } 111 | else if (action.Category(out var act)) switch (act) 112 | { 113 | case MusicStart.Action.Begin: 114 | ... 115 | case MusicStart.Action.BeginEditor: 116 | ... 117 | case MusicStart.Action.ToggleRivalView: 118 | ... 119 | case MusicStart.Action.ChangeChartDifficulty: 120 | ... 121 | } 122 | 123 | ... 124 | } 125 | ``` 126 | 127 | 128 | For how to do it in C# Jobs, please see the `Tests` folder. 129 | 130 | ## Why enum? Not string? 131 | 132 | - Strings are brittle and annoying. 133 | - Enums can auto complete. 134 | - Enums are easier to define than `const string`. You don't even have to name the variable. 135 | - Mass-rename by your IDE tooling. 136 | - You can use your IDE to easily find all places that dispatch a certain event by searching enum references. 137 | - Enum can be nested in the class so that dot notation looks nice. It allows you to for example, always name your enum as `Action`, so you don't have to worry about naming conflict. When used, it will looks like `MainMenu.Action.Back`, `ModeSelect.Action.Back` which is quite readable. 138 | - There is an optimization at compiler level that make it fast with `switch case`. It does not require equality comparing case by case but a jump table instead. If these `enum` were just normal `int` it would generate comparison assembly per case, same goes for `string`. This may matter if your action handling code path is hot. (And maybe being Burst compiled for even better assembly.) 139 | 140 | ## Pain points in doing so 141 | 142 | - Different enum may have an equal underlying `int` value. This makes naive enum-as-label implementation wrong as action in one category replacable by action in an another category. Enum Dispatcher can detect that the same `enum` value are coming from a different `enum` type by also including/caching type information. 143 | - Check for action by `==` is fine, but you can't do `switch case` if the receiving side doesn't contains enum typing information. If the receiving side contains the action type information, then it is not capable of handling action across multiple categories. Enum Dispatcher contains an action wrapper named `DispatchAction` instead of the `enum`. It contains various methods to help to determine the exact action while keeping the receiving side just know about `DispatchAction`. 144 | - Action category via `enum` requires bookkeeping the type. Enum Dispatcher cache `enum` types on-the-go inspired by Unity's Entities package's `TypeManager`. 145 | - Can you all that in C# Jobs? So you could check on action type and act all inside a single job instead of checking on the main thread and having to relay information to the job what to do. Yes, Enum Dispatcher can! With support from `JobDispatchAction` it brings together all its category and flags data to the job. On converting from `DispatchAction` to `JobDispatchAction`, it because all `struct` and `NativeArray` based. This bridges the whole thing to ECS as well. Unfortunately action payload cannot go to the job as it is based on `object` type. 146 | 147 | ## Dependencies 148 | 149 | - C# 7.0, it uses tuples extensively too. 150 | - Entities UPM packages and friends. 151 | 152 | ## Why it has to do anything with ECS at all? I don't want to depends on ECS package. 153 | 154 | I *could* design it as a `static` enum dispatcher where anyone can receive the action. However I decided to bring ECS into play : 155 | 156 | - Avoid using `static`, dispatched actions are now `World`-bound. (Though tecnically `World` are `static` beings) 157 | - Allows me to design a `System` which automatically subscribe/unsubscribe to Enum Dispatcher's action because it knows to look for "Dispatching System" in the same world. It works together with `JobDispatchAction` support, so you are not limited to just C# Jobs but use them with `JobComponentSystem`-based action handling. 158 | - (Real reason : Actually I pulled this out from my other hybrid ECS library for dealing with uGUI, so I need it to be compatible with ECS and jobs.) 159 | 160 | And so Enum Dispatcher's `asmdef` requires Entity package present. Install them from Package Manager. 161 | 162 | Also it is a good bridge from normal world to ECS. For example, Normally you connect the uGUI `Button`'s `On Click` to some public methods. It is not possible to connect with ECS's system since they are not in the scene. With Enum Dispatcher, all uGUI `Button` in the game no longer ever have to contains any logic other than dispatching an enum action. ECS system is now able to respond to button press, also your `MonoBehaviour` things can subscribe as an action receiver as well. Also it is awesome for unit testing now that you can mock user's behaviour by just dispatching actions over and over. 163 | 164 | ## Architecture 165 | 166 | - An ECS system `DispatchingSystem` holds C# `event`. You can subscribe or dispatch by getting this system's reference from your `World` and call its public method. You can declare the callback method anywhere, in `MonoBehaviour`, etc. 167 | - Call `dispatchingSystem.Dispatch` on the system instance will invoke all subscribers with that action immediately. You call it with your `enum`, but action handlers will receive `DispatchAction`. Alternatively, an easy utility `static` method `Dispatcher.Dispatch` will get `DispatchingSystem` in your `World.Active` first then do the same thing. Notice that to this point nothing is related to ECS yet. It didn't create any event entity. Just `.Invoke()`. At this point it is already usable as a general purpose enum-based event system. 168 | - On each dispatch call, there is one more system which bookkeep enum types of the action. Each `enum` will get its own index. Both `enum` type index and the `enum` integer value will be used together to represent one unique action. `DispatchAction` is an object containing those information. This bookkeeping system does so by using native containers, so this entire "type dictionary" they could be referenced safely from a job, allowing you to check action type on thread. 169 | - Any ECS system inherited from `ActionHandlerSystem` is automatically subscribed/unsubscribe to `DispatchingSystem` of the `World` it is currently in. `ActionHandlerSystem` receives actions immediately like manual subscribers, but you cannot respond to them just yet. They will all be queued, then on its `OnUpdate` you can respond to them with an opportunity to schedule a job since it is a subclass of `JobComponentSystem`. You should `override` the `virtual` method `OnAction` where it will give you actions one by one in order. `JobHandle` is provided in that respond context so all your jobs are hooked up to ECS job pipeline. This is why you can't respond to action immediately. You can check the action first then schedule appropriate jobs, or bring action into the job and check them inside so you could offload main thread. (If the check and respond is complicated) 170 | - `ActionHandlerSystem` is preconfigured to update in an update group called `ActionHandlerSystem.ActionHandlerGroup`. If you want to make sure your system updates after all actions are handled you can use `[UpdateAfter(typeof(ActionHandlerSystem.ActionHandlerGroup))]`, so you can use the result from scheduled jobs that was a response to an action this frame. 171 | 172 | ## How to use 173 | 174 | Please see usage examples from the `Tests` folder, where you will witness an epic fight with monsters. -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fb3268fdc7f6c40a2a511cc74f1a7f65 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Systems.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5894032cca241492484fc2c31c3d4ed3 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Systems/ActionHandlerSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Unity.Collections; 4 | using Unity.Entities; 5 | using Unity.Jobs; 6 | using Unity.Mathematics; 7 | using UnityEngine; 8 | 9 | 10 | namespace E7.EnumDispatcher 11 | { 12 | 13 | /// 14 | /// A system which can receive and turn them into job scheduling on its . 15 | /// 16 | /// The system contains `[UpdateAfter(typeof())]` 17 | /// Why? It is required so that action dispatched from normal Unity way is immediately handled in the same frame by default. In effect, you cannot add other update order that contains `PlayerLoop` phase because that would throw overspec error. 18 | /// 19 | /// 1. You dispatched from `MonoBehaviour`'s Update(), it is in Update phase. 20 | /// 2. You dispatched from a running `IEnumerator` coroutine, it is in *somewhere* IN Update phase, but after all `Update()` of `MonoBehaviour`. 21 | /// 3. Something like Animator update is in PreLateUpdate phase. If you use `SetTrigger` for example, it has to be before that or 22 | /// it could not consume the trigger in this frame, resulting in 1-frame late. 23 | /// [Try this tool to see more what's in each phase](https://gist.github.com/LotteMakesStuff/8534e01043826754344a570a4cf21002). 24 | /// 25 | /// Therefore the earliest phase that still works with all cases would be **after Update**. 26 | /// 27 | /// Note that "after" using with Unity's phase, means IN that phase but after everything else. 28 | /// You can both receive dispatches from a coroutine, then set animator trigger, then that trigger take effect immediately since it is still before PreLateUpdate. 29 | /// 30 | /// Also the system contains `[UpdateInGroup(typeof())]`. 31 | /// This allows you to place your system after this group. Useful when you want your system to have an intent of "using the action's result". 32 | /// 33 | [UpdateInGroup(typeof(ActionHandlerSystem.ActionHandlerGroup))] 34 | [UpdateAfter(typeof(UnityEngine.PlayerLoop.Update))] 35 | public abstract class ActionHandlerSystem : JobComponentSystem 36 | { 37 | /// 38 | /// You could put your system with on this group to ensure it updates after all action-based system. 39 | /// 40 | public static class ActionHandlerGroup { } 41 | 42 | EnumTypeManager ETM; 43 | DispatchingSystem DispatchingSystem; 44 | protected Queue queuedActions; 45 | bool created; 46 | 47 | /// 48 | /// Please call `base.OnCreate()` on your subclass if you have your own override !! 49 | /// 50 | protected override void OnCreate() 51 | { 52 | ETM = EnumTypeManager.Singleton; 53 | DispatchingSystem = World.GetOrCreateSystem(); 54 | queuedActions = new Queue(); 55 | DispatchingSystem.Subscribe(HandleAction); 56 | created = true; 57 | } 58 | 59 | #if UNITY_EDITOR 60 | private void NotCreatedCheck() 61 | { 62 | if(!created) 63 | { 64 | throw new Exception($"ActionHandlerSystem {this.GetType().Name} was not initialized! Did you forget calling `base.OnCreate()`?"); 65 | } 66 | } 67 | #endif 68 | 69 | /// 70 | /// Called immediately synchronously on dispatching action, but it will just queue the action to be 71 | /// converted to jobs on its turn to update. We have to be in the 's "pipeline" 72 | /// to ensure nice dependency chain. 73 | /// 74 | private void HandleAction(DispatchAction action) 75 | { 76 | //Collect jobs to put in dep chain when update arrives. 77 | queuedActions.Enqueue(new DispatchAction(action)); 78 | } 79 | 80 | protected override JobHandle OnUpdate(JobHandle inputDeps) 81 | { 82 | while(queuedActions.Count > 0) 83 | { 84 | inputDeps = OnAction(queuedActions.Dequeue(), inputDeps); 85 | } 86 | return inputDeps; 87 | } 88 | 89 | /// 90 | /// You should use do , in order to make which can be copied into a job. 91 | /// 92 | protected abstract JobHandle OnAction(DispatchAction da, JobHandle jobHandle); 93 | 94 | protected override void OnDestroy() 95 | { 96 | //In the case of world destroying that system might have gone first. 97 | DispatchingSystem?.Unsubscribe(HandleAction); 98 | } 99 | 100 | 101 | /// 102 | /// Call this on your ActionHandlerSystem's OnCreate. 103 | /// Do not dispose ActionCategory, it will be automatically on system's OnDestroy. 104 | /// 105 | protected ActionCategory GetActionCategory() where ENUM : struct, IConvertible 106 | { 107 | #if UNITY_EDITOR 108 | NotCreatedCheck(); 109 | #endif 110 | return new ActionCategory 111 | { 112 | categoryIndex = ETM.GetCategoryIndex(), 113 | fastConvert = ETM.Category().FastCastDictionary, 114 | }; 115 | } 116 | 117 | protected ActionFlag GetActionFlag(string flag) 118 | { 119 | #if UNITY_EDITOR 120 | NotCreatedCheck(); 121 | #endif 122 | return new ActionFlag(flag, ETM); 123 | } 124 | 125 | protected ActionExact GetActionExact(ENUM action) where ENUM : struct, IConvertible 126 | { 127 | #if UNITY_EDITOR 128 | NotCreatedCheck(); 129 | #endif 130 | return new ActionExact 131 | { 132 | categoryAndTypeIndex = new int2(ETM.GetCategoryIndex(), ETM.Category().FastCastToActionType(action)) 133 | }; 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /Systems/ActionHandlerSystem.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a33fe646ae0e0430183970b8aa0993c6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Systems/DispatchingSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Unity.Entities; 3 | using UnityEngine; 4 | 5 | namespace E7.EnumDispatcher 6 | { 7 | /// 8 | /// A system holding `event`. Each goes to all receivers of this `event`. 9 | /// System like is automatically a dispatch subscriber on its . 10 | /// 11 | public class DispatchingSystem : ComponentSystem 12 | { 13 | private event ActionHandlerDelegate DispatchTargets; 14 | private EnumTypeManager ETM; 15 | 16 | /// 17 | /// Create an action and dispatch. 18 | /// 19 | public void Dispatch(T e, 20 | params (Enum, object)[] payload) 21 | where T : struct, IConvertible 22 | => Dispatch(DispatchAction.Create(ETM, e, payload)); 23 | 24 | /// 25 | /// Signal that something changed. It is useful for stores to react to non-ECS data. 26 | /// 27 | public void SignalChanged(T e, 28 | params (Enum, object)[] payload) 29 | where T : struct, IConvertible 30 | => Dispatch(ChangedSignal.Create(ETM, e, payload)); 31 | 32 | /// 33 | /// Dispatch with pre-created action. Use method to create and cache an action. 34 | /// 35 | public void Dispatch(DispatchAction da) 36 | { 37 | DispatchTargets?.Invoke(da); 38 | } 39 | 40 | /// 41 | /// You could subscribe with any out-of-ECS callback, but remember to as well. 42 | /// 43 | public void Subscribe(ActionHandlerDelegate handler) => DispatchTargets += handler; 44 | public void Unsubscribe(ActionHandlerDelegate handler) => DispatchTargets -= handler; 45 | 46 | protected override void OnCreate() 47 | { 48 | base.OnCreate(); 49 | ETM = EnumTypeManager.Singleton; 50 | Enabled = false; 51 | } 52 | 53 | protected override void OnUpdate() { } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Systems/DispatchingSystem.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1f9df1bd6fe0b450bb250404098db052 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Systems/EnumTypeManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Unity.Collections; 5 | using Unity.Entities; 6 | using UnityEngine; 7 | 8 | #if UNITY_EDITOR 9 | using UnityEditor; 10 | #endif 11 | 12 | namespace E7.EnumDispatcher 13 | { 14 | /// 15 | /// A database to make enum-as-action works. 16 | /// Stole the idea from Unity ECS's TypeManager. Haha! 17 | /// 18 | [DisableAutoCreation] 19 | public class EnumTypeManager : ComponentSystem 20 | { 21 | const string etmWorldName = "Izumi ETM World"; 22 | static World etmWorld; 23 | static EnumTypeManager singletonEtm; 24 | 25 | /// 26 | /// To not litter your game's world with utility systems. 27 | /// 28 | public static EnumTypeManager Singleton 29 | { 30 | get 31 | { 32 | #if UNITY_EDITOR 33 | //On exiting play mode this is from F T T T -> F F T F 34 | //Debug.Log($"{EditorApplication.isPlayingOrWillChangePlaymode} && !{EditorApplication.isPlaying} && {etmWorld != null} && {etmWorld?.IsCreated}"); 35 | // if (EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isPlaying) 36 | // { 37 | // return null; 38 | // } 39 | #endif 40 | if (etmWorld == null || etmWorld.IsCreated == false) 41 | { 42 | etmWorld = new World(etmWorldName); 43 | singletonEtm = etmWorld.GetOrCreateSystem(); 44 | } 45 | return singletonEtm; 46 | } 47 | } 48 | 49 | //One system will be created per action category. This is to make things static-like while not really static. (Static in a world) 50 | public EnumTypeManager Category() where T : struct, IConvertible 51 | => World.GetOrCreateSystem>(); 52 | 53 | protected override void OnUpdate() { } 54 | protected override void OnCreate() 55 | { 56 | this.Enabled = false; 57 | } 58 | 59 | internal int GetCategoryIndex() where ENUM : struct, IConvertible 60 | { 61 | //Debug.Log($"Looking up {typeof(ENUM).FullName} {StaticLookup.typeIndex} "); 62 | var cat = Category(); 63 | if (cat.typeIndex == default(int)) 64 | { 65 | runningEnumTypeIndex++; 66 | cat.typeIndex = runningEnumTypeIndex; 67 | typeDict.Add(runningEnumTypeIndex, typeof(ENUM)); 68 | //Debug.Log($"Added category {typeof(ENUM).FullName} as {runningEnumTypeIndex}"); 69 | } 70 | return cat.typeIndex; 71 | } 72 | 73 | internal string GetFullNameFromIndex(int index) => GetTypeFromIndex(index).FullName; 74 | internal string GetValueNameFromIndex(int index, int valueIndex) => Enum.GetName(GetTypeFromIndex(index), valueIndex); 75 | 76 | /// 77 | /// Dictionary-cached `typeof` of that `index`. 78 | /// 79 | internal Type GetTypeFromIndex(int index) 80 | { 81 | if (typeDict.TryGetValue(index, out Type t)) 82 | { 83 | return t; 84 | } 85 | else 86 | { 87 | throw new ArgumentException($"Type index {index} is not yet indexed. Please ensure the index argument was made from `GetTypeIndex`."); 88 | } 89 | } 90 | 91 | internal int StringFlagToInt(string stringFlag) 92 | { 93 | if (stringFlagToIntFlagDict.TryGetValue(stringFlag, out int intFlag)) 94 | { 95 | return intFlag; 96 | } 97 | else 98 | { 99 | runningFlagIndex++; 100 | stringFlagToIntFlagDict.Add(stringFlag, runningFlagIndex); 101 | intFlagToStringFlagDict.Add(runningFlagIndex, stringFlag); 102 | return runningFlagIndex; 103 | } 104 | } 105 | 106 | internal string IntFlagToString(int intFlag) 107 | { 108 | if (intFlagToStringFlagDict.TryGetValue(intFlag, out string stringFlag)) 109 | { 110 | return stringFlag; 111 | } 112 | else 113 | { 114 | throw new ArgumentException($"{intFlag} is not corresponding to any previously registered string flag"); 115 | } 116 | } 117 | 118 | private int runningEnumTypeIndex = default(int); 119 | private Dictionary typeDict = new Dictionary(); 120 | 121 | private int runningFlagIndex = default(int); 122 | //Hash collision will be handled by dictionary. 123 | private Dictionary stringFlagToIntFlagDict = new Dictionary(); 124 | private Dictionary intFlagToStringFlagDict = new Dictionary(); 125 | } 126 | 127 | /// 128 | /// A utility system holding cached information for a specific type of enum. 129 | /// 130 | //Because it contains generic, it won't be auto created to the world. 131 | public class EnumTypeManager : ComponentSystem 132 | where T : struct, IConvertible 133 | { 134 | EnumTypeManager ETM; 135 | protected override void OnCreate() 136 | { 137 | ETM = World.GetOrCreateSystem(); 138 | this.Enabled = false; 139 | } 140 | 141 | bool castMapsGenerated; 142 | protected override void OnDestroy() 143 | { 144 | if (castMapsGenerated) 145 | { 146 | fastCastDictionary.Dispose(); 147 | foreach (var na in flagsDictionary.Values) 148 | { 149 | na.Dispose(); 150 | } 151 | } 152 | } 153 | 154 | protected override void OnUpdate() { } 155 | 156 | internal string[] names; 157 | internal int typeIndex; 158 | internal Type type; 159 | 160 | private NativeHashMap fastCastDictionary; 161 | internal NativeHashMap FastCastDictionary 162 | { 163 | get 164 | { 165 | if (!castMapsGenerated) GenerateAllCastMaps(); 166 | return fastCastDictionary; 167 | } 168 | } 169 | 170 | //enum does not implement IEquatable lol 171 | private Dictionary fastCastBackDictionary; 172 | internal Dictionary FastCastBackDictionary 173 | { 174 | get 175 | { 176 | if (!castMapsGenerated) GenerateAllCastMaps(); 177 | return fastCastBackDictionary; 178 | } 179 | } 180 | 181 | //We would like to be able to hand those flags directly to C# jobs. 182 | //So we are storing these native containers statically in the first place then share to everyone. 183 | private Dictionary> flagsDictionary; 184 | internal Dictionary> FlagsDictionary 185 | { 186 | get 187 | { 188 | if (!castMapsGenerated) GenerateAllCastMaps(); 189 | return flagsDictionary; 190 | } 191 | } 192 | 193 | /// 194 | /// Possible to call this manually to cache enums even before the first actual use. 195 | /// 196 | public void GenerateAllCastMaps() 197 | { 198 | var type = typeof(T); 199 | var values = (T[])(Enum.GetValues(type)); 200 | var intValues = values.Select(x => Convert.ToInt32(x)).ToArray(); 201 | fastCastDictionary = new NativeHashMap(values.Length, Allocator.Persistent); 202 | fastCastBackDictionary = new Dictionary(values.Length); 203 | flagsDictionary = new Dictionary>(values.Length); 204 | for (int i = 0; i < values.Length; i++) 205 | { 206 | fastCastDictionary.TryAdd(intValues[i], values[i]); 207 | fastCastBackDictionary.Add(values[i], intValues[i]); 208 | } 209 | 210 | for (int i = 0; i < values.Length; i++) 211 | { 212 | var memInfo = type.GetMember(values[i].ToString()); 213 | var attributes = memInfo[0].GetCustomAttributes(typeof(FAttribute), false); 214 | string[] getFlags = (attributes.Length > 0) ? ((FAttribute)attributes[0]).flags : new string[0]; 215 | int[] intFlags = getFlags.Select(x => ETM.StringFlagToInt(x)).ToArray(); 216 | 217 | NativeArray naIntFlags = new NativeArray(intFlags, Allocator.Persistent); 218 | flagsDictionary.Add(intValues[i], naIntFlags); 219 | } 220 | castMapsGenerated = true; 221 | } 222 | 223 | /// 224 | /// Cached `typeof(ENUM)`. 225 | /// 226 | public Type GetTypeOfEnum() 227 | { 228 | if (type == default(Type)) 229 | { 230 | type = typeof(T); 231 | } 232 | return type; 233 | } 234 | 235 | /// 236 | /// Array of flags are statically cached for each action type. 237 | /// Each `DispatchAction` would then get a copy of these arrays by reference. 238 | /// 239 | internal NativeArray GetFlags(T actionType) 240 | { 241 | if (FlagsDictionary.TryGetValue(FastCastToActionType(actionType), out NativeArray flags)) 242 | { 243 | return flags; 244 | } 245 | else 246 | { 247 | throw new System.Exception($"It should have cache everything in {typeof(T).Name}, why {actionType} not found?"); 248 | } 249 | } 250 | 251 | /// 252 | /// Dictionary-cached int + generic to ENUM cast. 253 | /// 254 | internal T FastCastFromActionType(int enumIntValue) 255 | { 256 | if (FastCastDictionary.TryGetValue(enumIntValue, out T castedValue)) 257 | { 258 | return castedValue; 259 | } 260 | else 261 | { 262 | throw new System.Exception($"It should have cache everything in {typeof(T).Name}, why {enumIntValue} not found?"); 263 | } 264 | } 265 | 266 | /// 267 | /// Dictionary-cached enum + generic to int cast. 268 | /// 269 | internal int FastCastToActionType(T enumValue) 270 | { 271 | if (FastCastBackDictionary.TryGetValue(enumValue, out int castedValue)) 272 | { 273 | //Debug.Log($"Fast casted {enumValue} to integer {castedValue}"); 274 | return castedValue; 275 | } 276 | else 277 | { 278 | throw new System.Exception($"It should have cache everything in {typeof(T).Name}, why {enumValue} not found?"); 279 | } 280 | } 281 | 282 | /// 283 | /// Cached enum names 284 | /// 285 | public string[] GetNames() 286 | { 287 | if (names == default(string[])) 288 | { 289 | names = Enum.GetNames(GetTypeOfEnum()); 290 | } 291 | return names; 292 | } 293 | 294 | public string FullNiceName() => GetTypeOfEnum().FullName.Replace('+', '.'); 295 | } 296 | } -------------------------------------------------------------------------------- /Systems/EnumTypeManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9dfbd517a282d4c959f77ea58d063b11 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 006e71d05b5e847df9db2f1177442dc9 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/DispatchActionTests.cs: -------------------------------------------------------------------------------- 1 | using E7.EnumDispatcher; 2 | using NUnit.Framework; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | using UnityEngine.TestTools; 7 | 8 | namespace E7.EnumDispatcher.Tests 9 | { 10 | internal class DispatchActionTests : EnumDispatcherTestBase 11 | { 12 | [Test] 13 | public void MatchInCategory() 14 | { 15 | var thunder = Dispatch(Magic.Thunder); 16 | 17 | Assert.That(thunder.Is(Magic.Thunder)); 18 | Assert.That(thunder.Is(Magic.Flare), Is.Not.True); 19 | Assert.That(thunder.Category(out _)); 20 | } 21 | 22 | [Test] 23 | public void SwitchCasing() 24 | { 25 | var act = Dispatch(Magic.Thunder); 26 | Assert.That(SwitchTest(act), Is.EqualTo(1)); 27 | 28 | act = Dispatch(Magic.Meteo); 29 | Assert.That(SwitchTest(act), Is.EqualTo(2), "It have to hit the default case not the Is case"); 30 | 31 | act = Dispatch(Items.XPotion); 32 | Assert.That(SwitchTest(act), Is.EqualTo(4), "It have to hit the Is case before the next switch case"); 33 | 34 | act = Dispatch(Items.Elixir); 35 | Assert.That(SwitchTest(act), Is.EqualTo(5)); 36 | 37 | act = Dispatch(Items.Potion); 38 | Assert.That(SwitchTest(act), Is.EqualTo(6)); 39 | 40 | act = Dispatch(Act.Jump); 41 | Assert.That(SwitchTest(act), Is.EqualTo(7)); 42 | 43 | int SwitchTest(DispatchAction da) 44 | { 45 | if (da.Category(out Magic m)) switch (m) 46 | { 47 | case Magic.Thunder: return 1; 48 | default: return 2; 49 | } 50 | else if(da.Is(Magic.Meteo)) return 3; 51 | else if(da.Is(Items.XPotion)) return 4; 52 | else if (da.Category(out Items i)) switch (i) 53 | { 54 | case Items.Elixir: return 5; 55 | default: return 6; 56 | } 57 | return 7; 58 | } 59 | } 60 | 61 | [Test] 62 | public void AsWorks() 63 | { 64 | var fire = Dispatch(Magic.Fire); 65 | Assert.That(fire.As(), Is.EqualTo(Magic.Fire)); 66 | Assert.That(fire.As(), Is.EqualTo(Items.Potion), "This is allowed, because As can assume any underlying int as any category."); 67 | } 68 | 69 | [Test] 70 | public void PayloadUnboxing() 71 | { 72 | var fire = Dispatch(Magic.Fire, (PayloadKey.HitStat, (crit: true, weakness: false)), (PayloadKey.Comment, "So hot")); 73 | 74 | if (fire.HasPayload(PayloadKey.HitStat, out bool criticalHit)) 75 | { 76 | Assert.Fail("Type is wrong, the out overload should not work."); 77 | } 78 | if (fire.HasPayload(PayloadKey.HitStat, out (bool crit, bool weakness) hitStat)) 79 | { 80 | //Payload can hold tuples too 81 | Assert.That(hitStat.crit); 82 | Assert.That(!hitStat.weakness); 83 | } 84 | else 85 | { 86 | Assert.Fail("When out type is castable it should go in the if"); 87 | } 88 | 89 | if(fire.HasPayload(PayloadKey.Comment, out int ohno)) 90 | { 91 | Assert.Fail("Type is wrong, the out overload should not work."); 92 | } 93 | if (fire.HasPayload(PayloadKey.HitStat, out string hot)) 94 | { 95 | Assert.Fail("Type is right but the payload key is wrong, it should not work."); 96 | } 97 | if(fire.HasPayload(PayloadKey.Comment, out string ok)) 98 | { 99 | Assert.That(ok, Is.EqualTo("So hot")); 100 | } 101 | 102 | //Test the GetPayload 103 | 104 | (bool crit, bool weakness) = fire.GetPayload<(bool, bool)>(PayloadKey.HitStat); 105 | 106 | Assert.That(crit); 107 | Assert.That(!weakness); 108 | 109 | Assert.Throws(() => fire.GetPayload(PayloadKey.HitStat)); 110 | 111 | var firePlain = Dispatch(Magic.Fire); 112 | 113 | Assert.Throws(() => firePlain.GetPayload(PayloadKey.HitStat)); 114 | 115 | fire = Dispatch(Magic.Fire, (PayloadKey.Attacker, 1), (FakePayloadKey.Attacker, 555)); 116 | Assert.That(fire.GetPayload(PayloadKey.Attacker), Is.EqualTo(1), "Check that payload key from 2 different enum with same value is discernable by the dict"); 117 | Assert.That(fire.GetPayload(FakePayloadKey.Attacker), Is.EqualTo(555), "Check that payload key from 2 different enum with same value is discernable by the dict"); 118 | 119 | } 120 | 121 | [Test] 122 | public void DoesNotMatchAcrossCategories() 123 | { 124 | var fire = Dispatch(Magic.Fire); 125 | var potion = Dispatch(Items.Potion); 126 | 127 | Assert.That(fire.Is(Items.Potion), Is.Not.True); 128 | Assert.That(fire.Category(out _), Is.Not.True); 129 | 130 | Assert.That(potion.Is(Magic.Fire), Is.Not.True); 131 | Assert.That(potion.Category(out _), Is.Not.True); 132 | } 133 | 134 | [Test] 135 | public void FlagsWorks() 136 | { 137 | var fire = Dispatch(Magic.Fire); 138 | var potion = Dispatch(Items.Potion); 139 | var ult_flare = Dispatch(Magic.Flare); 140 | var ult_elixir = Dispatch(Items.Elixir); 141 | var meteo = Dispatch(Magic.Meteo); 142 | var thunder = Dispatch(Magic.Thunder); 143 | 144 | Assert.That(fire.Flagged(Ultimate), Is.Not.True); 145 | Assert.That(potion.Flagged(Ultimate), Is.Not.True); 146 | Assert.That(ult_flare.Flagged(Ultimate)); 147 | Assert.That(ult_elixir.Flagged(Ultimate)); 148 | 149 | Assert.That(meteo.Flagged(AOEMagic),"Some action can has multiple flags"); 150 | Assert.That(meteo.Flagged(Ultimate),"Some action can has multiple flags"); 151 | 152 | Assert.That(thunder.Flagged(AOEMagic), "Some action can share a flag with other action in the same category, unlike enum values."); 153 | Assert.That(thunder.Flagged(Ultimate), Is.Not.True, "Flags that was shared with other action has no relationship with other flags."); 154 | } 155 | 156 | [Test] 157 | public void OptionalPayload() 158 | { 159 | var fire = Dispatch(Magic.Fire, (PayloadKey.Crit, true)); 160 | var thunder = Dispatch(Magic.Thunder, (PayloadKey.Crit, true), (PayloadKey.All, true)); 161 | 162 | Assert.Throws(() => fire.GetPayload(PayloadKey.Crit, PayloadKey.All), "No optional set, cannot find the 2nd key."); 163 | Assert.That(fire.GetPayload(PayloadKey.Crit, PayloadKey.All, optionals: (false, true)).Item1, Is.True); 164 | Assert.That(fire.GetPayload(PayloadKey.Crit, PayloadKey.All, optionals: (false, true)).Item2, Is.False, "Because optional the non existence key will be a default."); 165 | 166 | Assert.That(thunder.GetPayload(PayloadKey.Crit, PayloadKey.All, optionals: (true, true)).Item1, 167 | Is.True, 168 | "Optionals do not modify a payload that do exist."); 169 | Assert.That(thunder.GetPayload(PayloadKey.Crit, PayloadKey.All, optionals: (true, true)).Item2, 170 | Is.True, 171 | "Optionals do not modify a payload that do exist."); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Tests/DispatchActionTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 12b04d65fe29f4bbaa5d2873dfaf9e79 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/E7.EnumDispatcher.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "E7.EnumDispatcher.Tests", 3 | "references": [ 4 | "E7.EnumDispatcher", 5 | "Unity.Burst", 6 | "Unity.Entities", 7 | "Unity.Mathematics", 8 | "Unity.Collections", 9 | "Unity.Entities.Hybrid" 10 | ], 11 | "optionalUnityReferences": [ 12 | "TestAssemblies" 13 | ], 14 | "includePlatforms": [ 15 | "Editor" 16 | ], 17 | "excludePlatforms": [], 18 | "allowUnsafeCode": false, 19 | "overrideReferences": false, 20 | "precompiledReferences": [], 21 | "autoReferenced": true, 22 | "defineConstraints": [] 23 | } -------------------------------------------------------------------------------- /Tests/E7.EnumDispatcher.Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3c868fd6c5a414dea8ece67a15eca694 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests/EnumDispatcherTestBase.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace E7.EnumDispatcher.Tests 5 | { 6 | public class EnumDispatcherTestBase : MyECSTestsFixture 7 | { 8 | protected enum FakePayloadKey 9 | { 10 | Attacker, 11 | Attackee, 12 | Crit, 13 | Weakness, 14 | HitStat, 15 | Comment, 16 | Target, 17 | All, 18 | ThrownItem, 19 | } 20 | 21 | protected enum PayloadKey 22 | { 23 | Attacker, 24 | Attackee, 25 | Crit, 26 | Weakness, 27 | HitStat, 28 | Comment, 29 | Target, 30 | All, 31 | ThrownItem, 32 | } 33 | 34 | public class MainMenu 35 | { 36 | public enum Action 37 | { 38 | QuitGame, 39 | ToModeSelect, 40 | ToCredits, 41 | TouchedEmptyArea, 42 | [F(Navigation)] LeftButton, 43 | [F(Navigation)] RightButton, 44 | } 45 | 46 | public enum PayloadKey 47 | { 48 | TouchCoordinate, 49 | } 50 | 51 | protected const string Navigation = nameof(Navigation); 52 | } 53 | 54 | protected enum Magic 55 | { 56 | Fire, 57 | Ice, 58 | [F(AOEMagic)] Thunder, 59 | [F(Ultimate)] Holy, 60 | [F(Ultimate)] Flare, 61 | [F(Ultimate, AOEMagic)] Meteo, 62 | Osmose, 63 | [F(Sucks)] Poison, 64 | } 65 | 66 | protected enum Items 67 | { 68 | [F(Healing)] Potion, 69 | [F(Healing)] HiPotion, 70 | [F(Healing)] XPotion, 71 | [F(Healing, Ultimate)] Elixir, 72 | Ether, 73 | [F(Sucks)] SmokeBomb 74 | } 75 | 76 | protected enum Act 77 | { 78 | Jump 79 | } 80 | 81 | protected const string Sucks = nameof(Sucks); 82 | protected const string Ultimate = nameof(Ultimate); 83 | protected const string Healing = nameof(Healing); 84 | protected const string AOEMagic = nameof(AOEMagic); 85 | 86 | [SetUp] 87 | public void PrepareDispatcher() 88 | { 89 | Dispatcher.Active.Subscribe(TestHandler); 90 | } 91 | 92 | [TearDown] 93 | public void UnregisterDispatcher() 94 | { 95 | Dispatcher.Active.Unsubscribe(TestHandler); 96 | } 97 | 98 | 99 | DispatchAction dispatchedAction; 100 | protected void TestHandler(DispatchAction da) => dispatchedAction = da; 101 | 102 | protected DispatchAction Dispatch(T e, 103 | params (Enum, object)[] payload) 104 | where T : struct, IConvertible 105 | { 106 | Dispatcher.Dispatch(e,payload); 107 | return dispatchedAction; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Tests/EnumDispatcherTestBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 16a987611fdbc494c98a7813d8064fe5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/JobDispatchActionTests.cs: -------------------------------------------------------------------------------- 1 | using E7.EnumDispatcher; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using Unity.Entities; 7 | using Unity.Jobs; 8 | using Unity.Burst; 9 | using UnityEngine; 10 | using UnityEngine.TestTools; 11 | using Unity.Collections; 12 | 13 | namespace E7.EnumDispatcher.Tests 14 | { 15 | internal class JobDispatchActionTests : EnumDispatcherTestBase 16 | { 17 | private class TestActionHandlerSystem : ActionHandlerSystem 18 | { 19 | ActionCategory magicCategory; 20 | ActionCategory itemCategory; 21 | protected override void OnCreate() 22 | { 23 | base.OnCreate(); 24 | itemCategory = GetActionCategory(); 25 | magicCategory = GetActionCategory(); 26 | } 27 | 28 | public ActionFlag Flag(string flag) => GetActionFlag(flag); 29 | 30 | public ActionCategory Cat() 31 | where ENUM : struct, IConvertible 32 | => GetActionCategory(); 33 | 34 | public ActionExact Exactly(ENUM action) where ENUM : struct, IConvertible => GetActionExact(action); 35 | 36 | protected override JobHandle OnAction(DispatchAction da, JobHandle jobHandle) 37 | { 38 | return jobHandle; 39 | } 40 | 41 | public void JobSurvivalTest() 42 | { 43 | 44 | } 45 | 46 | } 47 | 48 | private TestActionHandlerSystem TAHS; 49 | 50 | [SetUp] 51 | public void AddHandlerSystem() 52 | { 53 | TAHS = World.CreateSystem(); 54 | } 55 | 56 | [Test] 57 | public void MatchInCategory() 58 | { 59 | var thunder = Dispatch(Magic.Thunder).CastJob(); 60 | var magicCategory = TAHS.Cat(); 61 | var thunderExact = TAHS.Exactly(Magic.Thunder); 62 | var flareExact = TAHS.Exactly(Magic.Flare); 63 | 64 | Assert.That(thunder.Is(thunderExact)); 65 | Assert.That(thunder.Is(flareExact), Is.Not.True); 66 | Assert.That(thunder.Category(magicCategory, out _)); 67 | } 68 | 69 | [Test] 70 | public void SwitchCasing() 71 | { 72 | var magicCategory = TAHS.Cat(); 73 | var itemsCategory = TAHS.Cat(); 74 | var meteoExact = TAHS.Exactly(Magic.Meteo); 75 | var xPotionExact = TAHS.Exactly(Items.XPotion); 76 | 77 | var act = Dispatch(Magic.Thunder).CastJob(); 78 | Assert.That(SwitchTest(act), Is.EqualTo(1)); 79 | 80 | act = Dispatch(Magic.Meteo).CastJob(); 81 | Assert.That(SwitchTest(act), Is.EqualTo(2), "It have to hit the default case not the Is case"); 82 | 83 | act = Dispatch(Items.XPotion).CastJob(); 84 | Assert.That(SwitchTest(act), Is.EqualTo(4), "It have to hit the Is case before the next switch case"); 85 | 86 | act = Dispatch(Items.Elixir).CastJob(); 87 | Assert.That(SwitchTest(act), Is.EqualTo(5)); 88 | 89 | act = Dispatch(Items.Potion).CastJob(); 90 | Assert.That(SwitchTest(act), Is.EqualTo(6)); 91 | 92 | act = Dispatch(Act.Jump).CastJob(); 93 | Assert.That(SwitchTest(act), Is.EqualTo(7)); 94 | 95 | int SwitchTest(JobDispatchAction jda) 96 | { 97 | using (NativeArray getResult = new NativeArray(1, Allocator.TempJob)) 98 | { 99 | new SwitchCasingJob() 100 | { 101 | da = jda, 102 | magicCategory = magicCategory, 103 | itemsCategory = itemsCategory, 104 | meteoExact = meteoExact, 105 | xPotionExact = xPotionExact, 106 | result = getResult, 107 | }.Schedule().Complete(); 108 | Assert.That(getResult[0], Is.Not.EqualTo(default(int))); 109 | return getResult[0]; 110 | } 111 | } 112 | } 113 | 114 | [BurstCompile] 115 | private struct SwitchCasingJob : IJob 116 | { 117 | public JobDispatchAction da; 118 | public ActionCategory magicCategory; 119 | public ActionCategory itemsCategory; 120 | public ActionExact meteoExact; 121 | public ActionExact xPotionExact; 122 | public NativeArray result; 123 | 124 | public void Execute() => result[0] = Yo(da); 125 | 126 | private int Yo(JobDispatchAction da) 127 | { 128 | if (da.Category(magicCategory, out Magic m)) switch (m) 129 | { 130 | case Magic.Thunder: return 1; 131 | default: return 2; 132 | } 133 | else if (da.Is(meteoExact)) return 3; 134 | else if (da.Is(xPotionExact)) return 4; 135 | else if (da.Category(itemsCategory, out Items i)) switch (i) 136 | { 137 | case Items.Elixir: return 5; 138 | default: return 6; 139 | } 140 | return 7; 141 | } 142 | } 143 | 144 | [Test] 145 | public void PayloadMetaJobifying() 146 | { 147 | var fire = Dispatch(Magic.Fire, (PayloadKey.HitStat, (crit: true, weakness: false, sohot: 50807))); 148 | var fireJob = fire.CastJob(); 149 | var payload = fire.GetPayload<(bool crit, bool weakness, int sohot)>(PayloadKey.HitStat); 150 | Assert.That(payload.crit); 151 | Assert.That(payload.weakness, Is.Not.True); 152 | Assert.That(payload.sohot, Is.EqualTo(50807)); 153 | 154 | var survival = new SurvivalJob() { jda = fireJob, payload = payload, ultimate = TAHS.Flag(Ultimate) }; 155 | survival.Schedule().Complete(); 156 | //Survived 157 | } 158 | 159 | //Burst does not compile JobPayload??? 160 | //[BurstCompile] 161 | private struct SurvivalJob : IJob 162 | { 163 | public JobDispatchAction jda; 164 | public (bool crit, bool weakness, int sohot) payload; 165 | public ActionFlag ultimate; 166 | public void Execute() 167 | { 168 | if (jda.Flagged(ultimate)) 169 | { 170 | int yay = payload.sohot * (payload.crit ? 5 : 2); 171 | } 172 | } 173 | } 174 | 175 | [Test] 176 | public void DoesNotMatchAcrossCategories() 177 | { 178 | var fire = Dispatch(Magic.Fire).CastJob(); 179 | var potion = Dispatch(Items.Potion).CastJob(); 180 | 181 | Assert.That(fire.Is(TAHS.Exactly(Items.Potion)), Is.Not.True); 182 | Assert.That(fire.Category(TAHS.Cat(), out _), Is.Not.True); 183 | 184 | Assert.That(potion.Is(TAHS.Exactly(Magic.Fire)), Is.Not.True); 185 | Assert.That(potion.Category(TAHS.Cat(), out _), Is.Not.True); 186 | } 187 | 188 | [Test] 189 | public void FlagsWorksInJob() 190 | { 191 | var fire = Dispatch(Magic.Fire).CastJob(); 192 | var potion = Dispatch(Items.Potion).CastJob(); 193 | var ult_flare = Dispatch(Magic.Flare).CastJob(); 194 | var ult_elixir = Dispatch(Items.Elixir).CastJob(); 195 | var meteo = Dispatch(Magic.Meteo).CastJob(); 196 | var thunder = Dispatch(Magic.Thunder).CastJob(); 197 | using (NativeArray fail = new NativeArray(1, Allocator.TempJob)) 198 | { 199 | new FlagTestJob() 200 | { 201 | fire = fire, 202 | potion = potion, 203 | ult_flare = ult_flare, 204 | ult_elixir = ult_elixir, 205 | fail = fail, 206 | meteo = meteo, 207 | thunder = thunder, 208 | ultimate = TAHS.Flag(Ultimate), 209 | aoeMagic = TAHS.Flag(AOEMagic) 210 | }.Schedule().Complete(); 211 | 212 | Assert.That(fail[0], Is.Zero); 213 | } 214 | } 215 | 216 | [BurstCompile] 217 | private struct FlagTestJob : IJob 218 | { 219 | public JobDispatchAction fire; 220 | public JobDispatchAction potion; 221 | public JobDispatchAction ult_flare; 222 | public JobDispatchAction ult_elixir; 223 | 224 | public JobDispatchAction meteo; 225 | public JobDispatchAction thunder; 226 | 227 | public NativeArray fail; 228 | public ActionFlag ultimate; 229 | public ActionFlag aoeMagic; 230 | public void Execute() 231 | { 232 | if (fire.Flagged(ultimate)) Fail(); 233 | if (potion.Flagged(ultimate)) Fail(); 234 | if (!ult_flare.Flagged(ultimate)) Fail(); 235 | if (!ult_elixir.Flagged(ultimate)) Fail(); 236 | if (!meteo.Flagged(ultimate)) Fail(); 237 | if (!meteo.Flagged(aoeMagic)) Fail(); 238 | if (!thunder.Flagged(aoeMagic)) Fail(); 239 | if (thunder.Flagged(ultimate)) Fail(); 240 | } 241 | 242 | private void Fail() => fail[0] = 555; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Tests/JobDispatchActionTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3016fc933cf6c49fc890b834140ea32e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/MyECSTestsFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using NUnit.Framework; 3 | using Unity.Entities; 4 | 5 | namespace E7.EnumDispatcher.Tests 6 | { 7 | /// 8 | /// Copied from ECSTestsFixture because test asm linking is kinda wonky right now 9 | /// 10 | public class MyECSTestsFixture 11 | { 12 | protected World m_PreviousWorld; 13 | protected World World; 14 | protected EntityManager m_Manager; 15 | protected EntityManager.EntityManagerDebug m_ManagerDebug; 16 | 17 | protected int StressTestEntityCount = 1000; 18 | 19 | [SetUp] 20 | public virtual void Setup() 21 | { 22 | // Redirect Log messages in NUnit which get swallowed (from GC invoking destructor in some cases) 23 | // System.Console.SetOut(NUnit.Framework.TestContext.Out); 24 | 25 | m_PreviousWorld = World.DefaultGameObjectInjectionWorld; 26 | World = World.DefaultGameObjectInjectionWorld = new World("Test World"); 27 | 28 | m_Manager = World.EntityManager; 29 | m_ManagerDebug = new EntityManager.EntityManagerDebug(m_Manager); 30 | } 31 | 32 | [TearDown] 33 | public virtual void TearDown() 34 | { 35 | if (m_Manager != null && m_Manager.IsCreated) 36 | { 37 | // Clean up systems before calling CheckInternalConsistency because we might have filters etc 38 | // holding on SharedComponentData making checks fail 39 | while (World.Systems.ToArray().Length > 0) 40 | { 41 | World.DestroySystem(World.Systems.ToArray()[0]); 42 | } 43 | 44 | m_ManagerDebug.CheckInternalConsistency(); 45 | 46 | World.Dispose(); 47 | World = null; 48 | } 49 | 50 | // Restore output 51 | var standardOutput = new System.IO.StreamWriter(System.Console.OpenStandardOutput()); 52 | standardOutput.AutoFlush = true; 53 | System.Console.SetOut(standardOutput); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/MyECSTestsFixture.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aa154e9723d5a4ae892753170490f996 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.e7.enum-dispatcher", 3 | "author": "Sirawat Pitaksarit / 5argon - Exceed7 Experiments", 4 | "displayName": "Enum Dispatcher", 5 | "version": "0.0.1", 6 | "unity": "2018.1", 7 | "description": "Flux/Redux-esque event dispatcher that allows you to use any enum as event's label. Supports event payload too!" 8 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e45784c7556a041e4be590a8a46dcfef 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------