Search Unity

Very Simple StateMachine Pattern - C#

Discussion in 'Scripting' started by rozgo, Jan 14, 2010.

  1. rozgo

    rozgo

    Joined:
    Feb 7, 2008
    Posts:
    158
    This is a stupid simple state machine pattern that works great with simple setups. Its no where near as featured as my other state machines, but still elegant. Keeps your state data scoped and your logic nicely inlined using coroutines. You can even change states from the inspector.

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class Monster : MonoBehaviour {
    5.  
    6.     public enum State {
    7.         Crawl,
    8.         Walk,
    9.         Die,
    10.     }
    11.  
    12.     public State state;
    13.  
    14.     IEnumerator CrawlState () {
    15.         Debug.Log("Crawl: Enter");
    16.         while (state == State.Crawl) {
    17.             yield return 0;
    18.         }
    19.         Debug.Log("Crawl: Exit");
    20.         NextState();
    21.     }
    22.  
    23.     IEnumerator WalkState () {
    24.         Debug.Log("Walk: Enter");
    25.         while (state == State.Walk) {
    26.             yield return 0;
    27.         }
    28.         Debug.Log("Walk: Exit");
    29.         NextState();
    30.     }
    31.  
    32.     IEnumerator DieState () {
    33.         Debug.Log("Die: Enter");
    34.         while (state == State.Die) {
    35.             yield return 0;
    36.         }
    37.         Debug.Log("Die: Exit");
    38.     }
    39.  
    40.     void Start () {
    41.         NextState();
    42.     }
    43.  
    44.     void NextState () {
    45.         string methodName = state.ToString() + "State";
    46.         System.Reflection.MethodInfo info =
    47.             GetType().GetMethod(methodName,
    48.                                 System.Reflection.BindingFlags.NonPublic |
    49.                                 System.Reflection.BindingFlags.Instance);
    50.         StartCoroutine((IEnumerator)info.Invoke(this, null));
    51.     }
    52.    
    53. }
    54.  
     
    teknic likes this.
  2. happybomb

    happybomb

    Joined:
    Apr 22, 2010
    Posts:
    21
    Very handy! Is there a way of calling a state method when you need to other than automatically every frame using a co-routine?
     
  3. mcroswell

    mcroswell

    Joined:
    Jan 6, 2010
    Posts:
    54
    happybomb, I think the idea is that you just change the public state variable.

    Still, I would also like to know if this isn't using up cpu's? Maybe sleep in the while-loop would be good?

    -Mike
     
  4. theBrandonWu

    theBrandonWu

    Joined:
    Sep 3, 2009
    Posts:
    244
    I am trying to understand this code but ran into an error that I don't quite understand. It keeps giving me an "NullReferenceException: Object reference not set to an instance of an object" error on this line.

    Code (csharp):
    1.          StartCoroutine((IEnumerator)info.Invoke(this, null));
    ...?
     
  5. Deleted User

    Deleted User

    Guest

  6. justinlloyd

    justinlloyd

    Joined:
    Aug 5, 2010
    Posts:
    1,679
    If you haven't looked at it already, I recommend checking out the Stateless project. Works great with Unity.
     
  7. theBrandonWu

    theBrandonWu

    Joined:
    Sep 3, 2009
    Posts:
    244
  8. justinlloyd

    justinlloyd

    Joined:
    Aug 5, 2010
    Posts:
    1,679
    Yes, that's the one. To convert to Unity you need to change the resource strings to hard-coded strings for the exceptions that are thrown. It takes about 10 minutes. That was the only change I had to make.

    The documentation isn't great unfortunately, giving just the most basic telephone call example, and the project, as you have no doubt experienced, can be difficult to track down due to its naming.
     
  9. saarbruck

    saarbruck

    Joined:
    Sep 23, 2011
    Posts:
    3
    My experience with stateless is that it generates ~250 bytes of garbage per state transition, largely due to its use of LINQ. We're also having trouble using it on iOS. (we're running out of "trampolines" possibly due to the deep call stacks). I pulled LINQ out and got the garbage generation down to zero, but have only started looking at the second issue.
     
  10. Metron

    Metron

    Joined:
    Aug 24, 2009
    Posts:
    999
    if anyone is interested, I'm willing to share my rough C# implementation of SCXML ... drop me a pm...
     
  11. justinlloyd

    justinlloyd

    Joined:
    Aug 5, 2010
    Posts:
    1,679
    Sorry, I haven't. I have not yet had need to use stateless on iOS (stuck on mostly Windows projects this past year or so with Unity). For iOS, and have state machine usage, I am not sure Stateless is the best option. You may want to look in to AForge or snag one of the many other lighter-weight, less generic state machine libraries out there.
     
  12. BSECaleb

    BSECaleb

    Joined:
    Jan 29, 2009
    Posts:
    72
    Did you ever resolve the issue with the trampolines? We use stateless for non-unity related .net here and would love to use it with unity as well...
     
  13. DannyB

    DannyB

    Joined:
    Jun 20, 2012
    Posts:
    214
    Stateless looks great. Is it really not usable for Unity iOS?
     
  14. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,413
    Nice script. Here's my simple change that will send a StateChanged event and has optional logging when a state enters.

    http://pastebin.com/hae9v4s7

    Here it is in template format. You can make it the default new script. (C:\Program Files (x86)\Unity4.1.5f1\Editor\Data\Resources\ScriptTemplates\81-C# Script-NewBehaviourScript.cs.txt).

    http://pastebin.com/xBe6Q9ay
     
  15. superme2012

    superme2012

    Joined:
    Nov 5, 2012
    Posts:
    207
    Dude this is what I was looking for,

    This line is a little new to me, never used code like It before:

    Code (csharp):
    1.    System.Reflection.MethodInfo info = GetType().GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    I understand what its results to, but not sure about BindingFlags.NonPublic and BindingFlags.Instance?

    And this runs the resulting “IEnumerator”:

    Code (csharp):
    1. StartCoroutine((IEnumerator)info.Invoke(this, null));
    Sweet!!

    I'm looking for a central controller for each game object as a module component and I think this is the way to do it. I did a few tests and then noticed State's are the main structure to the logic.

    I'm still a little new to C# and any light on how this works would be like very interesting.
     
  16. Apprentice

    Apprentice

    Joined:
    Feb 9, 2012
    Posts:
    74
    For simple state machine people just use a switch in their update... For a more complex one I would use a baseclass called state which has 2 empty functions: void ChangeState(State newState, State prevState);, void Execute(Entity AIDude);. You then just create inside your mainclass a State and call its execute function while giving your own reference per this statement. ir works very well and there is no reflection magic required.

    Edit: reflection shouldnt be done frequently.:roll:

    This is where I learned the basic stuff: http://www.amazon.com/Programming-Game-Example-Mat-Buckland/dp/1556220782
     
    Last edited: Aug 23, 2013
  17. superme2012

    superme2012

    Joined:
    Nov 5, 2012
    Posts:
    207
    Ah, I see.. so with this approach only one state can be run at any one given time?

    I originally had this idea of a switch board, where one class word be responsible for turning states on and off. And having a structure to utilise this approach.
     
  18. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,562
    The other thing I'd be careful with in the poster's example is this guy:

    Code (csharp):
    1.  
    2. string methodName = state.ToString() + "State";
    3.  
    You're going to be generating garbage doing those string concatenations every time you change states. A better approach would probably be to do something like store your state method names globally in a Dictionary using the the State (or integer value of it since it's an enum) as the key, then do:

    Code (csharp):
    1.  
    2.  
    3. string methodName = allMethods[state];
    4.  
    5. //OR if you're using Ints
    6.  
    7. string methodName = allMethods[(int)state];
    8.  
    9.  
     
  19. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,562
    You could pretty easily create your own generic StateMachine with a single code base that would allow your "States" to be any Enum type. Now, you can't exactly restrict generic parameters to Enums but you can whittle it down to struct, IConvertable and then check the type of T on instantiation. If you want something a little more dynamic, you could create a base class. Here's an example that just takes what you put into it, sets it as the current state, and returns it. Obviously this isn't very useful, but it's just a demonstration.

    Code (csharp):
    1.  
    2. using System;
    3.  
    4. namespace StateMachine
    5. {
    6.     public class Machine<T> where T : struct, IConvertible
    7.     {
    8.         protected T _currentState;
    9.  
    10.         public Machine()
    11.         {
    12.             if (!typeof(T).IsEnum)
    13.             {
    14.                 throw new ArgumentException("T must be an enumeration");
    15.             }
    16.         }
    17.  
    18.         public virtual T InOut(T input)
    19.         {
    20.             _currentState = input;
    21.             return input;
    22.         }
    23.     }
    24. }
    25.  
    Now, if you wanted, you could create a separate StateMachine for every enemy... so let's say you have a Enum type called: "EnemyState" that looks like this:

    Code (csharp):
    1.  
    2. public enum EnemyState
    3. {
    4.       Idle,
    5.       Sleeping,
    6.       Patrolling
    7. }
    8.  
    You could actually create an implementation of the base:

    Code (csharp):
    1.  
    2. public class EnemyMachine : Machine<EnemyState>
    3. {
    4.     //Your logic here
    5. }
    6.  
    So you could easily encapsulate some base logic in the base class but use your derived class for any very specific functionality and all you have to do is call:

    Code (csharp):
    1.  
    2.     EnemyMachine eMachine = new EnemyMachine;
    3.  
    And this will automatically be typed to "EnemyState" as that's what you specified when inheriting the base class.
     
    Last edited: Aug 23, 2013
    Rodolinc likes this.
  20. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,182
    there are some workarounds for this but none is really convenient. i don't want to advertise them just for information of interested people. i think this should be added to c# quickly but then it still takes a while until mono catches up and then it takes ages until ut incorporates it in unity ;).
     
  21. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,562
    Generics have been around awhile now. It's not likely that Enums will be added. I'm sure there's a reason they're not now. In fact, you can't restrict to any value type in particular. You're limited to saying where T : struct (which will only allow value types) and IConvertible which Enum implements. I used this "workaround" in my example above by then checking to see if the type was an Enum in the constructor. It's not really a constraint, but allows you to do the double check. It's certainly nothing you should be worried about using in your own code as long as you document the intent.
     
  22. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    29
    @rozgo Thanks for posting. This is a great foundation on which to build!
     
  23. Deleted User

    Deleted User

    Guest

    glenneroo likes this.
  24. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    29
    Here's a multipurpose state machine class based on the original post. It's intended to operate like a black box base class and run callback routines based on state/transition changes. Both states and transitions can be configured as needed. Oh, and it runs garbage free (tested with 2018.1/Win 7).

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Globalization;
    5. using UnityEngine;
    6.  
    7. public class StateMachine : MonoBehaviour
    8. {
    9.     public enum State
    10.     {
    11.         A,
    12.         B,
    13.         C,
    14.     }
    15.  
    16.     public enum Transition
    17.     {
    18.         ENTER,
    19.         ARRIVE,
    20.         EXIT
    21.     }
    22.  
    23.  
    24.     public State state;
    25.     public bool debugStates;
    26.     protected delegate IEnumerator Routine();
    27.     public void SetState(int state) { this.state = (State)state; }// for easy connection to UI dropdown
    28.     Dictionary<int, Dictionary<Transition, Routine>> states = new Dictionary<int, Dictionary<Transition, Routine>>();
    29.  
    30.     void Start()
    31.     {
    32.         InitStateMachine();
    33.  
    34.         //// Test
    35.         AssignCoroutine(State.A, Transition.ENTER, TestRoutine);
    36.         //AssignCoroutine(State.B, Transition.ENTER, TestRoutine);
    37.         AssignCoroutine(State.C, Transition.ENTER, TestRoutine);
    38.  
    39.         StartStateMachine();
    40.     }
    41.  
    42.     protected void InitStateMachine()
    43.     {
    44.         foreach (int key in Enum.GetValues(typeof(State)))
    45.         {
    46.             var transitions = new Dictionary<Transition, Routine>();
    47.             foreach (Transition t in Enum.GetValues(typeof(Transition)))
    48.             {
    49.                 Routine r = null;
    50.                 transitions.Add(t, r);
    51.             }
    52.             states.Add(key, transitions);
    53.         }
    54.     }
    55.  
    56.     protected void StartStateMachine()
    57.     {
    58.         StartCoroutine(Evaluate());
    59.     }
    60.  
    61.     protected void AssignCoroutine(State state, Transition transition, Routine routine)
    62.     {
    63.         states[(int)state][transition] = routine;
    64.     }
    65.  
    66.     protected void AddCoroutine(State state, Transition transition, Routine routine, bool clearExisting = false)
    67.     {
    68.         if (clearExisting) { states[(int)state][transition] = null; }
    69.         states[(int)state][transition] += routine;
    70.     }
    71.  
    72.     protected IEnumerator Evaluate()
    73.     {
    74.         while (true)
    75.         {
    76.             for (int i = 0; i < states.Count; i++)
    77.             {
    78.                 if ((int)state == i)
    79.                 {
    80.                     if (debugStates) { Debug.Log("ENTER: " + (State)i + "\n"); }
    81.                     if (states[i][Transition.ENTER] != null) { yield return states[i][Transition.ENTER].Invoke(); }
    82.  
    83.                     if (debugStates) { Debug.Log("ARRIVE: " + (State)i + "\n"); }
    84.                     if (states[i][Transition.ARRIVE] != null) { yield return states[i][Transition.ARRIVE].Invoke(); }
    85.  
    86.                     while ((int)state == i) { yield return null; } // yield until state change
    87.  
    88.                     if (debugStates) { Debug.Log("EXIT: " + (State)i + "\n"); }
    89.                     if (states[i][Transition.EXIT] != null) { yield return states[i][Transition.EXIT].Invoke(); }
    90.                 }
    91.             }
    92.         }
    93.     }
    94.  
    95.     IEnumerator TestRoutine()
    96.     {
    97.         Debug.Log("Test Routine\n");
    98.         yield return new WaitForSeconds(1); // Simulate processing
    99.         yield break;
    100.     }
    101. }
    102.  
     
    Last edited: Jul 21, 2018
    aripa and Viggy1996 like this.
unityunity