Finite State Machines: Stop Doing Them Wrong!
Let’s face it, AI in games might be grabbing headlines, but the unsung hero of believable character control remains the humble Finite State Machine (FSM). Many tutorials oversimplify FSMs, leaving you struggling when you try to implement anything beyond basic examples. That stops now. Forget spaghetti code and embrace the elegance of structured state management. This isn’t just about making a character move; it’s about building a foundation for complex behaviors.
Why You’re Probably Doing FSMs Wrong (and How to Fix It)
The most-downloaded assets on Wayline this month — sorted by what real developers actually use.
The biggest mistake beginners make is treating states as mere containers for code. A state should be responsible for only one thing: defining the character’s behavior during that state. All the logic about when to leave that state? That belongs outside the state itself, in a central manager.
Think of it this way: the state is the actor, and the manager is the director. The actor performs their lines (behavior) but doesn’t decide when the scene changes. The director (manager) decides when it’s time to move on to the next scene (state).
Building Blocks: States, Transitions, and Triggers
Let’s use the classic example: Idle, Walk, and Jump.
- State: Represents a specific behavior. In our example,
Idle,Walk, andJumpare states. Each state dictates how the character behaves. - Transition: The act of changing from one state to another. This is where many beginners stumble.
- Trigger: The condition that initiates a transition. This could be player input (e.g., pressing the spacebar), a game event (e.g., landing on the ground), or a combination of both.
Code Example: A Minimalist FSM in Unity (C#)
First, create a base class for all your states:
public abstract class State : MonoBehaviour
{
public abstract void Enter();
public abstract void Execute();
public abstract void Exit();
}
Now, let’s define the IdleState:
public class IdleState : State
{
public override void Enter()
{
Debug.Log("Entering Idle State");
}
public override void Execute()
{
// Implement Idle behavior, e.g., play idle animation
}
public override void Exit()
{
Debug.Log("Exiting Idle State");
}
}
And a simple WalkState:
public class WalkState : State
{
public float walkSpeed = 5f;
private CharacterController controller;
public override void Enter()
{
Debug.Log("Entering Walk State");
controller = GetComponent<CharacterController>();
if (controller == null)
{
Debug.LogError("CharacterController not found!");
enabled = false; // Disable the state if the controller is missing
}
}
public override void Execute()
{
// Implement Walk behavior, e.g., move the character
float horizontalInput = Input.GetAxis("Horizontal");
Vector3 moveDirection = new Vector3(horizontalInput, 0, 0).normalized; // Simple 2D movement for example
Vector3 velocity = moveDirection * walkSpeed;
if (controller != null) {
controller.Move(velocity * Time.deltaTime);
}
}
public override void Exit()
{
Debug.Log("Exiting Walk State");
}
}
Finally, the all-important FSMManager:
public class FSMManager : MonoBehaviour
{
public State currentState;
public void ChangeState(State newState)
{
if (currentState != null)
{
currentState.Exit();
}
currentState = newState;
if (currentState != null)
{
currentState.Enter();
}
}
void Update()
{
if (currentState != null)
{
currentState.Execute();
}
//Example transitions based on input. This is where the "director" makes decisions.
if (currentState is IdleState && Input.GetAxis("Horizontal") != 0)
{
ChangeState(GetComponent<WalkState>());
}
else if (currentState is WalkState && Input.GetAxis("Horizontal") == 0)
{
ChangeState(GetComponent<IdleState>());
}
}
}
Crucially: Attach all three scripts ( FSMManager, IdleState, WalkState) to the same GameObject that has a CharacterController. Then drag the IdleState component in the inspector to the currentState field on the FSMManager.
Pitfalls and How to Avoid Them
- The God Class Anti-Pattern: Don’t cram all the game logic into the
FSMManager. It should only be responsible for state transitions. The states themselves should handle the specific behavior. - Missing References: Ensure your states have access to the necessary components (e.g.,
CharacterController,Animator). UseGetComponentin theEnter()method and handle null references gracefully. - Over-Complicating Transitions: Keep transitions simple and explicit. Avoid complex nested
ifstatements. Consider using a state transition table for more complex scenarios.
Level Up: Beyond the Basics
This is a foundation. Next steps include:
- Animation Integration: Trigger animations based on the current state in the
Enter()andExit()methods. - State-Specific Data: Pass data to states when transitioning (e.g., jump height, walk speed).
- Hierarchical FSMs: Create nested states for more complex behaviors (e.g., a “Combat” state with sub-states like “Attack,” “Defend,” and “Evade”).
Actionable Insights
The key to a successful FSM is clear separation of concerns. The state defines what the character is doing, and the manager decides when to change states based on triggers. Embrace this principle, and you’ll avoid the common pitfalls that plague beginner FSM implementations. Stop treating states as just code containers; see them as mini-behaviors orchestrated by a central manager. This shift in perspective will transform your character control systems from a tangled mess into a clean, maintainable architecture.