へっぽこ日記

Unity関係のつぶやきまとめ。マイコンとかもいじります。

Stateパターンを試す

はじめに

Stateパターンを試す記事です。 ステートパターン自体は以前から利用していたのですが、公式とは異なる実装だったので、公式のStateパターンを基に試してみました。

サンプルプロジェクト github.com

そもそもStateパターンとは

Gofパターンの一種で、オブジェクトの状態をクラスとして管理し、状態が変わるごとに振る舞いを変えるパターン。振る舞いを条件ごとに変えるのなら、単純にIfなどを使えば良いのですが、新たに状態を追加した場合は全体の条件を見直す必要があったりと色々不便です。この問題を解決しようとする思想。

まずは、あまりよろしくない方法で状態ごとに振る舞いを実行したいと思います。

単純にEnumを使っての実装

これでも単純にIfとフラグを使って分けるよりかは良いのですが、新たに状態を加える場合はどうしてもEnumを触る必要があります。また、条件として加える際には、すべての分岐をチェックする必要があったりと、あまりよろしくないです。

 private enum State
    {
        Idle,
        Move,
        Jump
    }

    private State _state = State.Idle;

    private void Update()
    {
        OnStateChanged();
    }

    private void OnStateChanged()
    {
        switch (_state)
        {
            case State.Idle:
                if (Input.GetKeyDown(KeyCode.Space))
                {
                    _state = State.Jump;
                }
                else if (Input.GetKey(KeyCode.W))
                {
                    _state = State.Move;
                }
                else
                {
                    Idle();
                }

                break;

            case State.Jump:
                Jump();
                break;

            case State.Move:
                Move();
                break;
        }
    }

    private void Idle()
    {
        Log("Idle");
    }

    private void Move()
    {
        Log("Move");
         _state = State.Idle;
    }

    private void Jump()
    {
        Log("Jump");
        _state = State.Idle;
    }

    private void Log(string value)
    {
        Debug.Log(value);
    }

StateMachineを使って試してみる

先程のEnumで実装したものとは異なり、クラスを一つの状態として扱うように実装してみました。 キーを押すと、それに対応した状態がLogに表示されるサンプルです。公式のデモプロジェクトを基に作成しました。

状態をクラスごとに定義しているので、Enumの時の様に全体を見直す必要が無くなりました。そのため、新たに状態を追加したい場合も、他の条件や分岐を見直す必要がなく、単純にクラスを実装すれば良くなりました。

IState

各々の状態クラスで呼ばれるメソッドを定義しています。

 public void Enter();

 public void Update();

    public void Exit();

IdleState

IdleクラスやAttackクラスが状態クラスで、遷移条件が合致した場合に、StateMachineに遷移を依頼います。

 private Player _player;
    
    public IdleState(Player player)
    {
        _player = player;
    }
    
    public void Enter()
    {
        Debug.Log("IdleState Enter");
    }

    public void Update()
    {
        if (_player.IsAttacked)
        {
            _player.StatetateMachine.TransitionTo(_player.StatetateMachine.attackState);
        }
        Debug.Log("IdleState Update");
    }

    public void Exit()
    {
        Debug.Log("IdleState Exit");
    }

AttackState

 private Player _player;

    public AttackState(Player player)
    {
        _player = player;
    }

    public void Enter()
    {
        Debug.Log("AttackState Enter");
    }

    public void Update()
    {
        _player.StatetateMachine.TransitionTo(_player.StatetateMachine.idleState);
        Debug.Log("AttackState Update");
    }

    public void Exit()
    {
        Debug.Log("AttackState Exit");
    }

StateMachine

ここで状態をまとめて管理しており、現在の状態に対して、Interface経由でUpdateやEnterを呼び出しています。

 private IState state;

    public AttackState attackState;
    public IdleState idleState;

    public StateMachine(Player player)
    {
        attackState = new AttackState(player);
        idleState = new IdleState(player);
    }

    public void Initialize(IState state)
    {
        this.state = state;
        state.Enter();
    }

    public void TransitionTo(IState nextState)
    {
        state.Exit();
        state = nextState;
        nextState.Enter();
    }

    public void Update()
    {
         state?.Update();
    }

Player

ここでStateMachineを回して、状態に応じた振る舞いを依頼しています。

public StateMachine StatetateMachine;
    public bool IsAttacked = false;

    private void Start()
    {
        StatetateMachine = new StateMachine(this);
        StatetateMachine.Initialize(StatetateMachine.idleState);
    }

    private void Update()
    {
        IsAttacked = Input.GetKeyDown(KeyCode.Space);
        
        StatetateMachine.Update();
    }

おわりに

UnityだとAnimatorで使われていたりと、使いどころが多いので是非覚えておきたいですね。また、ここからObserverを混ぜて実装するのもありだと思います。

是非、読者登録をしていただくと助かります!