Need help with a State Machines where each state is its own Script...

Hey guys,

New Unity Dev here, trying to make my code at work cleaner and well more modular and faster by having the ability to have many people work on the project at once instead of 1 person.

A little background info.

I am doing Tutorials that are broke up into Lessons, In each lesson there are different ‘States’ In these states is where we do scaleform things and some simple unity Camera work and AI stuff.

Right now we have a Psuedo state machine in C# where its a HUGE switch statement, Instead each Case of the statement we have the normal stuff for a state machine ie:


    if(!Init)
    {
      //Init Stuff
    }
    else if(Exiting)
    {
      //Turn off and clean stuff up
    }
    
    else
    {
      //Your Update loop, make stuff happen here.
    }

Problem with this is, if the Lesson has 10000 different ‘States’ thats one MASSIVE switch statement and only 1 person can be working on that Lesson at a time. If I have a State machine where every ‘State’ is its own script, then i can split the work load between the employees and increase productivity and speed at which we develop these lessons.

I know I need 2 main scripts, the StateMachine it self, and then the BaseState that all other states are derived from.

My question is how would I go about this, when I switch states, I need the state machine to be able to know which states he has for that lesson IE: Lesson1_State1 to Lesson1_State20, and I also need the state machine to be able to call each of the seperate scripts functions. And in the StateMachine I need it to be able to just do like CurrentState.OnEnter() and CurrentState.Update(), etc etc…

Is there any issues with this, or How can I connect the Enum or the ‘CurrentState’ with a seperate script file?

I don't quite get it. If you want to have multiple workers on the same file, versioning systems like svn or mercurial should do the trick. If you want each state to have its own script, you can make vital variables of the main class public (I don't recall c# having friend classes) and make a new script for each state. Then, on each state, you just call the specific script's "doStuff()" method. Might use inheritance to make life easier. One point to note: will each script be a MonoBehaviour or a normal class? It's a tricky question =)

I've written a pretty good answer. Just waiting for it to be approved by a moderator :)

@Misaacs The code you showed somewhat remembers me about the objects for states pattern (http://en.wikipedia.org/wiki/State_pattern). It can reduce the size of the swicth statement, but I don't know if it is worth the extra complexity.

2 Answers

2

Okay, so this is how you would build the bare bones state machine… This wont fix your problem at hand, that s for the bin. Instead , take this and build your old needs into the way this is pipelined.

Firstly, you will need a good folder structure as we going to utilize namespaces (our own)

So, folder structure like this please…
Inside your Assets windows in Unity 3D make a folder and name it…“Code”, inside the “Code” folder make a directory of other folders named “Interfaces” “Scripts” “States”.
at this point you are good to go.
in your “Interfaces” folder create a new C# script, name it “IStateBase” (I for Interface).
in that script place this code…

using System;
namespace Assets.Code.Interfaces
{
	public interface IStateBase
	{
		//Interface implementable methods
		//base declarations should be placed here to derive out to classes inheriting form IStateBase
		void StateUpdate();

	}
}

Save that.
Now in your “Scripts” folder create a new C# script and name it “StateManager” or something like that, keeping it obvious is best.
in there paste this code below…

using UnityEngine;
// inherit state classes
using Assets.Code.States;
//inherit interface class
using Assets.Code.Interfaces;



public class StateManager : MonoBehaviour 
{
	//declare a private state class to cache and work on inside script , taken from the namespace declared at the top
	private IStateBase iActiveState; //activeState is reference to BeginState class
	
	private static StateManager instanceRef;

	// Use this for initialization
	void Start () 
	{
		//instantiate activeState object for use
		iActiveState = new BeginState(this);
		//Debug.Log ("This is an object of type: " + activeState);
	}
	
	
	// Update is called once per frame
	void Update () 
	{
		if(iActiveState != null)
		{
			iActiveState.StateUpdate();
		}
	}
	
	//publically available method to execute that will return a new state when executed 
	public void SwitchState(IStateBase newState)
	{
		iActiveState = newState;
	}
	
}

Save that!

Now, in your States folder, make some derived state classes, starting with this one. I named it BeginState, you can name it what you like but keeping these obvious works best really.

using UnityEngine;
using Assets.Code.Interfaces;
using Assets.Code;

namespace Assets.Code.States
{
	public class BeginState : IStateBase
	{
		//declare a statemanger class 
		private StateManager stateManager;
		private float futureTime = 0.0f;
		private int countdown = 0;

		private float screenDuration = 8.0f;
		//private static GUIHelper guiHelper;

		public BeginState(StateManager stateManagerRef) //constructor
		{
			//assign the argument delivered through stateManagerRef to the private instance of our StateManager class
			stateManager = stateManagerRef;
			//check we are in the start scene 
			if(Application.loadedLevelName != "Scene0")
			{
				Application.LoadLevel ("Scene0");
			}
			//assign futureTime the value derived from adding screenDuration to Unity`s Time.realtime variable
			futureTime = screenDuration + Time.realtimeSinceStartup;
			Debug.Log ("constructing BeginState");
		}
		//implementable methods declared in IStateBase and must be declared in any class using the IStateBase class
		//this simply means anything in IStateBase must be carried over to the class implementing it.

		//this is why it must be a base class that has derived IStates so we can deal with specific things set up form there
		public void StateUpdate()
		{
			//cache the value of this exact moment in time
			float asOfNow = Time.realtimeSinceStartup;
			//countdown
			countdown = (int)futureTime - (int)asOfNow;
			//timing above here
			//input conditional
			if(Input.GetKeyUp (KeyCode.Space) || countdown <= 0)
			{
				SwitchOver();
			}

			//just test if countdown is running in console - non-important code 
			if(Input.GetKeyUp(KeyCode.F))
			{
				Debug.Log (countdown.ToString());
			}
		}
/// <summary>
/// this on GUI method could be implemented through the interface IStateBase, as long as an OnGUI methood in the statemanager class called the replacement method that covers GUI in the Interface class
/// </summary>
		void OnGUI()
		{
			//GUI.BeginGroup(new Rect(guiHelper.FullSizeRect));
			if(GUI.Button(new Rect(10, 10, 500, 100), " Assign a Game Scene to this button"))
			{

					SwitchOver ();
			}
			//show a countdown timer display
			GUI.Box (new Rect(Screen.width / 2, 10, 150, 100), countdown.ToString());
			//GUI.EndGroup();
		}
/// <summary>
/// SwitchOver method made to change state level or scene. (could be another way to implement this? (IStateBase derived maybe or maybe not?))
/// </summary>
		void SwitchOver()
		{
			//Application.LoadLevel ("Scene1");
			//Debug.Log (Application.isLoadingLevel + Application.loadedLevelName);

			stateManager.SwitchState (new PlayState(stateManager));
		}

	}
}

Save that!

In your “States” folder make another C# state class, i`m calling this PlayState as the class names are becoming important for state transactions in the code (as can be seen in the above SwitchOver method code)…anyway.

paste the below in to your PlayState class…

using UnityEngine;
using Assets.Code.Interfaces;

namespace Assets.Code.States
{
	public class PlayState : IStateBase
	{
		//declare a statemanger class 
		private StateManager stateManager;
		public PlayState(StateManager stateManagerRef) //constructor
		{
			//assign the argument delivered through stateManagerRef to the private instance of our StateManager class
			stateManager = stateManagerRef;
			Debug.Log ("constructing PlayState");
		}
		//implementable methods declared in IStateBase and must be declared in any class using the IStateBase class
		//this simply means anything in IStateBase must be carried over to the class implementing it.

		//this is why it must be a base class that has derived IStates that specifically deal with specific things
		public void StateUpdate()
		{
			//if spacebar hit, switch state (same for all IStateBase state classes)
			if(Input.GetKeyUp (KeyCode.Space))
			{
				stateManager.SwitchState (new WonState(stateManager));
			}



		}
	}
}

Save That !

Here is another state, but by now, and with your own knowledge of staet machines , you shold be able to see that all im doing is what you did in your state machine, except I have taken the leg work out a single operation and given a set of classes some commonly implementable methods to handle the data (well one method in this demonstrative case to show that you can do this, I cant write the whole thing for you im afraid, I dont know what you are trying to make lol)
Anyway, the other demo state class…

using UnityEngine;
using Assets.Code.Interfaces;

namespace Assets.Code.States
{
	public class WonState : IStateBase
	{
		//declare a statemanger class 
		private StateManager stateManager;
		public WonState(StateManager stateManagerRef) //constructor
		{
			//assign the argument delivered through stateManagerRef to the private instance of our StateManager class
			stateManager = stateManagerRef;
			Debug.Log ("constructing WonState");
		}
		//implementable methods declared in IStateBase and must be declared in any class using the IStateBase class
		//this simply means anything in IStateBase must be carried over to the class implementing it.

		//this is why it must be a base class that has derived IStates that specifically deal with specific things
		public void StateUpdate()
		{
			if(Input.GetKeyUp (KeyCode.Space))
			{
				stateManager.SwitchState (new BeginState(stateManager));
			}
		}

		void OnGUI()
		{
			if(GUI.Button(new Rect(Screen.width / 2, Screen.height / 2, 150, 100), "Press Me"))
			{
				//Debug.Log (Application.isLoadingLevel)
				//Debug.Log (Application.loadedLevelName.ToString());
				stateManager.SwitchState (new BeginState(stateManager));
			}

		}

	}
}

Anyhoo, the bottomline to all this, is…
If you do Not know how to implement a state machine like this, you will undoubtedly come up against issue that reflect its initial planning. These are not super complicated, but do require a slightly different approach to using Unity (You might notice that for the most we are actually running classes outside of MonoBehaviour, where StateManager is handling the pipeline to MonoBehaviour after the classes have executed)

So, I hope this has helped you in some way. Your other options are delegates and the way you have tried already but, if you can get your head round what i`ve laid out here (and im happy to keep conversing about it), you cant go far wrong and should easily see how you can transfer your current state system to this optimized and scalable solution.

take care bud and thanks for reading this long winded Answer.

Gruffy :slight_smile:

P.S.
to se this work you would need to make two scenes and names them “Scene0” and “Scene1” repspectively.
You would also have an empty gameobject in “Scene0” with the “StateManager” script attached to it.
Run in Editor when “Scene0” is the active scene in the Unity Editor.

I doubt the original poster reads this anymore. good answer none the less. As for the usage of interfaces, I would rather inherit from an abstract base state and add interfaces to the inheriting classes. This makes it easier to share implementation between all the states.

There are two things you can do here.

1.) Is learn how to use a version control system as was mentioned in the comments above. You have a choice of SVN, Mercurial, Git and even the Unity Asset server if you have purchased it. For scripts though I prefer using Git and Github. It’s really awesome and worth learning.

2.) Make the state manager set a public “currentGameState” variable which can then be used in other scripts to know what code to run. I recommend using an ENUM as well just to make it so that your state machine stays tidy and its easy to reference states all around the place.

simple example.


    public enum GameState { State1, State2, State3};
    public GameState currentGameState = GameState.State1;

Do a whatever logic you want to set that current game state however you like. I, in my games, tend to also use a switch statement.


                    switch (TestedSituationWhichChangesState)
                    {
                        case TestedSituation1:
                            currentGameState = GameState.State1;
                            Debug.Log("Setting Game State 1");
                            break;
                        case TestedSituation2:
                            currentGameState = GameState.State2;
                            Debug.Log("Setting Game State 2");
                            break;
                        case TestedSituation3:
                            currentGameState = GameState.State3;
                            Debug.Log("Setting Game State 3");
                            break;
                     }

You can obviously use whatever logic you want above. The important thing is that you set the public variable of currentGameState to whatever state you want it to be.

You then just test for the condition when you run the code you want to run in another script. Make sure you reference the script with the currentGameState variable in it and test it however you like. An “if” statement is the simplest example but you can obviously do it with anything.


    private StateMachineScriptName stateMachine;
    
    void OnAwake()
    {
        stateMachine = GameManager.FindGameObject("NameOfGameObjectWhichHasTheStateMachineScriptOnIt").GetComponent<StateMachineScriptName>();
    
    }
    
    if(stateMachine.currentGameState = StateMachineScriptName.GameState.State1)
    {
        //Do State1 Game Code
    }

Does that help?