I designed a flexible state machine primarily for managing enemy AI behaviors, though it can be adapted for other uses. The main goal was to create a modular and reusable system that could be applied across future projects. The state machine handles state transitions both internally (within its own logic) and externally, which was necessary for scenarios like controlling enemy behavior dynamically.
State management complexity: Balancing flexibility for simple and complex tasks was a challenge, requiring the ability to switch states internally and externally without adding complexity.
Reusability vs. Specificity: Making the state machine reusable across projects while keeping it adaptable for specific tasks required careful structuring to maintain flexibility.
Lack of Initial Planning: Not fully understanding the requirements from the start led to unexpected changes and refactoring. This could have been avoided with better foresight, saving time and effort throughout development.
Flow Chart example
The state machine has been used in three projects so far, including two group projects and one personal project. It handled AI behaviors like enemy patrol, chase, and attack in one group project, while in another, it managed the state transitions of game objects. In my personal project, it controlled the growth stages of a tree, demonstrating its flexibility and modularity across different game mechanics.
Apple states (Overkill but it works)
Enemy AI
This project gave me hands-on experience with reusability in C#, teaching me how to structure modular code. I also learned the importance of defining requirements early on to avoid messy refactoring later. The best part was being able to just plug in code whenever I needed it, without having to rewrite anything.
public class StateMachine
{
private BaseState _currentState;
public StateMachine() { }
public void StateMachineUpdate()
{
if (_currentState != null)
_currentState.BaseStateUpdate();
}
public void StateMachineLateUpdate()
{
// Update the statemachine physics
if (_currentState != null)
_currentState.BaseStateLateUpdate();
}
public void StateMachineFixedUpdate()
{
// Update the statemachine physics
if (_currentState != null)
_currentState.BaseStateFixedUpdate();
}
/// <summary>
/// Sets the first state of the state machine
/// </summary>
/// <param name="initialState"></param>
public void SetInitialState(BaseState initialState)
{
_currentState = initialState;
initialState.Enter();
}
/// <summary>
/// Sets a new state for the state machine
/// </summary>
/// <param name="newState"></param>
public void ChangeState(BaseState newState)
{
if (_currentState != null)
_currentState.Exit();
_currentState = newState;
newState.Enter();
}
}
This project gave me hands-on experience with reusability in C#, teaching me how to structure modular code. I also learned the importance of defining requirements early on to avoid messy refactoring later. The best part was being able to just plug in code whenever I needed it, without having to rewrite anything.
public class BaseState
{
public string name;
protected StateMachine stateMachine;
public BaseState(string name, StateMachine stateMachine)
{
this.name = name;
this.stateMachine = stateMachine;
}
public virtual void Enter() { }
public virtual void BaseStateUpdate() { }
public virtual void BaseStateFixedUpdate() { }
public virtual void BaseStateLateUpdate() { }
public virtual void Exit() { }
}
This project taught me the importance of having some form of documentation. While I wouldn’t consider this real documentation, I created an example script to help my classmates build upon my code. This made it easier for them to understand and use, following the approach our school often takes: providing an example and leaving the rest to the student.
using UnityEngine;
/// <summary>
/// ExampleSM demonstrates a state machine in Unity that supports two ways of state transitions:
/// 1. States can request transitions themselves (internal logic).
/// 2. External systems or scripts can override the current state at any time (external logic).
/// </summary>
public class ExampleSM : StateMachine
{
[Header("Variables")]
// Time in seconds before transitioning to the InternalSwitchState as an example of state overriding.
public float FirstSwitchTime = 5; // We will wait 5 seconds before switching to the external switch state.
// Internal timer used for demonstration purposes to show how states can be overridden.
private float _time;
[Header("States")]
// A state accessible externally, allowing dynamic state switching by external systems.
[HideInInspector] public StateOne FirstSwitchState;
// The second state that the state machine transitions to after the internal logic completes.
[HideInInspector] public StateTwo SecondSwitchState;
/// <summary>
/// Unity's Awake method initializes the state machine and sets the initial state.
/// </summary>
private void Awake()
{
// Initialize the states and pass this state machine instance for context.
// This is necessary for the state machine to function correctly.
FirstSwitchState = new StateOne(this);
SecondSwitchState = new StateTwo(this);
// Set the initial state to FirstState, which determines the behavior at the start.
SetInitialState(FirstSwitchState);
}
/// <summary>
/// Unity's Update method demonstrates how to override the current state based on external logic.
/// </summary>
public void Update()
{
// Check if the 'G' key is pressed to trigger an external override.
if (Input.GetKeyDown(KeyCode.G))
{
/*
* Demonstrates external state switching:
* - This state machine supports overriding the current state from outside.
* - For example, a timer or other system can force a state change.
* - To handle such transitions in any state, the logic should be implemented here in this script.
*/
ChangeState(FirstSwitchState);
}
}
}
This project gave me hands-on experience with reusability in C#, teaching me how to structure modular and maintainable code. I also learned the importance of clearly defining requirements early on to minimize the need for refactoring later. The biggest takeaway was seeing the value of modular code in action, being able to plug in existing components seamlessly without rewriting anything.