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 自体は「どの状態を有効にするか」だけを担当します。

使い方の手順

  1. プレイヤー用の親オブジェクトを作成
    • Hierarchy で Right Click > Create Empty から「Player」を作成
    • 「Player」に CharacterController コンポーネントを追加
    • 上で紹介した StateMachine コンポーネントを「Player」に追加
    • _defaultStateNameIdle と入力
  2. 状態ごとの子オブジェクトを作成
    • 「Player」の子として空オブジェクトを2つ作成し、名前をそれぞれ Idle, Run にする
    • Idle オブジェクトに PlayerIdleState コンポーネントを追加
    • Run オブジェクトに PlayerRunState コンポーネントを追加
    • Run のオブジェクトにも CharacterController は不要です(親の Player にだけ付いていればOK)
  3. 状態名とオブジェクト名を一致させる

    ステートマシンは「子オブジェクトの名前」を状態名として扱います。
    Owner.ChangeState("Run") と書いたら、Run という名前の子オブジェクトがアクティブになります。
    名前を変えたら、スクリプト側の文字列も合わせて変更しましょう。

  4. 動作確認と応用例
    • プレイして、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 ファイルに処理を詰め込むのではなく、「状態」という自然な単位でスクリプトを分割していくと、プロジェクトが大きくなっても破綻しにくくなりますね。