Unityを触り始めた頃、「とりあえず Update() に全部書いておけば動くし楽!」と感じて、そのままプレイヤーの移動、アニメーション、入力、攻撃判定…と、あらゆる処理を1つのスクリプトに詰め込んでしまいがちですよね。
ところがこのやり方だと、
- 「Idle のときは何もしない」「Run のときだけ移動する」といった条件分岐だらけになる
if (isRunning) ... else if (isJumping) ...のような巨大なif-elseチェーンで読みづらくなる- 新しい状態(Attack、Dash など)を追加するたびに、同じスクリプトを開いて修正し続けることになる
といった問題が発生して、あっという間に「Godクラス」化してしまいます。
そこでこの記事では、状態ごとにコンポーネント(子オブジェクト)を分けて管理し、今アクティブな状態だけ処理を実行するためのコンポーネント、「StateMachine」を作っていきます。プレイヤーの「Idle」「Run」だけでなく、敵AIの「Patrol」「Chase」や、UIの「Open」「Close」などにも応用しやすい構成です。
【Unity】子ノードでシンプルな状態管理!「StateMachine」コンポーネント
今回の方針はシンプルです。
- 親オブジェクトに
StateMachineコンポーネントを付ける - その子オブジェクトに「Idle」「Run」などの状態オブジェクトを作る
- 各状態オブジェクトには
StateBehaviourを継承したスクリプトを付ける - 現在アクティブな状態だけが
OnStateUpdate()などの処理を受け取る
つまり、「状態そのもの」を GameObject として階層に分けてしまい、StateMachine は「どの子をアクティブにするか」を切り替えるだけの役割に絞ります。これで SRP(単一責任の原則)も守りやすくなりますね。
フルコード:StateMachine と StateBehaviour
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SimpleStateMachine
{
/// <summary>
/// 各状態の共通インターフェース。
/// これを継承して「IdleState」「RunState」などを作成します。
/// </summary>
public abstract class StateBehaviour : MonoBehaviour
{
/// <summary>
/// 所属しているステートマシンへの参照。
/// Awake 時に自動で設定されます。
/// </summary>
public StateMachine Owner { get; internal set; }
/// <summary>
/// 状態に入ったときに 1 度だけ呼ばれます。
/// </summary>
public virtual void OnStateEnter() { }
/// <summary>
/// 状態から抜けるときに 1 度だけ呼ばれます。
/// </summary>
public virtual void OnStateExit() { }
/// <summary>
/// 毎フレーム呼ばれる更新処理。
/// アクティブな状態に対してのみ呼び出されます。
/// </summary>
public virtual void OnStateUpdate() { }
/// <summary>
/// 物理更新。必要な場合のみオーバーライド。
/// </summary>
public virtual void OnStateFixedUpdate() { }
/// <summary>
/// レンダリング後の更新。必要な場合のみオーバーライド。
/// </summary>
public virtual void OnStateLateUpdate() { }
}
/// <summary>
/// 子オブジェクトにぶら下がった StateBehaviour を管理し、
/// 現在のアクティブな状態だけを更新するシンプルなステートマシン。
/// </summary>
public class StateMachine : MonoBehaviour
{
[Header("初期状態の設定(名前で指定)")]
[SerializeField]
private string _defaultStateName = string.Empty;
[Header("状態遷移時にログを出すか")]
[SerializeField]
private bool _logTransitions = false;
/// <summary>
/// 現在アクティブな状態
/// </summary>
public StateBehaviour CurrentState { get; private set; }
/// <summary>
/// 名前から状態を引くためのテーブル
/// </summary>
private readonly Dictionary<string, StateBehaviour> _states =
new Dictionary<string, StateBehaviour>(StringComparer.Ordinal);
/// <summary>
/// 利便性のため、現在の状態名を公開(null の場合は空文字)
/// </summary>
public string CurrentStateName => CurrentState ? CurrentState.name : string.Empty;
private void Awake()
{
// 子オブジェクトにある StateBehaviour を全て収集
_states.Clear();
var stateBehaviours = GetComponentsInChildren<StateBehaviour>(includeInactive: true);
foreach (var state in stateBehaviours)
{
// StateBehaviour からステートマシンへの参照を設定
state.Owner = this;
// ゲームオブジェクト名を状態名として扱う
var stateName = state.gameObject.name;
if (_states.ContainsKey(stateName))
{
Debug.LogWarning(
$"[StateMachine] 同じ名前の状態が複数あります: {stateName}",
this);
continue;
}
_states.Add(stateName, state);
// 起動時はとりあえず全状態を非アクティブにしておく
state.gameObject.SetActive(false);
}
// 初期状態が設定されていれば遷移
if (!string.IsNullOrEmpty(_defaultStateName))
{
if (!_states.ContainsKey(_defaultStateName))
{
Debug.LogWarning(
$"[StateMachine] デフォルト状態 '{_defaultStateName}' が見つかりません。",
this);
}
else
{
ChangeState(_defaultStateName);
}
}
}
private void Update()
{
// 現在の状態の Update 相当を呼ぶ
if (CurrentState != null)
{
CurrentState.OnStateUpdate();
}
}
private void FixedUpdate()
{
if (CurrentState != null)
{
CurrentState.OnStateFixedUpdate();
}
}
private void LateUpdate()
{
if (CurrentState != null)
{
CurrentState.OnStateLateUpdate();
}
}
/// <summary>
/// 名前を指定して状態を切り替える。
/// </summary>
/// <param name="stateName">子オブジェクトの名前</param>
public void ChangeState(string stateName)
{
if (string.IsNullOrEmpty(stateName))
{
Debug.LogWarning("[StateMachine] 空の状態名には遷移できません。", this);
return;
}
if (!_states.TryGetValue(stateName, out var nextState))
{
Debug.LogWarning($"[StateMachine] 状態 '{stateName}' が見つかりません。", this);
return;
}
if (nextState == CurrentState)
{
// 同じ状態への遷移は無視
return;
}
if (_logTransitions)
{
Debug.Log($"[StateMachine] {CurrentStateName} → {stateName}", this);
}
// 旧状態の終了処理
if (CurrentState != null)
{
CurrentState.OnStateExit();
CurrentState.gameObject.SetActive(false);
}
// 新状態への切り替え
CurrentState = nextState;
if (CurrentState != null)
{
CurrentState.gameObject.SetActive(true);
CurrentState.OnStateEnter();
}
}
/// <summary>
/// 型安全に状態を取得したい場合のヘルパー。
/// 例えば GetState<PlayerRunState>() のように使えます。
/// </summary>
public T GetState<T>() where T : StateBehaviour
{
foreach (var kvp in _states)
{
if (kvp.Value is T typed)
{
return typed;
}
}
return null;
}
/// <summary>
/// 現在の状態が指定した名前かどうかを判定するユーティリティ。
/// </summary>
public bool IsInState(string stateName)
{
return CurrentState != null && CurrentState.name == stateName;
}
}
}
サンプル:Idle / Run の状態クラス
上記のステートマシンを使うための具体例として、プレイヤーの「Idle」と「Run」を切り替える状態クラスを用意してみましょう。
using UnityEngine;
using SimpleStateMachine;
/// <summary>
/// 何もしていない待機状態。
/// </summary>
public class PlayerIdleState : StateBehaviour
{
[Header("移動開始に必要な入力のしきい値")]
[SerializeField]
private float _moveThreshold = 0.1f;
public override void OnStateEnter()
{
// Idle に入った瞬間に呼ばれる
// ここでアニメーションの切り替えなどを行う
Debug.Log("Enter Idle");
}
public override void OnStateUpdate()
{
// 入力を見て、一定以上動いたら Run に遷移する例
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
if (new Vector2(h, v).sqrMagnitude > _moveThreshold * _moveThreshold)
{
Owner.ChangeState("Run");
}
}
public override void OnStateExit()
{
Debug.Log("Exit Idle");
}
}
/// <summary>
/// 走行状態。移動処理をここに集約。
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class PlayerRunState : StateBehaviour
{
[Header("移動速度")]
[SerializeField]
private float _moveSpeed = 5f;
private CharacterController _controller;
private void Awake()
{
// 同じ GameObject に付いている CharacterController をキャッシュ
_controller = GetComponent<CharacterController>();
}
public override void OnStateEnter()
{
Debug.Log("Enter Run");
}
public override void OnStateUpdate()
{
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
Vector3 input = new Vector3(h, 0f, v);
if (input.sqrMagnitude < 0.01f)
{
// 入力がなくなったら Idle に戻る
Owner.ChangeState("Idle");
return;
}
// カメラ基準などに合わせて方向を変えたい場合はここで調整
Vector3 move = input.normalized * _moveSpeed;
// シンプルな地面移動
_controller.SimpleMove(move);
}
public override void OnStateExit()
{
Debug.Log("Exit Run");
}
}
このように、「Idle のときに何をするか」「Run のときに何をするか」を、それぞれ別コンポーネントに分けて書けるのがポイントです。StateMachine 自体は「どの状態を有効にするか」だけを担当します。
使い方の手順
-
プレイヤー用の親オブジェクトを作成
- Hierarchy で
Right Click > Create Emptyから「Player」を作成 - 「Player」に
CharacterControllerコンポーネントを追加 - 上で紹介した
StateMachineコンポーネントを「Player」に追加 _defaultStateNameにIdleと入力
- Hierarchy で
-
状態ごとの子オブジェクトを作成
- 「Player」の子として空オブジェクトを2つ作成し、名前をそれぞれ
Idle,Runにする IdleオブジェクトにPlayerIdleStateコンポーネントを追加RunオブジェクトにPlayerRunStateコンポーネントを追加RunのオブジェクトにもCharacterControllerは不要です(親の Player にだけ付いていればOK)
- 「Player」の子として空オブジェクトを2つ作成し、名前をそれぞれ
-
状態名とオブジェクト名を一致させる
ステートマシンは「子オブジェクトの名前」を状態名として扱います。
Owner.ChangeState("Run")と書いたら、Runという名前の子オブジェクトがアクティブになります。
名前を変えたら、スクリプト側の文字列も合わせて変更しましょう。 -
動作確認と応用例
- プレイして、WASD/矢印キーなどで入力すると Idle → Run、止まると Run → Idle に切り替わることを確認
- 同じ仕組みで、敵キャラに「Patrol」「Chase」「Attack」状態を作ることも可能
- 動く床やギミックに「Idle(停止)」「Active(動作中)」状態を作るのも簡単です
メリットと応用
StateMachine コンポーネントを使うことで、次のようなメリットがあります。
- 巨大な Update からの脱却
状態ごとにスクリプトを分けられるので、「Idle の処理」と「Run の処理」が別ファイル・別オブジェクトに分離されます。
if (isRunning) ... else if (isJumping) ...といった条件分岐のネストが減り、見通しがよくなります。 - プレハブの再利用性が上がる
「Player」プレハブの中に「Idle」「Run」「Attack」などの状態オブジェクトをセットにしておけば、他のシーンにそのままドラッグ&ドロップするだけで同じ挙動を再現できます。
状態を追加したいときも、新しい子オブジェクトとコンポーネントを追加するだけで済みます。 - レベルデザインがしやすい
敵やギミックの状態を「子オブジェクト」として視覚的に管理できるので、レベルデザイナーが Inspector から直接パラメータを調整しやすくなります。
例えば「Chase」状態の移動速度だけをシーン上で変えて、難易度を調整するなどが簡単です。 - SRP(単一責任)の実現
StateMachineは「状態の切り替え」だけを担当し、各状態は「その状態の挙動だけ」を担当します。
責務が分かれているので、バグ調査や機能追加のときに、どのスクリプトを触ればいいかが明確になります。
改造案:一定時間経過で自動的に別状態へ遷移させる
例えば「ダメージを受けたら 0.5 秒だけ Stun 状態に入り、その後自動で Idle に戻る」といった挙動を作りたい場合、StateBehaviour を継承したクラス内にタイマー付きの処理を追加できます。
using UnityEngine;
using SimpleStateMachine;
public class StunState : StateBehaviour
{
[SerializeField]
private float _stunDuration = 0.5f;
private float _timer;
public override void OnStateEnter()
{
_timer = 0f;
// ここでノックバックやエフェクト再生などを行う
}
public override void OnStateUpdate()
{
_timer += Time.deltaTime;
if (_timer >= _stunDuration)
{
// 一定時間経過したら Idle に戻る
Owner.ChangeState("Idle");
}
}
}
このように、状態ごとに小さなコンポーネントを増やしていくことで、プレイヤーや敵の挙動を柔軟に拡張できます。巨大な 1 ファイルに処理を詰め込むのではなく、「状態」という自然な単位でスクリプトを分割していくと、プロジェクトが大きくなっても破綻しにくくなりますね。
