LoginSignup
0
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-11-04

●はじめに

前:https://qiita.com/kiku09020/items/ec8e27454611cc359971

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

●Stateパターンとは

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

●プロジェクト内容

(MeshとかMaterialは省略)

○Scripts/ExampleUsage

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

image.png

○Scripts/Pattern

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

image.png
image.png

●コード内容

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

○UnrefactoredPlayerController

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

クリックしてコードを展開
.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() { }
    }
}

コメント翻訳

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

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

○PlayerController

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

クリックしてコードを展開
.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);
        }
    }
}

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

■メンバ変数

.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

.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

.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()があります
クリックしてコードを展開
.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();
            }
        }
    }
}

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

.cs
        public IState CurrentState { get; private set; }

        // reference to the state objects
        public WalkState walkState;
        public JumpState jumpState;
        public IdleState idleState;
  • IState型で、 現在の状態のプロパティCurrentState を定義しています。

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

■コンストラクタ

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

.cs
        // set the starting state
        public void Initialize(IState state)
        {
            CurrentState = state;
            state.Enter();
        }
  • 引数に渡された状態を初期状態として、その状態になった瞬間の処理を呼び出しています。
  • PlayerControllerStartで呼び出されています。

■TransitionTo

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

■Update

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

○IState

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

クリックしてコードを展開
.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
        }
    }
}

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

です。

○IdleState

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

クリックしてコードを展開
.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");
        }
    }
}

○JumpState

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

クリックしてコードを展開
.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");
        }

    }
}

○WalkState

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

クリックしてコードを展開
.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");
        }

    }
}

●さいごに

ちょっと複雑なところあるけど、慣れればいいかもね!!(適当)

次:

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3