Stateパターンを使ってみる

PONOS Advent Calendar 2020の15日目の記事です。
昨日は@FW14Bさんの「Unityで「おきあがりこぼし」を作ってみる」でした。

#はじめに
今回は自分がゲーム開発時によく使うStateパターンの基本についてお話させていただきます。

#Stateパターンって？
StateパターンとはGoFの定めたデザインパターンの１つ
StateパターンのStateは状態という意味
状態をクラスとして生成し、次々と状態を遷移させることで行動をその状態内で完結させる

#Stateパターンの構成
Stateパターンは
・State(状態)
・Context(状況判断)
・Client(利用者)
以上の３つから構成されることが多いです
#Stateパターンの良いところ
・状態がクラスで独立しているので変数などが競合しない
・別クラスが故に、利用者のコードがすっきり書ける
・遷移先が決められているのでバグを発見しやすい
#実際に書いてみる
##Context(状況判断)
#####Context(状況判断) - 基底 -

```cs
public abstract class StateControllerBase : MonoBehaviour
{
    protected Dictionary<int, StateChildBase> stateDic = new Dictionary<int, StateChildBase>();

    // 現在のステート
    public int CurrentState { protected set; get; }

    // 初期化処理
    public abstract void Initialize(int initializeStateType);

    // 更新処理
    public void UpdateSequence()
    {
        int nextState = (int)stateDic[CurrentState].StateUpdate();
        AutoStateTransitionSequence(nextState);
    }

    // ステートの自動遷移
    protected void AutoStateTransitionSequence(int nextState)
    {
        if (CurrentState == nextState)
        {
            return;
        }

        stateDic[CurrentState].OnExit();
        CurrentState = nextState;
        stateDic[CurrentState].OnEnter();
    }
}

```

#####Context(状況判断) - エネミーステートコントローラー -

```cs
public class EnemyStateController : StateControllerBase
{
    public enum StateType
    {
        Wait,
        Attack,
    }
    // 初期化処理
    public override void Initialize(int initializeStateType)
    {
        // 待機
        stateDic[(int)StateType.Wait] = gameObject.AddComponent<EnemyStateChild_Wait>();
        stateDic[(int)StateType.Wait].Initialize((int)StateType.Wait);

        // 攻撃
        stateDic[(int)StateType.Attack] = gameObject.AddComponent<EnemyStateChild_Attack>();
        stateDic[(int)StateType.Attack].Initialize((int)StateType.Attack);

        CurrentState = initializeStateType;
        stateDic[CurrentState].OnEnter();
    }
}
```
##State(状態)
#####State(状態) - 基底 -
```cs
public abstract class StateChildBase : MonoBehaviour
{
    // ステートコントローラー
    protected StateControllerBase controller;

    // 登録されたステートタイプ
    protected int StateType { set; get; }

    // 初期化処理
    public virtual void Initialize(int stateType)
    {
        StateType = stateType;
        controller = GetComponent<StateControllerBase>();
    }

    // 入場処理
    public abstract void OnEnter();

    // 退場処理
    public abstract void OnExit();

    /// 更新処理
    public abstract int StateUpdate();
}
```

#####State(状態) - 待機 -
```cs
public class EnemyStateChild_Wait : StateChildBase
{
    // タイムカウント
    float currentTimeCount;

    // 待機時間
    static readonly float waitDuration = 2f;

    public override void OnEnter()
    {
        Debug.Log("２秒間待つ！");
        currentTimeCount = 0f;
    }

    public override void OnExit()
    {
        // Do Nothing.
    }

    public override int StateUpdate()
    {
        currentTimeCount += Time.deltaTime;

        if (currentTimeCount >= waitDuration)
        {
            return (int)EnemyStateController.StateType.Attack;
        }

        return (int)StateType;
    }
}
```

#####State(状態) - 攻撃 -
```cs
public class EnemyStateChild_Attack : StateChildBase
{

    public override void OnEnter()
    {
        Debug.Log("攻撃！");
    }

    public override void OnExit()
    {
        // Do Nothing.
    }

    public override int StateUpdate()
    {
        return (int)EnemyStateController.StateType.Wait;
    }
}
```
##Client(利用者)
#####Enemyクラス
```cs
public class Enemy : MonoBehaviour
{
    // ステートコントローラー
    [SerializeField] EnemyStateController stateController = default;

    void Start()
    {
        stateController.Initialize((int)EnemyStateController.StateType.Wait);
    }

    void Update()
    {
        stateController.UpdateSequence();
    }
}
```
#使い方
Enemyオブジェクトを生成する。
![スクリーンショット 2020-12-14 15.59.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539810/d6f2daad-698b-cca1-a631-6349ae09f2fe.png)

Enemyの子にStateObjectを追加し、EnemyStateControllerをアタッチ。
![スクリーンショット 2020-12-14 15.59.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539810/7fc902bc-d3ec-4013-7584-c3fd3f33341b.png)

EnemyコンポーネントをEnemyオブジェクトに取り付け、Stateを参照させる。
![スクリーンショット 2020-12-14 15.59.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539810/13131087-46ef-4a80-5205-cb75cc77f38c.png)



#実行結果
2秒間経つとWaitからAttackへ状態遷移します。
そしてAttackが終わったらまたWaitへ状態遷移を繰り返します。
( 攻撃すると言ってもログを吐き続けるだけですが……！ )
#まとめ

今回は敵を想定としたシンプルなステートでしたが、より複雑にすることもできます。
例えば、Wait中に敵がダメージを受けたりするとのけぞったりする状態へ遷移したりなどもできます。

シーンにもキャラクターにも汎用的に使えますので、気になった方は是非使ってみてください〜！

明日は@honeniqさんの記事です。