NodeCanvas Forums › Custom Nodes & Tasks › SuperCodeActionState = Actions + Code. Feedback appreciated.
Hey guys, while building out a game flow state machine for our game, i found myself in a strange position with NodeCanvas. What I wanted was to be able to code out some specific things in a state but also wanted some of the flexibility that came along with the Actions in the inspector.
I wen’t ahead and whipped a custom state up that will allow me to do this. The below is definitely a very rough idea of what I was thinking, but I’m curious of what your thoughts are and how i might be able to improve it.
Thanks in advance!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
using System; using System.Collections.Generic; using System.Linq; using NodeCanvas; using NodeCanvas.Framework; using NodeCanvas.StateMachines; using ParadoxNotion.Design; using ParadoxNotion.Serialization.FullSerializer.Internal; using UnityEditor; using UnityEngine; public abstract class SuperCodeActionState : FSMState, ISubTasksContainer { [SerializeField] private ActionList _onEnterList; [SerializeField] private string codeOrderEnter = "Before"; [SerializeField] private ActionList _onUpdateList; [SerializeField] private string codeOrderUpdate = "Before"; [SerializeField] private ActionList _onExitList; [SerializeField] private string codeOrderExit = "Before"; private bool enterListFinished = false; private const string CODE_NAME = "[Code]"; //convert protected virtual void OnStateInit() { } protected virtual void OnStateValidate(Graph assignedGraph) { } protected virtual void OnStateEnter() { } protected virtual void OnStateUpdate() { } protected virtual void OnStateExit() { } protected virtual void OnStatePause() { } public Task[] GetSubTasks(){ return new Task[]{_onEnterList, _onUpdateList, _onExitList}; } protected sealed override void OnInit() { OnStateInit(); } public sealed override void OnValidate(Graph assignedGraph){ if (_onEnterList == null){ _onEnterList = (ActionList)Task.Create(typeof(ActionList), assignedGraph); _onEnterList.executionMode = ActionList.ActionsExecutionMode.ActionsRunInParallel; } if (_onUpdateList == null){ _onUpdateList = (ActionList)Task.Create(typeof(ActionList), assignedGraph); _onUpdateList.executionMode = ActionList.ActionsExecutionMode.ActionsRunInParallel; } if (_onExitList == null){ _onExitList = (ActionList)Task.Create(typeof(ActionList), assignedGraph); _onExitList.executionMode = ActionList.ActionsExecutionMode.ActionsRunInParallel; } OnStateValidate(assignedGraph); } protected sealed override void OnEnter(){ enterListFinished = false; if (codeOrderEnter == "Before") { OnStateEnter(); } OnUpdate(); } protected sealed override void OnUpdate(){ if (codeOrderEnter != "Before") { if(!enterListFinished && _onEnterList.ExecuteAction(graphAgent, graphBlackboard) != Status.Running){ enterListFinished = true; OnStateEnter(); if (_onUpdateList.actions.Count == 0 && GetType().GetDeclaredMethod("OnStateEnter") == null){ Finish(); } } } ExecuteInOrder( OnStateUpdate, () => { _onUpdateList.ExecuteAction(graphAgent, graphBlackboard); }, codeOrderUpdate == "Before" ); } protected sealed override void OnExit(){ _onEnterList.EndAction(null); _onUpdateList.EndAction(null); ExecuteInOrder( OnStateExit, () => { _onExitList.ExecuteAction(graphAgent, graphBlackboard); }, codeOrderExit == "Before" ); _onExitList.EndAction(null); } protected sealed override void OnPause(){ _onEnterList.PauseAction(); _onUpdateList.PauseAction(); OnStatePause(); } /// <summary> /// Allows us to quickly swap order of execution of two function based on some bool predicate /// </summary> /// <param name="functionA">Function to execute a</param> /// <param name="functionB">Function to execute b</param> /// <param name="functionAFirst">Should function A be executed first?</param> private void ExecuteInOrder(Action functionA, Action functionB, bool functionAFirst) { if (functionAFirst) { functionA(); functionB(); } else { functionB(); functionA(); } } //////////////////////////////////////// ///////////GUI AND EDITOR STUFF///////// //////////////////////////////////////// #if UNITY_EDITOR [SerializeField] private bool foldEnter; [SerializeField] private bool foldUpdate; [SerializeField] private bool foldExit; protected override void OnNodeGUI() { Action<string,string,List<ActionTask>,bool> displayActionTitle = (text,methodName,actionList,codeBefore) => { if (GetType().GetDeclaredMethod(methodName) == null) { GUILayout.Label($"<i>{actionList.Count} {text}</i>"); return; } if (codeBefore) { GUILayout.Label($"<i>{CODE_NAME} → {actionList.Count} {text}</i>"); return; } GUILayout.Label($"<i>{actionList.Count} {text} → {CODE_NAME}</i>"); }; if (_onEnterList.actions.Count > 0) { ExecuteInOrder( () => { if (GetType().GetDeclaredMethod("OnStateEnter") != null) { GUILayout.Label(CODE_NAME); } }, () => { GUILayout.Label(_onEnterList.summaryInfo); }, codeOrderEnter == "Before" ); } else { displayActionTitle("OnEnter Actions","OnStateEnter", _onEnterList.actions, codeOrderEnter == "Before"); } displayActionTitle("OnUpdate Actions","OnStateUpdate", _onUpdateList.actions, codeOrderUpdate == "Before"); displayActionTitle("OnExit Actions","OnStateExit", _onExitList.actions,codeOrderExit == "Before"); } protected override void OnNodeInspectorGUI(){ ShowBaseFSMInspectorGUI(); if (_onEnterList == null || _onUpdateList == null || _onExitList == null){ return; } Func<string,string,List<ActionTask>,bool,string> displayInspectorTitle = (text, methodName, actionList, codeBefore) => { if (GetType().GetDeclaredMethod(methodName) == null) { return $"{text} Actions ({actionList.Count})"; } if (codeBefore) { return $"{text} {CODE_NAME} → Actions ({actionList.Count})"; } return $"{text} Actions ({actionList.Count}) → {CODE_NAME}"; }; EditorUtils.CoolLabel("Actions/Code"); var options = new [] {"Before","After"}; var codeOrderName = "Code Executes"; foldEnter = UnityEditor.EditorGUILayout.Foldout(foldEnter, displayInspectorTitle("OnEnter", "OnStateEnter", _onEnterList.actions, codeOrderEnter == "Before")); if (foldEnter){ codeOrderEnter = EditorUtils.StringPopup(codeOrderName,codeOrderEnter,options.ToList()); EditorGUILayout.Space(); _onEnterList.ShowListGUI(); _onEnterList.ShowNestedActionsGUI(); } EditorUtils.Separator(); foldUpdate = UnityEditor.EditorGUILayout.Foldout(foldUpdate, displayInspectorTitle("OnUpdate", "OnStateUpdate", _onEnterList.actions, codeOrderUpdate == "Before")); if (foldUpdate){ codeOrderUpdate = EditorUtils.StringPopup(codeOrderName,codeOrderUpdate,options.ToList()); EditorGUILayout.Space(); _onUpdateList.ShowListGUI(); _onUpdateList.ShowNestedActionsGUI(); } EditorUtils.Separator(); foldExit = UnityEditor.EditorGUILayout.Foldout(foldExit, displayInspectorTitle("OnExit", "OnStateExit", _onExitList.actions, codeOrderExit == "Before")); if (foldExit){ codeOrderExit = EditorUtils.StringPopup(codeOrderName,codeOrderExit,options.ToList()); EditorGUILayout.Space(); _onExitList.ShowListGUI(); _onExitList.ShowNestedActionsGUI(); } } #endif } |
Here’s an updated version of the above. I think this one is much better, yet pretty rough none the less.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
using System; using System.Collections.Generic; using System.Linq; using NodeCanvas; using NodeCanvas.Framework; using NodeCanvas.StateMachines; using ParadoxNotion.Design; using ParadoxNotion.Serialization.FullSerializer.Internal; using UnityEditor; using UnityEngine; /// <summary> /// Allows for both Inspector Actions and Actions that can be defined in code through OnState(Enter/Update/Exit). /// <para> /// When overriding an OnState(CodeTask), the meat of your code should be encapsulated within the given tasks /// OnTask(Init/Execute/Update/Stop/Pause) methods. Any code outside these delegates will occur within the state Init. /// </para> /// <para>Example: OnStateEnter(CodeTask task) { task.OnTaskExecute = () => { //your code here }; }</para> /// <para>For further help reference the documentations ActionTask creation section.</para> /// </summary> [Color("79BACA")] [Description("The Super Code Action State is a cross between a coded state and Super Action State. It allows you to " + "not only define your actions for your state within the inspector but also within code using the following" + "primary callbacks. OnStateEnter, OnStateUpdate, OnStateExit. An example override of OnStateEnter will + " + "look like this task.OnTaskExecute = () { //your code here... };")] public abstract class SuperCodeActionState : FSMState, ISubTasksContainer { [SerializeField] private ActionList _onEnterList; [SerializeField] private ActionList _onUpdateList; [SerializeField] private ActionList _onExitList; private bool enterListFinished = false; [SerializeField,HideInInspector] private CodeTask enterTask; [SerializeField,HideInInspector] private CodeTask updateTask; [SerializeField,HideInInspector] private CodeTask exitTask; private const string CODE_NAME = "[Code]"; //convert protected virtual void OnStateInit() { } protected virtual void OnStateValidate(Graph assignedGraph) { } /// <summary> /// Override to create a [Code] block task that will execute as part of the OnEnter ActionList /// <para>In most cases your code should look like this. task.OnTaskExecute = () => { //your code here... }; </para> /// </summary> /// <param name="task">[Code] task you are creating that will execute in OnEnterActionList</param> protected virtual void OnStateEnter(CodeTask task) { } /// <summary> /// Override to create a [Code] block task that will execute as part of the OnUpdate ActionList /// <para>In most cases your code should look like this. task.OnTaskExecute = () => { //your code here... }; </para> /// </summary> /// <param name="task">[Code] task you are creating that will execute in OnUpdateActionList</param> protected virtual void OnStateUpdate(CodeTask task) { } /// <summary> /// Override to create a [Code] block task that will execute as part of the OnExit ActionList /// <para>In most cases your code should look like this. task.OnTaskExecute = () => { //your code here... }; </para> /// </summary> /// <param name="task">[Code] task you are creating that will execute in OnExitActionList</param> protected virtual void OnStateExit(CodeTask task) { } protected virtual void OnStatePause() { } public Task[] GetSubTasks(){ return new Task[]{_onEnterList, _onUpdateList, _onExitList}; } protected sealed override void OnInit() { OnStateInit(); OnStateEnter(enterTask); OnStateUpdate(updateTask); OnStateExit(exitTask); } public sealed override void OnValidate(Graph assignedGraph){ if (_onEnterList == null){ _onEnterList = (ActionList)Task.Create(typeof(ActionList), assignedGraph); _onEnterList.executionMode = ActionList.ActionsExecutionMode.ActionsRunInParallel; } if (_onUpdateList == null){ _onUpdateList = (ActionList)Task.Create(typeof(ActionList), assignedGraph); _onUpdateList.executionMode = ActionList.ActionsExecutionMode.ActionsRunInParallel; } if (_onExitList == null){ _onExitList = (ActionList)Task.Create(typeof(ActionList), assignedGraph); _onExitList.executionMode = ActionList.ActionsExecutionMode.ActionsRunInParallel; } //add code block if we need it remove it otherwise if (OnStateEnterDeclared && !OnStateEnterCodeBlockPresent) { enterTask = Task.Create<CodeTask>(assignedGraph); _onEnterList.AddAction(enterTask); } else if (!OnStateEnterDeclared && OnStateEnterCodeBlockPresent) { _onEnterList.actions.RemoveAll(task => task.GetType() == typeof(CodeTask)); } if (OnStateUpdateDeclared && !OnStateUpdateCodeBlockPresent) { updateTask = Task.Create<CodeTask>(assignedGraph); _onUpdateList.AddAction(updateTask); } else if (!OnStateUpdateDeclared && OnStateUpdateCodeBlockPresent) { _onUpdateList.actions.RemoveAll(task => task.GetType() == typeof(CodeTask)); } if (OnStateExitDeclared && !OnStateExitCodeBlockPresent) { exitTask = Task.Create<CodeTask>(assignedGraph); _onExitList.AddAction(exitTask); } else if (!OnStateExitDeclared && OnStateExitCodeBlockPresent) { _onExitList.actions.RemoveAll(task => task.GetType() == typeof(CodeTask)); } OnStateValidate(assignedGraph); } protected sealed override void OnEnter(){ enterListFinished = false; OnUpdate(); } protected sealed override void OnUpdate(){ if (!enterListFinished && _onEnterList.ExecuteAction(graphAgent, graphBlackboard) != Status.Running){ enterListFinished = true; if (_onUpdateList.actions.Count == 0){ Finish(); } } _onUpdateList.ExecuteAction(graphAgent, graphBlackboard); } protected sealed override void OnExit(){ _onEnterList.EndAction(null); _onUpdateList.EndAction(null); _onExitList.ExecuteAction(graphAgent, graphBlackboard); _onExitList.EndAction(null); } protected sealed override void OnPause(){ _onEnterList.PauseAction(); _onUpdateList.PauseAction(); OnStatePause(); } [Name(CODE_NAME)] [Description("DO NOT DELETE - Code declared in state script.")] protected class CodeTask : ActionTask { public Func<string> OnTaskInit { get; set; } public Action OnTaskExecute { get; set; } public Action OnTaskUpdate { get; set; } public Action OnTaskStop { get; set; } public Action OnTaskPause { get; set; } protected override string OnInit() { return OnTaskInit?.Invoke(); } protected override void OnExecute() { OnTaskExecute?.Invoke(); } protected override void OnUpdate() { OnTaskUpdate?.Invoke(); } protected override void OnStop() { OnTaskStop?.Invoke(); } protected override void OnPause() { OnTaskPause?.Invoke(); } } private bool OnStateEnterDeclared { get { return GetType().GetDeclaredMethod("OnStateEnter") != null; } } private bool OnStateUpdateDeclared { get { return GetType().GetDeclaredMethod("OnStateUpdate") != null; } } private bool OnStateExitDeclared { get { return GetType().GetDeclaredMethod("OnStateExit") != null; } } private bool OnStateEnterCodeBlockPresent { get { return _onEnterList.actions.OfType<CodeTask>().Any(); } } private bool OnStateUpdateCodeBlockPresent { get { return _onUpdateList.actions.OfType<CodeTask>().Any(); } } private bool OnStateExitCodeBlockPresent { get { return _onExitList.actions.OfType<CodeTask>().Any(); } } //////////////////////////////////////// ///////////GUI AND EDITOR STUFF///////// //////////////////////////////////////// #if UNITY_EDITOR [SerializeField] private bool foldEnter; [SerializeField] private bool foldUpdate; [SerializeField] private bool foldExit; protected override void OnNodeGUI() { Action<string,string,List<ActionTask>> displayActionTitle = (text,methodName,actionList) => { if (GetType().GetDeclaredMethod(methodName) == null) { GUILayout.Label($"<i>{actionList.Count} {text}</i>"); return; } GUILayout.Label($"<i>{CODE_NAME} + {actionList.Count} {text}</i>"); }; if (OnStateEnterDeclared && !OnStateEnterCodeBlockPresent || OnStateUpdateDeclared && !OnStateUpdateCodeBlockPresent || OnStateExitDeclared && !OnStateExitCodeBlockPresent) { GUILayout.Label($"<color=#ed2939>[Code] missing. Click 4 Details</color>"); } if (_onEnterList.actions.Count > 0) { GUILayout.Label(_onEnterList.summaryInfo); } else { displayActionTitle("OnEnter Actions","OnStateEnter", _onEnterList.actions); } displayActionTitle("OnUpdate Actions","OnStateUpdate", _onUpdateList.actions); displayActionTitle("OnExit Actions","OnStateExit", _onExitList.actions); } protected override void OnNodeInspectorGUI(){ ShowBaseFSMInspectorGUI(); EditorUtils.CoolLabel(CODE_NAME + " Dependencies"); DrawDefaultInspector(); EditorUtils.BoldSeparator(); if (_onEnterList == null || _onUpdateList == null || _onExitList == null){ return; } Func<string,string,List<ActionTask>,string> displayInspectorTitle = (text, methodName, actionList) => { if (GetType().GetDeclaredMethod(methodName) == null) { return $"{text} Actions ({actionList.Count})"; } return $"{text} {CODE_NAME} + Actions ({actionList.Count})"; }; EditorUtils.CoolLabel("Actions/Code"); var deletedButtonMessage = "Recreate [Code]"; //On Enter Actions foldEnter = EditorGUILayout.Foldout(foldEnter, displayInspectorTitle("OnEnter", "OnStateEnter", _onEnterList.actions)); if (!OnStateEnterCodeBlockPresent && OnStateEnterDeclared) { GUILayout.Label($"<color=#ed2939>[Code] block deleted. OnStateEnter code won't execute.</color>"); if(GUILayout.Button(deletedButtonMessage)) { enterTask = Task.Create<CodeTask>(graph); _onEnterList.AddAction(enterTask); } EditorGUILayout.Space(); } if (foldEnter){ _onEnterList.ShowListGUI(); _onEnterList.ShowNestedActionsGUI(); } EditorUtils.Separator(); //On Update Actions foldUpdate = EditorGUILayout.Foldout(foldUpdate, displayInspectorTitle("OnUpdate", "OnStateUpdate", _onUpdateList.actions)); if (!OnStateUpdateCodeBlockPresent && OnStateUpdateDeclared) { GUILayout.Label($"<color=#ed2939>[Code] block deleted. OnStateUpdate code won't execute.</color>"); if (GUILayout.Button(deletedButtonMessage)) { updateTask = Task.Create<CodeTask>(graph); _onUpdateList.AddAction(updateTask); } EditorGUILayout.Space(); } if (foldUpdate){ _onUpdateList.ShowListGUI(); _onUpdateList.ShowNestedActionsGUI(); } EditorUtils.Separator(); //On Exit Actions foldExit = EditorGUILayout.Foldout(foldExit, displayInspectorTitle("OnExit", "OnStateExit", _onExitList.actions)); if (!OnStateExitCodeBlockPresent && OnStateExitDeclared) { GUILayout.Label($"<color=#ed2939>[Code] block deleted. OnStateExit code won't execute.</color>"); if (GUILayout.Button(deletedButtonMessage)) { exitTask = Task.Create<CodeTask>(graph); _onExitList.AddAction(exitTask); } EditorGUILayout.Space(); } if (foldExit){ _onExitList.ShowListGUI(); _onExitList.ShowNestedActionsGUI(); } } #endif } |
Hey, thanks for sharing.
I am not exactly sure how this works, but it looks interesting as far as I understood 🙂
I think it looks a bit more complicated that it could be though. Is the end goal to have tasks, but also code the state behaviour in combination?
What is the difference from adding custom coded tasks in the SuperActionState directly? Within an action task, the OnExecute, OnUpdate and OnStop callbacks can be used to respectively code the OnEnter, OnUpdate and OnExit state calls.
I’d be interested in more details 🙂
Thanks.
Join us on Discord: https://discord.gg/97q2Rjh
Haven’t had a chance to reply to this until now.
In a lot of cases the SuperActionState in combination with ActionTasks makes a lot of sense. However right now one of my use cases is using NodeCanvas for the flow of my games states. In this case it is quite cumbersome to create a bunch of ActionTasks for the various things I want to do in each game state. Primarily I want to just code my states up with the various callbacks.
However there is moments where it is useful to use ActionTasks along with my state code which is why I coded this up :). Here are a few images of how it looks working.