├── .gitattributes ├── Code.meta ├── Code ├── Constants.cs ├── Constants.cs.meta ├── Interfaces.meta ├── Interfaces │ ├── IState.cs │ └── IState.cs.meta ├── StateMachine.cs ├── StateMachine.cs.meta ├── Transition.cs └── Transition.cs.meta ├── LICENSE.md ├── LICENSE.md.meta ├── NTC.FiniteStateMachine.asmdef ├── NTC.FiniteStateMachine.asmdef.meta ├── README.md ├── README.md.meta ├── package.json └── package.json.meta /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Code.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2df96cc133cb3dc4ca468e0abac985b3 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Code/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace NTC.FiniteStateMachine 2 | { 3 | internal static class Constants 4 | { 5 | internal const int DefaultCollectionSize = 16; 6 | } 7 | } -------------------------------------------------------------------------------- /Code/Constants.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: eec9d4b5f2824a00987b246f145f5727 3 | timeCreated: 1694889467 -------------------------------------------------------------------------------- /Code/Interfaces.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 991c6ef0693d4455b08f8298901d71a8 3 | timeCreated: 1694889917 -------------------------------------------------------------------------------- /Code/Interfaces/IState.cs: -------------------------------------------------------------------------------- 1 | namespace NTC.FiniteStateMachine 2 | { 3 | public interface IState 4 | { 5 | public TInitializer Initializer { get; } 6 | public void OnEnter() { } 7 | public void OnRun() { } 8 | public void OnExit() { } 9 | } 10 | } -------------------------------------------------------------------------------- /Code/Interfaces/IState.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 18d95537eb6a4f3d89fe5c6f8ec8ab03 3 | timeCreated: 1694889924 -------------------------------------------------------------------------------- /Code/StateMachine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NTC.FiniteStateMachine 5 | { 6 | public class StateMachine 7 | { 8 | private readonly Dictionary> _states = 9 | new Dictionary>(Constants.DefaultCollectionSize); 10 | 11 | private readonly List> _anyTransitions = 12 | new List>(Constants.DefaultCollectionSize); 13 | 14 | private readonly List> _transitions = 15 | new List>(Constants.DefaultCollectionSize); 16 | 17 | public StateMachine() 18 | { 19 | 20 | } 21 | 22 | public StateMachine(params IState[] states) 23 | { 24 | AddStates(states); 25 | } 26 | 27 | public bool TransitionsEnabled { get; set; } = true; 28 | 29 | public bool HasCurrentState { get; private set; } 30 | 31 | public bool HasStatesBeenAdded { get; private set; } 32 | 33 | public IState CurrentState { get; private set; } 34 | 35 | public Transition CurrentTransition { get; private set; } 36 | 37 | public void AddStates(params IState[] states) 38 | { 39 | #if DEBUG 40 | if (HasStatesBeenAdded) 41 | throw new Exception("States have already been added!"); 42 | 43 | if (states.Length == 0) 44 | throw new Exception("You are trying to add an empty state array!"); 45 | #endif 46 | foreach (var state in states) 47 | { 48 | AddState(state); 49 | } 50 | 51 | HasStatesBeenAdded = true; 52 | } 53 | 54 | public TState GetState() where TState : IState 55 | { 56 | return (TState) GetState(typeof(TState)); 57 | } 58 | 59 | public void SetState() where TState : IState 60 | { 61 | if (CurrentState is TState) 62 | return; 63 | 64 | SetState(typeof(TState)); 65 | } 66 | 67 | public void AddTransition(Func condition) 68 | where TStateFrom : IState 69 | where TStateTo : IState 70 | { 71 | #if DEBUG 72 | if (condition == null) 73 | throw new ArgumentNullException(nameof(condition)); 74 | #endif 75 | var stateFrom = GetState(typeof(TStateFrom)); 76 | var stateTo = GetState(typeof(TStateTo)); 77 | 78 | _transitions.Add(new Transition(stateFrom, stateTo, condition)); 79 | } 80 | 81 | public void AddAnyTransition(Func condition) 82 | where TStateTo : IState 83 | { 84 | #if DEBUG 85 | if (condition == null) 86 | throw new ArgumentNullException(nameof(condition)); 87 | #endif 88 | var stateTo = GetState(typeof(TStateTo)); 89 | 90 | _anyTransitions.Add(new Transition(null, stateTo, condition)); 91 | } 92 | 93 | public void SetStateByTransitions() 94 | { 95 | CurrentTransition = GetTransition(); 96 | 97 | if (CurrentTransition == null) 98 | return; 99 | 100 | if (CurrentState == CurrentTransition.To) 101 | return; 102 | 103 | SetState(CurrentTransition.To); 104 | } 105 | 106 | public void Run() 107 | { 108 | if (TransitionsEnabled) 109 | { 110 | SetStateByTransitions(); 111 | } 112 | 113 | if (HasCurrentState) 114 | { 115 | CurrentState.OnRun(); 116 | } 117 | } 118 | 119 | private void AddState(IState state) 120 | { 121 | #if DEBUG 122 | if (state == null) 123 | throw new ArgumentNullException(nameof(state)); 124 | #endif 125 | Type stateType = state.GetType(); 126 | #if DEBUG 127 | if (_states.ContainsKey(stateType)) 128 | throw new Exception($"You are trying to add the same state twice! The <{stateType}> already exists!"); 129 | #endif 130 | _states.Add(stateType, state); 131 | } 132 | 133 | private IState GetState(Type type) 134 | { 135 | if (_states.TryGetValue(type, out var state)) 136 | { 137 | return state; 138 | } 139 | 140 | throw new Exception($"You didn't add the <{type}> state!"); 141 | } 142 | 143 | private void SetState(Type type) 144 | { 145 | var state = GetState(type); 146 | 147 | SetState(state); 148 | } 149 | 150 | private void SetState(IState state) 151 | { 152 | if (HasCurrentState) 153 | { 154 | CurrentState.OnExit(); 155 | } 156 | 157 | CurrentState = state; 158 | HasCurrentState = true; 159 | CurrentState.OnEnter(); 160 | } 161 | 162 | private Transition GetTransition() 163 | { 164 | for (var i = 0; i < _anyTransitions.Count; i++) 165 | { 166 | if (_anyTransitions[i].Condition.Invoke()) 167 | { 168 | return _anyTransitions[i]; 169 | } 170 | } 171 | 172 | for (var i = 0; i < _transitions.Count; i++) 173 | { 174 | if (_transitions[i].From != CurrentState) 175 | { 176 | continue; 177 | } 178 | 179 | if (_transitions[i].Condition.Invoke()) 180 | { 181 | return _transitions[i]; 182 | } 183 | } 184 | 185 | return null; 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /Code/StateMachine.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 90a4af09a9324456a6fbc6634d295459 3 | timeCreated: 1653034665 -------------------------------------------------------------------------------- /Code/Transition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NTC.FiniteStateMachine 4 | { 5 | public class Transition 6 | { 7 | public readonly IState From; 8 | public readonly IState To; 9 | public readonly Func Condition; 10 | 11 | public Transition(IState from, IState to, Func condition) 12 | { 13 | From = from; 14 | To = to; 15 | Condition = condition; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Code/Transition.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 936bb8d6836572f408a69e2d2ce1cb74 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 MeeXaSiK (Night Train Code) nighttraincode@gmail.com 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: dfc73973858f2534a9c33c3d27e0de43 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /NTC.FiniteStateMachine.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NTC.FiniteStateMachine" 3 | } 4 | -------------------------------------------------------------------------------- /NTC.FiniteStateMachine.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c7a5a90242940f94990083eaabaf8aad 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚄 Finite State Machine 2 | [![License](https://img.shields.io/github/license/MeeXaSiK/FiniteStateMachine?color=318CE7&style=flat-square)](LICENSE.md) [![Version](https://img.shields.io/github/package-json/v/MeeXaSiK/FiniteStateMachine?color=318CE7&style=flat-square)](package.json) [![Unity](https://img.shields.io/badge/Unity-2021.3+-2296F3.svg?color=318CE7&style=flat-square)](https://unity.com/) 3 | 4 | This is a lightweight **Finite State Machine** for your **C#** projects 5 | * ▶️ High performance 6 | * ▶️ Supports transitions 7 | * ▶️ Abstracted from the game engine 8 | 9 | > Requires C# 8+ version 10 | 11 | # 🌐 Navigation 12 | 13 | * [Main](#-finite-state-machine) 14 | * [Installation](#-installation) 15 | * [How to use](#-how-to-use) 16 | * [Implementing StateMachine](#1-implement-statemachinetinitializer-field-or-property) 17 | * [Creating some states](#2-create-some-states) 18 | * [Installing states in your class](#3-install-states-in-your-class) 19 | * [Adding states to the StateMachine](#4-add-states-to-the-statemachine) 20 | * [Binding transitions for states](#5-bind-transitions-for-states) 21 | * [Launching the StateMachine](#6-launch-the-statemachine) 22 | * [Result](#what-should-be-the-result) 23 | * [Disabling Transitions](#-disabling-transitions) 24 | 25 | # ▶ Installation 26 | 27 | ## As a Unity module 28 | Supports installation as a Unity module via a git link in the **PackageManager** 29 | ``` 30 | https://github.com/MeeXaSiK/FiniteStateMachine.git 31 | ``` 32 | or direct editing of `Packages/manifest.json`: 33 | ``` 34 | "com.nighttraincode.fsm": "https://github.com/MeeXaSiK/FiniteStateMachine.git", 35 | ``` 36 | ## As source 37 | You can also clone the code into your Unity project. 38 | 39 | # 🔸 How to use 40 | 41 | ### 1. Implement `StateMachine` field or property 42 | 43 | `TInitializer` is the class to which the states will belong. 44 | 45 | The namespace you need is `using NTC.FiniteStateMachine`. 46 | 47 | ```csharp 48 | public readonly StateMachine StateMachine = new StateMachine(); 49 | ``` 50 | ```csharp 51 | public StateMachine StateMachine { get; } = new StateMachine(); 52 | ``` 53 | 54 | ### 2. Create some states 55 | 56 | Create some states for an entity and declare the methods you need: `OnEnter()`, `OnRun()`, `OnExit()`. 57 | 58 | | Method | Info | 59 | | ------ | ---- | 60 | | `OnEnter()` | Called once upon entering the state. | 61 | | `OnRun()` | Execution is determined by update methods. | 62 | | `OnExit()` | Called once when exiting the state. | 63 | 64 | For example, let's create two states: 65 | 66 | The `AwaitingState` is necessary for an entity to wait for some action. 67 | You also need to implement the `TInitializer` property. 68 | Let me remind you that the `TInitializer` is the class to which the state belongs. 69 | In this case, the initializer is `Sample`: 70 | 71 | ```csharp 72 | public class AwaitingState : IState 73 | { 74 | private readonly IFollower _follower; 75 | 76 | public AwaitingState(IFollower follower, Sample sample) 77 | { 78 | _follower = follower; 79 | Initializer = sample; 80 | } 81 | 82 | public Sample Initializer { get; } 83 | 84 | public void OnEnter() 85 | { 86 | _follower.StopFollow(); 87 | } 88 | } 89 | ``` 90 | 91 | The `FollowingState` is necessary for the entity to follow a target: 92 | 93 | ```csharp 94 | public class FollowingState : IState 95 | { 96 | private readonly IFollower _follower; 97 | private readonly Func _getTarget; 98 | 99 | public FollowingState(IFollower follower, Func getTarget, Sample sample) 100 | { 101 | _follower = follower; 102 | _getTarget = getTarget; 103 | Initializer = sample; 104 | } 105 | 106 | public Sample Initializer { get; } 107 | 108 | public void OnRun() 109 | { 110 | Transform target = _getTarget.Invoke(); 111 | 112 | if (target != null) 113 | { 114 | _follower.Follow(target.position); 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | ### 3. Install states in your class 121 | 122 | This **Finite State Machine** is abstracted from any game engine, but I'll show you how it works with an example in Unity: 123 | 124 | ```csharp 125 | [RequireComponent(typeof(IFollower))] 126 | public class Sample : MonoBehaviour 127 | { 128 | [SerializeField] private Health health; 129 | 130 | public StateMachine StateMachine { get; } = new StateMachine(); 131 | public Transform Target { get; set; } 132 | 133 | private AwaitingState _awaitingState; 134 | private FollowingState _followingState; 135 | 136 | private void Awake() 137 | { 138 | InstallStates(); 139 | } 140 | 141 | private void InstallStates() 142 | { 143 | var follower = GetComponent(); 144 | 145 | _awaitingState = new AwaitingState(follower, this); 146 | _followingState = new FollowingState(follower, GetTarget, this); 147 | } 148 | 149 | private Transform GetTarget() 150 | { 151 | return Target; 152 | } 153 | } 154 | ``` 155 | 156 | ### 4. Add states to the StateMachine 157 | 158 | You can add states using the `AddStates` method or the class constructor. 159 | 160 | ```csharp 161 | StateMachine.AddStates(_awaitingState, _followingState); 162 | ``` 163 | ```csharp 164 | StateMachine = new StateMachine(_awaitingState, _followingState); 165 | ``` 166 | 167 | > **Warning!** You can only add states to the `StateMachine` once! 168 | 169 | ### 5. Bind transitions for states 170 | 171 | We have methods for binding transitions such as `AddTransition` and `AddAnyTransition`. 172 | 173 | | Method | Info | 174 | | ------ | ---- | 175 | | `AddTransition(Func condition)` | Takes two states as arguments, from which state we want to go to the second state and the transition condition. | 176 | | `AddAnyTransition(Func condition)` | Takes as arguments the one state we want to switch to and the transition condition. | 177 | 178 | Let's imagine a situation: 179 | 180 | We want to switch from the `AwatingState` to the `FollowingState` if a target is found. We also want to switch back to the `AwaitingState` if the target is lost. `AddTransition` method will help us with this, let's add two transitions: 181 | 182 | From `AwaitingState` to `FollowingState` 183 | 184 | ```csharp 185 | StateMachine.AddTransition(condition: () => Target != null); 186 | ``` 187 | 188 | From `FollowingState` to `AwaitingState` 189 | 190 | ```csharp 191 | StateMachine.AddTransition(condition: () => Target == null); 192 | ``` 193 | 194 | We will also add a transition to the `AwaitingState` from any state. 195 | Why might this be needed? If the entity is not alive, then it is logical that it will not be able to move and will have to switch to the idle state. 196 | 197 | Add transition from any state to idle `AwaitingState` 198 | 199 | ```csharp 200 | StateMachine.AddAnyTransition(condition: () => health.IsAlive == false); 201 | ``` 202 | 203 | ### 6. Launch the StateMachine 204 | 205 | For the launch of `StateMachine` you need to set first state and call the `Run()` method in `Update()`: 206 | 207 | ```csharp 208 | private void Start() 209 | { 210 | StateMachine.SetState(); 211 | } 212 | 213 | private void Update() 214 | { 215 | StateMachine.Run(); 216 | } 217 | ``` 218 | 219 | ### What should be the result 220 | 221 | ```csharp 222 | using NTC.FiniteStateMachine; 223 | using UnityEngine; 224 | 225 | [RequireComponent(typeof(IFollower))] 226 | public class Sample : MonoBehaviour 227 | { 228 | [SerializeField] private Health health; 229 | 230 | public StateMachine StateMachine { get; } = new StateMachine(); 231 | public Transform Target { get; set; } 232 | 233 | private AwaitingState _awaitingState; 234 | private FollowingState _followingState; 235 | 236 | private void Awake() 237 | { 238 | InstallStates(); 239 | 240 | BindTransitions(); 241 | 242 | BindAnyTransitions(); 243 | 244 | StateMachine.SetState(); 245 | } 246 | 247 | private void Update() 248 | { 249 | StateMachine.Run(); 250 | } 251 | 252 | private void InstallStates() 253 | { 254 | var follower = GetComponent(); 255 | 256 | _awaitingState = new AwaitingState(follower, this); 257 | _followingState = new FollowingState(follower, GetTarget, this); 258 | 259 | StateMachine.AddStates(_awaitingState, _followingState); 260 | } 261 | 262 | private Transform GetTarget() 263 | { 264 | return Target; 265 | } 266 | 267 | private void BindTransitions() 268 | { 269 | StateMachine.AddTransition(condition: () => Target != null); 270 | StateMachine.AddTransition(condition: () => Target == null); 271 | } 272 | 273 | private void BindAnyTransitions() 274 | { 275 | StateMachine.AddAnyTransition(condition: () => health.IsAlive == false); 276 | } 277 | } 278 | ``` 279 | 280 | # 🔸 Disabling Transitions 281 | 282 | If you want to set states manually, you can disable `TransitionsEnabled` in the `StateMachine`: 283 | 284 | ```csharp 285 | StateMachine.TransitionsEnabled = false; 286 | ``` 287 | 288 | Then you can change state of `StateMachine` by method `SetState()`: 289 | 290 | ```csharp 291 | StateMachine.SetState(); 292 | ``` 293 | 294 | Also if you don't want the `StateMachine` to choose the state in the update method, you can use the method `SetStateByTransitions()` when you need: 295 | 296 | ```csharp 297 | StateMachine.SetStateByTransitions(); 298 | ``` 299 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8ba3cdc033f10d0419ac6180d5c626e8 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.nighttraincode.fsm", 3 | "author": 4 | { 5 | "name": "Night Train Code", 6 | "url": "https://www.youtube.com/@NightTrainCode/" 7 | }, 8 | "displayName": "Finite State Machine", 9 | "description": "This is a lightweight Finite State Machine.", 10 | "unity": "2021.3", 11 | "version": "2.0.1", 12 | "keywords": [ 13 | "finitestatemachine", 14 | "statemachine", 15 | "fsm", 16 | "nighttraincode", 17 | "performance", 18 | "ai" 19 | ], 20 | "dependencies": {}, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/MeeXaSiK/FiniteStateMachine.git" 24 | } 25 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c6291bedb63c46c428db6ab5bdd79bb6 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------