Search code examples
oopdesign-patternsgame-development

What would be a good way to overcome some of the vanilla State pattern limitations


I'm implementing the State pattern in the context of videogame development. For instance, a player has various states: Idle, Run, Attack. Each state is implemented in its own class. Importantly, each state is responsible for the transition to another state (e.g. the Idle state can transition to Attack, however the Jump state cannot transition to Idle until player touches the ground). See below a high-level idea of how I implemented it:

class Player
{
    private State _state = null;

    public Player()
    {
        this.TransitionTo(new IdleState(this));
    }

    public void TransitionTo(State state)
    {
        this._state = state;
        this._state.SetPlayer(this);
    }

    public void Jump()
    {
        this._state.jump();
    }
}

abstract class State
{
    protected Player _player;

    public void SetPlayer(Player player)
    {
        this._player = player;
    }

    public abstract void Jump();
}

class JumpState : State
{
    public override void Jump()
    {
        // double jump is not allowed
    }

}

class IdleState : State
{
    public override void Jump()
    {
        this._player.TransitionTo(new JumpState());
    }

}

This works well but I can see two big limitations with this approach:

  • I cannot swap a specific implementation of a state. For instance, an enemy could take damage while jumping, while another doesn't. A very simple override of the Jump state could do, but with my implementation a state is responsible to transition to another state and it does so by referencing a concrete implementation of the other state. E.g. in the code snippet above, note how the IdleState transitions to a specific JumpState. If a JumpNoDamageState existed, I would also have to create an IdleNoDamageState that uses that.
  • The state is tightly coupled to a specific character. For instance, let's say an enemy behaves exactly the same as another, except that this enemy can also Escape if health < 10% after taking damage. To do this, I need to recreate all Idle, Attack and Jump states to account for this, as each individual state needs to be aware of this new state and the possibility to transition to it.

Solution

  • The State pattern is an object-oriented recipe for implementing a finite-state machine (FSM). If you want to introduce new states, you'd be changing the definition of the FSM. If you want to distinguish between different FSMs, you may have to implement different State-based APIs.

    The other concern is probably a result of the State objects keeping state. Perhaps surprisingly,

    "State objects are often Singletons"

    - Design Patterns

    You'd usually design a State implementation by passing the so-called Context object to each State object. In this particular case, you should consider changing the API so that it looks like this:

    class Player
    {
        private State _state;
    
        public Player()
        {
            _state = new IdleState(this));
        }
    
        internal void TransitionTo(State state)
        {
            _state = state;
        }
    
        public void Jump()
        {
            _state.jump(this);
        }
    }
    
    abstract class State
    {
        public abstract void Jump(Player player);
    }
    
    class JumpState : State
    {
        // Singleton
        public static readonly State Instance = new JumpState();
        private JumpState() {}
    
        public override void Jump(Player player)
        {
            // double jump is not allowed
        }
    }
    
    class IdleState : State
    {
        // Singleton
        public static readonly State Instance = new IdleState();
        private IdleState() {}
    
        public override void Jump(Player player)
        {
            player.TransitionTo(JumpState.Instance);
        }
    }
    

    You'll often need to maintain state for separate objects, so that different objects transition through the FSM in each their own pathways. You keep state information on the Context object (here, Player), so that different Player objects can have different states, and pass through the same FSM in different ways.

    You can make the Context object polymorphic as well.

    For instance, let's say an enemy behaves exactly the same as another

    This sounds as though the Context might, indeed, need to be something more general than Player - perhaps Character?

    except that this enemy can also Escape if health < 10% after taking damage

    You could reuse the same FSM but instead have it check if the Context (Character?) may attempt to escape. This could be as simple as having a Boolean value on the Context object.