Skip to content

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 with C# 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

  1. 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.

Merge request reports

Loading