Unityを触り始めた頃は、つい Update() の中に「入力」「移動」「アニメ」「当たり判定」「UI更新」…と全部詰め込みがちですよね。
最初は動くので満足できますが、状態が増えるほどコードは条件分岐だらけになり、バグ修正もしんどくなります。
例えばプレイヤーの処理に、Idle / Run / Jump / Attack / Damage / Dead などが増えてくると、
// ありがちなアンチパターン
void Update()
{
if (isDead)
{
// 死亡処理
}
else if (isDamaged)
{
// ダメージ処理
}
else if (isAttacking)
{
// 攻撃処理
}
else if (isJumping)
{
// ジャンプ処理
}
else if (isRunning)
{
// 走り処理
}
else
{
// 待機処理
}
}
こんな感じで if / else の沼にはまると、「状態を1つ追加するだけ」で地獄を見ることになります。
そこでこの記事では、状態をノード(State)として分離し、切り替えだけを管理するためのコンポーネント、StateMachine を作っていきます。
【Unity】状態ごとにロジックを分割!「StateMachine」コンポーネント
今回のコンポーネントのゴールはシンプルです:
- Idle / Run / Jump などを「State」として別クラスに分割する
- 共通のインターフェースで
Enter / Exit / Updateを呼び出す StateMachineコンポーネントは「今どの状態か」を管理するだけ
これにより、巨大な Update 関数を避けつつ、状態ごとのロジックをスッキリ整理できます。
フルコード:StateMachine と サンプル状態クラス
以下は、プレイヤーの Idle / Run / Jump を例にした、コピペで動くシンプルなステートマシン実装です。
1ファイルにまとめていますが、実際のプロジェクトではクラスごとにファイルを分けると管理しやすいです。
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>状態の共通インターフェース</summary>
public interface IState
{
/// <summary>この状態に入ったとき1回だけ呼ばれる</summary>
void Enter();
/// <summary>この状態から抜けるとき1回だけ呼ばれる</summary>
void Exit();
/// <summary>毎フレーム呼ばれる更新処理</summary>
void Tick();
}
/// <summary>ジェネリックなステートマシン本体</summary>
public class StateMachine<TStateKey>
{
// 現在の状態キー(例:enum PlayerState)
public TStateKey CurrentKey { get; private set; }
// 現在アクティブな状態インスタンス
public IState CurrentState { get; private set; }
// キーごとに状態インスタンスを保持
private readonly Dictionary<TStateKey, IState> _states =
new Dictionary<TStateKey, IState>();
/// <summary>状態を登録する</summary>
public void AddState(TStateKey key, IState state)
{
if (_states.ContainsKey(key))
{
Debug.LogWarning($"StateMachine: すでに登録済みのキーです: {key}");
return;
}
_states.Add(key, state);
}
/// <summary>状態を切り替える</summary>
public void ChangeState(TStateKey key)
{
if (!_states.TryGetValue(key, out var nextState))
{
Debug.LogError($"StateMachine: 未登録の状態に切り替えようとしました: {key}");
return;
}
// 現在の状態から抜ける
CurrentState?.Exit();
// 新しい状態に切り替え
CurrentState = nextState;
CurrentKey = key;
// 新しい状態に入る
CurrentState.Enter();
}
/// <summary>現在の状態の更新処理を呼び出す</summary>
public void Tick()
{
CurrentState?.Tick();
}
}
/// <summary>プレイヤーの状態キーを表す enum</summary>
public enum PlayerState
{
Idle,
Run,
Jump
}
/// <summary>プレイヤー用の StateMachine MonoBehaviour</summary>
[RequireComponent(typeof(Rigidbody))]
public class PlayerStateMachine : MonoBehaviour
{
[Header("移動設定")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float jumpForce = 5f;
[Header("地面判定")]
[SerializeField] private Transform groundCheck; // 足元の位置
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
private Rigidbody _rb;
private StateMachine<PlayerState> _stateMachine;
// 入力値
private float _inputX;
private bool _jumpPressed;
private void Awake()
{
_rb = GetComponent<Rigidbody>();
// ステートマシンを生成
_stateMachine = new StateMachine<PlayerState>();
// 各状態を登録(this を渡してプレイヤー本体にアクセスできるようにする)
_stateMachine.AddState(PlayerState.Idle, new PlayerIdleState(this));
_stateMachine.AddState(PlayerState.Run, new PlayerRunState(this));
_stateMachine.AddState(PlayerState.Jump, new PlayerJumpState(this));
}
private void Start()
{
// 初期状態は Idle
_stateMachine.ChangeState(PlayerState.Idle);
}
private void Update()
{
// 入力の取得は MonoBehaviour 側でまとめて行う
_inputX = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
_jumpPressed = true;
}
// 現在の状態の更新処理を呼び出す
_stateMachine.Tick();
// 1フレーム限りの入力フラグをリセット
_jumpPressed = false;
}
/// <summary>地面に接地しているかどうかを返す</summary>
public bool IsGrounded()
{
if (groundCheck == null)
{
// groundCheck が設定されていない場合はとりあえず true 扱い
return true;
}
return Physics.CheckSphere(
groundCheck.position,
groundCheckRadius,
groundLayer,
QueryTriggerInteraction.Ignore
);
}
/// <summary>現在の入力の左右値(-1 ~ 1)を取得</summary>
public float GetInputX() => _inputX;
/// <summary>ジャンプボタンが押されたかどうか</summary>
public bool GetJumpPressed() => _jumpPressed;
/// <summary>指定した水平速度で移動する</summary>
public void MoveHorizontally(float speed)
{
var velocity = _rb.velocity;
velocity.x = speed;
_rb.velocity = velocity;
}
/// <summary>ジャンプ力を与える</summary>
public void Jump()
{
var velocity = _rb.velocity;
velocity.y = 0f; // 既存のY速度をリセットしてからジャンプ
_rb.velocity = velocity;
_rb.AddForce(Vector3.up * jumpForce, ForceMode.VelocityChange);
}
/// <summary>状態を外部から切り替えたいとき用のヘルパー(任意)</summary>
public void ChangeState(PlayerState next)
{
_stateMachine.ChangeState(next);
}
/// <summary>現在の状態キーを返す(デバッグ用)</summary>
public PlayerState GetCurrentState()
{
return _stateMachine.CurrentKey;
}
// ---- 以下、各状態のクラス定義 ----
/// <summary>待機状態</summary>
private class PlayerIdleState : IState
{
private readonly PlayerStateMachine _owner;
public PlayerIdleState(PlayerStateMachine owner)
{
_owner = owner;
}
public void Enter()
{
// Idle に入ったときの初期化処理
// 例:アニメーション再生など
// Debug.Log("Enter Idle");
_owner.MoveHorizontally(0f);
}
public void Exit()
{
// Idle から抜けるときの後始末
// Debug.Log("Exit Idle");
}
public void Tick()
{
// 入力があれば Run へ
float inputX = _owner.GetInputX();
if (Mathf.Abs(inputX) > 0.01f)
{
_owner.ChangeState(PlayerState.Run);
return;
}
// ジャンプ入力があり、地面にいれば Jump へ
if (_owner.GetJumpPressed() && _owner.IsGrounded())
{
_owner.ChangeState(PlayerState.Jump);
return;
}
// Idle 中は特に移動しない
_owner.MoveHorizontally(0f);
}
}
/// <summary>走り状態</summary>
private class PlayerRunState : IState
{
private readonly PlayerStateMachine _owner;
public PlayerRunState(PlayerStateMachine owner)
{
_owner = owner;
}
public void Enter()
{
// Debug.Log("Enter Run");
}
public void Exit()
{
// Debug.Log("Exit Run");
}
public void Tick()
{
float inputX = _owner.GetInputX();
// 入力がなくなったら Idle へ
if (Mathf.Abs(inputX) <= 0.01f)
{
_owner.ChangeState(PlayerState.Idle);
return;
}
// ジャンプ入力があり、地面にいれば Jump へ
if (_owner.GetJumpPressed() && _owner.IsGrounded())
{
_owner.ChangeState(PlayerState.Jump);
return;
}
// 入力方向に移動
float speed = inputX * _owner.moveSpeed;
_owner.MoveHorizontally(speed);
}
}
/// <summary>ジャンプ状態</summary>
private class PlayerJumpState : IState
{
private readonly PlayerStateMachine _owner;
private bool _hasJumped;
public PlayerJumpState(PlayerStateMachine owner)
{
_owner = owner;
}
public void Enter()
{
// 状態に入った瞬間に1回だけジャンプさせる
// Debug.Log("Enter Jump");
_owner.Jump();
_hasJumped = true;
}
public void Exit()
{
// Debug.Log("Exit Jump");
}
public void Tick()
{
float inputX = _owner.GetInputX();
// 空中でも左右移動は可能にする
float speed = inputX * _owner.moveSpeed;
_owner.MoveHorizontally(speed);
// 地面に着地したら Idle / Run へ戻す
if (_owner.IsGrounded() && _hasJumped)
{
// 着地時に入力があれば Run、なければ Idle
if (Mathf.Abs(inputX) > 0.01f)
{
_owner.ChangeState(PlayerState.Run);
}
else
{
_owner.ChangeState(PlayerState.Idle);
}
}
}
}
}
使い方の手順
ここでは「プレイヤーキャラクター」にこのステートマシンを適用する手順を説明します。
-
プレイヤーの GameObject を用意する
- Unity の Hierarchy で
Playerという名前の空オブジェクト、またはキャラモデル付きのオブジェクトを用意します。 Rigidbodyコンポーネントをアタッチします(Use GravityはオンのままでOK)。- 地面には
Collider(BoxCollider や MeshCollider)を付け、Groundという Layer を作って割り当てておきます。
- Unity の Hierarchy で
-
PlayerStateMachine スクリプトをアタッチする
- 上記コードを
PlayerStateMachine.csとして保存します。 - Player の GameObject にドラッグ&ドロップしてアタッチします。
- Inspector で以下を設定します:
- Move Speed:左右移動速度(例:5)
- Jump Force:ジャンプ力(例:5~7)
- Ground Check:Player の足元に Empty オブジェクトを作成して割り当て
- Ground Check Radius:接地判定の半径(例:0.2)
- Ground Layer:地面用 Layer(例:Ground)を指定
- 上記コードを
-
入力設定を確認する
- このサンプルは古典的な
Input.GetAxis("Horizontal")とInput.GetButtonDown("Jump")を使っています。 - Horizontal(A/D or ←/→)、Jump(Space)など、旧 Input Manager のデフォルト設定で動作します。
- 新 Input System を使う場合は、
Update()内の入力取得部分だけを自分の入力コードに差し替えてください。
- このサンプルは古典的な
-
動作確認する
- Play ボタンを押し、左右キーで Run に遷移し、Space キーで Jump に遷移するのを確認します。
- 必要であれば
GetCurrentState()を使って、現在の状態を UI Text に表示したり、Debug.Log()でログに出すと状態遷移が分かりやすくなります。
応用例:
- 敵キャラ:
EnemyStateMachineとしてPatrol / Chase / Attack / Deadなどの状態を持たせる。 - 動く床:
MovingPlatformStateMachineとしてIdle / Move / WaitAtEndのような状態で挙動を分ける。 - UI ウィンドウ:
UIStateMachineとしてHidden / Showing / Shown / Hidingの状態に分けてアニメ制御を行う。
メリットと応用
この StateMachine コンポーネントを使うメリットは、主に次の3つです。
- 状態ごとにクラスを分けられる
Idle / Run / Jumpなどを別クラスに切り出すことで、1ファイルあたりの責務が小さくなり、Godクラス化を防げます。
「ジャンプの挙動だけを直したい」ときに、ジャンプ状態のクラスだけを開けばよくなります。 - プレハブの再利用性が上がる
プレイヤーだけでなく、敵、ギミック、UI など、同じステートマシンの枠組みを使い回せます。
たとえばStateMachine<EnemyState>を持つEnemyStateMachineコンポーネントを作れば、敵の種類ごとに状態だけ差し替えたプレハブを量産できます。 - レベルデザインが楽になる
「この敵は Patrol → Chase → Attack の3状態」「このギミックは Idle → Move → Wait の2状態」といった設計を、状態 enum とクラスの組み合わせで視覚化できます。
デザイナーや他の開発者に仕様を説明するときも、「このノードからこのノードに遷移する」と伝えやすくなります。
さらに、状態のクラスを public にして別ファイルに出しておけば、差し替えや拡張も容易です。
例えば、PlayerAttackState を新しく追加する場合も、既存の Idle / Run / Jump クラスにはほとんど手を入れずに済みます。
改造案:一定時間で自動的に別の状態へ遷移するヘルパー
タイマー付きの状態を作りたいケースはよくあります(例:ダメージ硬直、攻撃後の隙、一時的なバフ状態など)。
そんなときに使える、「指定秒数経過したら別の状態へ戻る」簡易ヘルパーの例です。
/// <summary>指定時間経過後に元の状態へ戻る一時的な状態の例</summary>
private class TimedState : IState
{
private readonly PlayerStateMachine _owner;
private readonly PlayerState _returnState;
private readonly float _duration;
private float _timer;
public TimedState(PlayerStateMachine owner, PlayerState returnState, float duration)
{
_owner = owner;
_returnState = returnState;
_duration = duration;
}
public void Enter()
{
_timer = 0f;
// ここで一時的なエフェクトやアニメ再生など
}
public void Exit()
{
// 終了時の後始末(エフェクト停止など)
}
public void Tick()
{
_timer += Time.deltaTime;
if (_timer >= _duration)
{
// 規定時間が経過したら元の状態へ戻る
_owner.ChangeState(_returnState);
}
}
}
例えば「ダメージを受けたら 0.5 秒間だけ Damage 状態になってから元の状態に戻る」といった挙動を、このようなタイマー付き状態で実装できます。
このように、状態をクラスとして切り出しておくと、挙動のバリエーションを増やすのがとても楽になりますね。
