初めに
2Dアクションゲームを初めて作ってみた際に、各アクションの遷移についてこれをそのまま実装するとなると、 if 文や switch 文で実装をしていました。
if (!GameManager.instance.IsGameState()) return false;
if (stateManager.CurrentState is RopeState) return false;
if (stateManager.CurrentState is CannonState) return false;
if (isOnRail) return false;
return true;
このまま実装することもできはしましたが、状態やアクションが追加されるたびに if 文をつなぐことになるので状態の遷移状況などが把握しづらくなってしまいます。そこで今回のステートパターンを学んだ結果を載せました。
実装方法を色々調べてみた結果、導入方法は同じでも使い方が異なったり書き方がまちまちなので、どれが正解というかは私自身には分かりませんが、現時点で私が知ることができた方法などを載せていきます。
ステートパターンとは
プレイヤーなどの状態管理の処理を追いやすくするために使用する記述方法です。アクションごとにその状態に移行したとき、その状態であるとき、その状態から抜けたときの処理を記述して、そのなかで具体的に「どの処理からどの処理へはどういう条件で状態を切り替えるのか」が追いやすくなります。
ステートパターンの実装方法
インターフェース
ステートパターンは先ほどの通り Enter, Update, Exit のように状態ごとに処理があるため、こちらはインターフェースを用いて抽象的に記述ができます。
Enterはそのステートに入った時の処理。Updateはその状態で毎フレーム実行する処理、または次のステートに行くための条件などを入れる。最後にExitはその状態を抜けた場合に実行する処理を記述します。
// 記事に載せる
public interface IActionState
{
void Enter(); // 状態開始時
void Update(); // 毎フレーム
void Exit(); // 状態終了時
}
アクションごとのステートの状態
そして、各状態で止まる、歩く、ジャンプなど個別に IActionState を継承したクラスを用意します。そして Enter, Update, Exit に具体的な処理の内容を書いていきます。この際に以下では handler という CharacterHandler 変数を用意していますが、こちらは「何についてのアクションなのか」をこのステート側から判断できるように入れています。Enter 内で handler.Movement.Stop(); のようにすれば、この状態に移ったらプレイヤーの移動を停止するように記述することが可能です。
// IdleState と WalkState のみ全文掲載
public class IdleState : IActionState
{
private readonly CharacterHandler handler;
public IdleState(CharacterHandler handler)
{
this.handler = handler;
}
public void Enter()
{
Debug.Log("Idle開始");
handler.Movement.Stop(); //例)移動を止める処理
}
public void Update() { }
public void Exit()
{
Debug.Log("Idle終了");
}
}
public class WalkState : IActionState
{
// 同様の構造
}
ステートの状態を切り替える処理
最後にステートの状態を切り替える処理になります。
ChangeAction メソッドでは新しいステートに切り替える処理を実行した場合に、ここで現在のステートを終了して新しいステートに切り替えることを行っています。
また Update の処理によって、現在のステートに記述されている Update の処理が実行されるようになっています。
// 例:入力処理での状態遷移
if (Input.GetButtonDown("Jump") && isGrounded)
{
ChangeAction(new JumpState(this));
}
// CharacterHandler から抜粋
public class CharacterHandler : MonoBehaviour
{
private IActionState currentAction;
// 状態切り替えの部分
public void ChangeAction(IActionState newAction)
{
currentAction?.Exit();
currentAction = newAction;
currentAction.Enter();
}
void Update()
{
//現在のステートのUpdate()の処理を実行
currentAction?.Update();
}
}
処理の流れ
以上のコードは断片的なので処理の順番は以下の通りになります。
- 入力を受け取る
Update内で入力をチェックし、ChangeAction(new JumpState(this))を呼び出します。 - 現在のステートを終了する
によって現在のステートを終了します。
currentAction?.Exit();
例:IdleState ならDebug.Log("Idle終了")が呼ばれる。 - 新しいステートに切り替える
によって新しいステートを代入します。
currentAction = newAction;
直後にcurrentAction.Enter();が呼ばれ、ジャンプ開始処理が実行されます。 - 毎フレーム処理
void Update() { currentAction?.Update(); }によって、現在のステートのUpdate()が毎フレーム呼ばれます。
その後、条件を満たせばまた別のステートへ遷移します。
クラス図の参考例ですが以下の通りです。

追記
ステートパターンを自分なりに紹介してみたのですが、実際に独自のアクション要素をいくつも追加していくと、どうしてもコードが見づらくなったり、処理の流れが分かりにくく感じる場面が多くなりました。
もちろん、これは人によって向き・不向きがあるので、最終的には自分が理解しやすく、開発しやすい設計を選ぶのが一番だと思います。
まとめ
アクションゲームには攻撃やジャンプなど様々な状態遷移が頻繁に行われるため、if 文や switch 文だけだとコードが長くなり見づらいということが起きるので、このステートパターンは非常に便利だと思います。
インターフェースを使ったりなど少し実装が難しく感じますが、長期的に見て拡張性が高くなり使いやすいので、ぜひ使っていきたいですね。