SDP Refac/State Pattern
SDP Assignment
Already used design patterns (see technical documentation for more details):
Some implementations might differ from the original idea to adjust for personal needs for this project.
-
Component
Pattern for Player. -
Observer
Pattern withC# Actions
for Player Health and HUD communication and more. -
Dependency Injection
used to give object references instead of firing a lot of events. - Scene Management with
State
Pattern (kinda. Stack based but has elements of a Finite State Machine) -
Singleton
(was mandatory🤮 ) for Input and Resource Management. Also used for Scene Management - Probably some more.
Idea for the assignment
Currently the Square
class, which inherits from the abstract class Enemy
has 2 possible Actions: Attacking and Moving.
Goal: Create a State Pattern so it can transition between AttackState
and MovingState
. The MovingState
will be the default State, and when a specific timer is high enough, it will move to AttackState
do his Attack and move back to MovingState
Implementation
- Create a
IState
interface
private interface IState {
void Enter();
void Update(float deltaTime);
void Draw(RenderTarget window);
void Exit();
}
Every state should have an Enter()
method to setup the state, an Update()
method to handle the timers and transition to AttackState
if needed, a Draw()
method to draw the Attack Indicators if they are defined, and an Exit()
method to cleanup.
Square
now has a new private variable called currentState
:
private IState currentState;
The default state should be MovingState
:
private class MovingState : IState
{
private readonly Square square;
public MovingState(Square square)
{
this.square = square;
}
public void Enter()
{
square.attackTimer = 0.0f;
}
public void Draw(RenderTarget window) { }
public void Update(float deltaTime)
{
square.movementTimer += deltaTime;
if (square.movementTimer >= MOVEMENT_INTERVAL)
{
square.movementTimer = 0f;
square.SetNewTargetPosition();
}
square.Sprite.Position = Utils.Lerp(square.Sprite.Position, square.targetPosition, MOVEMENT_SPEED * deltaTime);
square.Position = square.Sprite.Position;
square.attackTimer += deltaTime;
if (square.attackTimer >= square.attackInterval)
{
square.ChangeState(new AttackingState(square));
}
}
public void Exit() { }
}
Now in the Square
constructor set the currentState
and call Enter()
:
public Square(Texture texture) : base(SceneManager.Instance.GameState.CalculateHP(300000), "SQUAREMAT")
{
// ...
currentState = new MovingState(this);
currentState.Enter();
}
Give the Square
a method to change States:
private void ChangeState(IState newState)
{
currentState.Exit();
currentState = newState;
currentState.Enter();
}
In Square
's Draw()
and Update()
methods, call the state's Draw()
and Update()
methods:
public override void Draw(RenderTarget window)
{
// ...
currentState.Draw(window);
}
public override void Update(float deltaTime)
{
// ...
currentState.Update(deltaTime);
}
Remove unused methods
Now the Square
should have 2 different states and switch/transition between them.
All other bosses only have one State: Attacking, thats why they are implemented as private fields of the Square class.
Why does this help?
Makes the Square class more readable. If the Boss can get more states, it is easily adjustable by creating new State classes which implement the IState interface.