The State Pattern allows for a class to change its behavior dynamically according to an internal state.
Let's say we want to design an Enemy class for a game. An Enemy object will be behaving differently depending on it's hit points or other stats, or what's happening around it in the world. If its hit points are low then maybe we use a different animation where it limps and if its hit points reach zero it dies. If it's aware of the Player it may start chasing, attacking, or evading behavior or when nothing in particular is going on perhaps it wanders around or patrols an area.
First let's try representing the Enemy's state with an
enumerator giving it only a couple states at first and throw together a quick Enemy class with some methods.
public enum EnemyState
{
Idle,
Dead,
}
public class Enemy
{
int _hitPoints = 20;
EnemyState _state = EnemyState.Idle;
public void WanderAround()
{
if (_state != EnemyState.Dead)
{
_state = EnemyState.Idle;
Console.WriteLine("The Enemy aimlessly wanders...");
}
else
{
Console.WriteLine("The Enemy is dead so it can't wander around.");
}
}
public void TakeDamage(int damage)
{
if (_state != EnemyState.Dead)
{
_hitPoints -= damage;
Console.WriteLine("Enemy takes " + damage.ToString() + " points of damage");
if (_hitPoints <= 0)
{
_state = EnemyState.Dead;
Console.WriteLine("Enemy is dead!");
}
}
else
{
Console.WriteLine("Hit points already at zero.");
}
}
public void Attack()
{
if (_state != EnemyState.Dead)
{
Console.WriteLine("The Enemy attacks!");
}
}
}
Pretty straight forward. The methods' behaviors depend on the internal state object, and the state is changed depending on internal conditions, completely hidden from the user. Seems okay until we need to add new states. Let's add an Evading state.
public enum EnemyState
{
Idle,
Dead,
Evading,
}
public class Enemy
{
int _hitPoints = 20;
EnemyState _state = EnemyState.Idle;
public void WanderAround()
{
if (_state == EnemyState.Dead)
{
Console.WriteLine("The Enemy is dead so it can't wander around.");
}
else if (_state == EnemyState.Evading)
{
Console.WriteLine("The Enemy can't wander around while it's running away!");
}
else
{
_state = EnemyState.Idle;
Console.WriteLine("The Enemy aimlessly wanders...");
}
}
public void TakeDamage(int damage)
{
if (_state != EnemyState.Dead)
{
_hitPoints -= damage;
Console.WriteLine("Enemy takes " + damage.ToString() + " points of damage");
if (_hitPoints <= 5)
{
//If the enemy's hit points drops low enough it
//becomes scared and runs away.
_state = EnemyState.Evading;
Console.WriteLine("Enemy is running away!");
}
if (_hitPoints <= 0)
{
_state = EnemyState.Dead;
Console.WriteLine("Enemy is dead!");
}
}
else
{
Console.WriteLine("Hit points already at zero.");
}
}
//A method to call when the enemy 'feels safe'
//enough to go back to its Idle state.
public void StopEvading()
{
if (_state == EnemyState.Dead)
{
Console.WriteLine("The enemy is dead");
}
else
{
_state = EnemyState.Idle;
//Start wandering aimlessly...
WanderAround();
}
}
public void Attack()
{
if (_state == EnemyState.Dead)
{
Console.WriteLine("The Enemy can't attack while dead.");
}
else if (_state == EnemyState.Evading)
{
Console.WriteLine("The Enemy won't attack if it's running away.");
}
else
{
Console.WriteLine("The Enemy attacks!");
}
}
}
Keeping track of the internal state is starting to become complicated. Every time we add a new state we need to account for it in every method, and if we want to add a method we need to account for the state how the state effects it and how it may change the state.
The State Pattern handles this by representing a state as an object itself. These state objects will handle the enemy's behavior when it is in the corresponding state.
Each State class will implement methods to handle requests from the enemy.
Let's look at some code. First we'll implement a State interface. This will replace our enum entirely.
public interface EnemyState
{
void HandleWanderAround();
void HandleDamage(int hitPoints);
void HandleStopEvading();
void HandleAttack();
}
Notice the method stubs are similar but not exactly like the ones in the Enemy class. The enemy will make requests to its state which will call the appropriate handling method.
Now let's rework the Enemy class.
public class Enemy
{
int _hitPoints = 20;
EnemyState _state;
public Enemy()
{
//We need to initialize the state to something other than null.
_state = new IdleState(this);
}
public void WanderAround()
{
_state.HandleWanderAround();
}
public void TakeDamage(int damage)
{
_hitPoints -= damage;
_state.HandleDamage(_hitPoints);
}
public void StopEvading()
{
_state.HandleStopEvading();
}
public void Attack()
{
_state.HandleAttack();
}
public EnemyState GetState()
{
return _state;
}
public void SetState(EnemyState state)
{
_state = state;
}
}
Much more concise, let's code our IdleState class to see how this works.
public class IdleState : EnemyState
{
Enemy _enemy;
public IdleState(Enemy enemy)
{
_enemy = enemy;
}
public void HandleWanderAround()
{
Console.WriteLine("The enemy wanders around.");
}
public void HandleDamage(int hitPoints)
{
if (hitPoints <= 5 && hitPoints > 0)
{
_enemy.SetState(new EvadingState(_enemy));
}
if (hitPoints <= 0)
{
_enemy.SetState(new DeadState(_enemy));
}
}
public void HandleStopEvading()
{
Console.WriteLine("The enemy isn't evading.");
}
public void HandleAttack()
{
Console.WriteLine("The enemy attacks!");
}
}
No long string of state checks required since we know that if these methods are being called, then this is the state the enemy is in. We have a reference to the enemy in case we want to set it's state from another.
Let's finish up with our DeadState and EvadingState classes.
public class EvadingState : EnemyState
{
Enemy _enemy;
public EvadingState(Enemy enemy)
{
_enemy = enemy;
}
public void HandleWanderAround()
{
Console.WriteLine("The enemy can't wander around if it's evading!");
}
public void HandleDamage(int hitPoints)
{
if (hitPoints <= 0)
{
_enemy.SetState(new DeadState(_enemy));
}
}
public void HandleStopEvading()
{
_enemy.SetState(new IdleState(_enemy));
_enemy.WanderAround();
}
public void HandleAttack()
{
Console.WriteLine("The enemy won't attack if it's running away.");
}
}
public class DeadState : EnemyState
{
Enemy _enemy;
public DeadState(Enemy enemy)
{
_enemy = enemy;
}
public void HandleWanderAround()
{
Console.WriteLine("The enemy can't wander around if it's dead!");
}
public void HandleDamage(int hitPoints)
{
Console.WriteLine("The enemy is already dead.");
}
public void HandleStopEvading()
{
Console.WriteLine("The enemy is dead.");
}
public void HandleAttack()
{
Console.WriteLine("The enemy can't attack if it's dead.");
}
}
Everything about an enemy's behavior while it's in a certain state is encapsulated in these State classes. We can change State behavior easier because we know where all the code associated with that state is and we can easily add new States without altering much existing code.
If you've noticed a similarity between the State Pattern and the Strategy Pattern, that's no coincidence; they're very similar. However the State Pattern acts more autonomously, switching from one state to the other based on internal criteria, on its own without the user even being aware of it. The user just calls the Enemy's methods and get the correct behavior!
Thanks for reading and I hope you're enjoying this series on design patterns!
No comments :
Post a Comment