Empower Evolution: State Schema in C# Game Mastery

In the sprawling universe of gaming, characters are the lifeblood, ever-shifting between states that determine their actions and reactions. As they traverse landscapes, face enemies, and engage with their surroundings, these states paint the narrative of their journey. Through the mists of complexity, a beacon emerges: the State Pattern.

The Complexity of Character Dynamics

Characters, like epic heroes of old, are entities of multifaceted realities. At one moment, they might be silently scouting an environment, and in the next, they’re plunged into the thick of battle. Directing these shifts with crude if-else conditions can turn the code into a labyrinth, tough to navigate and maintain.


The State Pattern: Charting the Hero’s Journey

The State Pattern is our cartographer, meticulously mapping each state of our hero’s voyage.

Consider our protagonist, a warrior named Arion, navigating the stages of his quest:

public interface ICharacterState
{
    void Handle(Context context);
}
 
public class ScoutingState : ICharacterState
{
    public void Handle(Context context)
    {
        // Observing the environment, strategizing...
    }
}
 
public class CombatState : ICharacterState
{
    public void Handle(Context context)
    {
        // Engaging foes, sword and shield at the ready...
    }
}
 
public class RestingState : ICharacterState
{
    public void Handle(Context context)
    {
        // Recuperating energy, mending wounds...
    }
}
 
public class MagicCastingState : ICharacterState
{
    public void Handle(Context context)
    {
        // Conjuring spells, channeling arcane energy...
    }
}
 
public class Context
{
    private ICharacterState _state;
 
    public Context(ICharacterState state)
    {
        this.TransitionTo(state);
    }
 
    public void TransitionTo(ICharacterState state)
    {
        this._state = state;
        this._state.Handle(this);
    }
}

Transitioning between Arion’s journey phases becomes an effortless endeavor:

//..
Context arionContext = new Context(new ScoutingState());
arionContext.TransitionTo(new CombatState());
arionContext.TransitionTo(new RestingState());
arionContext.TransitionTo(new MagicCastingState());
//..

Crafting Legends, One State at a Time

By harnessing the power of the State Pattern in C#, we create characters as dynamic and adaptable as the legends they’re inspired by. Our heroes don’t merely react; they evolve, shaping their destinies one state at a time. In the vast tapestry of game design, it’s not merely about the destination; it’s the journey that counts.

Real-World Applications of State Pattern and State Machines

Beyond the saga of our valiant Arion, the State Pattern and state machines offer tangible solutions to tangible problems in game design:

  1. Player Character Dynamics: Governing player actions, such as walking, sprinting, jumping, crouching, and swimming.
  2. AI Behaviors: Directing non-player characters (NPCs) as they shift from idle, to alert, to chase, and to attack modes.
  3. Game Progression: Tracking phases of a game, perhaps transitioning from exploration, to combat, to narrative sequences.
  4. UI Systems: Managing user interface elements as they change states – normal, hovered, pressed, and disabled.

Example of managing camera states with popular state machine asset from prime31 StateKit.

The CameraManager class in Unity is responsible for managing the behavior of a camera. It utilizes a state machine (_machine) to handle different camera modes (like orbit, fly, look around, and free).

  • The Initialize() method sets up the state machine with various camera states and attaches an event handler to log state changes.
  • Methods like SwitchToFlyMode(), SwitchToFreeMode(), and SwitchToLookMode() allow the camera’s state to be changed externally.
  • The Update() and LateUpdate() methods ensure that the current state of the camera is updated every frame, with LateUpdate() being called after all Update() methods in the game.

In essence, this class provides a structured way to switch between and manage different camera behaviors.

public enum CAM_STATE
    {
        Idle,
        Fly,
        Look,
        Free,
        Orbit
    }
 
    /// <summary>
    /// Handles camera behaviour
    /// Camera modes:
    ///     - orbit
    ///     - free
    ///     - look around
    ///     - fly
    /// </summary>
    public class CameraManager : MonoBehaviour, ICameraManager
    {
        private SKStateMachine<CameraManager> _machine;
 
        public IPromise Initialize()
        {
            _machine = new SKStateMachine<CameraManager>(this, new CamState_Idle());
            _machine.addState(new CamState_Orbit());
            _machine.addState(new CamState_Fly());
            _machine.addState(new CamState_Look());
            _machine.addState(new CamState_Free());
 
            _machine.onStateChanged += OnStateChange;
        }
         
        private void OnStateChange()
        {
            Debug.Log("state: " + _machine.currentState.ToString());
        }
 
        public void SwitchToFlyMode()
        {
            _machine.changeState<CamState_Fly>();
        }
 
        public void SwitchToFreeMode()
        {
            _machine.changeState<CamState_Free>();
        }
 
        public void SwitchToLookMode()
        {
            _machine.changeState<CamState_Look>();
        }
 
 
        private void Update()
        {
            _machine?.currentState?.update(Time.deltaTime);
        }
 
        private void LateUpdate()
        {
            _machine?.currentState?.lateUpdate(Time.deltaTime);
        }
    }

camera states are separated into their own classes (for simplicity reasons we are giving just two camera states):

for orbit camera state:

public class CamState_Orbit : SKState<CameraManager>
    {
        public override void onInitialized()
        {
            // executed when state is created and added to the machine
        }
 
        public override void begin()
        {
            // executed when entering orbit state
            // set target and setup input for orbit mode
        }
 
        public override void end()
        {
            // executed when leaving orbit state
            // reset input or subscribtions
            // stop processing in update/late update
        }
 
        public override void update(float deltaTime)
        {
            // process input in update
        }
 
        public override void lateUpdate(float deltaTime)
        {
            // if we need to process something in late update
            // we should do it here
        }
    }

The CamState_Orbit class is a specific state for the camera behavior, representing the “orbit” mode. It inherits from SKState<CameraManager>, implying it’s a specific state tailored for the CameraManager class.

Let’s summarize and explain each method:

  1. onInitialized():
    • Purpose: Executed when this state is first created and added to the state machine.
    • Usage: Any setup that’s required when the state is first initialized should go here. This is a one-time setup, distinct from the repeated setups that might occur every time the state is entered.
  2. begin():
    • Purpose: Executed every time the camera enters the “orbit” mode.
    • Usage: This is where you’d set any targets, adjust camera settings, or set up any input specifically for orbit mode. It preps the camera to function in the “orbit” mode.
  3. end():
    • Purpose: Executed every time the camera exits the “orbit” mode.
    • Usage: Useful for cleanup tasks, like resetting inputs, unsubscribing from events, or stopping any processes that were specific to the “orbit” mode. It ensures that anything started in the begin() method is appropriately stopped or reset.
  4. update(float deltaTime):
    • Purpose: Continuously updates while the camera is in “orbit” mode. Called once per frame.
    • Usage: Here, you’d typically process user inputs that affect the camera’s behavior in orbit mode, like adjusting the camera’s position based on user input.
  5. lateUpdate(float deltaTime):
    • Purpose: Also continuously updates while in “orbit” mode but is called after all update() methods in the game.
    • Usage: Useful for actions that depend on other objects’ positions having been updated already. For instance, if you want the camera to follow an object smoothly, you might adjust the camera’s position here, after the object has moved in the update() phase.

In summary, the CamState_Orbit class defines the behavior of the camera while in “orbit” mode. The methods within dictate how the camera prepares for this mode, behaves during this mode, and cleans up after this mode. It’s used within the CameraManager‘s state machine to manage this specific mode of camera operation.

for fly camera state:

public class CamState_Fly : SKState<CameraManager>
    {
        public override void onInitialized()
        {
            // executed when state is created and added to the machine
        }
 
        public override void begin()
        {
            // executed when entering fly state
            // set target and setup input for fly mode
        }
 
        public override void end()
        {
            // executed when leaving fly state
            // reset input or subscribtions
            // stop processing in update/late update
        }
 
        public override void update(float deltaTime)
        {
            // process input in update
        }
 
        public override void lateUpdate(float deltaTime)
        {
            // if we need to process something in late update
            // we should do it here
        }
    }

State machines, like the one used in the CameraManager, offer several benefits, especially for managing complex systems with distinct modes or states. Let’s delve into the benefits, using the example of the CameraManager and its states:

  1. Modularity and Organization:
    • Each camera state (e.g., CamState_Fly, CamState_Orbit) is defined in its own class, encapsulating the logic specific to that state.
    • This separation makes the codebase easier to understand and maintain since you can focus on one state at a time.
  2. Reduces Complexity:
    • Without a state machine, the CameraManager might be filled with numerous if-else conditions checking the current mode and adjusting behavior accordingly.
    • With the state machine, each state has its own logic, reducing the need for such branching conditions and making the code clearer.
  3. Flexibility:
    • It’s straightforward to add new camera states or modify existing ones without affecting the overall CameraManager or other states.
    • For example, if you want to add a new “Zoom” mode for the camera, you’d simply create a new CamState_Zoom class and add it to the state machine.
  4. Consistent Lifecycle Management:
    • Each state provides lifecycle methods (begin(), end(), etc.), ensuring a consistent approach to initializing, updating, and cleaning up.
    • In our example, every time the camera enters “fly” mode, the begin() method of CamState_Fly is executed, ensuring consistent setup.
  5. Event-Driven Logic:
    • State machines naturally fit into an event-driven paradigm. Transitions between states can be triggered by events (e.g., user input, in-game events).
    • For instance, a user pressing a specific key could trigger the SwitchToFlyMode() method, leading to a transition to the CamState_Fly state.
  6. Improved Debugging:
    • With the separation of states, it becomes easier to debug issues specific to a particular mode. If there’s a problem with the “fly” mode, you can focus on the CamState_Fly class.
    • Additionally, having methods like OnStateChange() in the CameraManager helps log and track state transitions, further aiding debugging.
  7. Enhanced Collaboration:
    • Different team members can work on different states without stepping on each other’s toes. One developer might be refining the “orbit” mode while another improves the “look around” mode.
  8. Predictable Behavior:
    • The state machine ensures that only one state is active at any given time. This prevents potential conflicts or unpredictable behaviors that might arise if multiple modes were inadvertently activated simultaneously.

In summary, using a state machine for managing camera behaviors (or any complex system with multiple modes/states) offers a structured, modular, and efficient approach. It simplifies code, enhances maintainability, and provides a consistent way to manage the lifecycle of each state.

Resources:

https://github.com/prime31/StateKit