UnityUnityメモ

Unityでステートマシンを活用!ゲーム開発を効率化する方法

Unity

1. はじめに

ゲーム開発を進めるうえで、キャラクターや敵の「状態管理(ステート管理)」は非常に重要です。たとえば、プレイヤーキャラクターが「待機 → 歩く → 走る → 攻撃」といった動作をスムーズに切り替えるには、現在の状態をしっかり管理し、適切なタイミングで次の状態へ移行できるようにする必要があります。もし状態管理が適切に行われていないと、キャラクターが急におかしな動きをしたり、意図しない挙動をしてしまうことがあります。

こうした状態を管理するために使われるのが**「ステートマシン(State Machine)」**です。ステートマシンとは、オブジェクトが持つ「状態(State)」を定義し、それぞれの状態間の遷移(Transition)をルールとして設定することで、適切な状態制御を可能にする仕組みです。Unityでは、C#のコードを使った手動のステートマシン管理や、Animator(アニメーター)を利用したビジュアルなステートマシンの管理が可能です。

ステートマシンを使うメリット

では、なぜステートマシンを活用するべきなのでしょうか? その主なメリットを紹介します。

  1. コードが整理され、可読性が向上する
    • ステートごとに処理を分けることで、スパゲッティコード(ゴチャゴチャになったコード)を防ぐことができます。
  2. バグを減らし、デバッグしやすくなる
    • 状態ごとの処理が明確になるため、どの状態で問題が発生しているのかが分かりやすくなります。
  3. 拡張しやすく、スケールしやすい
    • 状態を追加したり、遷移条件を変更するときに影響範囲が限定されるため、大規模なプロジェクトでも管理しやすい。
  4. ゲームの挙動を直感的に制御できる
    • 「攻撃中は移動できない」「ダメージを受けたら一定時間無敵になる」などのルールをシンプルに設定できる。

Unityにおけるステートマシンの活用シーン

Unityのステートマシンは、以下のような場面で特に役立ちます。

  • プレイヤーのアクション管理
    例: 待機(Idle)→ 歩行(Walk)→ 走る(Run)→ 攻撃(Attack)
  • 敵のAI制御
    例: 巡回(Patrol)→ 追跡(Chase)→ 攻撃(Attack)→ 逃走(Flee)
  • UIの状態管理
    例: メニュー表示(Main Menu)→ ゲーム画面(Game)→ ポーズ画面(Pause)→ ゲームオーバー(Game Over)

このように、ゲーム内のさまざまな動作を整理するのに、ステートマシンはとても便利です。では、実際にUnityでステートマシンを実装する方法について詳しく見ていきましょう!




2. Unityでステートマシンを実装する方法

Unityでゲームを開発するとき、キャラクターの動作や敵AIの挙動を管理するのはとても重要です。状態(ステート)が増えてくると、if文の条件分岐が増えすぎてコードが複雑になりがちです。
そこで「ステートマシン(State Machine)」を活用すると、管理しやすくなり、コードの可読性も向上します。

この記事では、C#スクリプトによるステート管理、Animatorを活用する方法、StateMachineBehaviourを使う方法の3つを紹介します。


2.1 手動での実装(C#によるステート管理)

ステートマシンの基本構造

ステートマシンの基本的な考え方は、「現在の状態を変数で管理し、条件に応じて遷移させる」ことです。
まず、状態を表す enum を作成し、それを管理する StateMachine クラスを作ります。

① ステートの定義
public enum PlayerState
{
Idle, // 待機
Walking, // 歩行
Jumping, // ジャンプ
Attacking // 攻撃
}
② ステートを管理するスクリプト

次に、現在の状態を持つ PlayerStateMachine クラスを作成します。

using UnityEngine;

public class PlayerStateMachine : MonoBehaviour
{
public PlayerState currentState = PlayerState.Idle; // 初期状態

void Update()
{
switch (currentState)
{
case PlayerState.Idle:
HandleIdle();
break;
case PlayerState.Walking:
HandleWalking();
break;
case PlayerState.Jumping:
HandleJumping();
break;
case PlayerState.Attacking:
HandleAttacking();
break;
}
}

void HandleIdle()
{
if (Input.GetKey(KeyCode.W))
{
ChangeState(PlayerState.Walking);
}
else if (Input.GetKeyDown(KeyCode.Space))
{
ChangeState(PlayerState.Jumping);
}
}

void HandleWalking()
{
if (!Input.GetKey(KeyCode.W))
{
ChangeState(PlayerState.Idle);
}
}

void HandleJumping()
{
// ジャンプ後の処理
ChangeState(PlayerState.Idle);
}

void HandleAttacking()
{
// 攻撃後の処理
ChangeState(PlayerState.Idle);
}

void ChangeState(PlayerState newState)
{
currentState = newState;
}
}

このようにすると、状態遷移が明確になり、不要なif文を減らせます。
また、ChangeState() メソッドを使うことで、状態の変更を統一できます。




2.2 UnityのAnimatorを活用する方法

Animatorには、視覚的にステートマシンを組む機能があり、キャラクターのアニメーション遷移を簡単に管理できます。

① Animator Controllerを作成

  1. Assetsウィンドウ右クリック → Create → Animator Controller を選択。
  2. 作成した Animator Controller をキャラクターの Animator コンポーネントに適用。

② ステートの追加

  1. Animatorウィンドウを開くWindow → Animation → Animator)。
  2. Idle, Walking, Jumping, Attacking などのステートを追加。
  3. 右クリック → Create State → Empty で新しいステートを作成し、名前を変更。

③ 遷移の設定

  1. Idle ステートから Walking ステートに 遷移線 を作成(右クリック → Make Transition)。
  2. Conditions(条件)を設定する。
    • Speed > 0.1 のときに Walking へ遷移。
    • Speed == 0 なら Idle に戻る。

④ スクリプトでアニメーションを制御

using UnityEngine;

public class PlayerAnimatorController : MonoBehaviour
{
private Animator animator;

void Start()
{
animator = GetComponent<Animator>();
}

void Update()
{
float speed = Input.GetKey(KeyCode.W) ? 1.0f : 0.0f;
animator.SetFloat("Speed", speed);

if (Input.GetKeyDown(KeyCode.Space))
{
animator.SetTrigger("Jump");
}
}
}

animator.SetFloat("Speed", speed);Speed パラメータを変更し、Walking ステートへの遷移を制御できます。
animator.SetTrigger("Jump");Jump アニメーションを再生できます。


2.3 UnityのStateMachineBehaviourを活用する方法

Animatorの StateMachineBehaviour を使うと、各ステートごとにスクリプトを書いて処理を分けられます。

StateMachineBehaviour の作成

  1. Assets ウィンドウで 右クリック → Create → C# Script を選択。
  2. スクリプト名を JumpState に変更し、以下のコードを記述。
using UnityEngine;

public class JumpState : StateMachineBehaviour
{
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Debug.Log("ジャンプ開始!");
}

override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// ステート更新中の処理
}

override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Debug.Log("ジャンプ終了!");
}
}

② Animatorのステートに適用

  1. Animator ウィンドウで Jump ステートをクリック。
  2. InspectorAdd Behaviour をクリックし、JumpState を追加。

これにより、Jump ステートに入ったときと出たときに処理を実行できます。


まとめ

Unityでステートマシンを実装する方法には、以下の3つがあります:

  1. C#スクリプトで手動管理 → 状態をスクリプトで細かく制御したい場合に適用
  2. Animatorを活用 → アニメーション遷移をビジュアル的に管理したい場合に便利
  3. StateMachineBehaviourを活用 → 各ステートの処理を分割して管理したいときに最適

状況に応じて適切な方法を選び、ゲームの状態管理をスムーズに行いましょう!

ここまで、C#でのステート管理やAnimatorを使った方法を紹介しましたが、コーディングなしで視覚的にステートを管理できるツールがあれば、もっと簡単に実装できます。
そこでおすすめなのが「PlayMaker」です。PlayMakerを使うと、プログラムを書かずにドラッグ&ドロップでステートマシンを作成でき、複雑な処理も直感的に管理できます。
👉 PlayMakerの詳細はこちら




3. ステートマシンのベストプラクティス

ステートマシンを使えば、ゲーム内の状態管理が整理され、バグの発生を防ぎやすくなります。しかし、適切に設計しないとステートが複雑になりすぎたり、管理が難しくなったりすることがあります。ここでは、Unityでステートマシンを活用する際のベストプラクティスを紹介します。


3.1 状態遷移を整理する

ステートマシンを適切に機能させるためには、不要なステートや複雑な遷移を減らし、管理しやすい設計にすることが重要です。

不要なステートを減らし、簡潔な設計にする

ステートが増えすぎると、コードが煩雑になり、バグの原因になります。
例えば、プレイヤーの移動ステートを考えると、次のような設計は不要に複雑です。

悪い例:

  • Idle(待機)
  • Walking(歩行)
  • Running(走る)
  • Crouching(しゃがむ)
  • CrouchWalking(しゃがみ歩き)
  • Jumping(ジャンプ)
  • Falling(落下)
  • Landing(着地)

このように細かく分けると管理が大変になります。

良い例:

  • Idle(待機)
  • Move(移動)
  • Jump(ジャンプ)
  • Fall(落下)

このように、「同じ動作カテゴリのものはまとめる」 ことで、シンプルな設計になります。


ステート遷移の条件を統一する

ステートの遷移がバラバラだと、バグが発生しやすくなります。
例えば、あるステートから別のステートに移行するための条件を明確に定義しておくと、予期せぬ挙動を防げます。

悪い例:

if (Input.GetKeyDown(KeyCode.Space)) 
{
state = CharacterState.Jumping;
}
if (velocity.y < 0)
{
state = CharacterState.Falling;
}
if (isGrounded)
{
state = CharacterState.Idle;
}

このコードでは、**「いつ Idle に戻るのか?」**が不明瞭で、異常なステート遷移が発生する可能性があります。

良い例:

switch (state)
{
case CharacterState.Idle:
if (Input.GetAxis("Horizontal") != 0) state = CharacterState.Move;
if (Input.GetKeyDown(KeyCode.Space)) state = CharacterState.Jump;
break;
case CharacterState.Move:
if (Input.GetKeyDown(KeyCode.Space)) state = CharacterState.Jump;
if (Input.GetAxis("Horizontal") == 0) state = CharacterState.Idle;
break;
case CharacterState.Jump:
if (velocity.y < 0) state = CharacterState.Fall;
break;
case CharacterState.Fall:
if (isGrounded) state = CharacterState.Idle;
break;
}

このように switch 文を使って遷移条件を整理 すると、何がどう変わるのかが明確になります。




3.2 シングルトンやイベントを活用する

状態遷移を管理しやすくするために、シングルトンやイベントを活用 すると便利です。

シングルトンを使ってステートを管理する

シングルトンを使うと、ステートの管理を一元化でき、どのスクリプトからでもアクセスしやすくなります。

シングルトンでステートを管理する例

public class StateManager : MonoBehaviour
{
public static StateManager Instance { get; private set; }

public enum GameState { Playing, Paused, GameOver }
public GameState CurrentState { get; private set; }

private void Awake()
{
if (Instance == null)
Instance = this;
else
Destroy(gameObject);
}

public void ChangeState(GameState newState)
{
CurrentState = newState;
Debug.Log("Game State Changed to: " + newState);
}
}

他のスクリプトから StateManager.Instance.ChangeState(GameState.Paused); と呼び出せば、ゲーム全体の状態を管理できます。


イベントを使ってステート変更を通知する

状態が変わったことを他のスクリプトに伝えるために、イベントを活用するとコードが整理されます。

イベントで状態変更を通知する例

public class StateManager : MonoBehaviour
{
public static event Action<GameState> OnStateChanged;
private static GameState currentState;

public static void ChangeState(GameState newState)
{
currentState = newState;
OnStateChanged?.Invoke(newState);
}
}

これにより、他のスクリプトで StateManager.OnStateChanged を監視し、適切に反応できます。


3.3 デバッグとメンテナンスを容易にする

ステートマシンの動作を確認しやすくするために、デバッグ機能を追加すると開発がスムーズになります。

デバッグ用UIを用意する

ステートをリアルタイムで確認できるようにすると、開発中のバグ修正がしやすくなります。

Unity UIでステートを表示する方法

  1. Canvasを作成し、Textオブジェクトを配置。
  2. 次のスクリプトを作成して Text にステートを表示する。
using UnityEngine;
using UnityEngine.UI;

public class StateDebugger : MonoBehaviour
{
public Text stateText;

private void Update()
{
stateText.text = "Current State: " + StateManager.Instance.CurrentState;
}
}

ステート変更をログに記録する

Unityのコンソールログに状態変更を記録すると、異常な遷移が発生していないかを確認できます。

ログに記録する方法

public void ChangeState(GameState newState)
{
Debug.Log($"ステート変更: {CurrentState} → {newState}");
CurrentState = newState;
}

まとめ

  • ステートを整理し、増えすぎないようにする
  • 遷移条件を統一し、シンプルなロジックにする
  • シングルトンやイベントを活用して管理しやすくする
  • デバッグ用のUIやログを活用し、異常な遷移を防ぐ

適切なステート管理を行うことで、コードが整理され、開発スピードが向上 します。ぜひ、ゲーム開発に活かしてみてください!




4. ステートマシンを応用したゲーム開発

ステートマシンは、ゲーム内でのキャラクターやAIの挙動を整理するのにとても便利です。ここでは、敵AIの挙動管理プレイヤーのアクション管理という2つの例を挙げて、どのようにステートマシンを活用できるのかを解説します。


4.1 敵AIにステートマシンを活用する

敵キャラクターの動きを考えると、「巡回 → 追跡 → 攻撃 → 待機」といった流れがよくあります。これをステートマシンで整理すると、コードがスッキリし、バグの少ない設計ができます。

敵AIのステート設計

まず、敵キャラクターの基本的なステートを定義しましょう。

  1. Patrol(巡回):敵がプレイヤーを探しながら移動する
  2. Chase(追跡):プレイヤーを発見し、追いかける
  3. Attack(攻撃):プレイヤーが攻撃範囲に入ったら攻撃する
  4. Return(待機):プレイヤーを見失ったら元の巡回地点に戻る

この流れをC#スクリプトで実装してみます。

C#での実装例

まず、敵の状態を表す**Enum(列挙型)**を作成します。

using UnityEngine;
using UnityEngine.AI; // NavMeshAgentを使う場合

public class EnemyAI : MonoBehaviour
{
private enum State { Patrol, Chase, Attack, Return }
private State currentState = State.Patrol;

public Transform[] patrolPoints;
private int currentPatrolIndex = 0;

public Transform player;
public float chaseRange = 5f;
public float attackRange = 1.5f;
private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();
GoToNextPatrolPoint();
}

void Update()
{
switch (currentState)
{
case State.Patrol:
Patrol();
break;
case State.Chase:
Chase();
break;
case State.Attack:
Attack();
break;
case State.Return:
ReturnToPatrol();
break;
}
}

void Patrol()
{
if (Vector3.Distance(transform.position, player.position) < chaseRange)
{
currentState = State.Chase;
return;
}

if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
GoToNextPatrolPoint();
}
}

void Chase()
{
agent.SetDestination(player.position);

if (Vector3.Distance(transform.position, player.position) < attackRange)
{
currentState = State.Attack;
}
else if (Vector3.Distance(transform.position, player.position) > chaseRange * 1.5f)
{
currentState = State.Return;
}
}

void Attack()
{
// プレイヤーに攻撃する処理
Debug.Log("攻撃!");

if (Vector3.Distance(transform.position, player.position) > attackRange)
{
currentState = State.Chase;
}
}

void ReturnToPatrol()
{
agent.SetDestination(patrolPoints[currentPatrolIndex].position);

if (Vector3.Distance(transform.position, patrolPoints[currentPatrolIndex].position) < 0.5f)
{
currentState = State.Patrol;
}
}

void GoToNextPatrolPoint()
{
if (patrolPoints.Length == 0) return;

agent.SetDestination(patrolPoints[currentPatrolIndex].position);
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
}
}

解説

  • Enumを使って状態を定義し、currentStateで現在のステートを管理。
  • Update()内で現在のステートに応じた処理を呼び出し。
  • Vector3.Distance()でプレイヤーとの距離を判定し、ステートを切り替え

このようにすると、敵AIの挙動が整理され、見やすくて管理しやすいコードになります。




4.2 プレイヤーのアクション管理

プレイヤーキャラクターの動きもステートマシンを活用するとスムーズに管理できます。例えば、以下のような状態遷移を考えます。

プレイヤーのステート設計

  1. Idle(待機):何もしていない状態
  2. Walk(歩行):移動している状態
  3. Jump(ジャンプ):ジャンプ中の状態
  4. Attack(攻撃):攻撃している状態

この状態をスクリプトで管理することで、分かりやすいコードにできます。

C#での実装例

using UnityEngine;

public class PlayerController : MonoBehaviour
{
private enum State { Idle, Walk, Jump, Attack }
private State currentState = State.Idle;

public float speed = 5f;
public float jumpForce = 7f;
private Rigidbody rb;

void Start()
{
rb = GetComponent<Rigidbody>();
}

void Update()
{
switch (currentState)
{
case State.Idle:
HandleIdle();
break;
case State.Walk:
HandleWalk();
break;
case State.Jump:
HandleJump();
break;
case State.Attack:
HandleAttack();
break;
}
}

void HandleIdle()
{
if (Input.GetAxis("Horizontal") != 0)
{
currentState = State.Walk;
}
else if (Input.GetButtonDown("Jump"))
{
Jump();
}
else if (Input.GetButtonDown("Fire1"))
{
currentState = State.Attack;
}
}

void HandleWalk()
{
float move = Input.GetAxis("Horizontal") * speed * Time.deltaTime;
transform.Translate(move, 0, 0);

if (move == 0)
{
currentState = State.Idle;
}
else if (Input.GetButtonDown("Jump"))
{
Jump();
}
}

void HandleJump()
{
if (rb.velocity.y == 0)
{
currentState = State.Idle;
}
}

void HandleAttack()
{
Debug.Log("攻撃!");
currentState = State.Idle;
}

void Jump()
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
currentState = State.Jump;
}
}

解説

  • Enumを使ってプレイヤーの状態を定義
  • Update()内で現在のステートに応じた処理を実行。
  • Input.GetAxis()プレイヤーの入力を取得し、ステートを変更。

これにより、「移動 → ジャンプ → 攻撃」などの流れが明確になり、スムーズな操作感を実現できます。


まとめ

  • ステートマシンを使うことで敵AIの行動プレイヤーのアクションを整理できる。
  • Enumを活用したC#スクリプトで、コードの可読性を向上。
  • ステートを適切に切り替えることで、ゲームの動作がスムーズになる。

ステートマシンを活用すれば、バグが少なく、メンテナンスしやすいゲームが作れます。ぜひ、あなたのゲーム開発にも活かしてみてください!




5. まとめ

ステートマシンは、ゲーム開発においてキャラクターやオブジェクトの状態を管理するための強力なツールです。適切に活用することで、コードの整理がしやすくなり、バグを減らしながらスムーズな動作を実現できます。

特に、敵AIやプレイヤーのアクション管理では、状態ごとの振る舞いを明確に分けることで、直感的で管理しやすいコード構成になります。また、Animatorを利用すれば、視覚的にステートの遷移を確認できるため、アニメーションを多用するプロジェクトでは特に有効です。

一方で、ステートの数が増えすぎたり、遷移の条件が複雑になりすぎたりすると、逆に管理が難しくなることもあります。そのため、設計段階で「どの状態が本当に必要か」をしっかり整理し、可能であればイベントやシングルトンを活用して最適化すると良いでしょう。

ゲームの規模や目的に応じて、シンプルなEnumベースのステート管理、Animatorによるビジュアル制御、StateMachineBehaviourを活用した高度な制御など、最適な方法を選ぶことが重要です。実際の開発では、これらを組み合わせて使うことで、より柔軟で拡張性の高い状態管理が可能になります。

ステートマシンを活用することで、ゲーム開発の効率が向上し、バグの少ない堅牢な設計ができるようになります。ぜひ、プロジェクトに合わせた方法で導入し、快適なゲーム開発を目指してみてください!




よくある質問(FAQ)

Q
ステートマシンを使わずに状態管理することは可能ですか?
A

可能ですが、コードが煩雑になりやすく、バグの原因になります。特に大規模なゲームでは管理が難しくなります。

Q
AnimatorとC#のステート管理はどちらを使うべきですか?
A

アニメーション主体ならAnimator、ロジック主体ならC#のステート管理がおすすめです。

Q
ステートが増えすぎた場合、どう整理すればいいですか?
A

ステートを細分化しすぎないようにし、似たようなステートを統合するか、サブステートマシンを活用すると良いでしょう。

タイトルとURLをコピーしました