├── Changelog.md ├── Changelog.md.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Samples~ ├── GuardAI │ ├── Animations.meta │ ├── Animations │ │ ├── GuardAnimator.controller │ │ ├── GuardAnimator.controller.meta │ │ ├── GuardHit.anim │ │ ├── GuardHit.anim.meta │ │ ├── GuardIdle.anim │ │ ├── GuardIdle.anim.meta │ │ ├── GuardTelegraph.anim │ │ ├── GuardTelegraph.anim.meta │ │ ├── PlayerAnimator.controller │ │ └── PlayerAnimator.controller.meta │ ├── GuardAI.unity │ ├── GuardAI.unity.meta │ ├── Pixel.png │ ├── Pixel.png.meta │ ├── Scripts.meta │ └── Scripts │ │ ├── GuardAI.cs │ │ ├── GuardAI.cs.meta │ │ ├── PlayerController.cs │ │ └── PlayerController.cs.meta └── Sample3d │ ├── Scenes.meta │ ├── Scenes │ ├── Sample.meta │ ├── Sample.unity │ ├── Sample.unity.meta │ └── Sample │ │ ├── NavMesh.asset │ │ └── NavMesh.asset.meta │ ├── Scripts.meta │ └── Scripts │ ├── CustomSendData.cs │ ├── CustomSendData.cs.meta │ ├── EnemyController.cs │ ├── EnemyController.cs.meta │ ├── PlayerController.cs │ └── PlayerController.cs.meta ├── Tests.meta ├── Tests ├── TestActionSystem.cs ├── TestActionSystem.cs.meta ├── TestActiveHierarchyPath.cs ├── TestActiveHierarchyPath.cs.meta ├── TestActiveStateChangedEvent.cs ├── TestActiveStateChangedEvent.cs.meta ├── TestCanExit.cs ├── TestCanExit.cs.meta ├── TestExampleScene.cs ├── TestExampleScene.cs.meta ├── TestExitTransitions.cs ├── TestExitTransitions.cs.meta ├── TestFsmBaseTransitions.cs ├── TestFsmBaseTransitions.cs.meta ├── TestGenericsInHierarchical.cs ├── TestGenericsInHierarchical.cs.meta ├── TestGhostStates.cs ├── TestGhostStates.cs.meta ├── TestHybridStateMachine.cs ├── TestHybridStateMachine.cs.meta ├── TestParallelStates.cs ├── TestParallelStates.cs.meta ├── TestRememberLastState.cs ├── TestRememberLastState.cs.meta ├── TestStartUp.cs ├── TestStartUp.cs.meta ├── TestStateAndTransitionGetters.cs ├── TestStateAndTransitionGetters.cs.meta ├── TestStateMachinePath.cs ├── TestStateMachinePath.cs.meta ├── TestStateMachineWalker.cs ├── TestStateMachineWalker.cs.meta ├── TestTransitionAfter.cs ├── TestTransitionAfter.cs.meta ├── TestTransitionAfterDynamic.cs ├── TestTransitionAfterDynamic.cs.meta ├── TestTransitionCallbacks.cs ├── TestTransitionCallbacks.cs.meta ├── TestTwoWayTransitions.cs ├── TestTwoWayTransitions.cs.meta ├── Tests.asmdef ├── Tests.asmdef.meta ├── Util.meta └── Util │ ├── Recorder.cs │ ├── Recorder.cs.meta │ ├── TestTimer.cs │ └── TestTimer.cs.meta ├── UnityHFSM.asmdef ├── UnityHFSM.asmdef.meta ├── docs.meta ├── docs ├── Banner.png ├── Banner.png.meta ├── Flowcharts.meta ├── Flowcharts │ ├── OnLogic.svg │ ├── OnLogic.svg.meta │ ├── OnLogic.txt │ ├── OnLogic.txt.meta │ ├── StateChange.svg │ ├── StateChange.svg.meta │ ├── StateChange.txt │ └── StateChange.txt.meta ├── Images.meta ├── Images │ ├── AnimatorAnyState.png │ ├── AnimatorAnyState.png.meta │ ├── AnimatorGraphExample.png │ ├── AnimatorGraphExample.png.meta │ ├── AnimatorGraphVideo.gif │ └── AnimatorGraphVideo.gif.meta ├── Inheritance.meta ├── Inheritance │ ├── StateInheritance.svg │ ├── StateInheritance.svg.meta │ ├── StateInheritance.txt │ ├── StateInheritance.txt.meta │ ├── TransitionInheritance.svg │ ├── TransitionInheritance.svg.meta │ ├── TransitionInheritance.txt │ └── TransitionInheritance.txt.meta ├── StateDiagrams.meta └── StateDiagrams │ ├── EnemySpyExample.meta │ └── EnemySpyExample │ ├── Hierarchical.afdesign │ ├── Hierarchical.afdesign.meta │ ├── Hierarchical.png │ ├── Hierarchical.png.meta │ ├── HierarchicalWithExitTransition.afdesign │ ├── HierarchicalWithExitTransition.afdesign.meta │ ├── HierarchicalWithExitTransition.png │ ├── HierarchicalWithExitTransition.png.meta │ ├── Simple.afdesign │ ├── Simple.afdesign.meta │ ├── Simple.png │ └── Simple.png.meta ├── package.json ├── package.json.meta ├── src.meta └── src ├── Base.meta ├── Base ├── IActionable.cs ├── IActionable.cs.meta ├── IStateMachine.cs ├── IStateMachine.cs.meta ├── IStateTimingManager.cs ├── IStateTimingManager.cs.meta ├── ITransitionListener.cs ├── ITransitionListener.cs.meta ├── ITriggerable.cs ├── ITriggerable.cs.meta ├── StateBase.cs ├── StateBase.cs.meta ├── TransitionBase.cs └── TransitionBase.cs.meta ├── Exceptions.meta ├── Exceptions ├── Common.cs ├── Common.cs.meta ├── ExceptionFormatter.cs ├── ExceptionFormatter.cs.meta ├── StateMachineException.cs └── StateMachineException.cs.meta ├── Inspection.meta ├── Inspection ├── IStateMachineHierarchyVisitor.cs ├── IStateMachineHierarchyVisitor.cs.meta ├── IStateVisitor.cs ├── IStateVisitor.cs.meta ├── IVisitableState.cs ├── IVisitableState.cs.meta ├── StateMachinePath.cs ├── StateMachinePath.cs.meta ├── StateMachineWalker.cs └── StateMachineWalker.cs.meta ├── StateMachine.meta ├── StateMachine ├── HybridStateMachine.cs ├── HybridStateMachine.cs.meta ├── StateMachine.cs ├── StateMachine.cs.meta ├── StateMachineShortcuts.cs └── StateMachineShortcuts.cs.meta ├── States.meta ├── States ├── ActionState.cs ├── ActionState.cs.meta ├── ActionStorage.cs ├── ActionStorage.cs.meta ├── CoState.cs ├── CoState.cs.meta ├── DecoratedState.cs ├── DecoratedState.cs.meta ├── ParallelStates.cs ├── ParallelStates.cs.meta ├── State.cs ├── State.cs.meta ├── StateDecorator.cs └── StateDecorator.cs.meta ├── Transitions.meta ├── Transitions ├── DecoratedTransition.cs ├── DecoratedTransition.cs.meta ├── ReverseTransition.cs ├── ReverseTransition.cs.meta ├── Transition.cs ├── Transition.cs.meta ├── TransitionAfter.cs ├── TransitionAfter.cs.meta ├── TransitionAfterDynamic.cs ├── TransitionAfterDynamic.cs.meta ├── TransitionDecorator.cs ├── TransitionDecorator.cs.meta ├── TransitionOnKey.cs ├── TransitionOnKey.cs.meta ├── TransitionOnMouse.cs └── TransitionOnMouse.cs.meta ├── Util.meta ├── Util ├── ITimer.cs ├── ITimer.cs.meta ├── Timer.cs └── Timer.cs.meta ├── Visualization.meta └── Visualization ├── HfsmAnimatorGraph.cs ├── HfsmAnimatorGraph.cs.meta ├── VisualizationNamespace.cs └── VisualizationNamespace.cs.meta /Changelog.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc49e7fbdd881334a9b839023fb7d3ec 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Inspiaaa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6413897038cad524db6434d5cbef747c 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6806ec013537df54f9a3075a7a898e88 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 229a78c003ab67e4c936fa881e53a1a8 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations/GuardAnimator.controller: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!1102 &-8157401055890453298 4 | AnimatorState: 5 | serializedVersion: 5 6 | m_ObjectHideFlags: 1 7 | m_CorrespondingSourceObject: {fileID: 0} 8 | m_PrefabInstance: {fileID: 0} 9 | m_PrefabAsset: {fileID: 0} 10 | m_Name: GuardIdle 11 | m_Speed: 1 12 | m_CycleOffset: 0 13 | m_Transitions: [] 14 | m_StateMachineBehaviours: [] 15 | m_Position: {x: 50, y: 50, z: 0} 16 | m_IKOnFeet: 0 17 | m_WriteDefaultValues: 1 18 | m_Mirror: 0 19 | m_SpeedParameterActive: 0 20 | m_MirrorParameterActive: 0 21 | m_CycleOffsetParameterActive: 0 22 | m_TimeParameterActive: 0 23 | m_Motion: {fileID: 7400000, guid: a79bc753cc36f324cbdf3a4bc1c47da5, type: 2} 24 | m_Tag: 25 | m_SpeedParameter: 26 | m_MirrorParameter: 27 | m_CycleOffsetParameter: 28 | m_TimeParameter: 29 | --- !u!1102 &-6448965651480101462 30 | AnimatorState: 31 | serializedVersion: 5 32 | m_ObjectHideFlags: 1 33 | m_CorrespondingSourceObject: {fileID: 0} 34 | m_PrefabInstance: {fileID: 0} 35 | m_PrefabAsset: {fileID: 0} 36 | m_Name: GuardHit 37 | m_Speed: 1 38 | m_CycleOffset: 0 39 | m_Transitions: [] 40 | m_StateMachineBehaviours: [] 41 | m_Position: {x: 50, y: 50, z: 0} 42 | m_IKOnFeet: 0 43 | m_WriteDefaultValues: 1 44 | m_Mirror: 0 45 | m_SpeedParameterActive: 0 46 | m_MirrorParameterActive: 0 47 | m_CycleOffsetParameterActive: 0 48 | m_TimeParameterActive: 0 49 | m_Motion: {fileID: 7400000, guid: 52cfa3260a8571a42973d6531210a6f0, type: 2} 50 | m_Tag: 51 | m_SpeedParameter: 52 | m_MirrorParameter: 53 | m_CycleOffsetParameter: 54 | m_TimeParameter: 55 | --- !u!1107 &-236004146183636749 56 | AnimatorStateMachine: 57 | serializedVersion: 5 58 | m_ObjectHideFlags: 1 59 | m_CorrespondingSourceObject: {fileID: 0} 60 | m_PrefabInstance: {fileID: 0} 61 | m_PrefabAsset: {fileID: 0} 62 | m_Name: Base Layer 63 | m_ChildStates: 64 | - serializedVersion: 1 65 | m_State: {fileID: -8157401055890453298} 66 | m_Position: {x: 90, y: 200, z: 0} 67 | - serializedVersion: 1 68 | m_State: {fileID: 5048928867411741521} 69 | m_Position: {x: 240, y: 302, z: 0} 70 | - serializedVersion: 1 71 | m_State: {fileID: -6448965651480101462} 72 | m_Position: {x: 340, y: 180, z: 0} 73 | m_ChildStateMachines: [] 74 | m_AnyStateTransitions: [] 75 | m_EntryTransitions: [] 76 | m_StateMachineTransitions: {} 77 | m_StateMachineBehaviours: [] 78 | m_AnyStatePosition: {x: 50, y: 20, z: 0} 79 | m_EntryPosition: {x: 50, y: 120, z: 0} 80 | m_ExitPosition: {x: 800, y: 120, z: 0} 81 | m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} 82 | m_DefaultState: {fileID: -8157401055890453298} 83 | --- !u!91 &9100000 84 | AnimatorController: 85 | m_ObjectHideFlags: 0 86 | m_CorrespondingSourceObject: {fileID: 0} 87 | m_PrefabInstance: {fileID: 0} 88 | m_PrefabAsset: {fileID: 0} 89 | m_Name: GuardAnimator 90 | serializedVersion: 5 91 | m_AnimatorParameters: [] 92 | m_AnimatorLayers: 93 | - serializedVersion: 5 94 | m_Name: Base Layer 95 | m_StateMachine: {fileID: -236004146183636749} 96 | m_Mask: {fileID: 0} 97 | m_Motions: [] 98 | m_Behaviours: [] 99 | m_BlendingMode: 0 100 | m_SyncedLayerIndex: -1 101 | m_DefaultWeight: 0 102 | m_IKPass: 0 103 | m_SyncedLayerAffectsTiming: 0 104 | m_Controller: {fileID: 9100000} 105 | --- !u!1102 &5048928867411741521 106 | AnimatorState: 107 | serializedVersion: 5 108 | m_ObjectHideFlags: 1 109 | m_CorrespondingSourceObject: {fileID: 0} 110 | m_PrefabInstance: {fileID: 0} 111 | m_PrefabAsset: {fileID: 0} 112 | m_Name: GuardTelegraph 113 | m_Speed: 1 114 | m_CycleOffset: 0 115 | m_Transitions: [] 116 | m_StateMachineBehaviours: [] 117 | m_Position: {x: 50, y: 50, z: 0} 118 | m_IKOnFeet: 0 119 | m_WriteDefaultValues: 1 120 | m_Mirror: 0 121 | m_SpeedParameterActive: 0 122 | m_MirrorParameterActive: 0 123 | m_CycleOffsetParameterActive: 0 124 | m_TimeParameterActive: 0 125 | m_Motion: {fileID: 7400000, guid: 615178bc16ad1dc4194f787225281e67, type: 2} 126 | m_Tag: 127 | m_SpeedParameter: 128 | m_MirrorParameter: 129 | m_CycleOffsetParameter: 130 | m_TimeParameter: 131 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations/GuardAnimator.controller.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7292e45449a1da74b95dc2784f7e83c5 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 0 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations/GuardHit.anim.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 52cfa3260a8571a42973d6531210a6f0 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 0 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations/GuardIdle.anim.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a79bc753cc36f324cbdf3a4bc1c47da5 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 0 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations/GuardTelegraph.anim.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 615178bc16ad1dc4194f787225281e67 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 0 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations/PlayerAnimator.controller: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!91 &9100000 4 | AnimatorController: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_Name: PlayerAnimator 10 | serializedVersion: 5 11 | m_AnimatorParameters: [] 12 | m_AnimatorLayers: 13 | - serializedVersion: 5 14 | m_Name: Base Layer 15 | m_StateMachine: {fileID: 3765213416021972827} 16 | m_Mask: {fileID: 0} 17 | m_Motions: [] 18 | m_Behaviours: [] 19 | m_BlendingMode: 0 20 | m_SyncedLayerIndex: -1 21 | m_DefaultWeight: 0 22 | m_IKPass: 0 23 | m_SyncedLayerAffectsTiming: 0 24 | m_Controller: {fileID: 9100000} 25 | --- !u!1107 &3765213416021972827 26 | AnimatorStateMachine: 27 | serializedVersion: 5 28 | m_ObjectHideFlags: 1 29 | m_CorrespondingSourceObject: {fileID: 0} 30 | m_PrefabInstance: {fileID: 0} 31 | m_PrefabAsset: {fileID: 0} 32 | m_Name: Base Layer 33 | m_ChildStates: 34 | - serializedVersion: 1 35 | m_State: {fileID: 5976747321182654432} 36 | m_Position: {x: 332, y: 141, z: 0} 37 | m_ChildStateMachines: [] 38 | m_AnyStateTransitions: [] 39 | m_EntryTransitions: [] 40 | m_StateMachineTransitions: {} 41 | m_StateMachineBehaviours: [] 42 | m_AnyStatePosition: {x: 50, y: 20, z: 0} 43 | m_EntryPosition: {x: 50, y: 120, z: 0} 44 | m_ExitPosition: {x: 800, y: 120, z: 0} 45 | m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} 46 | m_DefaultState: {fileID: 5976747321182654432} 47 | --- !u!1102 &5976747321182654432 48 | AnimatorState: 49 | serializedVersion: 5 50 | m_ObjectHideFlags: 1 51 | m_CorrespondingSourceObject: {fileID: 0} 52 | m_PrefabInstance: {fileID: 0} 53 | m_PrefabAsset: {fileID: 0} 54 | m_Name: GuardIdle 55 | m_Speed: 1 56 | m_CycleOffset: 0.5 57 | m_Transitions: [] 58 | m_StateMachineBehaviours: [] 59 | m_Position: {x: 50, y: 50, z: 0} 60 | m_IKOnFeet: 0 61 | m_WriteDefaultValues: 1 62 | m_Mirror: 0 63 | m_SpeedParameterActive: 0 64 | m_MirrorParameterActive: 0 65 | m_CycleOffsetParameterActive: 0 66 | m_TimeParameterActive: 0 67 | m_Motion: {fileID: 7400000, guid: a79bc753cc36f324cbdf3a4bc1c47da5, type: 2} 68 | m_Tag: 69 | m_SpeedParameter: 70 | m_MirrorParameter: 71 | m_CycleOffsetParameter: 72 | m_TimeParameter: 73 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Animations/PlayerAnimator.controller.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f02e0ea1f25498148969c4a11419e4fd 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 0 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/GuardAI/GuardAI.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 83153214e0e00ef4685a95b983ffad7c 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/Samples~/GuardAI/Pixel.png -------------------------------------------------------------------------------- /Samples~/GuardAI/Pixel.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fe26cdb7a1577e44f910622b0cdf393f 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: 0 36 | aniso: -1 37 | mipBias: -100 38 | wrapU: 1 39 | wrapV: 1 40 | wrapW: 1 41 | nPOTScale: 0 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 1 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 1 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 1 54 | spriteTessellationDetail: -1 55 | textureType: 8 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | maxTextureSizeSet: 0 59 | compressionQualitySet: 0 60 | textureFormatSet: 0 61 | ignorePngGamma: 0 62 | applyGammaDecoding: 0 63 | platformSettings: 64 | - serializedVersion: 3 65 | buildTarget: DefaultTexturePlatform 66 | maxTextureSize: 2048 67 | resizeAlgorithm: 0 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | androidETC2FallbackOverride: 0 75 | forceMaximumCompressionQuality_BC6H_BC7: 0 76 | - serializedVersion: 3 77 | buildTarget: Standalone 78 | maxTextureSize: 2048 79 | resizeAlgorithm: 0 80 | textureFormat: -1 81 | textureCompression: 1 82 | compressionQuality: 50 83 | crunchedCompression: 0 84 | allowsAlphaSplitting: 0 85 | overridden: 0 86 | androidETC2FallbackOverride: 0 87 | forceMaximumCompressionQuality_BC6H_BC7: 0 88 | - serializedVersion: 3 89 | buildTarget: WebGL 90 | maxTextureSize: 2048 91 | resizeAlgorithm: 0 92 | textureFormat: -1 93 | textureCompression: 1 94 | compressionQuality: 50 95 | crunchedCompression: 0 96 | allowsAlphaSplitting: 0 97 | overridden: 0 98 | androidETC2FallbackOverride: 0 99 | forceMaximumCompressionQuality_BC6H_BC7: 0 100 | spriteSheet: 101 | serializedVersion: 2 102 | sprites: [] 103 | outline: [] 104 | physicsShape: [] 105 | bones: [] 106 | spriteID: 5e97eb03825dee720800000000000000 107 | internalID: 0 108 | vertices: [] 109 | indices: 110 | edges: [] 111 | weights: [] 112 | secondaryTextures: [] 113 | spritePackingTag: 114 | pSDRemoveMatte: 0 115 | pSDShowRemoveMatteOption: 0 116 | userData: 117 | assetBundleName: 118 | assetBundleVariant: 119 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 822ed91898f3c1b47aa1ace075869b88 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Scripts/GuardAI.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.UI; 5 | using UnityHFSM; // Import UnityHFSM 6 | 7 | namespace UnityHFSM.Samples.GuardAI { 8 | 9 | public class GuardAI : MonoBehaviour 10 | { 11 | // Declare the finite state machine 12 | private StateMachine fsm; 13 | 14 | // Parameters (can be changed in the inspector) 15 | public float searchSpotRange = 10; 16 | public float attackRange = 3; 17 | 18 | public float searchTime = 20; // in seconds 19 | 20 | public float patrolSpeed = 2; 21 | public float chaseSpeed = 4; 22 | public float attackSpeed = 2; 23 | 24 | public Vector2[] patrolPoints; 25 | 26 | // Internal fields 27 | private Animator animator; 28 | private Text stateDisplayText; 29 | private int patrolDirection = 1; 30 | private Vector2 lastSeenPlayerPosition; 31 | 32 | // Helper methods (depend on how your scene has been set up) 33 | private Vector2 playerPosition => PlayerController.Instance.transform.position; 34 | private float distanceToPlayer => Vector2.Distance(playerPosition, transform.position); 35 | 36 | void Start() 37 | { 38 | animator = GetComponent(); 39 | stateDisplayText = GetComponentInChildren(); 40 | 41 | fsm = new StateMachine(); 42 | 43 | // Fight FSM 44 | var fightFsm = new HybridStateMachine( 45 | beforeOnLogic: state => MoveTowards(playerPosition, attackSpeed, minDistance: 1), 46 | needsExitTime: true 47 | ); 48 | 49 | fightFsm.AddState("Wait", onEnter: state => animator.Play("GuardIdle")); 50 | fightFsm.AddState("Telegraph", onEnter: state => animator.Play("GuardTelegraph")); 51 | fightFsm.AddState("Hit", 52 | onEnter: state => { 53 | animator.Play("GuardHit"); 54 | // TODO: Cause damage to player if in range. 55 | } 56 | ); 57 | 58 | // Because the exit transition should have the highest precedence, 59 | // it is added before the other transitions. 60 | fightFsm.AddExitTransition("Wait"); 61 | 62 | fightFsm.AddTransition(new TransitionAfter("Wait", "Telegraph", 0.5f)); 63 | fightFsm.AddTransition(new TransitionAfter("Telegraph", "Hit", 0.42f)); 64 | fightFsm.AddTransition(new TransitionAfter("Hit", "Wait", 0.5f)); 65 | 66 | // Root FSM 67 | fsm.AddState("Patrol", new CoState(this, Patrol, loop: false)); 68 | fsm.AddState("Chase", new State( 69 | onLogic: state => MoveTowards(playerPosition, chaseSpeed) 70 | )); 71 | fsm.AddState("Fight", fightFsm); 72 | fsm.AddState("Search", new CoState(this, Search, loop: false)); 73 | 74 | fsm.SetStartState("Patrol"); 75 | 76 | fsm.AddTriggerTransition("PlayerSpotted", "Patrol", "Chase"); 77 | fsm.AddTwoWayTransition("Chase", "Fight", t => distanceToPlayer <= attackRange); 78 | fsm.AddTransition("Chase", "Search", 79 | t => distanceToPlayer > searchSpotRange, 80 | onTransition: t => lastSeenPlayerPosition = playerPosition); 81 | fsm.AddTransition("Search", "Chase", t => distanceToPlayer <= searchSpotRange); 82 | fsm.AddTransition(new TransitionAfter("Search", "Patrol", searchTime)); 83 | 84 | fsm.Init(); 85 | } 86 | 87 | void Update() 88 | { 89 | fsm.OnLogic(); 90 | stateDisplayText.text = fsm.GetActiveHierarchyPath(); 91 | } 92 | 93 | // Triggers the `PlayerSpotted` event. 94 | void OnTriggerEnter2D(Collider2D other) { 95 | if (other.CompareTag("Player")) 96 | { 97 | fsm.Trigger("PlayerSpotted"); 98 | } 99 | } 100 | 101 | private void MoveTowards(Vector2 target, float speed, float minDistance=0) 102 | { 103 | transform.position = Vector3.MoveTowards( 104 | transform.position, 105 | target, 106 | Mathf.Max(0, Mathf.Min(speed * Time.deltaTime, Vector2.Distance(transform.position, target) - minDistance)) 107 | ); 108 | } 109 | 110 | private IEnumerator MoveToPosition(Vector2 target, float speed, float tolerance=0.05f) 111 | { 112 | while (Vector2.Distance(transform.position, target) > tolerance) 113 | { 114 | MoveTowards(target, speed); 115 | // Wait one frame. 116 | yield return null; 117 | } 118 | } 119 | 120 | private IEnumerator Patrol() 121 | { 122 | int currentPointIndex = FindClosestPatrolPoint(); 123 | 124 | while (true) 125 | { 126 | yield return MoveToPosition(patrolPoints[currentPointIndex], patrolSpeed); 127 | 128 | // Wait at each patrol point. 129 | yield return new WaitForSeconds(3); 130 | 131 | currentPointIndex += patrolDirection; 132 | 133 | // Once the bot reaches the end or the beginning of the patrol path, 134 | // it reverses the direction. 135 | if (currentPointIndex >= patrolPoints.Length || currentPointIndex < 0) 136 | { 137 | currentPointIndex = Mathf.Clamp(currentPointIndex, 0, patrolPoints.Length-1); 138 | patrolDirection *= -1; 139 | } 140 | } 141 | } 142 | 143 | private int FindClosestPatrolPoint() 144 | { 145 | float minDistance = Vector2.Distance(transform.position, patrolPoints[0]); 146 | int minIndex = 0; 147 | 148 | for (int i = 1; i < patrolPoints.Length; i ++) 149 | { 150 | float distance = Vector2.Distance(transform.position, patrolPoints[i]); 151 | if (distance < minDistance) 152 | { 153 | minDistance = distance; 154 | minIndex = i; 155 | } 156 | } 157 | 158 | return minIndex; 159 | } 160 | 161 | private IEnumerator Search() 162 | { 163 | yield return MoveToPosition(lastSeenPlayerPosition, chaseSpeed); 164 | 165 | while (true) 166 | { 167 | yield return new WaitForSeconds(2); 168 | 169 | yield return MoveToPosition( 170 | (Vector2)transform.position + Random.insideUnitCircle * 10, 171 | patrolSpeed 172 | ); 173 | } 174 | } 175 | } 176 | 177 | } -------------------------------------------------------------------------------- /Samples~/GuardAI/Scripts/GuardAI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 482017910df5af6488d641e351384e8e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Samples~/GuardAI/Scripts/PlayerController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace UnityHFSM.Samples.GuardAI { 6 | 7 | public class PlayerController : MonoBehaviour 8 | { 9 | public static PlayerController Instance { get; private set; } 10 | 11 | public float speed = 5; 12 | 13 | void Start() 14 | { 15 | Instance = this; 16 | Application.targetFrameRate = 60; 17 | } 18 | 19 | void Update() 20 | { 21 | Vector2 input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")); 22 | input = Vector2.ClampMagnitude(input, 1); 23 | input *= speed; 24 | 25 | transform.position += (Vector3) (input * Time.deltaTime); 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /Samples~/GuardAI/Scripts/PlayerController.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 98123b84c75915c488ac9f2183e21025 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scenes.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1377506dbbb16b04fa5c310cfce00c0b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scenes/Sample.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 394c8562b60b24a4bbb06756c2b61806 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scenes/Sample.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 351544494c73a324496d617d11c4b608 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scenes/Sample/NavMesh.asset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/Samples~/Sample3d/Scenes/Sample/NavMesh.asset -------------------------------------------------------------------------------- /Samples~/Sample3d/Scenes/Sample/NavMesh.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ba1600092c2f2e94da5770533e83ac20 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 23800000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3965a2faa1871e84eb78214d42132c1d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scripts/CustomSendData.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityHFSM; // Import the required classes for the state machine 3 | 4 | namespace UnityHFSM.Samples.Sample3d 5 | { 6 | class CustomSendData : StateBase 7 | { 8 | MonoBehaviour mono; 9 | 10 | // Important: The constructor must call StateBase's constructor (here: base(...)) 11 | // because it declares whether the state needsExitTime 12 | public CustomSendData(MonoBehaviour mono) : base(needsExitTime: false) 13 | { 14 | // We need to have access to the MonoBehaviour so that we can rotate it. 15 | // => Keep a reference 16 | this.mono = mono; 17 | } 18 | 19 | public override void OnEnter() 20 | { 21 | // Write your code for OnEnter here 22 | // If you don't have any, you can just leave this entire method override out 23 | } 24 | 25 | public override void OnLogic() 26 | { 27 | this.mono.transform.eulerAngles += new Vector3(0, 0, 100 * Time.deltaTime); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Samples~/Sample3d/Scripts/CustomSendData.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fcf135e390738b3419a14fa1989f1830 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scripts/EnemyController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using UnityEngine; 3 | using UnityHFSM; // Import the required classes for the state machine 4 | 5 | namespace UnityHFSM.Samples.Sample3d 6 | { 7 | public class EnemyController : MonoBehaviour 8 | { 9 | private StateMachine fsm; 10 | public float playerScanningRange = 4f; 11 | public float ownScanningRange = 6f; 12 | 13 | float DistanceToPlayer() 14 | { 15 | // This implementation is an example and may differ for your scene setup 16 | Vector3 player = PlayerController.Instance.transform.position; 17 | return Vector3.Distance(transform.position, player); 18 | } 19 | 20 | void MoveTowardsPlayer(float speed) 21 | { 22 | // This implementation is an example and may differ for your scene setup 23 | Vector3 player = PlayerController.Instance.transform.position; 24 | transform.position = Vector3.MoveTowards(transform.position, player, speed * Time.deltaTime); 25 | } 26 | 27 | void RotateAtSpeed(float speed) 28 | { 29 | transform.eulerAngles += new Vector3(0, 0, speed * Time.deltaTime); 30 | } 31 | 32 | IEnumerator SendData(CoState state) 33 | { 34 | while (state.timer.Elapsed < 2) 35 | { 36 | RotateAtSpeed(100f); 37 | // Wait until the next frame 38 | yield return null; 39 | } 40 | 41 | while (state.timer.Elapsed < 4) 42 | { 43 | RotateAtSpeed(-100f); 44 | yield return null; 45 | } 46 | 47 | state.timer.Reset(); 48 | // Because needsExitTime is true, we have to tell the FSM when it can 49 | // safely exit the state 50 | state.fsm.StateCanExit(); 51 | } 52 | 53 | void Start() 54 | { 55 | fsm = new StateMachine(); 56 | 57 | // This is the nested state machine 58 | StateMachine extractIntel = new StateMachine(needsExitTime: false); 59 | fsm.AddState("ExtractIntel", extractIntel); 60 | 61 | extractIntel.AddState("SendData", 62 | onLogic: (state) => 63 | { 64 | // When the state has been active for more than 5 seconds, 65 | // notify the fsm that the state can cleanly exit 66 | if (state.timer.Elapsed > 5) 67 | state.fsm.StateCanExit(); 68 | 69 | // Make the enemy turn at 100 degrees per second 70 | RotateAtSpeed(100f); 71 | }, 72 | // This means the state won't instantly exit when a transition should happen 73 | // but instead the state machine waits until it is given permission to change state 74 | needsExitTime: true 75 | ); 76 | 77 | // Unity Coroutines 78 | // extractIntel.AddState("SendData", new CoState( 79 | // this 80 | // onLogic: SendData, 81 | // needsExitTime: true 82 | // )); 83 | 84 | // Class based architecture 85 | // extractIntel.AddState("SendData", new CustomSendData(this)); 86 | 87 | extractIntel.AddState("CollectData", 88 | onLogic: (state) => 89 | { 90 | if (state.timer.Elapsed > 5) state.fsm.StateCanExit(); 91 | }, 92 | needsExitTime: true 93 | ); 94 | 95 | // A transition without a condition 96 | extractIntel.AddTransition("SendData", "CollectData"); 97 | extractIntel.AddTransition("CollectData", "SendData"); 98 | extractIntel.SetStartState("CollectData"); 99 | 100 | fsm.AddState("FollowPlayer", 101 | onLogic: (state) => 102 | { 103 | MoveTowardsPlayer(1); 104 | if (DistanceToPlayer() < ownScanningRange) 105 | { 106 | fsm.RequestStateChange("ExtractIntel"); 107 | } 108 | } 109 | ); 110 | 111 | fsm.AddState("FleeFromPlayer", 112 | onLogic: (state) => MoveTowardsPlayer(-1) 113 | ); 114 | 115 | // This configures the entry point of the state machine 116 | fsm.SetStartState("FollowPlayer"); 117 | 118 | fsm.AddTransition( 119 | "ExtractIntel", 120 | "FollowPlayer", 121 | (transition) => DistanceToPlayer() > ownScanningRange); 122 | 123 | fsm.AddTransition( 124 | "FollowPlayer", 125 | "ExtractIntel", 126 | (transition) => DistanceToPlayer() < ownScanningRange); 127 | 128 | fsm.AddTransition( 129 | "ExtractIntel", 130 | "FleeFromPlayer", 131 | (transition) => DistanceToPlayer() < playerScanningRange); 132 | 133 | fsm.AddTransition( 134 | "FleeFromPlayer", 135 | "ExtractIntel", 136 | (transition) => DistanceToPlayer() > playerScanningRange); 137 | 138 | // Initialises the state machine and must be called before OnLogic() is called 139 | fsm.Init(); 140 | } 141 | 142 | void Update() 143 | { 144 | fsm.OnLogic(); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scripts/EnemyController.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: deb07b2e02383ac4dacfaadd8705370c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Samples~/Sample3d/Scripts/PlayerController.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityHFSM.Samples.Sample3d 4 | { 5 | [RequireComponent(typeof(Rigidbody))] 6 | public class PlayerController : MonoBehaviour 7 | { 8 | public static PlayerController Instance { get; private set; } 9 | 10 | [SerializeField] 11 | private float speed = 2; 12 | private Rigidbody rb; 13 | 14 | private void Awake() 15 | { 16 | if (Instance != null && Instance != this) 17 | { 18 | Destroy(gameObject); 19 | } 20 | else 21 | { 22 | DontDestroyOnLoad(gameObject); 23 | Instance = this; 24 | } 25 | } 26 | 27 | private void Start() 28 | { 29 | rb = GetComponent(); 30 | } 31 | 32 | private void FixedUpdate() 33 | { 34 | Vector3 playerInput = new Vector3( 35 | Input.GetAxis("Horizontal"), 36 | 0f, 37 | Input.GetAxis("Vertical") 38 | ); 39 | 40 | rb.velocity = playerInput * speed; 41 | } 42 | 43 | private void OnDestroy() 44 | { 45 | if (Instance == this) 46 | { 47 | Instance = null; 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Samples~/Sample3d/Scripts/PlayerController.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 75c1283e75af203489a1f62d344a6a2d 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: 45c2e04073d60be46aaa70504e1a61da 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/TestActionSystem.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace UnityHFSM.Tests 5 | { 6 | public class TestActions 7 | { 8 | private Recorder recorder; 9 | private StateMachine fsm; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | recorder = new Recorder(); 15 | fsm = new StateMachine(); 16 | } 17 | 18 | [Test] 19 | public void Test_calling_existing_action_works() 20 | { 21 | bool called = false; 22 | var state = new ActionState(false).AddAction("Action", () => called = true); 23 | 24 | Assert.IsFalse(called); 25 | state.OnAction("Action"); 26 | Assert.IsTrue(called); 27 | } 28 | 29 | [Test] 30 | public void Test_calling_non_existant_action_does_not_throw_exception_when_no_actions_added() 31 | { 32 | var state = new ActionState(false); 33 | 34 | Assert.DoesNotThrow(() => state.OnAction("NonExistantAction")); 35 | Assert.DoesNotThrow(() => state.OnAction("NonExistantAction", "")); 36 | } 37 | 38 | [Test] 39 | public void Test_calling_non_existant_action_does_nothing() 40 | { 41 | bool called = false; 42 | var state = new ActionState(false).AddAction("Action", () => called = true); 43 | 44 | state.OnAction("NonExistantAction"); 45 | Assert.IsFalse(called); 46 | } 47 | 48 | [Test] 49 | public void Test_calling_existing_action_with_param_works() 50 | { 51 | int value = 0; 52 | var state = new ActionState(false).AddAction("Action", param => value = param); 53 | 54 | state.OnAction("Action", 5); 55 | Assert.AreEqual(5, value); 56 | } 57 | 58 | [Test] 59 | public void Test_calling_non_existant_action_with_param_does_nothing() 60 | { 61 | int value = 0; 62 | var state = new ActionState(false).AddAction("Action", param => value = param); 63 | 64 | state.OnAction("NonExistantAction", 0); 65 | Assert.AreEqual(0, value); 66 | } 67 | 68 | [Test] 69 | public void Test_calling_action_with_wrong_param_type_fails() 70 | { 71 | int value = 0; 72 | var state = new ActionState(false).AddAction("Action", param => value = param); 73 | 74 | Assert.Throws(() => state.OnAction("Action", false)); 75 | } 76 | 77 | [Test] 78 | public void Test_fsm_propagates_action_to_active_state() 79 | { 80 | bool called = false; 81 | fsm.AddState("A", new State().AddAction("Action", () => called = true)); 82 | fsm.Init(); 83 | 84 | Assert.IsFalse(called); 85 | fsm.OnAction("Action"); 86 | Assert.IsTrue(called); 87 | } 88 | 89 | [Test] 90 | public void Test_nested_fsm_propagates_action_to_active_state() 91 | { 92 | bool called = false; 93 | var nested = new StateMachine(); 94 | fsm.AddState("Nested", nested); 95 | nested.AddState("A", new State().AddAction("Action", () => called = true)); 96 | fsm.Init(); 97 | 98 | fsm.OnAction("Action"); 99 | Assert.IsTrue(called); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/TestActionSystem.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fb9f0840cc1710b41af6fe88312170ab 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestActiveHierarchyPath.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestHierarchyPath 6 | { 7 | [Test] 8 | public void Test_string_is_correct_for_nested_fsm_using_string_ids() 9 | { 10 | var fsm = new StateMachine(); 11 | var move = new StateMachine(); 12 | var jump = new StateMachine(); 13 | 14 | fsm.AddState("Move", move); 15 | move.AddState("Jump", jump); 16 | jump.AddState("Falling"); 17 | 18 | fsm.Init(); 19 | 20 | Assert.AreEqual("/Move/Jump/Falling", fsm.GetActiveHierarchyPath()); 21 | } 22 | 23 | [Test] 24 | public void Test_string_is_empty_when_state_machine_is_not_active() 25 | { 26 | var fsm = new StateMachine(); 27 | fsm.AddState("A"); 28 | Assert.AreEqual("", fsm.GetActiveHierarchyPath()); 29 | } 30 | 31 | [Test] 32 | public void Test_string_is_correct_when_using_mixed_types_for_ids() 33 | { 34 | var root = new StateMachine(); 35 | var a = new StateMachine(); 36 | var b = new StateMachine(); 37 | 38 | root.AddState("A", a); 39 | a.AddState(5, b); 40 | b.AddState(false); 41 | 42 | root.Init(); 43 | 44 | Assert.AreEqual("/A/5/False", root.GetActiveHierarchyPath()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/TestActiveHierarchyPath.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5550003acb0ae8f4f991f7a109b40df9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestActiveStateChangedEvent.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestActiveStateChangedEvent 6 | { 7 | private StateMachine fsm; 8 | private Recorder recorder; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | fsm = new StateMachine(); 14 | recorder = new Recorder(); 15 | } 16 | 17 | [Test] 18 | public void Test_active_state_changed_event() 19 | { 20 | fsm.StateChanged += state => recorder.RecordCustom($"StateChanged({state.name})"); 21 | 22 | fsm.AddState("A", recorder.TrackedState); 23 | fsm.AddState("B", recorder.TrackedState); 24 | fsm.AddState("C", recorder.TrackedState); 25 | 26 | fsm.AddTransition("A", "B"); 27 | fsm.AddTransition("B", "C"); 28 | 29 | fsm.SetStartState("A"); 30 | fsm.Init(); 31 | 32 | recorder.Expect 33 | .Enter("A") 34 | .Custom("StateChanged(A)") 35 | .All(); 36 | 37 | fsm.OnLogic(); 38 | recorder.Expect 39 | .Exit("A") 40 | .Enter("B") 41 | .Custom("StateChanged(B)") 42 | .Logic("B") 43 | .All(); 44 | 45 | fsm.OnLogic(); 46 | recorder.Expect 47 | .Exit("B") 48 | .Enter("C") 49 | .Custom("StateChanged(C)") 50 | .Logic("C") 51 | .All(); 52 | 53 | fsm.OnExit(); 54 | recorder.Expect 55 | .Exit("C") 56 | .All(); 57 | } 58 | 59 | [Test] 60 | public void Test_active_state_changed_event_works_with_ghost_states() 61 | { 62 | fsm.StateChanged += state => recorder.RecordCustom($"StateChanged({state.name})"); 63 | 64 | fsm.AddState("A", recorder.Track(new State(isGhostState: true))); 65 | fsm.AddState("B", recorder.Track(new State(isGhostState: true))); 66 | fsm.AddState("C", recorder.Track(new State(isGhostState: true))); 67 | 68 | fsm.AddTransition("A", "B"); 69 | fsm.AddTransition("B", "C"); 70 | 71 | fsm.Init(); 72 | recorder.Expect 73 | .Enter("A") 74 | .Custom("StateChanged(A)") 75 | .Exit("A") 76 | .Enter("B") 77 | .Custom("StateChanged(B)") 78 | .Exit("B") 79 | .Enter("C") 80 | .Custom("StateChanged(C)") 81 | .All(); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Tests/TestActiveStateChangedEvent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 595493d7152c49a19b6f406d6cb7559d 3 | timeCreated: 1684143774 -------------------------------------------------------------------------------- /Tests/TestCanExit.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestCanExit 6 | { 7 | private StateMachine fsm; 8 | 9 | [SetUp] 10 | public void Setup() 11 | { 12 | fsm = new StateMachine(); 13 | } 14 | 15 | [Test] 16 | public void Test_fsm_waits_on_state_with_needsExitTime_when_canExit_false() 17 | { 18 | fsm.AddState("A", needsExitTime: true, canExit: state => false); 19 | fsm.AddState("B"); 20 | 21 | fsm.AddTransition("A", "B"); 22 | 23 | fsm.Init(); 24 | fsm.OnLogic(); 25 | Assert.AreEqual("A", fsm.ActiveStateName); 26 | } 27 | 28 | [Test] 29 | public void Test_state_with_needsExitTime_can_exit_instantly_when_canExit_true() 30 | { 31 | fsm.AddState("A", needsExitTime: true, canExit: state => true); 32 | fsm.AddState("B"); 33 | 34 | fsm.AddTransition("A", "B"); 35 | 36 | fsm.Init(); 37 | fsm.OnLogic(); 38 | Assert.AreEqual("B", fsm.ActiveStateName); 39 | } 40 | 41 | [Test] 42 | public void Test_state_with_needsExitTime_can_exit_later_when_canExit_switches_to_true() 43 | { 44 | var canExit = false; 45 | fsm.AddState("A", needsExitTime: true, canExit: state => canExit); 46 | fsm.AddState("B"); 47 | 48 | fsm.AddTransition("A", "B"); 49 | 50 | fsm.Init(); 51 | fsm.OnLogic(); 52 | Assert.AreEqual("A", fsm.ActiveStateName); 53 | 54 | canExit = true; 55 | fsm.OnLogic(); 56 | Assert.AreEqual("B", fsm.ActiveStateName); 57 | } 58 | 59 | [Test] 60 | public void Test_state_with_needsExitTime_calls_onLogic_before_transitioning_on_delayed_transition() 61 | { 62 | var canExit = false; 63 | 64 | var recorder = new Recorder(); 65 | 66 | fsm.AddState("A", recorder.Track(new State( 67 | onLogic: state => recorder.RecordCustom("UserOnLogic"), 68 | needsExitTime: true, 69 | canExit: state => canExit))); 70 | fsm.AddState("B", recorder.TrackedState); 71 | 72 | fsm.Init(); 73 | fsm.OnLogic(); 74 | 75 | fsm.RequestStateChange("B"); 76 | Assert.AreEqual("A", fsm.ActiveStateName); 77 | 78 | recorder.DiscardAll(); 79 | 80 | canExit = true; 81 | fsm.OnLogic(); 82 | 83 | recorder.Expect 84 | .Logic("A") 85 | .Custom("UserOnLogic") 86 | .Exit("A") 87 | .Enter("B") 88 | .All(); 89 | 90 | fsm.OnLogic(); 91 | recorder.Expect.Logic("B").All(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/TestCanExit.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1eb24ee6d5969db43b72a06dc65bd65d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestExampleScene.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | /* 6 | Mermaid state diagram for reference: 7 | 8 | ``` 9 | stateDiagram-v2 10 | [*] --> Follow 11 | Extract: Extract Intel 12 | Flee: Flee From Player 13 | Follow: Follow Player 14 | 15 | Flee --> Extract 16 | Extract --> Flee 17 | 18 | Follow --> Extract 19 | Extract --> Follow 20 | 21 | state Extract { 22 | Send: Send Data 23 | Collect: Collect Data 24 | 25 | [*] --> Collect 26 | Collect --> Send 27 | Send --> Collect 28 | 29 | Collect --> [*] 30 | } 31 | ``` 32 | */ 33 | 34 | public class TestExampleScene 35 | { 36 | private Recorder recorder; 37 | private StateMachine fsm; 38 | 39 | [SetUp] 40 | public void Setup() 41 | { 42 | recorder = new Recorder(); 43 | fsm = new StateMachine(); 44 | } 45 | 46 | [Test] 47 | public void Test_example_scene() 48 | { 49 | // Advanced test that checks multiple different components working together. 50 | // This is the hierarchical state machine example from the README 51 | // that controls the beheviour of an Enemy Spy unit in a space game. 52 | 53 | // ExtractIntel state 54 | StateMachine extractIntel = new StateMachine(needsExitTime: true); 55 | extractIntel.AddState("CollectData", recorder.TrackedState); 56 | extractIntel.AddState("SendData", recorder.TrackedState); 57 | 58 | bool shouldCollectData = true; 59 | extractIntel.SetStartState("CollectData"); 60 | extractIntel.AddExitTransition("CollectData"); 61 | extractIntel.AddTransition("CollectData", "SendData", t => !shouldCollectData); 62 | extractIntel.AddTransition("SendData", "CollectData", t => shouldCollectData); 63 | 64 | // States 65 | fsm.AddState("FleeFromPlayer", recorder.TrackedState); 66 | fsm.AddState("FollowPlayer", recorder.TrackedState); 67 | fsm.AddState("ExtractIntel", recorder.Track(extractIntel)); 68 | 69 | // Transitions 70 | bool isInPlayerScanningRange = false; 71 | bool isPlayerInOwnScanningRange = false; 72 | 73 | fsm.AddTransition( 74 | "ExtractIntel", 75 | "FollowPlayer", 76 | t => !isPlayerInOwnScanningRange 77 | ); 78 | 79 | fsm.AddTransition( 80 | "FollowPlayer", 81 | "ExtractIntel", 82 | t => isPlayerInOwnScanningRange 83 | ); 84 | 85 | fsm.AddTransition( 86 | "ExtractIntel", 87 | "FleeFromPlayer", 88 | t => isInPlayerScanningRange 89 | ); 90 | 91 | fsm.AddTransition( 92 | "FleeFromPlayer", 93 | "ExtractIntel", 94 | t => !isInPlayerScanningRange 95 | ); 96 | 97 | // Start 98 | fsm.SetStartState("FollowPlayer"); 99 | fsm.Init(); 100 | recorder.Expect.Enter("FollowPlayer").All(); 101 | 102 | // Follow the player for one frame 103 | fsm.OnLogic(); 104 | recorder.Expect.Logic("FollowPlayer").All(); 105 | 106 | // Player gets in scanning range => Start collecting data 107 | isPlayerInOwnScanningRange = true; 108 | fsm.OnLogic(); 109 | recorder.Expect 110 | .Exit("FollowPlayer") 111 | .Enter("ExtractIntel") 112 | .Enter("CollectData") 113 | .Logic("ExtractIntel") 114 | .Logic("CollectData") 115 | .All(); 116 | 117 | fsm.OnLogic(); 118 | recorder.Expect 119 | .Logic("ExtractIntel") 120 | .Logic("CollectData") 121 | .All(); 122 | 123 | // In the ExtractIntel state: Send the data 124 | shouldCollectData = false; 125 | fsm.OnLogic(); 126 | recorder.Expect 127 | .Logic("ExtractIntel") 128 | .Exit("CollectData") 129 | .Enter("SendData") 130 | .Logic("SendData") 131 | .All(); 132 | 133 | fsm.OnLogic(); 134 | recorder.Expect 135 | .Logic("ExtractIntel") 136 | .Logic("SendData") 137 | .All(); 138 | 139 | // In the ExtractIntel state: Collect data 140 | shouldCollectData = true; 141 | fsm.OnLogic(); 142 | recorder.Expect 143 | .Logic("ExtractIntel") 144 | .Exit("SendData") 145 | .Enter("CollectData") 146 | .Logic("CollectData") 147 | .All(); 148 | 149 | // Collect data is interrupted by the player who can now see the Spy Enemy on the edge 150 | // of the radar. 151 | isInPlayerScanningRange = true; 152 | fsm.OnLogic(); 153 | recorder.Expect 154 | .Logic("ExtractIntel") 155 | .Exit("ExtractIntel") 156 | .Exit("CollectData") 157 | .Enter("FleeFromPlayer") 158 | .All(); 159 | 160 | fsm.OnLogic(); 161 | recorder.Expect.Logic("FleeFromPlayer").All(); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Tests/TestExampleScene.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4d54d35b3516e16499b01bc9ca90b43c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestExitTransitions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 70045b993f90c3c4eb50a516361f8c39 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestFsmBaseTransitions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bac665ddc380d2d40b3b143a8b7f5f96 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestGenericsInHierarchical.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestGenericsInHierarchical 6 | { 7 | private enum States 8 | { 9 | INT_FSM, STRING_FSM 10 | } 11 | 12 | [Test] 13 | public void Test_using_different_type_in_nested_fsm_works() 14 | { 15 | var fsm = new StateMachine(); 16 | var intFsm = new StateMachine(needsExitTime: false); 17 | var stringFsm = new StateMachine(); 18 | 19 | fsm.AddState(States.INT_FSM, intFsm); 20 | fsm.AddState(States.STRING_FSM, stringFsm); 21 | 22 | intFsm.AddState(1); 23 | intFsm.AddState(2); 24 | intFsm.AddTransition(1, 2); 25 | 26 | stringFsm.AddState("A"); 27 | stringFsm.AddState("B"); 28 | stringFsm.AddTransition("A", "B"); 29 | 30 | fsm.Init(); 31 | 32 | Assert.AreEqual(States.INT_FSM, fsm.ActiveStateName); 33 | Assert.AreEqual(1, intFsm.ActiveStateName); 34 | 35 | fsm.OnLogic(); 36 | Assert.AreEqual(2, intFsm.ActiveStateName); 37 | 38 | fsm.RequestStateChange(States.STRING_FSM); 39 | Assert.AreEqual(States.STRING_FSM, fsm.ActiveStateName); 40 | Assert.AreEqual("A", stringFsm.ActiveStateName); 41 | 42 | fsm.OnLogic(); 43 | Assert.AreEqual("B", stringFsm.ActiveStateName); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/TestGenericsInHierarchical.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 61e4331129196ff4cb9e9e4d887f6417 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestGhostStates.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestGhostStates 6 | { 7 | private Recorder recorder; 8 | private StateMachine fsm; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | recorder = new Recorder(); 14 | fsm = new StateMachine(); 15 | } 16 | 17 | [Test] 18 | public void Test_fsm_performs_one_transition_for_non_ghost_state() 19 | { 20 | fsm.AddState("A", recorder.TrackedState); 21 | fsm.AddState("B", recorder.TrackedState); 22 | fsm.AddState("C", recorder.TrackedState); 23 | 24 | fsm.AddTransition("A", "B"); 25 | fsm.AddTransition("B", "C"); 26 | 27 | fsm.SetStartState("A"); 28 | 29 | fsm.Init(); 30 | recorder.DiscardAll(); 31 | 32 | fsm.OnLogic(); 33 | recorder.Expect 34 | .Exit("A") 35 | .Enter("B") 36 | .Logic("B") 37 | .All(); 38 | } 39 | 40 | [Test] 41 | public void Test_fsm_quickly_transitions_over_ghost_state_for_on_logic() 42 | { 43 | fsm.AddState("A", recorder.TrackedState); 44 | fsm.AddState("B", recorder.Track(new StateBase(needsExitTime: false, isGhostState: true))); 45 | fsm.AddState("C", recorder.Track(new StateBase(needsExitTime: false, isGhostState: true))); 46 | fsm.AddState("D", recorder.TrackedState); 47 | 48 | fsm.AddTransition("A", "B"); 49 | fsm.AddTransition("B", "C"); 50 | fsm.AddTransition("C", "D"); 51 | 52 | fsm.SetStartState("A"); 53 | 54 | fsm.Init(); 55 | recorder.DiscardAll(); 56 | 57 | fsm.OnLogic(); 58 | recorder.Expect 59 | .Exit("A") 60 | .Enter("B") 61 | .Exit("B") 62 | .Enter("C") 63 | .Exit("C") 64 | .Enter("D") 65 | .Logic("D") 66 | .All(); 67 | } 68 | 69 | [Test] 70 | public void Test_fsm_respects_needsExitTime_of_ghost_state() 71 | { 72 | fsm.AddState("A", recorder.TrackedState); 73 | fsm.AddState("B", recorder.Track( 74 | new State(needsExitTime: true, isGhostState: false) 75 | )); 76 | fsm.AddState("C", recorder.TrackedState); 77 | 78 | fsm.AddTransition("A", "B"); 79 | fsm.AddTransition("B", "C"); 80 | 81 | fsm.SetStartState("A"); 82 | 83 | fsm.Init(); 84 | recorder.DiscardAll(); 85 | 86 | fsm.OnLogic(); 87 | recorder.Expect 88 | .Exit("A") 89 | .Enter("B") 90 | .Logic("B") 91 | .All(); 92 | 93 | fsm.OnLogic(); 94 | recorder.Expect.Logic("B").All(); 95 | 96 | fsm.StateCanExit(); 97 | recorder.Expect 98 | .Exit("B") 99 | .Enter("C"); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/TestGhostStates.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cfcfbf1cfda49bf4a951550ac5ba3cfe 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestHybridStateMachine.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestHybridStateMachine 6 | { 7 | private Recorder recorder; 8 | private StateMachine fsm; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | recorder = new Recorder(); 14 | fsm = new StateMachine(); 15 | } 16 | 17 | [Test] 18 | public void Test_hybrid_does_not_throw_exceptions_when_no_callbacks_given() 19 | { 20 | var hybrid = new HybridStateMachine(); 21 | fsm.AddState("Main", recorder.Track(hybrid)); 22 | fsm.AddState("Other", recorder.TrackedState); 23 | hybrid.AddState("A", recorder.TrackedState); 24 | 25 | Assert.DoesNotThrow(() => fsm.Init()); 26 | Assert.DoesNotThrow(() => fsm.OnLogic()); 27 | Assert.DoesNotThrow(() => fsm.RequestStateChange("Other")); 28 | } 29 | 30 | [Test] 31 | public void Test_hybrid_behaves_like_normal_fsm() 32 | { 33 | var hybrid = new HybridStateMachine(); 34 | fsm.AddState("Main", recorder.Track(hybrid)); 35 | fsm.AddState("Other", recorder.TrackedState); 36 | hybrid.AddState("A", recorder.TrackedState); 37 | 38 | fsm.Init(); 39 | recorder.Expect 40 | .Enter("Main") 41 | .Enter("A") 42 | .All(); 43 | 44 | fsm.OnLogic(); 45 | recorder.Expect 46 | .Logic("Main") 47 | .Logic("A") 48 | .All(); 49 | 50 | fsm.RequestStateChange("Other"); 51 | recorder.Expect 52 | .Exit("Main") 53 | .Exit("A") 54 | .Enter("Other") 55 | .All(); 56 | } 57 | 58 | [Test] 59 | public void Test_order_of_callbacks_is_correct() 60 | { 61 | var hybrid = new HybridStateMachine( 62 | beforeOnEnter: fsm => recorder.RecordCustom("BeforeEnter"), 63 | afterOnEnter: fsm => recorder.RecordCustom("AfterEnter"), 64 | beforeOnLogic: fsm => recorder.RecordCustom("BeforeLogic"), 65 | afterOnLogic: fsm => recorder.RecordCustom("AfterLogic"), 66 | beforeOnExit: fsm => recorder.RecordCustom("BeforeExit"), 67 | afterOnExit: fsm => recorder.RecordCustom("AfterExit") 68 | ); 69 | hybrid.AddState("A", recorder.TrackedState); 70 | 71 | hybrid.OnEnter(); 72 | recorder.Expect 73 | .Custom("BeforeEnter") 74 | .Enter("A") 75 | .Custom("AfterEnter") 76 | .All(); 77 | 78 | hybrid.OnLogic(); 79 | recorder.Expect 80 | .Custom("BeforeLogic") 81 | .Logic("A") 82 | .Custom("AfterLogic") 83 | .All(); 84 | 85 | hybrid.OnExit(); 86 | recorder.Expect 87 | .Custom("BeforeExit") 88 | .Exit("A") 89 | .Custom("AfterExit") 90 | .All(); 91 | } 92 | 93 | [Test] 94 | public void Test_hybrid_actions_are_called_after_sub_state_actions() 95 | { 96 | var hybrid = new HybridStateMachine(); 97 | 98 | hybrid.AddState("A", new State() 99 | .AddAction("Normal", () => recorder.RecordCustom("A.Normal()")) 100 | .AddAction("Parameter", value => recorder.RecordCustom($"A.Parameter({value})")) 101 | ); 102 | 103 | hybrid.AddAction("Normal", () => recorder.RecordCustom("Hybrid.Normal()")); 104 | hybrid.AddAction("Parameter", value => recorder.RecordCustom($"Hybrid.Parameter({value})")); 105 | 106 | hybrid.Init(); 107 | 108 | hybrid.OnAction("Normal"); 109 | recorder.Expect 110 | .Custom("Hybrid.Normal()") 111 | .Custom("A.Normal()") 112 | .All(); 113 | 114 | hybrid.OnAction("Parameter", 10); 115 | recorder.Expect 116 | .Custom("Hybrid.Parameter(10)") 117 | .Custom("A.Parameter(10)") 118 | .All(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/TestHybridStateMachine.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5fc8fece98e5a8943ba538849e9f11d3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestParallelStates.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 55ac25ac7cb58aa4fbb84bb3dec1ea1f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestRememberLastState.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestRememberLastState 6 | { 7 | private StateMachine fsm; 8 | 9 | [SetUp] 10 | public void Setup() 11 | { 12 | fsm = new StateMachine(); 13 | } 14 | 15 | private void AssertStatesAreActive(string activeHierarchyPath) 16 | { 17 | Assert.AreEqual(activeHierarchyPath, fsm.GetActiveHierarchyPath()); 18 | } 19 | 20 | [Test] 21 | public void Test_nested_fsm_returns_to_start_state_by_default_on_enter() 22 | { 23 | var nested = new StateMachine(); 24 | nested.AddState("X"); 25 | nested.AddState("Y"); 26 | nested.SetStartState("Y"); 27 | 28 | fsm.AddState("A", nested); 29 | fsm.AddState("B"); 30 | 31 | fsm.Init(); 32 | AssertStatesAreActive("/A/Y"); 33 | 34 | nested.RequestStateChange("X"); 35 | AssertStatesAreActive("/A/X"); 36 | 37 | fsm.RequestStateChange("B"); 38 | AssertStatesAreActive("/B"); 39 | 40 | fsm.RequestStateChange("A"); 41 | AssertStatesAreActive("/A/Y"); // Y is the default start state. 42 | } 43 | 44 | [Test] 45 | public void Test_remember_last_state_works() 46 | { 47 | var nested = new StateMachine(rememberLastState: true); 48 | nested.AddState("X"); 49 | nested.AddState("Y"); 50 | nested.AddState("Z"); 51 | nested.SetStartState("X"); 52 | 53 | nested.AddTransition("X", "Y"); 54 | nested.AddTransition("Y", "Z"); 55 | 56 | fsm.AddState("A", nested); 57 | fsm.AddState("B"); 58 | 59 | fsm.Init(); 60 | AssertStatesAreActive("/A/X"); 61 | 62 | fsm.OnLogic(); 63 | fsm.OnLogic(); 64 | AssertStatesAreActive("/A/Z"); 65 | 66 | // Here A (the nested fsm) exits. This normally means that when it enters again, 67 | // it will enter in its original start state (X). 68 | fsm.RequestStateChange("B"); 69 | AssertStatesAreActive("/B"); 70 | 71 | fsm.RequestStateChange("A"); 72 | AssertStatesAreActive("/A/Z"); // Z is the remembered last state. 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/TestRememberLastState.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b06d5499e93920a4b811c3feb3a9f267 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestStartUp.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestStartUp 6 | { 7 | private Recorder recorder; 8 | private StateMachine fsm; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | recorder = new Recorder(); 14 | fsm = new StateMachine(); 15 | } 16 | 17 | [Test] 18 | public void TestStartupEvents() 19 | { 20 | fsm.AddState("A", recorder.Track(new State())); 21 | recorder.Expect.Empty(); 22 | 23 | fsm.Init(); 24 | recorder.Expect.Enter("A").All(); 25 | Assert.AreEqual("A", fsm.ActiveStateName); 26 | 27 | fsm.OnLogic(); 28 | recorder.Expect.Logic("A").All(); 29 | } 30 | 31 | [Test] 32 | public void Test_fsm_starts_in_implicit_start_state() 33 | { 34 | fsm.AddState("A"); 35 | fsm.AddState("B"); 36 | fsm.Init(); 37 | Assert.AreEqual("A", fsm.ActiveStateName); 38 | } 39 | 40 | [Test] 41 | public void Test_fsm_starts_in_explicit_start_state() 42 | { 43 | fsm.AddState("A"); 44 | fsm.AddState("B"); 45 | fsm.SetStartState("B"); 46 | fsm.Init(); 47 | Assert.AreEqual("B", fsm.ActiveStateName); 48 | } 49 | 50 | [Test] 51 | public void Test_setting_start_state_before_adding_the_state_works() 52 | { 53 | fsm.SetStartState("B"); 54 | fsm.AddState("A"); 55 | fsm.AddState("B"); 56 | fsm.Init(); 57 | Assert.AreEqual("B", fsm.ActiveStateName); 58 | } 59 | 60 | [Test] 61 | public void Test_setting_start_state_while_running_does_nothing() 62 | { 63 | fsm.AddState("A"); 64 | fsm.AddState("B"); 65 | fsm.Init(); 66 | 67 | fsm.OnLogic(); 68 | fsm.SetStartState("B"); 69 | fsm.OnLogic(); 70 | Assert.AreEqual("A", fsm.ActiveStateName); 71 | } 72 | 73 | [Test] 74 | public void Test_accessing_active_state_before_init_fails() 75 | { 76 | fsm.AddState("A"); 77 | StateBase activeState; 78 | Assert.Throws(() => activeState = fsm.ActiveState); 79 | } 80 | 81 | [Test] 82 | public void Test_calling_init_before_adding_a_state_fails() 83 | { 84 | Assert.Throws(() => fsm.Init()); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/TestStartUp.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b34a118c68bca1b4c84e64c0fda564cb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestStateAndTransitionGetters.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Collections.Generic; 3 | using UnityHFSM.Exceptions; 4 | 5 | namespace UnityHFSM.Tests 6 | { 7 | public class TestStateAndTransitionGetters 8 | { 9 | private StateMachine fsm; 10 | 11 | private List> states; 12 | private List stateNames; 13 | private List> transitions; 14 | private List> transitionsFromAny; 15 | private Dictionary>> triggerTransitionsByEvent; 16 | private Dictionary>> triggerTransitionsFromAnyByEvent; 17 | 18 | [SetUp] 19 | public void Setup() 20 | { 21 | states = new List>(); 22 | stateNames = new List(); 23 | transitions = new List>(); 24 | transitionsFromAny = new List>(); 25 | triggerTransitionsByEvent = new Dictionary>>(); 26 | triggerTransitionsFromAnyByEvent = new Dictionary>>(); 27 | } 28 | 29 | private void CreateEmptyStateMachine() 30 | { 31 | fsm = new StateMachine(); 32 | } 33 | 34 | private void CreateExampleStateMachine() 35 | { 36 | fsm = new StateMachine(); 37 | 38 | fsm.SetStartState("A"); 39 | 40 | AddState("A"); 41 | AddState("B"); 42 | AddState("C"); 43 | 44 | AddTransition("A", "B"); 45 | AddTransition("A", "C"); 46 | AddTransition("C", "A"); 47 | 48 | AddTriggerTransition("event1", "A", "B"); 49 | AddTriggerTransition("event1", "A", "C"); 50 | AddTriggerTransition("event2", "A", "C"); 51 | AddTriggerTransition("event1", "C", "A"); 52 | AddTriggerTransition("event2", "B", "C"); 53 | 54 | AddTransitionFromAny("A"); 55 | AddTransitionFromAny("A"); 56 | AddTransitionFromAny("B"); 57 | 58 | AddTriggerTransitionFromAny("event1", "A"); 59 | AddTriggerTransitionFromAny("event1", "A"); 60 | AddTriggerTransitionFromAny("event2", "A"); 61 | AddTriggerTransitionFromAny("event2", "C"); 62 | } 63 | 64 | [Test] 65 | public void Test_GetStartStateName() 66 | { 67 | CreateExampleStateMachine(); 68 | Assert.AreEqual("A", fsm.GetStartStateName()); 69 | } 70 | 71 | [Test] 72 | public void Test_GetStartStateName_on_empty_fsm() 73 | { 74 | CreateEmptyStateMachine(); 75 | Assert.Throws(() => fsm.GetStartStateName()); 76 | } 77 | 78 | [Test] 79 | public void Test_GetAllStateNames() 80 | { 81 | CreateExampleStateMachine(); 82 | Assert.That(fsm.GetAllStateNames(), Is.EquivalentTo(stateNames)); 83 | } 84 | 85 | [Test] 86 | public void Test_GetAllStateNames_on_empty_fsm() 87 | { 88 | CreateEmptyStateMachine(); 89 | Assert.That(fsm.GetAllStateNames(), Is.EquivalentTo(stateNames)); 90 | } 91 | 92 | [Test] 93 | public void Test_GetAllStates() 94 | { 95 | CreateExampleStateMachine(); 96 | Assert.That(fsm.GetAllStates(), Is.EquivalentTo(states)); 97 | } 98 | 99 | [Test] 100 | public void Test_GetAllStates_on_empty_fsm() 101 | { 102 | CreateEmptyStateMachine(); 103 | Assert.That(fsm.GetAllStates(), Is.EquivalentTo(states)); 104 | } 105 | 106 | [Test] 107 | public void Test_GetAllTransitions() 108 | { 109 | CreateExampleStateMachine(); 110 | Assert.That(fsm.GetAllTransitions(), Is.EquivalentTo(transitions)); 111 | } 112 | 113 | [Test] 114 | public void Test_GetAllTransitions_on_empty_fsm() 115 | { 116 | CreateEmptyStateMachine(); 117 | Assert.That(fsm.GetAllTransitions(), Is.EquivalentTo(transitions)); 118 | } 119 | 120 | [Test] 121 | public void Test_GetAllTransitionsFromAny() 122 | { 123 | CreateExampleStateMachine(); 124 | Assert.That(fsm.GetAllTransitionsFromAny(), Is.EquivalentTo(transitionsFromAny)); 125 | } 126 | 127 | [Test] 128 | public void Test_GetAllTransitionsFromAny_on_empty_fsm() 129 | { 130 | CreateEmptyStateMachine(); 131 | Assert.That(fsm.GetAllTransitionsFromAny(), Is.EquivalentTo(transitionsFromAny)); 132 | } 133 | 134 | [Test] 135 | public void Test_GetAllTriggerTransitions() 136 | { 137 | CreateExampleStateMachine(); 138 | Assert.That(fsm.GetAllTriggerTransitions(), Is.EquivalentTo(triggerTransitionsByEvent)); 139 | } 140 | 141 | [Test] 142 | public void Test_GetAllTriggerTransitions_on_empty_fsm() 143 | { 144 | CreateEmptyStateMachine(); 145 | Assert.That(fsm.GetAllTriggerTransitions(), Is.EquivalentTo(triggerTransitionsByEvent)); 146 | } 147 | 148 | [Test] 149 | public void Test_GetAllTriggerTransitionsFromAny() 150 | { 151 | CreateExampleStateMachine(); 152 | Assert.That(fsm.GetAllTriggerTransitionsFromAny(), Is.EquivalentTo(triggerTransitionsFromAnyByEvent)); 153 | } 154 | 155 | [Test] 156 | public void Test_GetAllTriggerTransitionsFromAny_on_empty_fsm() 157 | { 158 | CreateEmptyStateMachine(); 159 | Assert.That(fsm.GetAllTriggerTransitionsFromAny(), Is.EquivalentTo(triggerTransitionsFromAnyByEvent)); 160 | } 161 | 162 | private void AddState(string name) 163 | { 164 | var state = new State(); 165 | stateNames.Add(name); 166 | states.Add(state); 167 | fsm.AddState(name, state); 168 | } 169 | 170 | private void AddTransition(string from, string to) 171 | { 172 | var transition = new Transition(from, to); 173 | fsm.AddTransition(transition); 174 | transitions.Add(transition); 175 | } 176 | 177 | private void AddTriggerTransition(string trigger, string from, string to) 178 | { 179 | var transition = new Transition(from, to); 180 | fsm.AddTriggerTransition(trigger, transition); 181 | 182 | if (!triggerTransitionsByEvent.ContainsKey(trigger)) 183 | { 184 | triggerTransitionsByEvent.Add(trigger, new List>()); 185 | } 186 | 187 | triggerTransitionsByEvent[trigger].Add(transition); 188 | } 189 | 190 | private void AddTransitionFromAny(string to) 191 | { 192 | var transition = new Transition(null, to); 193 | fsm.AddTransitionFromAny(transition); 194 | transitionsFromAny.Add(transition); 195 | } 196 | 197 | private void AddTriggerTransitionFromAny(string trigger, string to) 198 | { 199 | var transition = new Transition("", to); 200 | fsm.AddTriggerTransitionFromAny(trigger, transition); 201 | 202 | if (!triggerTransitionsFromAnyByEvent.ContainsKey(trigger)) 203 | { 204 | triggerTransitionsFromAnyByEvent.Add(trigger, new List>()); 205 | } 206 | 207 | triggerTransitionsFromAnyByEvent[trigger].Add(transition); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Tests/TestStateAndTransitionGetters.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1d76bc654ebd477e8b761dd40d772bd0 3 | timeCreated: 1742743710 -------------------------------------------------------------------------------- /Tests/TestStateMachinePath.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UnityHFSM.Inspection; 3 | 4 | namespace UnityHFSM.Tests 5 | { 6 | public class TestStateMachinePath 7 | { 8 | [Test] 9 | public void Test_same_paths_are_equal() 10 | { 11 | StateMachinePath root = StateMachinePath.Root; 12 | 13 | StateMachinePath pathA1 = new StateMachinePath(root, "A"); 14 | StateMachinePath pathA2 = new StateMachinePath(pathA1, 1); 15 | 16 | StateMachinePath pathB1 = new StateMachinePath(root, "A"); 17 | StateMachinePath pathB2 = new StateMachinePath(pathB1, 1); 18 | 19 | Assert.AreEqual(root, root); 20 | Assert.AreEqual(pathA1, pathB1); 21 | Assert.AreEqual(pathA2, pathB2); 22 | 23 | Assert.AreEqual(pathA1.GetHashCode(), pathB1.GetHashCode()); 24 | Assert.AreEqual(pathA2.GetHashCode(), pathB2.GetHashCode()); 25 | 26 | Assert.True(pathA1 == pathB1); 27 | Assert.True(pathA2 == pathB2); 28 | } 29 | 30 | [Test] 31 | public void Test_different_paths_are_not_equal() 32 | { 33 | StateMachinePath root = StateMachinePath.Root; 34 | StateMachinePath path1 = new StateMachinePath(root, "A"); 35 | StateMachinePath path2 = new StateMachinePath(path1, 1); 36 | 37 | Assert.AreNotEqual(root, path1); 38 | Assert.AreNotEqual(path1, path2); 39 | } 40 | 41 | [Test] 42 | public void Test_paths_with_same_string_but_different_types_are_not_equal() 43 | { 44 | StateMachinePath a1 = new StateMachinePath(0); 45 | StateMachinePath a2 = new StateMachinePath(a1, 1); 46 | StateMachinePath a3 = new StateMachinePath(a2, 2); 47 | 48 | StateMachinePath b1 = new StateMachinePath(0); 49 | StateMachinePath b2 = new StateMachinePath(b1, 1); 50 | StateMachinePath b3 = new StateMachinePath(b2, 2); 51 | 52 | Assert.AreNotEqual(a3, b3); 53 | Assert.AreEqual(a3.ToString(), b3.ToString()); 54 | } 55 | 56 | [Test] 57 | public void Test_IsChildPathOf() 58 | { 59 | StateMachinePath root = StateMachinePath.Root; 60 | StateMachinePath path1 = new StateMachinePath(root, "A"); 61 | StateMachinePath path2 = new StateMachinePath(path1, 1); 62 | StateMachinePath unrelatedPath = new StateMachinePath(5); 63 | 64 | Assert.False(root.IsChildPathOf(root)); 65 | 66 | Assert.True(path1.IsChildPathOf(root)); 67 | Assert.True(path2.IsChildPathOf(root)); 68 | Assert.True(path2.IsChildPathOf(path1)); 69 | Assert.False(path1.IsChildPathOf(path2)); 70 | 71 | Assert.False(unrelatedPath.IsChildPathOf(root)); 72 | Assert.False(path2.IsChildPathOf(unrelatedPath)); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/TestStateMachinePath.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0cec13571b4341799018b7a090204fda 3 | timeCreated: 1742919239 -------------------------------------------------------------------------------- /Tests/TestStateMachineWalker.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UnityHFSM.Inspection; 3 | 4 | namespace UnityHFSM.Tests 5 | { 6 | public class TestStateMachineWalker 7 | { 8 | [Test] 9 | public void Test_GetActiveStatePath() 10 | { 11 | var nested2 = new StateMachine(); 12 | nested2.AddState(true); 13 | 14 | var nested1 = new StateMachine(); 15 | nested1.AddState(5, nested2); 16 | 17 | var fsm = new StateMachine(); 18 | fsm.AddState("A", nested1); 19 | fsm.AddState("B"); 20 | 21 | fsm.SetStartState("A"); 22 | fsm.Init(); 23 | 24 | StateMachinePath expectedPath = StateMachinePath.Root.Join("A").Join(5).Join(true); 25 | StateMachinePath path = StateMachineWalker.GetActiveStatePath(fsm); 26 | Assert.AreEqual(expectedPath, path); 27 | 28 | fsm.RequestStateChange("B"); 29 | 30 | expectedPath = StateMachinePath.Root.Join("B"); 31 | path = StateMachineWalker.GetActiveStatePath(fsm); 32 | Assert.AreEqual(expectedPath, path); 33 | } 34 | 35 | [Test] 36 | public void Test_GetPathOfState() 37 | { 38 | var nested2 = new StateMachine(); 39 | var stateTrue = new State(); 40 | var stateFalse = new State(); 41 | nested2.AddState(true, stateTrue); 42 | nested2.AddState(false, stateFalse); 43 | 44 | var nested1 = new StateMachine(); 45 | nested1.AddState(5, nested2); 46 | 47 | var fsm = new StateMachine(); 48 | fsm.AddState("A", nested1); 49 | var stateB = new State(); 50 | fsm.AddState("B", stateB); 51 | 52 | Assert.AreEqual( 53 | StateMachinePath.Root, 54 | StateMachineWalker.GetPathOfState(fsm) 55 | ); 56 | 57 | Assert.AreEqual( 58 | StateMachinePath.Root.Join("A"), 59 | StateMachineWalker.GetPathOfState(nested1) 60 | ); 61 | 62 | Assert.AreEqual( 63 | StateMachinePath.Root.Join("B"), 64 | StateMachineWalker.GetPathOfState(stateB) 65 | ); 66 | 67 | Assert.AreEqual( 68 | StateMachinePath.Root.Join("A").Join(5), 69 | StateMachineWalker.GetPathOfState(nested2) 70 | ); 71 | 72 | Assert.AreEqual( 73 | StateMachinePath.Root.Join("A").Join(5).Join(true), 74 | StateMachineWalker.GetPathOfState(stateTrue) 75 | ); 76 | } 77 | 78 | [Test] 79 | public void Test_GetStringPathOfState() 80 | { 81 | var nested2 = new StateMachine(); 82 | var stateTrue = new State(); 83 | var stateFalse = new State(); 84 | nested2.AddState(true, stateTrue); 85 | nested2.AddState(false, stateFalse); 86 | 87 | var nested1 = new StateMachine(); 88 | nested1.AddState(5, nested2); 89 | 90 | var fsm = new StateMachine(); 91 | fsm.AddState("A", nested1); 92 | var stateB = new State(); 93 | fsm.AddState("B", stateB); 94 | 95 | Assert.AreEqual( 96 | StateMachineWalker.GetPathOfState(fsm).ToString(), 97 | StateMachineWalker.GetStringPathOfState(fsm) 98 | ); 99 | 100 | Assert.AreEqual( 101 | StateMachineWalker.GetPathOfState(nested1).ToString(), 102 | StateMachineWalker.GetStringPathOfState(nested1) 103 | ); 104 | 105 | Assert.AreEqual( 106 | StateMachineWalker.GetPathOfState(stateB).ToString(), 107 | StateMachineWalker.GetStringPathOfState(stateB) 108 | ); 109 | 110 | Assert.AreEqual( 111 | StateMachineWalker.GetPathOfState(nested2).ToString(), 112 | StateMachineWalker.GetStringPathOfState(nested2) 113 | ); 114 | 115 | Assert.AreEqual( 116 | StateMachineWalker.GetPathOfState(stateTrue).ToString(), 117 | StateMachineWalker.GetStringPathOfState(stateTrue) 118 | ); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/TestStateMachineWalker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8042ecada7c14c7b88b4b39335a6352e 3 | timeCreated: 1742991324 -------------------------------------------------------------------------------- /Tests/TestTransitionAfter.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace UnityHFSM.Tests 5 | { 6 | public class TestTransitionAfter 7 | { 8 | private (TransitionAfter transition, TestTimer timer) 9 | CreateTransitionAfterWithDelay(float delay, Func, bool> condition = null) 10 | { 11 | TransitionAfter transition = new TransitionAfter("A", "B", delay: delay, condition: condition); 12 | transition.OnEnter(); 13 | TestTimer timer = new TestTimer(); 14 | transition.timer = timer; 15 | return (transition, timer); 16 | } 17 | 18 | [Test] 19 | public void Test_ShouldTransition_is_false_when_not_elapsed() 20 | { 21 | var (transition, timer) = CreateTransitionAfterWithDelay(2); 22 | timer.Elapsed = 0; 23 | Assert.IsFalse(transition.ShouldTransition()); 24 | } 25 | 26 | [Test] 27 | public void Test_ShouldTransition_is_true_when_elapsed() 28 | { 29 | var (transition, timer) = CreateTransitionAfterWithDelay(1); 30 | timer.Elapsed = 3; 31 | Assert.IsTrue(transition.ShouldTransition()); 32 | } 33 | 34 | [Test] 35 | public void Test_ShouldTransition_is_false_when_elapsed_but_condition_is_false() 36 | { 37 | var (transition, timer) = CreateTransitionAfterWithDelay(1, t => false); 38 | timer.Elapsed = 3; 39 | Assert.IsFalse(transition.ShouldTransition()); 40 | } 41 | 42 | [Test] 43 | public void Test_ShouldTransition_is_true_when_elapsed_and_condition_is_true() 44 | { 45 | var (transition, timer) = CreateTransitionAfterWithDelay(1, t => true); 46 | timer.Elapsed = 3; 47 | Assert.IsTrue(transition.ShouldTransition()); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/TestTransitionAfter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 648a6e40bb26f6a4aa6cd66bf363da67 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestTransitionAfterDynamic.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace UnityHFSM.Tests 5 | { 6 | public class TestTransitionAfterDynamic 7 | { 8 | private StateMachine fsm; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | fsm = new StateMachine(); 14 | fsm.AddState("A"); 15 | fsm.AddState("B"); 16 | fsm.SetStartState("A"); 17 | } 18 | 19 | private TestTimer CreateTransitionWithDelay( 20 | Func, float> delay, 21 | Func, bool> condition = null, 22 | bool onlyEvaluateDelayOnEnter = false) 23 | { 24 | var transition = new TransitionAfterDynamic( 25 | "A", 26 | "B", 27 | delay: delay, 28 | condition: condition, 29 | onlyEvaluateDelayOnEnter: onlyEvaluateDelayOnEnter 30 | ); 31 | 32 | var timer = new TestTimer(); 33 | transition.timer = timer; 34 | 35 | fsm.AddTransition(transition); 36 | 37 | return timer; 38 | } 39 | 40 | /// 41 | /// Creates a delay function that uses the passed delays (delays parameter). 42 | /// On the first call it uses the first delay, on the second call the second delay, and so forth. 43 | /// If no delays remain, it uses the last delay. 44 | /// 45 | private Func, float> CreateDelays(params float[] delays) 46 | { 47 | int index = 0; 48 | 49 | return t => { 50 | if (index >= delays.Length) 51 | return delays[delays.Length - 1]; 52 | 53 | return delays[index++]; 54 | }; 55 | } 56 | 57 | [Test] 58 | public void Test_internal_test_util_CreateDelays_works() 59 | { 60 | var delays = CreateDelays(1, 2, 3, 4); 61 | 62 | Assert.AreEqual(1, delays(null)); 63 | Assert.AreEqual(2, delays(null)); 64 | Assert.AreEqual(3, delays(null)); 65 | Assert.AreEqual(4, delays(null)); 66 | Assert.AreEqual(4, delays(null)); 67 | } 68 | 69 | [Test] 70 | public void Test_uses_first_delay_when_onlyEvaluateDelayOnEnter_is_true() 71 | { 72 | var timer = CreateTransitionWithDelay(CreateDelays(1, 100), onlyEvaluateDelayOnEnter: true); 73 | 74 | fsm.Init(); 75 | timer.Elapsed = 0; 76 | 77 | fsm.OnLogic(); 78 | fsm.OnLogic(); 79 | 80 | Assert.AreEqual("A", fsm.ActiveStateName); 81 | 82 | timer.Elapsed = 2; 83 | fsm.OnLogic(); 84 | 85 | Assert.AreEqual("B", fsm.ActiveStateName); 86 | } 87 | 88 | [Test] 89 | public void Test_uses_later_delays_when_onlyEvaluateDelayOnEnter_is_false() 90 | { 91 | var timer = CreateTransitionWithDelay(CreateDelays(100, 100, 100, 1), onlyEvaluateDelayOnEnter: false); 92 | 93 | fsm.Init(); 94 | timer.Elapsed = 2; 95 | 96 | fsm.OnLogic(); // delay = 100 97 | fsm.OnLogic(); // delay = 100 98 | fsm.OnLogic(); // delay = 100 99 | 100 | Assert.AreEqual("A", fsm.ActiveStateName); 101 | 102 | fsm.OnLogic(); // delay = 1 < timer.Elapsed 103 | 104 | Assert.AreEqual("B", fsm.ActiveStateName); 105 | } 106 | 107 | [Test] 108 | public void Test_respects_condition() 109 | { 110 | var shouldTransition = false; 111 | 112 | var timer = CreateTransitionWithDelay( 113 | CreateDelays(100, 1), 114 | condition: t => shouldTransition, 115 | onlyEvaluateDelayOnEnter: false); 116 | 117 | fsm.Init(); 118 | timer.Elapsed = 2; 119 | 120 | fsm.OnLogic(); // delay = 100 121 | fsm.OnLogic(); // delay = 1 122 | Assert.AreEqual("A", fsm.ActiveStateName); 123 | 124 | shouldTransition = true; 125 | fsm.OnLogic(); 126 | Assert.AreEqual("B", fsm.ActiveStateName); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/TestTransitionAfterDynamic.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6d3004d609b387541a9ff45b23fbb84e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestTransitionCallbacks.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestTransitionCallbacks 6 | { 7 | private Recorder recorder; 8 | private StateMachine fsm; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | recorder = new Recorder(); 14 | fsm = new StateMachine(); 15 | } 16 | 17 | [Test] 18 | public void Test_transition_callbacks_work_in_flat_fsm_with_instant_transition() 19 | { 20 | fsm.AddState("A", recorder.TrackedState); 21 | fsm.AddState("B", recorder.TrackedState); 22 | 23 | fsm.AddTransition("A", "B", 24 | onTransition: t => recorder.RecordCustom("CallbackBefore"), 25 | afterTransition: t => recorder.RecordCustom("CallbackAfter") 26 | ); 27 | 28 | fsm.Init(); 29 | recorder.DiscardAll(); 30 | 31 | fsm.OnLogic(); 32 | recorder.Expect 33 | .Custom("CallbackBefore") 34 | .Exit("A") 35 | .Enter("B") 36 | .Custom("CallbackAfter") 37 | .Logic("B") 38 | .All(); 39 | } 40 | 41 | [Test] 42 | public void Test_transition_callbacks_work_in_flat_fsm_with_delayed_transition() 43 | { 44 | fsm.AddState("A", recorder.Track(new State(needsExitTime: true))); 45 | fsm.AddState("B", recorder.TrackedState); 46 | 47 | fsm.AddTransition("A", "B", 48 | onTransition: t => recorder.RecordCustom("CallbackBefore"), 49 | afterTransition: t => recorder.RecordCustom("CallbackAfter") 50 | ); 51 | 52 | fsm.Init(); 53 | recorder.DiscardAll(); 54 | 55 | fsm.OnLogic(); 56 | recorder.Expect.Logic("A").All(); 57 | 58 | fsm.StateCanExit(); 59 | recorder.Expect 60 | .Custom("CallbackBefore") 61 | .Exit("A") 62 | .Enter("B") 63 | .Custom("CallbackAfter") 64 | .All(); 65 | } 66 | 67 | [Test] 68 | public void Test_transition_callbacks_work_in_flat_fsm_with_ghost_state() 69 | { 70 | fsm.AddState("A", recorder.TrackedState); 71 | fsm.AddState("B", recorder.Track(new State(isGhostState: true))); 72 | fsm.AddState("C", recorder.TrackedState); 73 | 74 | fsm.AddTransition("A", "B", 75 | onTransition: t => recorder.RecordCustom("A->B Before"), 76 | afterTransition: t => recorder.RecordCustom("A->B After") 77 | ); 78 | fsm.AddTransition("B", "C", 79 | onTransition: t => recorder.RecordCustom("B->C Before"), 80 | afterTransition: t => recorder.RecordCustom("B->C After") 81 | ); 82 | 83 | fsm.Init(); 84 | recorder.DiscardAll(); 85 | 86 | fsm.OnLogic(); 87 | recorder.Expect 88 | .Custom("A->B Before") 89 | .Exit("A") 90 | .Enter("B") 91 | .Custom("A->B After") 92 | .Custom("B->C Before") 93 | .Exit("B") 94 | .Enter("C") 95 | .Custom("B->C After") 96 | .Logic("C") 97 | .All(); 98 | } 99 | 100 | [Test] 101 | public void Test_transition_callbacks_work_in_nested_fsm_with_exit_transition() 102 | { 103 | var nested = new StateMachine(needsExitTime: true); 104 | nested.AddState("Nested", recorder.TrackedState); 105 | 106 | fsm.AddState("A", recorder.Track(nested)); 107 | fsm.AddState("B", recorder.TrackedState); 108 | fsm.AddTransition("A", "B"); 109 | 110 | nested.AddExitTransition("Nested", 111 | onTransition: t => recorder.RecordCustom("CallbackBefore"), 112 | afterTransition: t => recorder.RecordCustom("CallbackAfter") 113 | ); 114 | 115 | fsm.Init(); 116 | recorder.Expect 117 | .Enter("A") 118 | .Enter("Nested") 119 | .All(); 120 | 121 | fsm.OnLogic(); 122 | recorder.Expect 123 | .Logic("A") 124 | .Custom("CallbackBefore") 125 | .Exit("A") 126 | .Exit("Nested") 127 | .Enter("B") 128 | .Custom("CallbackAfter") 129 | .All(); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/TestTransitionCallbacks.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1640454e6677f0a4b807f31862256b78 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/TestTwoWayTransitions.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | public class TestTwoWayTransitions 6 | { 7 | private Recorder recorder; 8 | private StateMachine fsm; 9 | 10 | [SetUp] 11 | public void Setup() 12 | { 13 | recorder = new Recorder(); 14 | fsm = new StateMachine(); 15 | } 16 | 17 | [Test] 18 | public void Test_two_way_transitions_work_both_ways() 19 | { 20 | bool shouldBeInB = false; 21 | 22 | fsm.AddState("A", recorder.TrackedState); 23 | fsm.AddState("B", recorder.TrackedState); 24 | fsm.AddTwoWayTransition("A", "B", t => shouldBeInB); 25 | 26 | fsm.Init(); 27 | fsm.OnLogic(); 28 | Assert.AreEqual("A", fsm.ActiveStateName); 29 | 30 | shouldBeInB = true; 31 | fsm.OnLogic(); 32 | Assert.AreEqual("B", fsm.ActiveStateName); 33 | 34 | shouldBeInB = false; 35 | fsm.OnLogic(); 36 | Assert.AreEqual("A", fsm.ActiveStateName); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/TestTwoWayTransitions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 505567989d4bb634e867ef153c71d301 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UnityHFSM.Tests", 3 | "references": [ 4 | "UnityEngine.TestRunner", 5 | "UnityEditor.TestRunner", 6 | "UnityHFSM" 7 | ], 8 | "includePlatforms": [ 9 | "Editor" 10 | ], 11 | "excludePlatforms": [], 12 | "allowUnsafeCode": false, 13 | "overrideReferences": true, 14 | "precompiledReferences": [ 15 | "nunit.framework.dll" 16 | ], 17 | "autoReferenced": false, 18 | "defineConstraints": [ 19 | "UNITY_INCLUDE_TESTS" 20 | ], 21 | "versionDefines": [], 22 | "noEngineReferences": false 23 | } -------------------------------------------------------------------------------- /Tests/Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 57120a2a67971934ba35a036d8308cc0 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests/Util.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 30d7efc0acca1304d85e1e57cca0c480 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Util/Recorder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ee881ae68874b51499c9a40e1fb61af8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Util/TestTimer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityHFSM.Tests 4 | { 5 | /// 6 | /// Default timer that calculates the elapsed time based on Time.time. 7 | /// 8 | public class TestTimer : ITimer 9 | { 10 | public float Elapsed { get; set; } 11 | 12 | public void Reset() 13 | { 14 | Elapsed = 0; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/Util/TestTimer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bbd2a8a0dc6ca674f99340543f7182f6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /UnityHFSM.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UnityHFSM" 3 | } 4 | -------------------------------------------------------------------------------- /UnityHFSM.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 810f54f43f696374fb3b49d45b461274 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ece5b9b847e5acb47bfb8390fa82417d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /docs/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/Banner.png -------------------------------------------------------------------------------- /docs/Banner.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d994e1e24a0a65e43b2102dff82deee0 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: -1 36 | aniso: -1 37 | mipBias: -100 38 | wrapU: 1 39 | wrapV: 1 40 | wrapW: 1 41 | nPOTScale: 0 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 1 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 1 54 | spriteTessellationDetail: -1 55 | textureType: 8 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | maxTextureSizeSet: 0 59 | compressionQualitySet: 0 60 | textureFormatSet: 0 61 | ignorePngGamma: 0 62 | applyGammaDecoding: 0 63 | platformSettings: 64 | - serializedVersion: 3 65 | buildTarget: DefaultTexturePlatform 66 | maxTextureSize: 2048 67 | resizeAlgorithm: 0 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | androidETC2FallbackOverride: 0 75 | forceMaximumCompressionQuality_BC6H_BC7: 0 76 | spriteSheet: 77 | serializedVersion: 2 78 | sprites: [] 79 | outline: [] 80 | physicsShape: [] 81 | bones: [] 82 | spriteID: 5e97eb03825dee720800000000000000 83 | internalID: 0 84 | vertices: [] 85 | indices: 86 | edges: [] 87 | weights: [] 88 | secondaryTextures: [] 89 | spritePackingTag: 90 | pSDRemoveMatte: 0 91 | pSDShowRemoveMatteOption: 0 92 | userData: 93 | assetBundleName: 94 | assetBundleVariant: 95 | -------------------------------------------------------------------------------- /docs/Flowcharts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 06ec660a7f986c2468db88e1999ee71f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /docs/Flowcharts/OnLogic.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3f70a87fd9617ce4e8d31bce94f9e9ac 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/Flowcharts/OnLogic.txt: -------------------------------------------------------------------------------- 1 | graph TD 2 | Start([OnLogic]) 3 | GlobalTransitions{Is a global
transition triggered?} 4 | LocalTransitions{Is a direct
transition triggered?} 5 | ActiveState["ActiveState.OnLogic()"] 6 | Transition 7 | NewState["NewState.OnLogic()"] 8 | Return([Return]) 9 | 10 | Start --> GlobalTransitions 11 | GlobalTransitions --> |No| LocalTransitions 12 | GlobalTransitions --> |Yes| Transition 13 | LocalTransitions --> |No| ActiveState 14 | LocalTransitions --> |Yes| Transition 15 | 16 | Transition --> NewState 17 | 18 | ActiveState --> Return 19 | NewState --> Return 20 | -------------------------------------------------------------------------------- /docs/Flowcharts/OnLogic.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 421750d8daf98bc43b1a166cd08d38e9 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/Flowcharts/StateChange.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4fdddb7a7518a6b4293fb2466cdd0119 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/Flowcharts/StateChange.txt: -------------------------------------------------------------------------------- 1 | graph TD 2 | Start([State Transition]) --> NeedsExitTime{"Does activeState
need exit time"} 3 | 4 | NeedsExitTime -->|No| End([Change State]) 5 | 6 | NeedsExitTime -->|Yes| RequestExit["FSM calls
activeState.OnExitRequest()"] 7 | RequestExit --> IsCanExitDefinined{"Is
activeState.canExit
defined"} 8 | IsCanExitDefinined -->|Yes| CanExitCallsStateCanExit{"Does
canExit() return
true"} 9 | IsCanExitDefinined -->|No| Later 10 | 11 | CanExitCallsStateCanExit -->|Yes| End 12 | CanExitCallsStateCanExit -->|No| Later["Later in
activeState.OnLogic()"] 13 | 14 | subgraph "Delayed State Change" 15 | Later --> CallStateCanExit["fsm.StateCanExit()"] 16 | end 17 | CallStateCanExit --> End 18 | -------------------------------------------------------------------------------- /docs/Flowcharts/StateChange.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2b6629234f8e83b46869641e10570112 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/Images.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 63203b2708f510b4aa09bbaec4832345 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /docs/Images/AnimatorAnyState.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/Images/AnimatorAnyState.png -------------------------------------------------------------------------------- /docs/Images/AnimatorAnyState.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9c0ee0cf7095c4542b4f7e584374aabd 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: -1 36 | aniso: -1 37 | mipBias: -100 38 | wrapU: 1 39 | wrapV: 1 40 | wrapW: 1 41 | nPOTScale: 0 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 1 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 1 54 | spriteTessellationDetail: -1 55 | textureType: 8 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | maxTextureSizeSet: 0 59 | compressionQualitySet: 0 60 | textureFormatSet: 0 61 | ignorePngGamma: 0 62 | applyGammaDecoding: 0 63 | platformSettings: 64 | - serializedVersion: 3 65 | buildTarget: DefaultTexturePlatform 66 | maxTextureSize: 2048 67 | resizeAlgorithm: 0 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | androidETC2FallbackOverride: 0 75 | forceMaximumCompressionQuality_BC6H_BC7: 0 76 | spriteSheet: 77 | serializedVersion: 2 78 | sprites: [] 79 | outline: [] 80 | physicsShape: [] 81 | bones: [] 82 | spriteID: 5e97eb03825dee720800000000000000 83 | internalID: 0 84 | vertices: [] 85 | indices: 86 | edges: [] 87 | weights: [] 88 | secondaryTextures: [] 89 | spritePackingTag: 90 | pSDRemoveMatte: 0 91 | pSDShowRemoveMatteOption: 0 92 | userData: 93 | assetBundleName: 94 | assetBundleVariant: 95 | -------------------------------------------------------------------------------- /docs/Images/AnimatorGraphExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/Images/AnimatorGraphExample.png -------------------------------------------------------------------------------- /docs/Images/AnimatorGraphExample.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a7fd682b545cbd74782578e1c1b55071 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 13 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | flipGreenChannel: 0 24 | isReadable: 0 25 | streamingMipmaps: 0 26 | streamingMipmapsPriority: 0 27 | vTOnly: 0 28 | ignoreMipmapLimit: 0 29 | grayScaleToAlpha: 0 30 | generateCubemap: 6 31 | cubemapConvolution: 0 32 | seamlessCubemap: 0 33 | textureFormat: 1 34 | maxTextureSize: 2048 35 | textureSettings: 36 | serializedVersion: 2 37 | filterMode: 1 38 | aniso: 1 39 | mipBias: 0 40 | wrapU: 1 41 | wrapV: 1 42 | wrapW: 1 43 | nPOTScale: 0 44 | lightmap: 0 45 | compressionQuality: 50 46 | spriteMode: 1 47 | spriteExtrude: 1 48 | spriteMeshType: 1 49 | alignment: 0 50 | spritePivot: {x: 0.5, y: 0.5} 51 | spritePixelsToUnits: 100 52 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 53 | spriteGenerateFallbackPhysicsShape: 1 54 | alphaUsage: 1 55 | alphaIsTransparency: 1 56 | spriteTessellationDetail: -1 57 | textureType: 8 58 | textureShape: 1 59 | singleChannelComponent: 0 60 | flipbookRows: 1 61 | flipbookColumns: 1 62 | maxTextureSizeSet: 0 63 | compressionQualitySet: 0 64 | textureFormatSet: 0 65 | ignorePngGamma: 0 66 | applyGammaDecoding: 0 67 | swizzle: 50462976 68 | cookieLightType: 0 69 | platformSettings: 70 | - serializedVersion: 3 71 | buildTarget: DefaultTexturePlatform 72 | maxTextureSize: 2048 73 | resizeAlgorithm: 0 74 | textureFormat: -1 75 | textureCompression: 1 76 | compressionQuality: 50 77 | crunchedCompression: 0 78 | allowsAlphaSplitting: 0 79 | overridden: 0 80 | ignorePlatformSupport: 0 81 | androidETC2FallbackOverride: 0 82 | forceMaximumCompressionQuality_BC6H_BC7: 0 83 | - serializedVersion: 3 84 | buildTarget: Standalone 85 | maxTextureSize: 2048 86 | resizeAlgorithm: 0 87 | textureFormat: -1 88 | textureCompression: 1 89 | compressionQuality: 50 90 | crunchedCompression: 0 91 | allowsAlphaSplitting: 0 92 | overridden: 0 93 | ignorePlatformSupport: 0 94 | androidETC2FallbackOverride: 0 95 | forceMaximumCompressionQuality_BC6H_BC7: 0 96 | spriteSheet: 97 | serializedVersion: 2 98 | sprites: [] 99 | outline: [] 100 | physicsShape: [] 101 | bones: [] 102 | spriteID: 5e97eb03825dee720800000000000000 103 | internalID: 0 104 | vertices: [] 105 | indices: 106 | edges: [] 107 | weights: [] 108 | secondaryTextures: [] 109 | nameFileIdTable: {} 110 | mipmapLimitGroupName: 111 | pSDRemoveMatte: 0 112 | userData: 113 | assetBundleName: 114 | assetBundleVariant: 115 | -------------------------------------------------------------------------------- /docs/Images/AnimatorGraphVideo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/Images/AnimatorGraphVideo.gif -------------------------------------------------------------------------------- /docs/Images/AnimatorGraphVideo.gif.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 44753eeace4ca7542b7d6d3838396cbf 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 13 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | flipGreenChannel: 0 24 | isReadable: 0 25 | streamingMipmaps: 0 26 | streamingMipmapsPriority: 0 27 | vTOnly: 0 28 | ignoreMipmapLimit: 0 29 | grayScaleToAlpha: 0 30 | generateCubemap: 6 31 | cubemapConvolution: 0 32 | seamlessCubemap: 0 33 | textureFormat: 1 34 | maxTextureSize: 2048 35 | textureSettings: 36 | serializedVersion: 2 37 | filterMode: 1 38 | aniso: 1 39 | mipBias: 0 40 | wrapU: 1 41 | wrapV: 1 42 | wrapW: 1 43 | nPOTScale: 0 44 | lightmap: 0 45 | compressionQuality: 50 46 | spriteMode: 1 47 | spriteExtrude: 1 48 | spriteMeshType: 1 49 | alignment: 0 50 | spritePivot: {x: 0.5, y: 0.5} 51 | spritePixelsToUnits: 100 52 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 53 | spriteGenerateFallbackPhysicsShape: 1 54 | alphaUsage: 1 55 | alphaIsTransparency: 1 56 | spriteTessellationDetail: -1 57 | textureType: 8 58 | textureShape: 1 59 | singleChannelComponent: 0 60 | flipbookRows: 1 61 | flipbookColumns: 1 62 | maxTextureSizeSet: 0 63 | compressionQualitySet: 0 64 | textureFormatSet: 0 65 | ignorePngGamma: 0 66 | applyGammaDecoding: 0 67 | swizzle: 50462976 68 | cookieLightType: 0 69 | platformSettings: 70 | - serializedVersion: 3 71 | buildTarget: DefaultTexturePlatform 72 | maxTextureSize: 2048 73 | resizeAlgorithm: 0 74 | textureFormat: -1 75 | textureCompression: 1 76 | compressionQuality: 50 77 | crunchedCompression: 0 78 | allowsAlphaSplitting: 0 79 | overridden: 0 80 | ignorePlatformSupport: 0 81 | androidETC2FallbackOverride: 0 82 | forceMaximumCompressionQuality_BC6H_BC7: 0 83 | - serializedVersion: 3 84 | buildTarget: Standalone 85 | maxTextureSize: 2048 86 | resizeAlgorithm: 0 87 | textureFormat: -1 88 | textureCompression: 1 89 | compressionQuality: 50 90 | crunchedCompression: 0 91 | allowsAlphaSplitting: 0 92 | overridden: 0 93 | ignorePlatformSupport: 0 94 | androidETC2FallbackOverride: 0 95 | forceMaximumCompressionQuality_BC6H_BC7: 0 96 | spriteSheet: 97 | serializedVersion: 2 98 | sprites: [] 99 | outline: [] 100 | physicsShape: [] 101 | bones: [] 102 | spriteID: 5e97eb03825dee720800000000000000 103 | internalID: 0 104 | vertices: [] 105 | indices: 106 | edges: [] 107 | weights: [] 108 | secondaryTextures: [] 109 | nameFileIdTable: {} 110 | mipmapLimitGroupName: 111 | pSDRemoveMatte: 0 112 | userData: 113 | assetBundleName: 114 | assetBundleVariant: 115 | -------------------------------------------------------------------------------- /docs/Inheritance.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 962e0ccb7d8c7ca4fae167bfebe5d7a0 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /docs/Inheritance/StateInheritance.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2eed291bd8eb7574e81681a8208dbd6e 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/Inheritance/StateInheritance.txt: -------------------------------------------------------------------------------- 1 | classDiagram 2 | StateBase <|-- StringStateBase 3 | 4 | StateBase <|-- ActionState 5 | ActionState <|-- StringActionState 6 | 7 | ActionState <|-- State 8 | State <|-- StringState 9 | 10 | ActionState <|-- CoState 11 | CoState <|-- StringCoState 12 | 13 | StateBase <|-- StateMachine 14 | StateMachine <|-- StringStateMachine 15 | StateMachine <|-- HybridStateMachine 16 | HybridStateMachine <|-- StringHybridStateMachine 17 | 18 | 19 | %% TODO: Add interfaces (IStateMachine, ITriggerable) -------------------------------------------------------------------------------- /docs/Inheritance/StateInheritance.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 12614306641686e479e70188dc6013ee 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/Inheritance/TransitionInheritance.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5b3142909b7418247a613db2fcb7c737 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/Inheritance/TransitionInheritance.txt: -------------------------------------------------------------------------------- 1 | classDiagram 2 | class TransitionBase 3 | TransitionBase <|-- StringTransitionBase 4 | 5 | TransitionBase <|-- Transition 6 | Transition <|-- StringTransition 7 | 8 | TransitionBase <|-- TransitionAfter 9 | TransitionAfter <|-- StringTransitionAfter 10 | 11 | TransitionBase <|-- TransitionAfterDynamic 12 | TransitionAfterDynamic <|-- StringTransitionDynamic 13 | 14 | TransitionBase <|-- TransitionOnKey 15 | TransitionOnKey <|-- StringTransitionOnKey 16 | -------------------------------------------------------------------------------- /docs/Inheritance/TransitionInheritance.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 17e0bb15e42368d4db6b6f40688c5c6b 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/StateDiagrams.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b1ba9f754babd2f4b8a9fc05481c961f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b39781f7b4ef074d8905d2bc037236c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Hierarchical.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/StateDiagrams/EnemySpyExample/Hierarchical.afdesign -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Hierarchical.afdesign.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b0af72f8945fcb649bb19423924296ca 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Hierarchical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/StateDiagrams/EnemySpyExample/Hierarchical.png -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Hierarchical.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d2890846664b0834184f75f6c5619ca4 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: 1 36 | aniso: 1 37 | mipBias: 0 38 | wrapU: 1 39 | wrapV: 1 40 | wrapW: 1 41 | nPOTScale: 0 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 1 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 1 54 | spriteTessellationDetail: -1 55 | textureType: 8 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | flipbookRows: 1 59 | flipbookColumns: 1 60 | maxTextureSizeSet: 0 61 | compressionQualitySet: 0 62 | textureFormatSet: 0 63 | ignorePngGamma: 0 64 | applyGammaDecoding: 0 65 | platformSettings: 66 | - serializedVersion: 3 67 | buildTarget: DefaultTexturePlatform 68 | maxTextureSize: 2048 69 | resizeAlgorithm: 0 70 | textureFormat: -1 71 | textureCompression: 1 72 | compressionQuality: 50 73 | crunchedCompression: 0 74 | allowsAlphaSplitting: 0 75 | overridden: 0 76 | androidETC2FallbackOverride: 0 77 | forceMaximumCompressionQuality_BC6H_BC7: 0 78 | spriteSheet: 79 | serializedVersion: 2 80 | sprites: [] 81 | outline: [] 82 | physicsShape: [] 83 | bones: [] 84 | spriteID: 5e97eb03825dee720800000000000000 85 | internalID: 0 86 | vertices: [] 87 | indices: 88 | edges: [] 89 | weights: [] 90 | secondaryTextures: [] 91 | spritePackingTag: 92 | pSDRemoveMatte: 0 93 | pSDShowRemoveMatteOption: 0 94 | userData: 95 | assetBundleName: 96 | assetBundleVariant: 97 | -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/HierarchicalWithExitTransition.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/StateDiagrams/EnemySpyExample/HierarchicalWithExitTransition.afdesign -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/HierarchicalWithExitTransition.afdesign.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ebd97751728406745b850f7d208cb07d 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/HierarchicalWithExitTransition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/StateDiagrams/EnemySpyExample/HierarchicalWithExitTransition.png -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/HierarchicalWithExitTransition.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1b871d926646db244b9e0169c1178236 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: -1 36 | aniso: -1 37 | mipBias: -100 38 | wrapU: 1 39 | wrapV: 1 40 | wrapW: 1 41 | nPOTScale: 0 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 1 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 1 54 | spriteTessellationDetail: -1 55 | textureType: 8 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | maxTextureSizeSet: 0 59 | compressionQualitySet: 0 60 | textureFormatSet: 0 61 | ignorePngGamma: 0 62 | applyGammaDecoding: 0 63 | platformSettings: 64 | - serializedVersion: 3 65 | buildTarget: DefaultTexturePlatform 66 | maxTextureSize: 2048 67 | resizeAlgorithm: 0 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | androidETC2FallbackOverride: 0 75 | forceMaximumCompressionQuality_BC6H_BC7: 0 76 | spriteSheet: 77 | serializedVersion: 2 78 | sprites: [] 79 | outline: [] 80 | physicsShape: [] 81 | bones: [] 82 | spriteID: 5e97eb03825dee720800000000000000 83 | internalID: 0 84 | vertices: [] 85 | indices: 86 | edges: [] 87 | weights: [] 88 | secondaryTextures: [] 89 | spritePackingTag: 90 | pSDRemoveMatte: 0 91 | pSDShowRemoveMatteOption: 0 92 | userData: 93 | assetBundleName: 94 | assetBundleVariant: 95 | -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Simple.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/StateDiagrams/EnemySpyExample/Simple.afdesign -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Simple.afdesign.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dcf7c7995c3419e46965f58facc5250e 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inspiaaa/UnityHFSM/b35a8ab302a4d837429818dc29a4ddb522efac7f/docs/StateDiagrams/EnemySpyExample/Simple.png -------------------------------------------------------------------------------- /docs/StateDiagrams/EnemySpyExample/Simple.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 817603fc4db787547a469fd806baa03f 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: 1 36 | aniso: 1 37 | mipBias: 0 38 | wrapU: 1 39 | wrapV: 1 40 | wrapW: 1 41 | nPOTScale: 0 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 1 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 1 54 | spriteTessellationDetail: -1 55 | textureType: 8 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | flipbookRows: 1 59 | flipbookColumns: 1 60 | maxTextureSizeSet: 0 61 | compressionQualitySet: 0 62 | textureFormatSet: 0 63 | ignorePngGamma: 0 64 | applyGammaDecoding: 0 65 | platformSettings: 66 | - serializedVersion: 3 67 | buildTarget: DefaultTexturePlatform 68 | maxTextureSize: 2048 69 | resizeAlgorithm: 0 70 | textureFormat: -1 71 | textureCompression: 1 72 | compressionQuality: 50 73 | crunchedCompression: 0 74 | allowsAlphaSplitting: 0 75 | overridden: 0 76 | androidETC2FallbackOverride: 0 77 | forceMaximumCompressionQuality_BC6H_BC7: 0 78 | spriteSheet: 79 | serializedVersion: 2 80 | sprites: [] 81 | outline: [] 82 | physicsShape: [] 83 | bones: [] 84 | spriteID: 5e97eb03825dee720800000000000000 85 | internalID: 0 86 | vertices: [] 87 | indices: 88 | edges: [] 89 | weights: [] 90 | secondaryTextures: [] 91 | spritePackingTag: 92 | pSDRemoveMatte: 0 93 | pSDShowRemoveMatteOption: 0 94 | userData: 95 | assetBundleName: 96 | assetBundleVariant: 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.inspiaaa.unityhfsm", 3 | "displayName": "UnityHFSM", 4 | "description": "A simple yet powerful class-based hierarchical finite state machine for Unity3D", 5 | 6 | "version": "2.2.0", 7 | "license": "MIT", 8 | "keywords": ["fsm", "hierarchical", "state-machine"], 9 | "documentationUrl": "https://github.com/Inspiaaa/UnityHFSM", 10 | 11 | "author": { 12 | "name": "Inspiaaa", 13 | "url": "https://github.com/Inspiaaa" 14 | }, 15 | 16 | "samples": [ 17 | { 18 | "displayName": "Guard AI", 19 | "path": "Samples~/GuardAI" 20 | }, 21 | { 22 | "displayName": "Sample3d", 23 | "path": "Samples~/Sample3d" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5247a8bfa21cdf64d8031f6fb5c77ba6 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /src.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cb2c36010092b44a8f7855ab974c052 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/Base.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5f607a429ca267343908aefef3df9f88 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/Base/IActionable.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace UnityHFSM 3 | { 4 | /// 5 | /// Interface for states that support custom actions. Actions are like the 6 | /// builtin events OnEnter / OnLogic / ... but are defined by the user. 7 | /// 8 | public interface IActionable 9 | { 10 | void OnAction(TEvent trigger); 11 | void OnAction(TEvent trigger, TData data); 12 | } 13 | 14 | /// 15 | public interface IActionable : IActionable 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Base/IActionable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aa10ba2235ccb6d4897c64b62d94c7da 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Base/IStateMachine.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace UnityHFSM 3 | { 4 | /// 5 | /// An abstraction layer that provides a subset of features that every parent 6 | /// state machine has to provide in order to implement the timing mechanics of 7 | /// transitions. In addition to the methods provided by , 8 | /// this interface also provides access to the current and pending states, which 9 | /// can be useful for transitions. 10 | /// 11 | public interface IStateMachine : IStateTimingManager 12 | { 13 | /// 14 | /// The target state of a pending (delayed) transition. Returns null if no 15 | /// transition is pending or when an exit transition is pending. 16 | /// 17 | StateBase PendingState { get; } 18 | 19 | /// 20 | TStateId PendingStateName { get; } 21 | 22 | /// 23 | /// The currently active state of the state machine. 24 | /// 25 | /// 26 | /// Note that when a state is "active", the "ActiveState" may not return a reference to this state. 27 | /// Depending on the classes used, it may for example return a reference to a wrapper state. 28 | /// 29 | StateBase ActiveState { get; } 30 | 31 | /// 32 | TStateId ActiveStateName { get; } 33 | 34 | StateBase GetState(TStateId name); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Base/IStateMachine.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6e47a57cc1abf904ea3e7b8dcd06075a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Base/IStateTimingManager.cs: -------------------------------------------------------------------------------- 1 | namespace UnityHFSM 2 | { 3 | /// 4 | /// An abstraction layer that provides a subset of features that every parent 5 | /// state machine has to provide in order to implement the timing mechanics of 6 | /// transitions. 7 | /// It is useful, as it allows the parent state machine to be interchangeable and independent 8 | /// of the implementation details of the child states. 9 | /// 10 | /// 11 | /// In particular, this means that child states do not have to provide the full list 12 | /// of generic type parameters required by their parent state machine. 13 | /// Otherwise, hierarchical state machines with different types for each level would 14 | /// be impossible to implement. 15 | /// 16 | public interface IStateTimingManager 17 | { 18 | /// 19 | void StateCanExit(); 20 | 21 | bool HasPendingTransition { get; } 22 | 23 | IStateTimingManager ParentFsm { get; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Base/IStateTimingManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 737773c512604d96877b3552c8c3c52a 3 | timeCreated: 1742661866 -------------------------------------------------------------------------------- /src/Base/ITransitionListener.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace UnityHFSM 3 | { 4 | public interface ITransitionListener 5 | { 6 | void BeforeTransition(); 7 | void AfterTransition(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Base/ITransitionListener.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0b83cc0bf23d0140ae052dedafb7358 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Base/ITriggerable.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace UnityHFSM 3 | { 4 | /// 5 | /// Interface for states that can receive events (triggers), such as StateMachines. 6 | /// 7 | /// 8 | public interface ITriggerable 9 | { 10 | /// 11 | /// Called when a trigger is activated. 12 | /// 13 | /// The name / identifier of the trigger 14 | void Trigger(TEvent trigger); 15 | } 16 | 17 | /// 18 | public interface ITriggerable : ITriggerable 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Base/ITriggerable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 140bcf6aa2fc61e43a8ddd1eb69daabe 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Base/StateBase.cs: -------------------------------------------------------------------------------- 1 | 2 | using UnityHFSM.Inspection; 3 | 4 | namespace UnityHFSM 5 | { 6 | /// 7 | /// The base class of all states. 8 | /// 9 | public class StateBase : IVisitableState 10 | { 11 | public readonly bool needsExitTime; 12 | public readonly bool isGhostState; 13 | public TStateId name; 14 | 15 | public IStateTimingManager fsm; 16 | 17 | /// 18 | /// Initialises a new instance of the StateBase class. 19 | /// 20 | /// Determines if the state is allowed to instantly 21 | /// exit on a transition (false), or if the state machine should wait until 22 | /// the state is ready for a state change (true). 23 | /// If true, this state becomes a ghost state, a 24 | /// state the state machine does not want to stay in. That means that if the 25 | /// fsm transitions to this state, it will test all outgoing transitions instantly 26 | /// and not wait until the next OnLogic call. 27 | public StateBase(bool needsExitTime, bool isGhostState = false) 28 | { 29 | this.needsExitTime = needsExitTime; 30 | this.isGhostState = isGhostState; 31 | } 32 | 33 | /// 34 | /// Called to initialise the state, after values like name and fsm have been set. 35 | /// 36 | public virtual void Init() 37 | { 38 | 39 | } 40 | 41 | /// 42 | /// Called when the state machine transitions to this state (enters this state). 43 | /// 44 | public virtual void OnEnter() 45 | { 46 | 47 | } 48 | 49 | /// 50 | /// Called while this state is active. 51 | /// 52 | public virtual void OnLogic() 53 | { 54 | 55 | } 56 | 57 | /// 58 | /// Called when the state machine transitions from this state to another state (exits this state). 59 | /// 60 | public virtual void OnExit() 61 | { 62 | 63 | } 64 | 65 | /// 66 | /// (Only if needsExitTime is true): 67 | /// Called when a state transition from this state to another state should happen. 68 | /// If it can exit, it should call fsm.StateCanExit() 69 | /// and if it can not exit right now, it should call fsm.StateCanExit() later 70 | /// in e.g. OnLogic(). 71 | /// 72 | public virtual void OnExitRequest() 73 | { 74 | 75 | } 76 | 77 | /// 78 | /// Returns a string representation of all active states in the hierarchy, 79 | /// e.g. "/Move/Jump/Falling". 80 | /// In contrast, the state machine's ActiveStateName property only returns the name 81 | /// of its active state, not of any nested states. 82 | /// 83 | public virtual string GetActiveHierarchyPath() 84 | { 85 | return name.ToString(); 86 | } 87 | 88 | /// 89 | /// Accepts a visitor that can perform operations on the current state. 90 | /// This method is part of the Visitor Pattern, which allows adding new behavior 91 | /// to state machine states without modifying their class structure. It is used to 92 | /// implement dynamic inspection tools for hierarchical state machines. 93 | /// 94 | public virtual void AcceptVisitor(IStateVisitor visitor) 95 | { 96 | visitor.VisitRegularState(this); 97 | } 98 | } 99 | 100 | /// 101 | public class StateBase : StateBase 102 | { 103 | /// 104 | public StateBase(bool needsExitTime, bool isGhostState = false) 105 | : base(needsExitTime, isGhostState) 106 | { 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Base/StateBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 88efc1f501df4ca4b8e17964553e15f1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Base/TransitionBase.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace UnityHFSM 3 | { 4 | /// 5 | /// The base class of all transitions. 6 | /// 7 | public class TransitionBase : ITransitionListener 8 | { 9 | public readonly TStateId from; 10 | public readonly TStateId to; 11 | 12 | public readonly bool forceInstantly; 13 | public bool isExitTransition; 14 | 15 | public IStateMachine fsm; 16 | 17 | /// 18 | /// Initialises a new instance of the TransitionBase class. 19 | /// 20 | /// The name / identifier of the active state. 21 | /// The name / identifier of the next state. 22 | /// Ignores the needsExitTime of the active state 23 | /// if forceInstantly is true. => Forces an instant transition 24 | public TransitionBase(TStateId from, TStateId to, bool forceInstantly = false) 25 | { 26 | this.from = from; 27 | this.to = to; 28 | this.forceInstantly = forceInstantly; 29 | this.isExitTransition = false; 30 | } 31 | 32 | /// 33 | /// Called to initialise the transition, after values like fsm have been set. 34 | /// 35 | public virtual void Init() 36 | { 37 | 38 | } 39 | 40 | /// 41 | /// Called when the state machine enters the from state. 42 | /// 43 | public virtual void OnEnter() 44 | { 45 | 46 | } 47 | 48 | /// 49 | /// Called to determine whether the state machine should transition to the to state. 50 | /// 51 | /// True if the state machine should change states / transition. 52 | public virtual bool ShouldTransition() 53 | { 54 | return true; 55 | } 56 | 57 | /// 58 | /// Callback method that is called just before the transition happens. 59 | /// 60 | public virtual void BeforeTransition() 61 | { 62 | 63 | } 64 | 65 | /// 66 | /// Callback method that is called just after the transition happens. 67 | /// 68 | public virtual void AfterTransition() 69 | { 70 | 71 | } 72 | } 73 | 74 | /// 75 | public class TransitionBase : TransitionBase 76 | { 77 | /// 78 | public TransitionBase(string @from, string to, bool forceInstantly = false) : base(@from, to, forceInstantly) 79 | { 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Base/TransitionBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 46bbce5834b962b4ebea07f026a2f59b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Exceptions.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 43413f11ac3727840af03bc91b638956 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/Exceptions/Common.cs: -------------------------------------------------------------------------------- 1 | 2 | using UnityHFSM.Inspection; 3 | 4 | namespace UnityHFSM.Exceptions 5 | { 6 | public static class Common 7 | { 8 | public static StateMachineException NotInitialized( 9 | StateBase fsm, 10 | string context = null, 11 | string problem = null, 12 | string solution = null) 13 | { 14 | return CreateStateMachineException( 15 | fsm, 16 | context, 17 | problem ?? "The active state is null because the state machine has not been set up yet.", 18 | solution ?? ("Call fsm.SetStartState(...) and fsm.Init() or fsm.OnEnter() " 19 | + "to initialize the state machine.") 20 | ); 21 | } 22 | 23 | public static StateMachineException StateNotFound( 24 | StateBase fsm, 25 | string stateName, 26 | string context = null, 27 | string problem = null, 28 | string solution = null) 29 | { 30 | return CreateStateMachineException( 31 | fsm, 32 | context, 33 | problem ?? $"The state \"{stateName}\" has not been defined yet / doesn't exist.", 34 | solution ?? ("\n" 35 | + "1. Check that there are no typos in the state names and transition from and to names\n" 36 | + "2. Add this state before calling Init / OnEnter / OnLogic / RequestStateChange / ...") 37 | ); 38 | } 39 | 40 | public static StateMachineException MissingStartState( 41 | StateBase fsm, 42 | string context = null, 43 | string problem = null, 44 | string solution = null) 45 | { 46 | return CreateStateMachineException( 47 | fsm, 48 | context, 49 | problem ?? ("No start state is selected. " 50 | + "The state machine needs at least one state to function properly."), 51 | solution ?? ("Make sure that there is at least one state in the state machine " 52 | + "before running Init() or OnEnter() by calling fsm.AddState(...).") 53 | ); 54 | } 55 | 56 | public static StateMachineException QuickIndexerMisusedForGettingState( 57 | StateBase fsm, 58 | string stateName) 59 | { 60 | return CreateStateMachineException( 61 | fsm, 62 | context: "Getting a nested state machine with the indexer", 63 | problem: "The selected state is not a state machine.", 64 | solution: ("This method is only there for quickly accessing a nested state machine. " 65 | + $"To get the selected state, use GetState(\"{stateName}\")") 66 | ); 67 | } 68 | 69 | private static StateMachineException CreateStateMachineException( 70 | StateBase fsm, 71 | string context = null, 72 | string problem = null, 73 | string solution = null) 74 | { 75 | string path = StateMachineWalker.GetStringPathOfState(fsm); 76 | 77 | return new StateMachineException(ExceptionFormatter.Format( 78 | location: $"state machine '{path}'", 79 | context: context, 80 | problem: problem, 81 | solution: solution 82 | )); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Exceptions/Common.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 115dbcbacecdf9f4790e75a689313023 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Exceptions/ExceptionFormatter.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace UnityHFSM.Exceptions 3 | { 4 | public static class ExceptionFormatter 5 | { 6 | public static string Format( 7 | string context = null, 8 | string problem = null, 9 | string solution = null) 10 | { 11 | return Format( 12 | location: null, 13 | context: context, 14 | problem: problem, 15 | solution: solution 16 | ); 17 | } 18 | 19 | public static string Format( 20 | string location, 21 | string context, 22 | string problem, 23 | string solution) 24 | { 25 | string message = "\n"; 26 | 27 | if (location != null) 28 | { 29 | message += "In " + location + "\n"; 30 | } 31 | 32 | if (context != null) 33 | { 34 | message += "Context: " + context + "\n"; 35 | } 36 | 37 | if (problem != null) 38 | { 39 | message += "Problem: " + problem + "\n"; 40 | } 41 | 42 | if (solution != null) 43 | { 44 | message += "Solution: " + solution + "\n"; 45 | } 46 | 47 | return message; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Exceptions/ExceptionFormatter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7730424f90798f445aab436e1be38a40 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Exceptions/StateMachineException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM.Exceptions 4 | { 5 | [Serializable] 6 | public class StateMachineException : Exception 7 | { 8 | public StateMachineException(string message) : base(message) { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Exceptions/StateMachineException.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7d3d76876ebd302479eb22881a4551e7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Inspection.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff8864425d1b4eeba7cb477228eb0f49 3 | timeCreated: 1742862182 -------------------------------------------------------------------------------- /src/Inspection/IStateMachineHierarchyVisitor.cs: -------------------------------------------------------------------------------- 1 | namespace UnityHFSM.Inspection 2 | { 3 | /// 4 | /// Interface for objects that recursively traverse the states of a state machine 5 | /// via a . 6 | /// 7 | public interface IStateMachineHierarchyVisitor 8 | { 9 | void VisitStateMachine( 10 | StateMachinePath fsmPath, 11 | StateMachine fsm); 12 | 13 | void VisitRegularState( 14 | StateMachinePath statePath, 15 | StateBase state); 16 | 17 | /// 18 | /// Called after the current state machine and all its child states (and child state machines) have 19 | /// been visited. 20 | /// 21 | void ExitStateMachine( 22 | StateMachinePath fsmPath, 23 | StateMachine fsm); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Inspection/IStateMachineHierarchyVisitor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2902706324464f6196b424fbc07bed43 3 | timeCreated: 1742812387 -------------------------------------------------------------------------------- /src/Inspection/IStateVisitor.cs: -------------------------------------------------------------------------------- 1 | namespace UnityHFSM.Inspection 2 | { 3 | /// 4 | /// Defines the interface for a visitor that can perform operations on different states 5 | /// of the state machine. This is part of the Visitor Pattern, which allows new behavior 6 | /// to be added to existing state classes without modifying their code. It is used to 7 | /// implement dynamic inspection tools for hierarchical state machines. 8 | /// 9 | public interface IStateVisitor 10 | { 11 | void VisitStateMachine(StateMachine fsm); 12 | 13 | void VisitRegularState(StateBase state); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Inspection/IStateVisitor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ed6d5152e8f744aa80f072754e892a68 3 | timeCreated: 1742899826 -------------------------------------------------------------------------------- /src/Inspection/IVisitableState.cs: -------------------------------------------------------------------------------- 1 | using UnityHFSM.Inspection; 2 | 3 | namespace UnityHFSM 4 | { 5 | public interface IVisitableState 6 | { 7 | /// 8 | /// Accepts a visitor that can perform operations on the current state. 9 | /// This method is part of the Visitor Pattern, which allows adding new behavior 10 | /// to state machine states without modifying their class structure. It is used to 11 | /// implement dynamic inspection tools for hierarchical state machines. 12 | /// 13 | void AcceptVisitor(IStateVisitor visitor); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Inspection/IVisitableState.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: de58ef6295ba49cd9578aed2b9417949 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Inspection/StateMachinePath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace UnityHFSM.Inspection 5 | { 6 | /// 7 | /// Light-weight, hashable and equatable type that represents the path to a state within a hierarchical 8 | /// state machine. It supports different state ID types for each level. Each instance represents a node 9 | /// that is linked to the StateMachinePath of its parent node. 10 | /// 11 | /// 12 | /// In contrast to string-based paths, this type does not suffer from accidental naming-collisions 13 | /// when "magic characters" are used in state names. 14 | /// 15 | public abstract class StateMachinePath : IEquatable 16 | { 17 | public static readonly StateMachinePath Root = RootStateMachinePath.instance; 18 | 19 | public readonly StateMachinePath parentPath; 20 | 21 | public bool IsRoot => this is RootStateMachinePath; 22 | 23 | public abstract string LastNodeName { get; } 24 | 25 | protected StateMachinePath(StateMachinePath parentPath) { 26 | this.parentPath = parentPath; 27 | } 28 | 29 | public bool IsChildPathOf(StateMachinePath parent) 30 | { 31 | for (StateMachinePath ancestor = parentPath; ancestor != null; ancestor = ancestor.parentPath) 32 | { 33 | if (ancestor == parent) 34 | { 35 | return true; 36 | } 37 | } 38 | 39 | return false; 40 | } 41 | 42 | public override bool Equals(object obj) 43 | { 44 | return Equals(obj as StateMachinePath); 45 | } 46 | 47 | public abstract override int GetHashCode(); 48 | public abstract bool Equals(StateMachinePath other); 49 | public abstract override string ToString(); 50 | 51 | public static bool operator ==(StateMachinePath left, StateMachinePath right) 52 | { 53 | if (left is null && right is null) 54 | return true; 55 | if (left is null) 56 | return false; 57 | return left.Equals(right); 58 | } 59 | 60 | public static bool operator !=(StateMachinePath left, StateMachinePath right) 61 | { 62 | return !(left == right); 63 | } 64 | 65 | public StateMachinePath Join(TStateId name) 66 | { 67 | return new StateMachinePath(this, name); 68 | } 69 | } 70 | 71 | /// 72 | /// Represents a state / state machine within a . 73 | /// 74 | public class StateMachinePath : StateMachinePath, IEquatable> 75 | { 76 | public readonly TStateId name; 77 | 78 | public override string LastNodeName => name.ToString(); 79 | 80 | public StateMachinePath(TStateId name) : base(null) 81 | { 82 | this.name = name; 83 | } 84 | 85 | public StateMachinePath(StateMachinePath parentPath, TStateId name) : base(parentPath) 86 | { 87 | this.name = name; 88 | } 89 | 90 | public override string ToString() 91 | { 92 | return (parentPath?.ToString() ?? "") + "/" + name.ToString(); 93 | } 94 | 95 | public override bool Equals(StateMachinePath path) 96 | { 97 | return Equals(path as StateMachinePath); 98 | } 99 | 100 | public bool Equals(StateMachinePath other) 101 | { 102 | if (other is null) 103 | return false; 104 | 105 | if (this.parentPath is null && other.parentPath is not null) 106 | return false; 107 | 108 | if (this.parentPath is not null && other.parentPath is null) 109 | return false; 110 | 111 | if (!EqualityComparer.Default.Equals(this.name, other.name)) 112 | return false; 113 | 114 | return this.parentPath?.Equals(other.parentPath) ?? true; 115 | } 116 | 117 | public override int GetHashCode() 118 | { 119 | int ownHash = EqualityComparer.Default.GetHashCode(name); 120 | 121 | return parentPath is null 122 | ? ownHash 123 | : HashCode.Combine(parentPath.GetHashCode(), ownHash); 124 | } 125 | } 126 | 127 | /// 128 | /// Represents the of the root state machine. 129 | /// 130 | public class RootStateMachinePath : StateMachinePath, IEquatable 131 | { 132 | public const string name = "Root"; 133 | public static readonly RootStateMachinePath instance = new RootStateMachinePath(); 134 | 135 | public override string LastNodeName => name; 136 | 137 | private RootStateMachinePath() : base(null) { } 138 | 139 | public override int GetHashCode() 140 | { 141 | return name.GetHashCode(); 142 | } 143 | 144 | public override bool Equals(StateMachinePath other) 145 | { 146 | return Equals(other as RootStateMachinePath); 147 | } 148 | 149 | public bool Equals(RootStateMachinePath other) 150 | { 151 | return other is not null; 152 | } 153 | 154 | public override string ToString() 155 | { 156 | return name; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Inspection/StateMachinePath.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0addf645ef3c4691821230c3f2a3e8e8 3 | timeCreated: 1742813909 -------------------------------------------------------------------------------- /src/Inspection/StateMachineWalker.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Collections.Generic; 3 | 4 | namespace UnityHFSM.Inspection 5 | { 6 | /// 7 | /// Utility class for methods that traverse a hierarchical state machine. 8 | /// 9 | public class StateMachineWalker 10 | { 11 | /// 12 | /// Recursively walks through the hierarchy, visiting every state. 13 | /// 14 | private class HierarchyWalker : IStateVisitor 15 | { 16 | private StateMachinePath path; 17 | private readonly IStateMachineHierarchyVisitor hierarchyVisitor; 18 | 19 | public HierarchyWalker(IStateMachineHierarchyVisitor hierarchyVisitor) 20 | { 21 | this.hierarchyVisitor = hierarchyVisitor; 22 | } 23 | 24 | public void VisitStateMachine(StateMachine fsm) 25 | { 26 | // Push this state machine on to the path. 27 | path = path is null 28 | ? StateMachinePath.Root 29 | : new StateMachinePath(path, fsm.name); 30 | 31 | hierarchyVisitor.VisitStateMachine(path, fsm); 32 | 33 | foreach (var state in fsm.GetAllStates()) 34 | { 35 | state.AcceptVisitor(this); 36 | } 37 | 38 | hierarchyVisitor.ExitStateMachine(path, fsm); 39 | 40 | // Pop the state machine from the path. 41 | path = path.parentPath; 42 | } 43 | 44 | public void VisitRegularState(StateBase state) 45 | { 46 | var statePath = new StateMachinePath(path, state.name); 47 | hierarchyVisitor.VisitRegularState(statePath, state); 48 | } 49 | } 50 | 51 | /// 52 | /// Extracts the path to the active state. 53 | /// 54 | private class ActiveStateVisitor : IStateVisitor 55 | { 56 | public StateMachinePath activePath; 57 | 58 | public void VisitStateMachine(StateMachine fsm) 59 | { 60 | activePath = activePath is null 61 | ? StateMachinePath.Root 62 | : new StateMachinePath(activePath, fsm.name); 63 | 64 | fsm.ActiveState.AcceptVisitor(this); 65 | } 66 | 67 | public void VisitRegularState(StateBase state) 68 | { 69 | activePath = new StateMachinePath(activePath, state.name); 70 | } 71 | } 72 | 73 | /// 74 | /// Extracts the path to a given state from the root. 75 | /// 76 | private class StatePathExtractor : IStateVisitor 77 | { 78 | public StateMachinePath path; 79 | 80 | public StatePathExtractor(StateBase state) 81 | { 82 | VisitParent(state.fsm); 83 | state.AcceptVisitor(this); 84 | } 85 | 86 | private void VisitParent(IStateTimingManager parent) 87 | { 88 | if (parent == null) 89 | return; 90 | 91 | // Construct the path to the parent of this state (from root). 92 | VisitParent(parent.ParentFsm); 93 | // Add this state to the path. 94 | (parent as IVisitableState)?.AcceptVisitor(this); 95 | } 96 | 97 | public void VisitStateMachine(StateMachine fsm) 98 | { 99 | if (fsm.IsRootFsm) 100 | { 101 | path = StateMachinePath.Root; 102 | } 103 | else 104 | { 105 | AddToPath(fsm.name); 106 | } 107 | } 108 | 109 | public void VisitRegularState(StateBase state) 110 | { 111 | AddToPath(state.name); 112 | } 113 | 114 | private void AddToPath(TStateId name) 115 | { 116 | path = path == null 117 | ? new StateMachinePath(name) 118 | : new StateMachinePath(path, name); 119 | } 120 | } 121 | 122 | /// 123 | /// An optimised variant of the when only 124 | /// a string path is needed. 125 | /// 126 | private class StringStatePathExtractor : IStateVisitor 127 | { 128 | public string path; 129 | 130 | public StringStatePathExtractor(StateBase state) 131 | { 132 | VisitParent(state.fsm); 133 | state.AcceptVisitor(this); 134 | } 135 | 136 | private void VisitParent(IStateTimingManager parent) 137 | { 138 | if (parent == null) 139 | return; 140 | 141 | // Construct the path to the parent of this state (from root). 142 | VisitParent(parent.ParentFsm); 143 | // Add this state to the path. 144 | (parent as IVisitableState)?.AcceptVisitor(this); 145 | } 146 | 147 | public void VisitStateMachine(StateMachine fsm) 148 | { 149 | if (fsm.IsRootFsm) 150 | { 151 | path = RootStateMachinePath.name; 152 | } 153 | else 154 | { 155 | AddToPath(fsm.name); 156 | } 157 | } 158 | 159 | public void VisitRegularState(StateBase state) 160 | { 161 | AddToPath(state.name); 162 | } 163 | 164 | private void AddToPath(TStateId name) 165 | { 166 | path = path == null 167 | ? name.ToString() 168 | : path + "/" + name; 169 | } 170 | } 171 | 172 | /// 173 | /// Recursively traverses the state machine in a pre-order manner, calling the visitor methods 174 | /// on each state machine / child state / child fsm. 175 | /// 176 | public static void Walk( 177 | StateMachine fsm, 178 | IStateMachineHierarchyVisitor visitor) 179 | { 180 | new HierarchyWalker(visitor).VisitStateMachine(fsm); 181 | } 182 | 183 | /// 184 | /// Gets the path to the lowermost active state in the hierarchy. 185 | /// 186 | public static StateMachinePath GetActiveStatePath(StateMachine fsm) 187 | { 188 | var visitor = new ActiveStateVisitor(); 189 | visitor.VisitStateMachine(fsm); 190 | return visitor.activePath; 191 | } 192 | 193 | /// 194 | /// Gets the path from the root state machine to the given state. 195 | /// 196 | public static StateMachinePath GetPathOfState(StateBase state) 197 | { 198 | var visitor = new StatePathExtractor(state); 199 | return visitor.path; 200 | } 201 | 202 | /// 203 | /// Optimised variant of the method that returns 204 | /// a string path from the root state machine to the given state. 205 | /// 206 | public static string GetStringPathOfState(StateBase state) 207 | { 208 | var visitor = new StringStatePathExtractor(state); 209 | return visitor.path; 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Inspection/StateMachineWalker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8ffe3bcb8a84400eaa92a09734cbb3b5 3 | timeCreated: 1742812257 -------------------------------------------------------------------------------- /src/StateMachine.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5236c5df4e4a8e44b91927164dfe1aad 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/StateMachine/HybridStateMachine.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e42a344301120f84b9495ee772e50d8a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/StateMachine/StateMachine.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a70752d4a153a88489bd9ea67bd881d9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/StateMachine/StateMachineShortcuts.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bba0c684ed91e3a4da35cd3dd08c77e6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/States.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 90e19358431926544a180fb752350850 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/States/ActionState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// Base class of states that support custom actions. 7 | /// 8 | /// 9 | public class ActionState : StateBase, IActionable 10 | { 11 | // Lazy initialized 12 | private ActionStorage actionStorage; 13 | 14 | /// 15 | /// Initialises a new instance of the ActionState class. 16 | /// 17 | /// 18 | public ActionState(bool needsExitTime, bool isGhostState = false) 19 | : base(needsExitTime: needsExitTime, isGhostState: isGhostState) 20 | { 21 | } 22 | 23 | /// 24 | /// Adds an action that can be called with OnAction(). Actions are like the builtin events 25 | /// OnEnter / OnLogic / ... but are defined by the user. 26 | /// 27 | /// Name of the action. 28 | /// Function that should be called when the action is run. 29 | /// Itself to allow for a fluent interface. 30 | public ActionState AddAction(TEvent trigger, Action action) 31 | { 32 | actionStorage = actionStorage ?? new ActionStorage(); 33 | actionStorage.AddAction(trigger, action); 34 | return this; 35 | } 36 | 37 | /// 38 | /// Adds an action that can be called with OnAction<T>(). This overload allows you to 39 | /// run a function that takes one data parameter. 40 | /// Actions are like the builtin events OnEnter / OnLogic / ... but are defined by the user. 41 | /// 42 | /// Name of the action. 43 | /// Function that should be called when the action is run. 44 | /// Data type of the parameter of the function. 45 | /// Itself to allow for a fluent interface. 46 | public ActionState AddAction(TEvent trigger, Action action) 47 | { 48 | actionStorage = actionStorage ?? new ActionStorage(); 49 | actionStorage.AddAction(trigger, action); 50 | return this; 51 | } 52 | 53 | /// 54 | /// Runs an action with the given name. 55 | /// If the action is not defined / hasn't been added, nothing will happen. 56 | /// 57 | /// Name of the action. 58 | public void OnAction(TEvent trigger) 59 | => actionStorage?.RunAction(trigger); 60 | 61 | /// 62 | /// Runs an action with a given name and lets you pass in one parameter to the action function. 63 | /// If the action is not defined / hasn't been added, nothing will happen. 64 | /// 65 | /// Name of the action. 66 | /// Data to pass as the first parameter to the action. 67 | /// Type of the data parameter. 68 | public void OnAction(TEvent trigger, TData data) 69 | => actionStorage?.RunAction(trigger, data); 70 | } 71 | 72 | /// 73 | public class ActionState : ActionState 74 | { 75 | /// 76 | public ActionState(bool needsExitTime, bool isGhostState = false) 77 | : base(needsExitTime: needsExitTime, isGhostState: isGhostState) 78 | { 79 | } 80 | } 81 | 82 | /// 83 | public class ActionState : ActionState 84 | { 85 | /// 86 | public ActionState(bool needsExitTime, bool isGhostState = false) 87 | : base(needsExitTime: needsExitTime, isGhostState: isGhostState) 88 | { 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/States/ActionState.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff1aad5efbfc811468d5058a889136cc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/States/ActionStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityHFSM.Exceptions; 4 | 5 | namespace UnityHFSM 6 | { 7 | /// 8 | /// Class that can store and run actions. 9 | /// It makes implementing an action system easier in the various state classes. 10 | /// 11 | public class ActionStorage 12 | { 13 | private readonly Dictionary actionsByEvent = new Dictionary(); 14 | 15 | /// 16 | /// Returns the action belonging to the specified event. 17 | /// If it does not exist null is returned. 18 | /// If the type of the existing action does not match the desired type an exception 19 | /// is thrown. 20 | /// 21 | /// Name of the action. 22 | /// Type of the function (delegate) belonging to the action. 23 | /// The action with the specified name. 24 | private TTarget TryGetAndCastAction(TEvent trigger) where TTarget : Delegate 25 | { 26 | Delegate action = null; 27 | actionsByEvent.TryGetValue(trigger, out action); 28 | 29 | if (action is null) 30 | { 31 | return null; 32 | } 33 | 34 | TTarget target = action as TTarget; 35 | 36 | if (target is null) 37 | { 38 | throw new InvalidOperationException(ExceptionFormatter.Format( 39 | context: $"Trying to call the action '{trigger}'.", 40 | problem: $"The expected argument type ({typeof(TTarget)}) does not match the " 41 | + $"type of the added action ({action}).", 42 | solution: "Check that the type of action that was added matches the type of action that is called. \n" 43 | + "E.g. AddAction(...) => OnAction(...) \n" 44 | + "E.g. AddAction(...) => OnAction(...) \n" 45 | + "E.g. NOT: AddAction(...) => OnAction(...)" 46 | )); 47 | } 48 | 49 | return target; 50 | } 51 | 52 | /// 53 | /// Adds an action that can be called with . Actions are like the builtin events 54 | /// OnEnter / OnLogic / ... but are defined by the user. 55 | /// 56 | /// Name of the action. 57 | /// Function that should be called when the action is run. 58 | public void AddAction(TEvent trigger, Action action) 59 | { 60 | actionsByEvent[trigger] = action; 61 | } 62 | 63 | /// 64 | /// Adds an action that can be called with . This overload allows you to 65 | /// run a function that takes one data parameter. 66 | /// 67 | /// Name of the action. 68 | /// Function that should be called when the action is run. 69 | /// Data type of the parameter of the function. 70 | public void AddAction(TEvent trigger, Action action) 71 | { 72 | actionsByEvent[trigger] = action; 73 | } 74 | 75 | /// 76 | /// Runs an action with the given name. 77 | /// If the action is not defined / hasn't been added, nothing will happen. 78 | /// 79 | /// Name of the action. 80 | public void RunAction(TEvent trigger) 81 | => TryGetAndCastAction(trigger)?.Invoke(); 82 | 83 | /// 84 | /// Runs an action with a given name and lets you pass in one parameter to the action function. 85 | /// If the action is not defined / hasn't been added, nothing will happen. 86 | /// 87 | /// Name of the action. 88 | /// Data to pass as the first parameter to the action. 89 | /// Type of the data parameter. 90 | public void RunAction(TEvent trigger, TData data) 91 | => TryGetAndCastAction>(trigger)?.Invoke(data); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/States/ActionStorage.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4f2c911eca9445b48bb2d0499b6175fb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/States/CoState.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 574de01d38e85a644bf52468c384fc3e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/States/DecoratedState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// A class that allows you to run additional functions (companion code) 7 | /// before and after the wrapped state's code. 8 | /// It does not interfere with the wrapped state's timing / needsExitTime / ... behaviour. 9 | /// 10 | public class DecoratedState : StateBase, ITriggerable, IActionable 11 | { 12 | private readonly Action> 13 | beforeOnEnter, 14 | afterOnEnter, 15 | 16 | beforeOnLogic, 17 | afterOnLogic, 18 | 19 | beforeOnExit, 20 | afterOnExit; 21 | 22 | public readonly StateBase state; 23 | 24 | public DecoratedState( 25 | StateBase state, 26 | 27 | Action> beforeOnEnter = null, 28 | Action> afterOnEnter = null, 29 | 30 | Action> beforeOnLogic = null, 31 | Action> afterOnLogic = null, 32 | 33 | Action> beforeOnExit = null, 34 | Action> afterOnExit = null) : base(state.needsExitTime, state.isGhostState) 35 | { 36 | this.state = state; 37 | 38 | this.beforeOnEnter = beforeOnEnter; 39 | this.afterOnEnter = afterOnEnter; 40 | 41 | this.beforeOnLogic = beforeOnLogic; 42 | this.afterOnLogic = afterOnLogic; 43 | 44 | this.beforeOnExit = beforeOnExit; 45 | this.afterOnExit = afterOnExit; 46 | } 47 | 48 | public override void Init() 49 | { 50 | state.name = name; 51 | state.fsm = fsm; 52 | 53 | state.Init(); 54 | } 55 | 56 | public override void OnEnter() 57 | { 58 | beforeOnEnter?.Invoke(this); 59 | state.OnEnter(); 60 | afterOnEnter?.Invoke(this); 61 | } 62 | 63 | public override void OnLogic() 64 | { 65 | beforeOnLogic?.Invoke(this); 66 | state.OnLogic(); 67 | afterOnLogic?.Invoke(this); 68 | } 69 | 70 | public override void OnExit() 71 | { 72 | beforeOnExit?.Invoke(this); 73 | state.OnExit(); 74 | afterOnExit?.Invoke(this); 75 | } 76 | 77 | public override void OnExitRequest() 78 | { 79 | state.OnExitRequest(); 80 | } 81 | 82 | public void Trigger(TEvent trigger) 83 | { 84 | (state as ITriggerable)?.Trigger(trigger); 85 | } 86 | 87 | public void OnAction(TEvent trigger) 88 | { 89 | (state as IActionable)?.OnAction(trigger); 90 | } 91 | 92 | public void OnAction(TEvent trigger, TData data) 93 | { 94 | (state as IActionable)?.OnAction(trigger, data); 95 | } 96 | 97 | public override string GetActiveHierarchyPath() 98 | { 99 | return state.GetActiveHierarchyPath(); 100 | } 101 | } 102 | 103 | /// 104 | public class DecoratedState : DecoratedState 105 | { 106 | public DecoratedState( 107 | StateBase state, 108 | Action> beforeOnEnter = null, 109 | Action> afterOnEnter = null, 110 | Action> beforeOnLogic = null, 111 | Action> afterOnLogic = null, 112 | Action> beforeOnExit = null, 113 | Action> afterOnExit = null) 114 | : base( 115 | state, 116 | beforeOnEnter, 117 | afterOnEnter, 118 | beforeOnLogic, 119 | afterOnLogic, 120 | beforeOnExit, 121 | afterOnExit) { } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/States/DecoratedState.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0be0f0556fb445c4a5f5f1b212414750 3 | timeCreated: 1742855933 -------------------------------------------------------------------------------- /src/States/ParallelStates.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b3eff52e7c122a43a00268a9ee341ea 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/States/State.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// The "normal" state class that can run code on enter, on logic and on exit, 7 | /// while also handling the timing of the next state transition. 8 | /// 9 | public class State : ActionState 10 | { 11 | private readonly Action> onEnter; 12 | private readonly Action> onLogic; 13 | private readonly Action> onExit; 14 | private readonly Func, bool> canExit; 15 | 16 | public ITimer timer; 17 | 18 | /// 19 | /// Initialises a new instance of the State class. 20 | /// 21 | /// A function that is called when the state machine enters this state. 22 | /// A function that is called by the logic function of the state machine if this 23 | /// state is active. 24 | /// A function that is called when the state machine exits this state. 25 | /// (Only if needsExitTime is true): 26 | /// Function that determines if the state is ready to exit (true) or not (false). 27 | /// It is called OnExitRequest and on each logic step when a transition is pending. 28 | /// Determines if the state is allowed to instantly 29 | /// exit on a transition (false), or if the state machine should wait until the state is ready for a 30 | /// state change (true). 31 | public State( 32 | Action> onEnter = null, 33 | Action> onLogic = null, 34 | Action> onExit = null, 35 | Func, bool> canExit = null, 36 | bool needsExitTime = false, 37 | bool isGhostState = false) : base(needsExitTime, isGhostState) 38 | { 39 | this.onEnter = onEnter; 40 | this.onLogic = onLogic; 41 | this.onExit = onExit; 42 | this.canExit = canExit; 43 | 44 | this.timer = new Timer(); 45 | } 46 | 47 | public override void OnEnter() 48 | { 49 | timer.Reset(); 50 | 51 | onEnter?.Invoke(this); 52 | } 53 | 54 | public override void OnLogic() 55 | { 56 | onLogic?.Invoke(this); 57 | 58 | // Check whether the state is ready to exit after calling onLogic, as it may trigger a transition. 59 | // Calling onLogic beforehand would lead to invalid behaviour as it would be called, even though this state 60 | // is not active anymore. 61 | if (needsExitTime && canExit != null && fsm.HasPendingTransition && canExit(this)) 62 | { 63 | fsm.StateCanExit(); 64 | } 65 | } 66 | 67 | public override void OnExit() 68 | { 69 | onExit?.Invoke(this); 70 | } 71 | 72 | public override void OnExitRequest() 73 | { 74 | if (canExit != null && canExit(this)) 75 | { 76 | fsm.StateCanExit(); 77 | } 78 | } 79 | } 80 | 81 | /// 82 | public class State : State 83 | { 84 | /// 85 | public State( 86 | Action> onEnter = null, 87 | Action> onLogic = null, 88 | Action> onExit = null, 89 | Func, bool> canExit = null, 90 | bool needsExitTime = false, 91 | bool isGhostState = false) 92 | : base( 93 | onEnter, 94 | onLogic, 95 | onExit, 96 | canExit, 97 | needsExitTime: needsExitTime, 98 | isGhostState: isGhostState) 99 | { 100 | } 101 | } 102 | 103 | /// 104 | public class State : State 105 | { 106 | /// 107 | public State( 108 | Action> onEnter = null, 109 | Action> onLogic = null, 110 | Action> onExit = null, 111 | Func, bool> canExit = null, 112 | bool needsExitTime = false, 113 | bool isGhostState = false) 114 | : base( 115 | onEnter, 116 | onLogic, 117 | onExit, 118 | canExit, 119 | needsExitTime: needsExitTime, 120 | isGhostState: isGhostState) 121 | { 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/States/State.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bdceaa8cffeca9b4898dbb62a0d49898 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/States/StateDecorator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// A helper class that helps you decorate multiple states with the same user code. 7 | /// It produces objects based on the provided parameters. 8 | /// 9 | public class StateDecorator 10 | { 11 | private readonly Action> 12 | beforeOnEnter, 13 | afterOnEnter, 14 | 15 | beforeOnLogic, 16 | afterOnLogic, 17 | 18 | beforeOnExit, 19 | afterOnExit; 20 | 21 | /// 22 | /// Initialises a new instance of the StateDecorator class. 23 | /// 24 | public StateDecorator( 25 | Action> beforeOnEnter = null, 26 | Action> afterOnEnter = null, 27 | 28 | Action> beforeOnLogic = null, 29 | Action> afterOnLogic = null, 30 | 31 | Action> beforeOnExit = null, 32 | Action> afterOnExit = null) 33 | { 34 | this.beforeOnEnter = beforeOnEnter; 35 | this.afterOnEnter = afterOnEnter; 36 | 37 | this.beforeOnLogic = beforeOnLogic; 38 | this.afterOnLogic = afterOnLogic; 39 | 40 | this.beforeOnExit = beforeOnExit; 41 | this.afterOnExit = afterOnExit; 42 | } 43 | 44 | public DecoratedState Decorate(StateBase state) 45 | { 46 | return new DecoratedState( 47 | state, 48 | beforeOnEnter, 49 | afterOnEnter, 50 | beforeOnLogic, 51 | afterOnLogic, 52 | beforeOnExit, 53 | afterOnExit 54 | ); 55 | } 56 | } 57 | 58 | /// 59 | public class StateDecorator : StateDecorator 60 | { 61 | public StateDecorator( 62 | Action> beforeOnEnter = null, 63 | Action> afterOnEnter = null, 64 | 65 | Action> beforeOnLogic = null, 66 | Action> afterOnLogic = null, 67 | 68 | Action> beforeOnExit = null, 69 | Action> afterOnExit = null) : base( 70 | beforeOnEnter, afterOnEnter, 71 | beforeOnLogic, afterOnLogic, 72 | beforeOnExit, afterOnExit) 73 | { 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/States/StateDecorator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2a04886ed07c68d4c99b4a8cefe6df8e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Transitions.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c5cf595c03f9bb14ca14061f2b1d7179 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/Transitions/DecoratedTransition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// A class that allows you to run additional functions (companion code) 7 | /// before and after the wrapped transition's code. 8 | /// 9 | public class DecoratedTransition : TransitionBase 10 | { 11 | private readonly Action> 12 | beforeOnEnter, 13 | afterOnEnter, 14 | 15 | beforeShouldTransition, 16 | afterShouldTransition, 17 | 18 | beforeOnTransition, 19 | afterOnTransition, 20 | 21 | beforeAfterTransition, 22 | afterAfterTransition; 23 | 24 | public readonly TransitionBase transition; 25 | 26 | public DecoratedTransition( 27 | TransitionBase transition, 28 | 29 | Action> beforeOnEnter = null, 30 | Action> afterOnEnter = null, 31 | 32 | Action> beforeShouldTransition = null, 33 | Action> afterShouldTransition = null, 34 | 35 | Action> beforeOnTransition = null, 36 | Action> afterOnTransition = null, 37 | 38 | Action> beforeAfterTransition = null, 39 | Action> afterAfterTransition = null 40 | ) : base( 41 | transition.from, transition.to, forceInstantly: transition.forceInstantly) 42 | { 43 | this.transition = transition; 44 | 45 | this.beforeOnEnter = beforeOnEnter; 46 | this.afterOnEnter = afterOnEnter; 47 | 48 | this.beforeShouldTransition = beforeShouldTransition; 49 | this.afterShouldTransition = afterShouldTransition; 50 | 51 | this.beforeOnTransition = beforeOnTransition; 52 | this.afterOnTransition = afterOnTransition; 53 | 54 | this.beforeAfterTransition = beforeAfterTransition; 55 | this.afterAfterTransition = afterAfterTransition; 56 | } 57 | 58 | public override void Init() 59 | { 60 | transition.fsm = this.fsm; 61 | } 62 | 63 | public override void OnEnter() 64 | { 65 | beforeOnEnter?.Invoke(transition); 66 | transition.OnEnter(); 67 | afterOnEnter?.Invoke(transition); 68 | } 69 | 70 | public override bool ShouldTransition() 71 | { 72 | beforeShouldTransition?.Invoke(transition); 73 | bool shouldTransition = transition.ShouldTransition(); 74 | afterShouldTransition?.Invoke(transition); 75 | return shouldTransition; 76 | } 77 | 78 | public override void BeforeTransition() 79 | { 80 | beforeOnTransition?.Invoke(transition); 81 | transition.BeforeTransition(); 82 | afterOnTransition?.Invoke(transition); 83 | } 84 | 85 | public override void AfterTransition() 86 | { 87 | beforeAfterTransition?.Invoke(transition); 88 | transition.AfterTransition(); 89 | afterAfterTransition?.Invoke(transition); 90 | } 91 | } 92 | 93 | /// 94 | public class DecoratedTransition : DecoratedTransition 95 | { 96 | public DecoratedTransition( 97 | TransitionBase transition, 98 | Action> beforeOnEnter = null, 99 | Action> afterOnEnter = null, 100 | Action> beforeShouldTransition = null, 101 | Action> afterShouldTransition = null, 102 | Action> beforeOnTransition = null, 103 | Action> afterOnTransition = null, 104 | Action> beforeAfterTransition = null, 105 | Action> afterAfterTransition = null) 106 | : base( 107 | transition, 108 | beforeOnEnter, 109 | afterOnEnter, 110 | beforeShouldTransition, 111 | afterShouldTransition, 112 | beforeOnTransition, 113 | afterOnTransition, 114 | beforeAfterTransition, 115 | afterAfterTransition) { } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Transitions/DecoratedTransition.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35fb0afeda614b9a99be00f928205f21 3 | timeCreated: 1742856579 -------------------------------------------------------------------------------- /src/Transitions/ReverseTransition.cs: -------------------------------------------------------------------------------- 1 | namespace UnityHFSM 2 | { 3 | /// 4 | /// A ReverseTransition wraps another transition, but reverses it. The "from" 5 | /// and "to" states are swapped. Only when the condition of the wrapped transition 6 | /// is false does it transition. 7 | /// The BeforeTransition and AfterTransition callbacks of the wrapped transition 8 | /// are also swapped. 9 | /// 10 | public class ReverseTransition : TransitionBase 11 | { 12 | public readonly TransitionBase wrappedTransition; 13 | private readonly bool shouldInitWrappedTransition; 14 | 15 | public ReverseTransition( 16 | TransitionBase wrappedTransition, 17 | bool shouldInitWrappedTransition = true) 18 | : base( 19 | from: wrappedTransition.to, 20 | to: wrappedTransition.from, 21 | forceInstantly: wrappedTransition.forceInstantly) 22 | { 23 | this.wrappedTransition = wrappedTransition; 24 | this.shouldInitWrappedTransition = shouldInitWrappedTransition; 25 | } 26 | 27 | public override void Init() 28 | { 29 | if (shouldInitWrappedTransition) 30 | { 31 | wrappedTransition.fsm = this.fsm; 32 | wrappedTransition.Init(); 33 | } 34 | } 35 | 36 | public override void OnEnter() 37 | { 38 | wrappedTransition.OnEnter(); 39 | } 40 | 41 | public override bool ShouldTransition() 42 | { 43 | return !wrappedTransition.ShouldTransition(); 44 | } 45 | 46 | public override void BeforeTransition() 47 | { 48 | wrappedTransition.AfterTransition(); 49 | } 50 | 51 | public override void AfterTransition() 52 | { 53 | wrappedTransition.BeforeTransition(); 54 | } 55 | } 56 | 57 | /// 58 | public class ReverseTransition : ReverseTransition 59 | { 60 | /// 61 | public ReverseTransition( 62 | TransitionBase wrappedTransition, 63 | bool shouldInitWrappedTransition = true) 64 | : base(wrappedTransition, shouldInitWrappedTransition) 65 | { 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Transitions/ReverseTransition.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65690b6cca772914f8cc4a61ddbe73df 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Transitions/Transition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// A class used to determine whether the state machine should transition to another state. 7 | /// 8 | public class Transition : TransitionBase 9 | { 10 | private readonly Func, bool> condition; 11 | private readonly Action> beforeTransition; 12 | private readonly Action> afterTransition; 13 | 14 | /// 15 | /// Initialises a new instance of the Transition class. 16 | /// 17 | /// A function that returns true if the state machine 18 | /// should transition to the to state. 19 | /// Callback function that is called just before the transition happens. 20 | /// Callback function that is called just after the transition happens. 21 | /// 22 | public Transition( 23 | TStateId from, 24 | TStateId to, 25 | Func, bool> condition = null, 26 | Action> onTransition = null, 27 | Action> afterTransition = null, 28 | bool forceInstantly = false) : base(from, to, forceInstantly) 29 | { 30 | this.condition = condition; 31 | this.beforeTransition = onTransition; 32 | this.afterTransition = afterTransition; 33 | } 34 | 35 | public override bool ShouldTransition() 36 | { 37 | if (condition == null) 38 | return true; 39 | 40 | return condition(this); 41 | } 42 | 43 | public override void BeforeTransition() => beforeTransition?.Invoke(this); 44 | public override void AfterTransition() => afterTransition?.Invoke(this); 45 | } 46 | 47 | /// 48 | public class Transition : Transition 49 | { 50 | /// 51 | public Transition( 52 | string @from, 53 | string to, 54 | Func, bool> condition = null, 55 | Action> onTransition = null, 56 | Action> afterTransition = null, 57 | bool forceInstantly = false) : base( 58 | @from, 59 | to, 60 | condition, 61 | onTransition: onTransition, 62 | afterTransition: afterTransition, 63 | forceInstantly: forceInstantly) 64 | { 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Transitions/Transition.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bec1e52e7f1b6e340b15b9d61a69eb85 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Transitions/TransitionAfter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// A class used to determine whether the state machine should transition to another state 7 | /// depending on a delay and an optional condition. 8 | /// 9 | public class TransitionAfter : TransitionBase 10 | { 11 | public float delay; 12 | public ITimer timer; 13 | 14 | private readonly Func, bool> condition; 15 | 16 | private readonly Action> beforeTransition; 17 | private readonly Action> afterTransition; 18 | 19 | /// 20 | /// Initialises a new instance of the TransitionAfter class. 21 | /// 22 | /// The delay that must elapse before the transition can occur 23 | /// A function that returns true if the state machine 24 | /// should transition to the to state. 25 | /// It is only called after the delay has elapsed and is optional. 26 | /// 28 | public TransitionAfter( 29 | TStateId from, 30 | TStateId to, 31 | float delay, 32 | Func, bool> condition = null, 33 | Action> onTransition = null, 34 | Action> afterTransition = null, 35 | bool forceInstantly = false) : base(from, to, forceInstantly) 36 | { 37 | this.delay = delay; 38 | this.condition = condition; 39 | this.beforeTransition = onTransition; 40 | this.afterTransition = afterTransition; 41 | this.timer = new Timer(); 42 | } 43 | 44 | public override void OnEnter() 45 | { 46 | timer.Reset(); 47 | } 48 | 49 | public override bool ShouldTransition() 50 | { 51 | if (timer.Elapsed < delay) 52 | return false; 53 | 54 | if (condition == null) 55 | return true; 56 | 57 | return condition(this); 58 | } 59 | 60 | public override void BeforeTransition() => beforeTransition?.Invoke(this); 61 | public override void AfterTransition() => afterTransition?.Invoke(this); 62 | } 63 | 64 | /// 65 | public class TransitionAfter : TransitionAfter 66 | { 67 | /// 68 | public TransitionAfter( 69 | string @from, 70 | string to, 71 | float delay, 72 | Func, bool> condition = null, 73 | Action> onTransition = null, 74 | Action> afterTransition = null, 75 | bool forceInstantly = false) : base( 76 | @from, 77 | to, 78 | delay, 79 | condition, 80 | onTransition: onTransition, 81 | afterTransition: afterTransition, 82 | forceInstantly: forceInstantly) 83 | { 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Transitions/TransitionAfter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 716a11fac7945f542aebb233c0d8ce45 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Transitions/TransitionAfterDynamic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// A class used to determine whether the state machine should transition to another state 7 | /// depending on a dynamically computed delay and an optional condition. 8 | /// 9 | public class TransitionAfterDynamic : TransitionBase 10 | { 11 | public ITimer timer; 12 | private float delay; 13 | private readonly bool onlyEvaluateDelayOnEnter; 14 | private readonly Func, float> delayCalculator; 15 | 16 | private readonly Func, bool> condition; 17 | 18 | private readonly Action> beforeTransition; 19 | private readonly Action> afterTransition; 20 | 21 | /// 22 | /// Initialises a new instance of the TransitionAfterDynamic class. 23 | /// 24 | /// A function that dynamically computes the delay time. 25 | /// A function that returns true if the state machine 26 | /// should transition to the to state. 27 | /// It is only called after the delay has elapsed and is optional. 28 | /// If true, the dynamic delay is only recalculated 29 | /// when the from enters. If false, the delay is evaluated in each logic step. 30 | /// 32 | public TransitionAfterDynamic( 33 | TStateId from, 34 | TStateId to, 35 | Func, float> delay, 36 | Func, bool> condition = null, 37 | bool onlyEvaluateDelayOnEnter = false, 38 | Action> onTransition = null, 39 | Action> afterTransition = null, 40 | bool forceInstantly = false) : base(from, to, forceInstantly) 41 | { 42 | this.delayCalculator = delay; 43 | this.condition = condition; 44 | this.onlyEvaluateDelayOnEnter = onlyEvaluateDelayOnEnter; 45 | this.beforeTransition = onTransition; 46 | this.afterTransition = afterTransition; 47 | this.timer = new Timer(); 48 | } 49 | 50 | public override void OnEnter() 51 | { 52 | timer.Reset(); 53 | if (onlyEvaluateDelayOnEnter) 54 | { 55 | delay = delayCalculator(this); 56 | } 57 | } 58 | 59 | public override bool ShouldTransition() 60 | { 61 | if (!onlyEvaluateDelayOnEnter) 62 | { 63 | delay = delayCalculator(this); 64 | } 65 | 66 | if (timer.Elapsed < delay) 67 | return false; 68 | 69 | if (condition == null) 70 | return true; 71 | 72 | return condition(this); 73 | } 74 | 75 | public override void BeforeTransition() => beforeTransition?.Invoke(this); 76 | public override void AfterTransition() => afterTransition?.Invoke(this); 77 | } 78 | 79 | /// 80 | public class TransitionAfterDynamic : TransitionAfterDynamic 81 | { 82 | /// 83 | public TransitionAfterDynamic( 84 | string @from, 85 | string to, 86 | Func, float> delay, 87 | Func, bool> condition = null, 88 | bool onlyEvaluateDelayOnEnter = false, 89 | Action> onTransition = null, 90 | Action> afterTransition = null, 91 | bool forceInstantly = false) : base( 92 | @from, 93 | to, 94 | delay, 95 | condition, 96 | onlyEvaluateDelayOnEnter: onlyEvaluateDelayOnEnter, 97 | onTransition: onTransition, 98 | afterTransition: afterTransition, 99 | forceInstantly: forceInstantly) 100 | { 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Transitions/TransitionAfterDynamic.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5d5cb0754fe9e6c4a8ced8932bba450c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Transitions/TransitionDecorator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// A helper class that helps you decorate multiple transitions with the same user code. 7 | /// It produces objects based on the provided parameters. 8 | /// 9 | public class TransitionDecorator 10 | { 11 | private readonly Action> 12 | beforeOnEnter, 13 | afterOnEnter, 14 | 15 | beforeShouldTransition, 16 | afterShouldTransition, 17 | 18 | beforeOnTransition, 19 | afterOnTransition, 20 | 21 | beforeAfterTransition, 22 | afterAfterTransition; 23 | 24 | public TransitionDecorator( 25 | Action> beforeOnEnter = null, 26 | Action> afterOnEnter = null, 27 | 28 | Action> beforeShouldTransition = null, 29 | Action> afterShouldTransition = null, 30 | 31 | Action> beforeOnTransition = null, 32 | Action> afterOnTransition = null, 33 | 34 | Action> beforeAfterTransition = null, 35 | Action> afterAfterTransition = null) 36 | { 37 | this.beforeOnEnter = beforeOnEnter; 38 | this.afterOnEnter = afterOnEnter; 39 | 40 | this.beforeShouldTransition = beforeShouldTransition; 41 | this.afterShouldTransition = afterShouldTransition; 42 | 43 | this.beforeOnTransition = beforeOnTransition; 44 | this.afterOnTransition = afterOnTransition; 45 | 46 | this.beforeAfterTransition = beforeAfterTransition; 47 | this.afterAfterTransition = afterAfterTransition; 48 | } 49 | 50 | public DecoratedTransition Decorate(TransitionBase transition) 51 | { 52 | return new DecoratedTransition( 53 | transition, 54 | beforeOnEnter, 55 | afterOnEnter, 56 | beforeShouldTransition, 57 | afterShouldTransition, 58 | beforeOnTransition, 59 | afterOnTransition, 60 | beforeAfterTransition, 61 | afterAfterTransition 62 | ); 63 | } 64 | } 65 | 66 | /// 67 | public class TransitionDecorator : TransitionDecorator 68 | { 69 | public TransitionDecorator( 70 | Action> beforeOnEnter = null, 71 | Action> afterOnEnter = null, 72 | Action> beforeShouldTransition = null, 73 | Action> afterShouldTransition = null, 74 | Action> beforeOnTransition = null, 75 | Action> afterOnTransition = null, 76 | Action> beforeAfterTransition = null, 77 | Action> afterAfterTransition = null) 78 | : base( 79 | beforeOnEnter, 80 | afterOnEnter, 81 | beforeShouldTransition, 82 | afterShouldTransition, 83 | beforeOnTransition, 84 | afterOnTransition, 85 | beforeAfterTransition, 86 | afterAfterTransition) { } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Transitions/TransitionDecorator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ab46914d08d50f64fa088f3ce299c429 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Transitions/TransitionOnKey.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// Built-in keyboard-related transition types to increase readability and reduce boilerplate. 7 | /// 8 | public static class TransitionOnKey 9 | { 10 | /// 11 | /// A transition type that triggers while a key is down. 12 | /// It behaves like Input.GetKey(...). 13 | /// 14 | public class Down : TransitionBase 15 | { 16 | private readonly KeyCode keyCode; 17 | 18 | /// The KeyCode of the key to watch. 19 | public Down( 20 | TStateId from, 21 | TStateId to, 22 | KeyCode key, 23 | bool forceInstantly = false) : base(from, to, forceInstantly) 24 | { 25 | keyCode = key; 26 | } 27 | 28 | public override bool ShouldTransition() 29 | { 30 | return Input.GetKey(keyCode); 31 | } 32 | } 33 | 34 | /// 35 | /// A transition type that triggers when a key was just down and is up now. 36 | /// It behaves like Input.GetKeyUp(...). 37 | /// 38 | /// 39 | public class Released : TransitionBase 40 | { 41 | private readonly KeyCode keyCode; 42 | 43 | /// The KeyCode of the key to watch. 44 | public Released( 45 | TStateId from, 46 | TStateId to, 47 | KeyCode key, 48 | bool forceInstantly = false) : base(from, to, forceInstantly) 49 | { 50 | keyCode = key; 51 | } 52 | 53 | public override bool ShouldTransition() 54 | { 55 | return Input.GetKeyUp(keyCode); 56 | } 57 | } 58 | 59 | /// 60 | /// A transition type that triggers when a key was just up and is down now. 61 | /// It behaves like Input.GetKeyDown(...). 62 | /// 63 | public class Pressed : TransitionBase 64 | { 65 | private readonly KeyCode keyCode; 66 | 67 | /// The KeyCode of the key to watch. 68 | public Pressed( 69 | TStateId from, 70 | TStateId to, 71 | KeyCode key, 72 | bool forceInstantly = false) : base(from, to, forceInstantly) 73 | { 74 | keyCode = key; 75 | } 76 | 77 | public override bool ShouldTransition() 78 | { 79 | return Input.GetKeyDown(keyCode); 80 | } 81 | } 82 | 83 | /// 84 | /// A transition type that triggers while a key is up. 85 | /// It behaves like !Input.GetKey(...). 86 | /// 87 | public class Up : TransitionBase 88 | { 89 | private readonly KeyCode keyCode; 90 | 91 | /// The KeyCode of the key to watch. 92 | public Up( 93 | TStateId from, 94 | TStateId to, 95 | KeyCode key, 96 | bool forceInstantly = false) : base(from, to, forceInstantly) 97 | { 98 | keyCode = key; 99 | } 100 | 101 | public override bool ShouldTransition() 102 | { 103 | return !Input.GetKey(keyCode); 104 | } 105 | } 106 | 107 | /// 108 | public class Down : Down 109 | { 110 | public Down( 111 | string @from, 112 | string to, 113 | KeyCode key, 114 | bool forceInstantly = false) : base(@from, to, key, forceInstantly) 115 | { 116 | } 117 | } 118 | 119 | /// 120 | public class Released : Released 121 | { 122 | public Released( 123 | string @from, 124 | string to, 125 | KeyCode key, 126 | bool forceInstantly = false) : base(@from, to, key, forceInstantly) 127 | { 128 | } 129 | } 130 | 131 | /// 132 | public class Pressed : Pressed 133 | { 134 | public Pressed( 135 | string @from, 136 | string to, 137 | KeyCode key, 138 | bool forceInstantly = false) : base(@from, to, key, forceInstantly) 139 | { 140 | } 141 | } 142 | 143 | /// 144 | public class Up : Up 145 | { 146 | public Up( 147 | string @from, 148 | string to, 149 | KeyCode key, 150 | bool forceInstantly = false) : base(@from, to, key, forceInstantly) 151 | { 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Transitions/TransitionOnKey.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 505c206c4b2474b41ab083eb05a07f4c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Transitions/TransitionOnMouse.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// Built-in mouse-related transition types to increase readability and reduce boilerplate. 7 | /// 8 | public static class TransitionOnMouse 9 | { 10 | /// 11 | /// A transition type that triggers while a mouse button is down. 12 | /// It behaves like Input.GetMouseButton(...). 13 | /// 14 | public class Down : TransitionBase 15 | { 16 | private readonly int button; 17 | 18 | /// The mouse button to watch. 19 | public Down( 20 | TStateId from, 21 | TStateId to, 22 | int button, 23 | bool forceInstantly = false) : base(from, to, forceInstantly) 24 | { 25 | this.button = button; 26 | } 27 | 28 | public override bool ShouldTransition() 29 | { 30 | return Input.GetMouseButton(button); 31 | } 32 | } 33 | 34 | /// 35 | /// A transition type that triggers when a mouse button was just down and is up now. 36 | /// It behaves like Input.GetMouseButtonUp(...). 37 | /// 38 | public class Released : TransitionBase 39 | { 40 | private readonly int button; 41 | 42 | /// The mouse button to watch. 43 | public Released( 44 | TStateId from, 45 | TStateId to, 46 | int button, 47 | bool forceInstantly = false) : base(from, to, forceInstantly) 48 | { 49 | this.button = button; 50 | } 51 | 52 | public override bool ShouldTransition() 53 | { 54 | return Input.GetMouseButtonUp(button); 55 | } 56 | } 57 | 58 | /// 59 | /// A transition type that triggers when a mouse button was just up and is down now. 60 | /// It behaves like Input.GetMouseButtonDown(...). 61 | /// 62 | public class Pressed : TransitionBase 63 | { 64 | private readonly int button; 65 | 66 | /// The mouse button to watch. 67 | public Pressed( 68 | TStateId from, 69 | TStateId to, 70 | int button, 71 | bool forceInstantly = false) : base(from, to, forceInstantly) 72 | { 73 | this.button = button; 74 | } 75 | 76 | public override bool ShouldTransition() 77 | { 78 | return Input.GetMouseButtonDown(button); 79 | } 80 | } 81 | 82 | /// 83 | /// A transition type that triggers while a mouse button is up. 84 | /// It behaves like !Input.GetMouseButton(...). 85 | /// 86 | public class Up : TransitionBase 87 | { 88 | private readonly int button; 89 | 90 | /// The mouse button to watch. 91 | public Up( 92 | TStateId from, 93 | TStateId to, 94 | int button, 95 | bool forceInstantly = false) : base(from, to, forceInstantly) 96 | { 97 | this.button = button; 98 | } 99 | 100 | public override bool ShouldTransition() 101 | { 102 | return !Input.GetMouseButton(button); 103 | } 104 | } 105 | 106 | /// 107 | public class Down : Down 108 | { 109 | public Down( 110 | string @from, 111 | string to, 112 | int button, 113 | bool forceInstantly = false) : base(@from, to, button, forceInstantly) 114 | { 115 | } 116 | } 117 | 118 | /// 119 | public class Released : Released 120 | { 121 | public Released( 122 | string @from, 123 | string to, 124 | int button, 125 | bool forceInstantly = false) : base(@from, to, button, forceInstantly) 126 | { 127 | } 128 | } 129 | 130 | /// 131 | public class Pressed : Pressed 132 | { 133 | public Pressed( 134 | string @from, 135 | string to, 136 | int button, 137 | bool forceInstantly = false) : base(@from, to, button, forceInstantly) 138 | { 139 | } 140 | } 141 | 142 | /// 143 | public class Up : Up 144 | { 145 | public Up( 146 | string @from, 147 | string to, 148 | int button, 149 | bool forceInstantly = false) : base(@from, to, button, forceInstantly) 150 | { 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Transitions/TransitionOnMouse.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 338a5ba99bf2c4841ac0e06837c84948 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Util.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b228f60823d9b154bab7c15f482c8bcb 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/Util/ITimer.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace UnityHFSM 3 | { 4 | public interface ITimer 5 | { 6 | float Elapsed 7 | { 8 | get; 9 | } 10 | 11 | void Reset(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Util/ITimer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5fe77c5f0300eea41aaed846bcd68741 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Util/Timer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityHFSM 4 | { 5 | /// 6 | /// Default timer that calculates the elapsed time based on Time.time. 7 | /// 8 | public class Timer : ITimer 9 | { 10 | public float startTime; 11 | public float Elapsed => Time.time - startTime; 12 | 13 | public void Reset() 14 | { 15 | startTime = Time.time; 16 | } 17 | 18 | public static bool operator >(Timer timer, float duration) 19 | => timer.Elapsed > duration; 20 | 21 | public static bool operator <(Timer timer, float duration) 22 | => timer.Elapsed < duration; 23 | 24 | public static bool operator >=(Timer timer, float duration) 25 | => timer.Elapsed >= duration; 26 | 27 | public static bool operator <=(Timer timer, float duration) 28 | => timer.Elapsed <= duration; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Util/Timer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2d43c26d97721964594885ff44f0aeac 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Visualization.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cc832bfdeef34377a606e353136ddc05 3 | timeCreated: 1742748147 -------------------------------------------------------------------------------- /src/Visualization/HfsmAnimatorGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2eb94d9c7c996574e8acc6d525a07749 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /src/Visualization/VisualizationNamespace.cs: -------------------------------------------------------------------------------- 1 | namespace UnityHFSM.Visualization 2 | { 3 | // Weirdly, when Unity builds a project and a namespace contains no classes (e.g. because of preprocessor 4 | // directives that remove code from the build), the namespace no longer exists. This can cause build problems 5 | // if it is referenced in a "using" directive, even when no editor-only class is used in the code. 6 | // The following class keeps this namespace alive, so that developers do not have to add a preprocessor 7 | // guard to their code when they reference this namespace. 8 | 9 | /// 10 | /// Helper class to keep the namespace available when building the project. 11 | /// 12 | internal class _VisualizationNamespacePlaceholder 13 | { 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Visualization/VisualizationNamespace.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bce2802bc82846e19c69ee65138ba677 3 | timeCreated: 1743100136 --------------------------------------------------------------------------------