I am personally not a huge fan of switch statements in c# code, and get a little annoyed when I see state machines running with them. Using them is not wrong, I just have my own gripes against it and never implement it that way when I see it in a course. Sometimes it requires a little more thinking, but what I do for simple state machines is to use a delegate pattern instead.
Level 1
Let’s take a simple example. Let’s say we have an enemy that is randomly roaming the world and will chase the player if the player gets within a specific range. It will also stop chasing when the player is no longer in range and start roaming again. Very simple example. I left out the code that doesn’t matter here
public enum EnemyState
{
Roaming,
Chasing
}
private EnemyState _currentState = EnemyState.Roaming;
private void Update()
{
switch (_currentState)
{
case EnemyState.Roaming:
RoamTheLand();
break;
case EnemyState.Chasing:
ChaseThePlayer();
break;
}
}
private void RoamTheLand()
{
// Code to roam the land
// some code to detect the player
if (PlayerIsInRange())
{
// Switch to chasing state
_currentState = EnemyState.Chasing;
}
}
private void ChaseThePlayer();
{
// Code to chase the player
// some code to detect the player
if (!PlayerIsInRange())
{
// Switch to roaming state
_currentState = EnemyState.Roaming;
}
}
That’s it. On every update the enemy will roam the land until the player gets within a certain distance (or whatever the code does) and then switch over to chasing the player until the player is no longer within that condition. Then it switches back to roaming.
I have written this in a way that is realy easy to convert to the delegate pattern I mentioned earlier. Delegates are like addresses to methods. Instead of having an enum
that holds the state we’re currently in, we’ll just change the address of the method that is doing the work. We’ll use the Action
delegate found in the System
namespace
private Action _currentState;
private void Awake()
{
// Set the starting state. Note that this is a delegate, we're not calling the method here (no parentheses)
_currentState = RoamingTheLand;
}
private void Update()
{
// Call the current state delegate
_currentState?.Invoke();
}
private void RoamTheLand()
{
// Code to roam the land
// some code to detect the player
if (PlayerIsInRange())
{
// Switch to chasing state - we change the delegate to the other method
_currentState = ChaseThePlayer;
}
}
private void ChaseThePlayer();
{
// Code to chase the player
// some code to detect the player
if (!PlayerIsInRange())
{
// Switch to roaming state - we change the delegate to the other method
_currentState = RoamingTheLand;
}
}
That’s it. A simple state machine without switch statements.
Level 2
Of course you don’t need to use a delegate, you can use a little more complex system as well. Instead of the delegate, we will create a State
class that will be able to perform some actions when the state is entered and exited. For smaller state machines this is not critical, but i will create an interface for this state. Note that this is partly based on the state machine used in the Third Personal Combat and Traversal course and if you haven’t checked it out yet, you should definitely try. It’s the Level 3 that won’t be in this post
public interface IState
{
void Enter();
void Tick(float deltaTime);
void Exit();
}
The interface here has an Enter
and Exit
method that we will call when switching to and from states. This allows for some state setup and cleanup. The Tick
method will be executed on every update
Next, our states. We will stick with the above example
public class RoamingState : IState
{
// We keep a reference to the enemy this state belongs to
private Enemy _parent;
// Constructor that takes this state's parent
public RoamingState(Enemy parent)
{
_parent = parent;
}
public void Enter()
{
// some code to setup the roaming state, like setting the correct animation, etc.
}
public void Tick(float deltaTime)
{
// Code to roam the land
// some code to detect the player - this method is likely on the enemy but could be in the state, too
if (_parent.PlayerIsInRange())
{
// TODO: Switch to chasing state - we'll get to this in a moment
}
}
public void Exit()
{
// some code to cleanup the roaming state
}
}
public class ChasingState : IState
{
// We keep a reference to the enemy this state belongs to
private Enemy _parent;
// Constructor that takes this state's parent
public ChasingState(Enemy parent)
{
_parent = parent;
}
public void Enter()
{
// some code to setup the chasing state, like setting the correct animation, etc.
}
public void Tick(float deltaTime)
{
// Code to chase the player
// some code to detect the player
if (!PlayerIsInRange())
{
// TODO: Switch to roaming state - we'll get to this in a moment
}
}
public void Exit()
{
// some code to cleanup the chasing state
}
}
I have left two huge TODO’s in the states here which we will get to in a moment. Now that we have the two states, we need to update our enemy to use it
private IState _currentState;
private void SwitchState(IState newState)
{
// Exit the current state if there is one
_currentState?.Exit();
// Set the new state
_currentState = newState;
// Enter the new (now current) state
_currentState?.Enter();
}
private void Awake()
{
// Set the initial state
SwitchState(new RoamingState(this));
}
private void Update()
{
// Call the tick on the state
_currentState?.Tick(Time.deltaTime);
}
This adds a method we will use to change states. This method will ensure that the Exit()
method is called on the current state before switching to the new state and calling the Enter()
method on the new state. We also call the Tick
method in update, allowing the state to do whatever it does
This now brings us back to those TODOs. We will use the new method to switch to the relevant state (Unchanged code omitted)
public class RoamingState : IState
{
public void Tick(float deltaTime)
{
// Code to roam the land
// some code to detect the player - this method is likely on the enemy but could be in the state, too
if (_parent.PlayerIsInRange())
{
// Switch to chasing state
_parent.SwitchState(new ChasingState(_parent));
}
}
}
public class ChasingState : IState
{
public void Tick(float deltaTime)
{
// Code to chase the player
// some code to detect the player
if (!PlayerIsInRange())
{
// Switch to roaming state
_parent.SwitchState(new RoamingState(_parent));
}
}
}
Now we have a slightly more complex state machine. But it can get even more complicated
Level 3
For level 3, check out the Third Person Combat and Traversal course. It’s worth it if you want to get into FSMs a little more - This one changed my ‘gamedev’ life…
Level 4
Level 4 will introduce transitions. This gets quite involved. Check out Jason Weimann’s video regarding that here: