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:
- Player Character Dynamics: Governing player actions, such as walking, sprinting, jumping, crouching, and swimming.
- AI Behaviors: Directing non-player characters (NPCs) as they shift from idle, to alert, to chase, and to attack modes.
- Game Progression: Tracking phases of a game, perhaps transitioning from exploration, to combat, to narrative sequences.
- 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()
, andSwitchToLookMode()
allow the camera’s state to be changed externally. - The
Update()
andLateUpdate()
methods ensure that the current state of the camera is updated every frame, withLateUpdate()
being called after allUpdate()
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:
- 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.
- 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.
- 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.
- 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.
- 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.
- Purpose: Also continuously updates while in “orbit” mode but is called after all
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:
- 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.
- Each camera state (e.g.,
- Reduces Complexity:
- Without a state machine, the
CameraManager
might be filled with numerousif-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.
- Without a state machine, the
- 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.
- It’s straightforward to add new camera states or modify existing ones without affecting the overall
- 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 ofCamState_Fly
is executed, ensuring consistent setup.
- Each state provides lifecycle methods (
- 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 theCamState_Fly
state.
- 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 theCameraManager
helps log and track state transitions, further aiding 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
- 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.
- 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