●はじめに
前:https://qiita.com/kiku09020/items/ec8e27454611cc359971
プロジェクト内の順番とは異なりますが、個人的に早めに知っておきたいので、今回はStatePatternを学んでみます
今回から、シーンがついてて視覚的にわかりやすくなると思います。
●Stateパターンとは
ゲームに必ずと言っていいほど出てくる 「状態」を管理するデザインパターン です。
初めのほうはswitch文とenumで管理しちゃうのも手ですが、大規模になってくると、コードが肥大化してしまいます。
なので、今回のStateパターンを使用して、コンパクトなコードを組んで、ドヤ顔しましょう。
●プロジェクト内容
(MeshとかMaterialは省略)
○Scripts/ExampleUsage
プレイヤーに関するスクリプトが入ってるフォルダです。
○Scripts/Pattern
Stateパターンの核となるスクリプトが入ってるフォルダです。
●コード内容
今回は、少し内容量が多いので、重要な部分だけを説明していきます。
○UnrefactoredPlayerController
ダメなプレイヤーの動作スクリプトです。僕を含めた初心者がよくやるやつです。Switchとenumでやっちゃてるやつです。
クリックしてコードを展開
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パターンを利用して作られたものです。
クリックしてコードを展開
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);
}
}
}
ちょっと長いので、詳しく上から順番に見ていきます。
■メンバ変数
[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
private void Awake()
{
charController = GetComponent<CharacterController>();
// initialize state machine
playerStateMachine = new StateMachine(this);
}
private void Start()
{
playerStateMachine.Initialize(playerStateMachine.idleState);
}
-
charController
は、コンポーネントから取得してます。 -
playerStateMachine
は、インスタンス化してます。 -
また、
Start()
内で初期状態を待機状態にしてます。
■Update
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()
があります
クリックしてコードを展開
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();
}
}
}
}
■メンバ変数、プロパティ
public IState CurrentState { get; private set; }
// reference to the state objects
public WalkState walkState;
public JumpState jumpState;
public IdleState idleState;
-
IState
型で、 現在の状態のプロパティCurrentState
を定義しています。 -
また 、各状態のクラスを宣言 しています。これは、
StateMachine
がインスタンス化するときに、各状態をインスタンス化させるときに使われます。(↓で説明)
■コンストラクタ
// 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
// set the starting state
public void Initialize(IState state)
{
CurrentState = state;
state.Enter();
}
- 引数に渡された状態を初期状態として、その状態になった瞬間の処理を呼び出しています。
-
PlayerController
のStart
で呼び出されています。
■TransitionTo
// exit this state and enter another
public void TransitionTo(IState nextState)
{
CurrentState.Exit();
CurrentState = nextState;
nextState.Enter();
}
- ①現在の状態の終了処理をした後に、②現在の状態を引数の状態に変更して、③変更された状態になった瞬間の処理を呼び出しています。
- 各状態のUpdate内で、条件が成立されたときに呼び出されます。
■Update
// allow the StateMachine to update this state
public void Update()
{
if (CurrentState != null)
{
CurrentState.Update();
}
}
- 現在の状態のUpdateを繰り返し呼び出します。
- ※このクラスにMonoBehaviorを継承しちゃうと、UnityのUpdateになっちゃうので注意
○IState
各状態に実装されるインターフェースです。
クリックしてコードを展開
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のisGround
がfalse
だったらジャンプ状態に遷移 して、
playerのvelocity
の絶対値が0.1
以上だったら移動状態に遷移 する 感じです。
クリックしてコードを展開
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以下だったら待機状態、そうじゃなかったら移動状態にする 感じです
クリックしてコードを展開
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以下だったら待機状態に遷移 する感じです
クリックしてコードを展開
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");
}
}
}
●さいごに
ちょっと複雑なところあるけど、慣れればいいかもね!!(適当)
次: