【Unity】公式プロジェクトから、デザインパターンを学んでみよう(Part 6:Stateパターン)

# ●はじめに
前：https://qiita.com/kiku09020/items/ec8e27454611cc359971

プロジェクト内の順番とは異なりますが、個人的に早めに知っておきたいので、今回はStatePatternを学んでみます
今回から、シーンがついてて視覚的にわかりやすくなると思います。

# ●Stateパターンとは
ゲームに必ずと言っていいほど出てくる **「状態」を管理するデザインパターン** です。
初めのほうはswitch文とenumで管理しちゃうのも手ですが、大規模になってくると、コードが肥大化してしまいます。
なので、今回のStateパターンを使用して、コンパクトなコードを組んで、ドヤ顔しましょう。

# ●プロジェクト内容
(MeshとかMaterialは省略)

## ○Scripts/ExampleUsage
プレイヤーに関するスクリプトが入ってるフォルダです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/d05babed-9b59-c566-f01c-59f2385745d6.png)



## ○Scripts/Pattern
Stateパターンの核となるスクリプトが入ってるフォルダです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/13a03bc3-f293-586a-5780-b94c70b73fa0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/0b85bc7c-9c30-82c4-7238-2ce646f8ca91.png)



# ●コード内容
**今回は、少し内容量が多いので、重要な部分だけを説明していきます。**

## ○UnrefactoredPlayerController
ダメなプレイヤーの動作スクリプトです。僕を含めた初心者がよくやるやつです。Switchとenumでやっちゃてるやつです。

<details><summary><b>クリックしてコードを展開</b></summary>

```.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.State
{
    public enum PlayerControllerState
    {
        Idle,
        Walk,
        Jump
    }

    public class UnrefactoredPlayerController : MonoBehaviour
    {
        // this works but does not scale; you would need to add a case
        // each time you created a new internal state. Use the state pattern instead
        private PlayerControllerState state;

        private void Update()
        {
            GetInput();

            switch (state)
            {
                case PlayerControllerState.Idle:
                    Idle();     break;
                case PlayerControllerState.Walk:
                    Walk();     break;
                case PlayerControllerState.Jump:
                    Jump();     break;
            }
        }

        private void GetInput(){ /* process walk and jump controls */ }

        private void Walk() { }
        private void Idle() { }
        private void Jump() { }
    }
}

```
</details>

コメント翻訳
>これは機能しますが、スケーリングしません。 
 新しい内部状態を作成するたびにケースを追加する必要があります。 
 代わりにStateパターンを使用してください

この通りです。次のが理想的なコードになります。


## ○PlayerController
先ほどのやつをStateパターンを利用して作られたものです。

<details><summary><b>クリックしてコードを展開</b></summary>

```.cs
namespace DesignPatterns.State
{
    public class PlayerController : MonoBehaviour
    {
        [SerializeField] private bool isGrounded = true;    // 着地中か

        private StateMachine playerStateMachine;
        private CharacterController charController;

        /* プロパティ */
        public bool IsGrounded => isGrounded;
        public CharacterController CharController => charController;
        public StateMachine PlayerStateMachine => playerStateMachine;

        private void Awake()
        {
            charController = GetComponent<CharacterController>();

            // initialize state machine
            playerStateMachine = new StateMachine(this);
        }

        private void Start()
        {
            playerStateMachine.Initialize(playerStateMachine.idleState);
        }

        private void Update()
        {
            // update the current State
            playerStateMachine.Update();
        }

        private void LateUpdate()
        {
            CalculateVertical();
            Move();
        }

        private void Move(){ }

        private void CalculateVertical()
        {
            isGrounded = Physics.CheckSphere(spherePosition, 0.5f, groundLayers, QueryTriggerInteraction.Ignore);
        }
    }
}

```
</details>

ちょっと長いので、詳しく上から順番に見ていきます。

### ■メンバ変数

::: note
```.cs
        [SerializeField] private bool isGrounded = true;    // 着地中か

        private StateMachine playerStateMachine;
        private CharacterController charController;

        /* プロパティ */
        public bool IsGrounded => isGrounded;
        public CharacterController CharController => charController;
        public StateMachine PlayerStateMachine => playerStateMachine;
```

- `isGround`は、着地しているかどうかのフラグです。

- `playerStateMachine`は、状態遷移をするために必要なクラスの変数です。

- `charController`は、Playerオブジェクトについてるコンポーネントの1つです。これで移動距離の計算、速度を計測したりしてるっぽい
:::

### ■Awake,Start

::: note
```.cs
        private void Awake()
        {
            charController = GetComponent<CharacterController>();

            // initialize state machine
            playerStateMachine = new StateMachine(this);
        }

        private void Start()
        {
            playerStateMachine.Initialize(playerStateMachine.idleState);
        }
```

- `charController`は、コンポーネントから取得してます。
- `playerStateMachine`は、インスタンス化してます。

- また、`Start()`内で初期状態を待機状態にしてます。
:::

### ■Update

::: note
```.cs
        private void Update()
        {
            // update the current State
            playerStateMachine.Update();
        }

        private void LateUpdate()
        {
            CalculateVertical();
            Move();
        }
```

`Update()`内で、`StateMachine`の更新を行ってます。
また、`LateUpdate()`内では、縦方向の計算や移動をさせています。
縦方向の移動量などを計算する`CalculateVertival()`で着地中かどうかを判定しています。

このコード内に書かれているのは、**移動、縦方向の計算だけで、残りは外部のクラスが行っています。**
**状態の遷移や判定をStateMachineとか各状態のクラスが行っているので、冗長なコードにならないで済んでます。**
Stateパターンを使用した場合のスッキリさがよくわかるコードです。
:::

## ○StateMachine
- コンストラクタである`StateMachine()`
- 初期状態を指定する`Initialize()`
- 状態を遷移する`TransitionTo()`
- 各状態の処理を更新をする`Update()`があります

<details><summary><b>クリックしてコードを展開</b></summary>

```.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.State
{
    // handles
    [Serializable]
    public class StateMachine
    {
        public IState CurrentState { get; private set; }

        // reference to the state objects
        public WalkState walkState;
        public JumpState jumpState;
        public IdleState idleState;

        // pass in necessary parameters into constructor 
        public StateMachine(PlayerController player)
        {
            // create an instance for each state and pass in PlayerController
            this.walkState = new WalkState(player);
            this.jumpState = new JumpState(player);
            this.idleState = new IdleState(player);
        }

        // set the starting state
        public void Initialize(IState state)
        {
            CurrentState = state;
            state.Enter();
        }

        // exit this state and enter another
        public void TransitionTo(IState nextState)
        {
            CurrentState.Exit();
            CurrentState = nextState;
            nextState.Enter();
        }

        // allow the StateMachine to update this state
        public void Update()
        {
            if (CurrentState != null)
            {
                CurrentState.Update();
            }
        }
    }
}

```
</details>


### ■メンバ変数、プロパティ

::: note
```.cs
        public IState CurrentState { get; private set; }

        // reference to the state objects
        public WalkState walkState;
        public JumpState jumpState;
        public IdleState idleState;
```

- `IState`型で、 **現在の状態のプロパティ`CurrentState`** を定義しています。

- また **、各状態のクラスを宣言** しています。これは、`StateMachine`がインスタンス化するときに、各状態をインスタンス化させるときに使われます。(↓で説明)
:::



### ■コンストラクタ

:::note
```StateMachine.cs
        // pass in necessary parameters into constructor 
        public StateMachine(PlayerController player)
        {
            // create an instance for each state and pass in PlayerController
            this.walkState = new WalkState(player);
            this.jumpState = new JumpState(player);
            this.idleState = new IdleState(player);
        }
```
- **全ての状態をインスタンス化** しているコンストラクタです。引数から`Player`の情報を各インスタンスに渡してます
- `PlayerController`のAwakeで呼び出されています。
:::

### ■Initialize

:::note
```.cs
        // set the starting state
        public void Initialize(IState state)
        {
            CurrentState = state;
            state.Enter();
        }
```

- **引数に渡された状態を初期状態として、その状態になった瞬間の処理を呼び出しています。**
- `PlayerController`の`Start`で呼び出されています。
:::

### ■TransitionTo

:::note
```.cs
        // exit this state and enter another
        public void TransitionTo(IState nextState)
        {
            CurrentState.Exit();
            CurrentState = nextState;
            nextState.Enter();
        }
```
- **①現在の状態の終了処理をした後に、②現在の状態を引数の状態に変更して、③変更された状態になった瞬間の処理を呼び出しています。**
- **各状態のUpdate内で、条件が成立されたときに呼び出されます。**
:::

### ■Update

:::note
```.cs
        // allow the StateMachine to update this state
        public void Update()
        {
            if (CurrentState != null)
            {
                CurrentState.Update();
            }
        }
```
- **現在の状態のUpdateを繰り返し呼び出します。**
- ※このクラスにMonoBehaviorを継承しちゃうと、UnityのUpdateになっちゃうので注意
:::




## ○IState
各状態に実装されるインターフェースです。

<details><summary><b>クリックしてコードを展開</b></summary>

```.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.State
{
    public interface IState: IColorable
    {
        public void Enter()
        {
            // code that runs when we first enter the state
        }

        public void Update()
        {
            // per-frame logic, include condition to transition to a new state
        }

        public void Exit()
        {
            // code that runs when we exit the state
        }
    }
}

```
</details>

:::note
- **Enter : その状態になった瞬間の処理**
- **Update : 毎フレーム行う処理**
- **Exit : 次の状態に遷移する瞬間の処理**

です。
:::




## ○IdleState
待機状態のクラスです。
**playerの`isGround`が`false`だったらジャンプ状態に遷移** して、
**playerの`velocity`の絶対値が`0.1`以上だったら移動状態に遷移** する　感じです。

<details><summary><b>クリックしてコードを展開</b></summary>

```.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.State
{
    public class IdleState : IState
    {

        private PlayerController player;

        // color to change player (alternately: pass in color value with constructor)
        private Color meshColor = Color.gray;
        public Color MeshColor { get => meshColor; set => meshColor = value; }

        // pass in any parameters you need in the constructors
        public IdleState(PlayerController player)
        {
            this.player = player;
        }

        public void Enter()
        {
            // code that runs when we first enter the state
            //Debug.Log("Entering Idle State");
        }

        // per-frame logic, include condition to transition to a new state
        public void Update()
        {
            // if we're no longer grounded, transition to jumping
            if (!player.IsGrounded)
            {
                player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.jumpState);
            }

            // if we move above a minimum threshold, transition to walking
            if (Mathf.Abs(player.CharController.velocity.x) > 0.1f || Mathf.Abs(player.CharController.velocity.z) > 0.1f)
            {
                player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.walkState);
            }
        }

        public void Exit()
        {
            // code that runs when we exit the state
            //Debug.Log("Exiting Idle State");
        }
    }
}

```
</details>





## ○JumpState
ジャンプ状態のクラスです。
**velocityが0.1以下だったら待機状態、そうじゃなかったら移動状態にする** 感じです

<details><summary><b>クリックしてコードを展開</b></summary>

```.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.State
{
    public class JumpState : IState
    {
        private PlayerController player;

        // color to change player (alternately: pass in color value with constructor)
        private Color meshColor = Color.red;
        public Color MeshColor { get => meshColor; set => meshColor = value; }

        // pass in any parameters you need in the constructors
        public JumpState(PlayerController player)
        {
            this.player = player;
        }

        public void Enter()
        {
            // code that runs when we first enter the state
            //Debug.Log("Entering Jump State");
        }

        // per-frame logic, include condition to transition to a new state
        public void Update()
        {

            //Debug.Log("Updating Jump State");

            if (player.IsGrounded)
            {
                if (Mathf.Abs(player.CharController.velocity.x) > 0.1f || Mathf.Abs(player.CharController.velocity.z) > 0.1f)
                {
                    player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.idleState);
                }
                else
                {
                    player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.walkState);
                }
            }
        }

        public void Exit()
        {
            // code that runs when we exit the state
            //Debug.Log("Exiting Jump State");
        }

    }
}

```
</details>





## ○WalkState
移動状態のクラスです
**isGroundがfalseだったらジャンプ状態に遷移**、
**velocityが0.1以下だったら待機状態に遷移** する感じです

<details><summary><b>クリックしてコードを展開</b></summary>

```.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.State
{
    public class WalkState : IState
    {
        // color to change player (alternately: pass in with constructor)
        private Color meshColor = Color.blue;
        public Color MeshColor { get => meshColor; set => meshColor = value; }

        private PlayerController player;

        // pass in any parameters you need in the constructors
        public WalkState(PlayerController player)
        {
            this.player = player;
        }

        public void Enter()
        {
            // code that runs when we first enter the state
            //Debug.Log("Entering Walk State");
        }

        // per-frame logic, include condition to transition to a new state
        public void Update()
        {
            // if we are no longer grounded, transition to jumping
            if (!player.IsGrounded)
            {
                player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.jumpState);
            }

            // if we slow to within a minimum velocity, transition to idling/standing
            if (Mathf.Abs(player.CharController.velocity.x) < 0.1f && Mathf.Abs(player.CharController.velocity.z) < 0.1f)
            {
                player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.idleState);
            }
        }

        public void Exit()
        {
            // code that runs when we exit the state
            //Debug.Log("Exiting Walk State");
        }

    }
}

```
</details>





# ●さいごに
ちょっと複雑なところあるけど、慣れればいいかもね！！(適当)


次：