Godot 4でプレイヤーや敵キャラを作り込んでいくと、_physics_process()の中が
「Idle, Run, Jump, Attack…」のif/elif地獄になりがちですよね。

典型的にはこんな感じです:


func _physics_process(delta: float) -> void:
    if state == "idle":
        # 入力待ち
    elif state == "run":
        # 移動処理
    elif state == "jump":
        # ジャンプ処理
    elif state == "fall":
        # 落下処理
    # ...

状態が増えるほど条件分岐が肥大化し、「どの状態で何をしているのか」が追いにくくなります。
さらに、Playerを継承してEnemyを作る、みたいな継承ベースの設計をすると、
「一部の状態だけ挙動を変えたい」「この敵はJumpがいらない」といった要件に対応しづらくなります。

そこで今回は「状態そのものをノードとして切り出す」コンポーネント指向のアプローチとして、
「StateMachine」コンポーネントを紹介します。
各状態を独立したノード(ステート)として実装し、プレイヤーや敵のシーンにポン付けするだけ
きれいに状態管理ができるようにしていきましょう。

【Godot 4】状態をノードで切り替える!「StateMachine」コンポーネント

今回のコンポーネントは:

  • StateMachine … 親ノードにアタッチする「状態管理コンポーネント」
  • State … 各状態のベースクラス(Idle, Run, Jump などの親)
  • IdleState / RunState / JumpState … 具体的な状態ノード

という構成にします。
ポイントは「親(Playerなど)は StateMachine にだけ依存し、個々の状態クラスを知らなくてよい」という点です。
これにより、状態の追加・削除・差し替えがノード単位で完結し、シーン設計がかなりスッキリします。


GDScriptフルコード

まずはそのままコピペで使えるフルコードから。
ファイルは分けて保存する想定です:

  • res://components/state_machine/State.gd
  • res://components/state_machine/StateMachine.gd
  • res://components/state_machine/states/IdleState.gd
  • res://components/state_machine/states/RunState.gd
  • res://components/state_machine/states/JumpState.gd

State.gd(ステートのベースクラス)


extends Node
class_name State
"""
状態(ステート)のベースクラス。
すべての具体的な状態(Idle, Run, Jump など)はこのクラスを継承します。

・StateMachine からは「State」として扱われるだけで、
  具体的な型(IdleState など)を知らなくても動くように設計しています。
"""

## このステートを管理しているステートマシンへの参照
var state_machine: Node = null

## ステート名(デバッグ用)。未設定ならノード名を使う。
@export var state_name: String = ""

## デバッグログを出すかどうか
@export var debug_log: bool = false


func _ready() -> void:
    # StateMachine 側から上書きされることを想定
    if state_name.is_empty():
        state_name = name


# --- ライフサイクルコールバック ---
# これらは StateMachine から呼ばれます。

func enter(prev_state: State) -> void:
    """
    このステートに遷移してきたときに一度だけ呼ばれる。
    アニメーションの再生開始、変数の初期化などに使います。
    """
    if debug_log:
        print("[State] enter: %s (from %s)" % [state_name, prev_state and prev_state.state_name or "null"])


func exit(next_state: State) -> void:
    """
    このステートから別のステートへ遷移するときに一度だけ呼ばれる。
    アニメーションの停止やタイマーのクリアなどに使います。
    """
    if debug_log:
        print("[State] exit: %s (to %s)" % [state_name, next_state and next_state.state_name or "null"])


func handle_input(event: InputEvent) -> void:
    """
    入力イベントを処理したい場合にオーバーライドします。
    例: ジャンプボタンが押されたら JumpState へ遷移要求を出す、など。
    """
    pass


func update(delta: float) -> void:
    """
    _process 相当の更新処理。
    アニメーション制御やタイマー処理など、物理に依存しないロジックに使います。
    """
    pass


func physics_update(delta: float) -> void:
    """
    _physics_process 相当の更新処理。
    CharacterBody2D の移動処理など、物理フレームに同期したいロジックに使います。
    """
    pass


# --- ユーティリティ ---

func request_state_change(new_state_name: String) -> void:
    """
    ステート自身からステート遷移をリクエストするためのヘルパー。
    StateMachine 側の change_state() を叩きます。
    """
    if state_machine and state_machine.has_method("change_state"):
        state_machine.change_state(new_state_name)

StateMachine.gd(コンポーネント本体)


extends Node
class_name StateMachine
"""
親ノード(例: Player, Enemy)にアタッチするステートマシンコンポーネント。

・子ノードとして State を継承したノードをぶら下げておき、
  その中から current_state_name で指定したステートをアクティブにします。

・各ステートの enter/exit/update/physics_update/handle_input を適切なタイミングで呼び出します。

・親ノードは「StateMachine に対して状態名を指定するだけ」で挙動を切り替えられるようになります。
"""

## 最初にアクティブにするステート名
@export var initial_state_name: String = "Idle"

## 現在のステート名(インスペクタで確認用)
@export var current_state_name: String = ""

## ステート遷移時のログを出すかどうか
@export var debug_log: bool = true

## 入力イベントをステートへ転送するかどうか
@export var forward_input: bool = true

## _process をステートへ転送するかどうか
@export var use_update: bool = false

## _physics_process をステートへ転送するかどうか
@export var use_physics_update: bool = true


## 現在アクティブなステート
var current_state: State = null

## 名前 <-> ステートノード のマップ
var _states: Dictionary = {}


func _ready() -> void:
    _cache_states()
    if initial_state_name != "":
        change_state(initial_state_name)
    else:
        # 最初のステートが指定されていなければ、最初に見つかったステートを使う
        if _states.size() > 0:
            var first_state_name := _states.keys()[0]
            change_state(first_state_name)


func _unhandled_input(event: InputEvent) -> void:
    if forward_input and current_state:
        current_state.handle_input(event)


func _process(delta: float) -> void:
    if use_update and current_state:
        current_state.update(delta)


func _physics_process(delta: float) -> void:
    if use_physics_update and current_state:
        current_state.physics_update(delta)


# --- ステート管理 ---

func _cache_states() -> void:
    """
    子ノードから State を継承したものを探してキャッシュします。
    """
    _states.clear()

    for child in get_children():
        if child is State:
            var state: State = child
            # State 側に StateMachine の参照を渡しておく
            state.state_machine = self

            var name_key := state.state_name
            if name_key.is_empty():
                name_key = state.name

            _states[name_key] = state

    if debug_log:
        print("[StateMachine] cached states: ", _states.keys())


func change_state(new_state_name: String) -> void:
    """
    ステートを切り替えます。
    ・同じステートに切り替えようとした場合は何もしません。
    ・存在しないステート名が指定された場合は警告を出します。
    """
    if not _states.has(new_state_name):
        push_warning("[StateMachine] state '%s' not found on node '%s'" % [new_state_name, name])
        return

    if current_state_name == new_state_name:
        return

    var prev_state := current_state
    var next_state: State = _states[new_state_name]

    if debug_log:
        print("[StateMachine] change_state: %s -> %s" % [prev_state and prev_state.state_name or "null", new_state.state_name])

    # exit を先に呼ぶ
    if prev_state:
        prev_state.exit(next_state)

    current_state = next_state
    current_state_name = new_state_name

    # enter を後から呼ぶ
    current_state.enter(prev_state)


func get_state(state_name: String) -> State:
    """
    名前からステートインスタンスを取得します。
    直接ステートにアクセスしたい場合に使います。
    """
    if _states.has(state_name):
        return _states[state_name]
    return null

IdleState.gd(待機状態)


extends State
class_name IdleState
"""
待機状態(Idle)。
・左右入力があれば Run へ
・ジャンプボタンが押されれば Jump へ
といったシンプルな状態を想定しています。

「どのノードを動かすか」は親(例: Player)を参照して行います。
ここでは CharacterBody2D を想定して、親の velocity をいじる例を示します。
"""

## このステートが操作する対象(例: Player / Enemy など)
@export var actor: Node = null

## 左右移動の入力名
@export var move_left_action: String = "ui_left"
@export var move_right_action: String = "ui_right"

## ジャンプ入力のアクション名
@export var jump_action: String = "ui_accept"


func enter(prev_state: State) -> void:
    .enter(prev_state)
    # アイドルに入ったときに速度をゼロにしたい、など
    if actor and actor.has_variable("velocity"):
        actor.velocity.x = 0


func physics_update(delta: float) -> void:
    # 左右入力があれば Run へ
    var move_input := 0.0
    if Input.is_action_pressed(move_left_action):
        move_input -= 1.0
    if Input.is_action_pressed(move_right_action):
        move_input += 1.0

    if move_input != 0.0:
        request_state_change("Run")
        return

    # ジャンプ入力で Jump へ
    if Input.is_action_just_pressed(jump_action):
        request_state_change("Jump")

RunState.gd(走行状態)


extends State
class_name RunState
"""
走行状態(Run)。
・左右入力に応じて速度を設定
・入力がなくなれば Idle へ
・ジャンプボタンで Jump へ
"""

@export var actor: Node = null

## 左右移動の入力名
@export var move_left_action: String = "ui_left"
@export var move_right_action: String = "ui_right"

## ジャンプ入力のアクション名
@export var jump_action: String = "ui_accept"

## 走行速度
@export var speed: float = 200.0


func physics_update(delta: float) -> void:
    if not actor:
        return

    var move_input := 0.0
    if Input.is_action_pressed(move_left_action):
        move_input -= 1.0
    if Input.is_action_pressed(move_right_action):
        move_input += 1.0

    # 入力がなくなれば Idle へ
    if move_input == 0.0:
        request_state_change("Idle")
        return

    # 速度設定(CharacterBody2D を想定)
    if actor.has_variable("velocity"):
        actor.velocity.x = move_input * speed

    # ジャンプ入力で Jump へ
    if Input.is_action_just_pressed(jump_action):
        request_state_change("Jump")

JumpState.gd(ジャンプ状態)


extends State
class_name JumpState
"""
ジャンプ状態(Jump)。
・ジャンプ初速を与える
・落下し始めたら Idle or Run に戻す(ここでは Idle に単純化)
"""

@export var actor: Node = null

## ジャンプ初速(上向きがマイナスと仮定)
@export var jump_velocity: float = -300.0

## 重力(Actor 側で処理しているなら不要)
@export var gravity: float = 900.0


func enter(prev_state: State) -> void:
    .enter(prev_state)
    if not actor:
        return

    if actor.has_variable("velocity"):
        actor.velocity.y = jump_velocity


func physics_update(delta: float) -> void:
    if not actor:
        return

    # 簡易的な重力適用(実際は Actor 側でやってもOK)
    if actor.has_variable("velocity"):
        actor.velocity.y += gravity * delta

    # CharacterBody2D を想定して「床に着地したら Idle に戻る」
    if actor.has_method("is_on_floor") and actor.is_on_floor():
        request_state_change("Idle")

使い方の手順

① シーン構成を用意する

例として、2Dのプレイヤーキャラクターを想定します。
シーン構成はこんな感じにします:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── StateMachine (Node)  ← StateMachine.gd をアタッチ
      ├── IdleState (Node)  ← IdleState.gd をアタッチ
      ├── RunState  (Node)  ← RunState.gd をアタッチ
      └── JumpState (Node)  ← JumpState.gd をアタッチ
  • PlayerCharacterBody2D で、velocity: Vector2 を持っている前提です。
  • StateMachine ノードに StateMachine.gd をアタッチします。
  • その子として IdleState / RunState / JumpState ノードを作成し、各スクリプトをアタッチします。

② StateMachine コンポーネントを設定する

StateMachine ノードを選択し、インスペクタで以下を設定します:

  • initial_state_name: Idle(IdleState の state_name に合わせる)
  • use_physics_update: On(物理処理をステートに任せる)
  • use_update: 必要なら On(アニメ制御などをステートに分けたいとき)
  • debug_log: 最初は On にして挙動を確認すると安心です。

各ステートノード(IdleState/RunState/JumpState)側では、actor に Player をドラッグ&ドロップして設定しておきましょう。

③ Player 側のコードをシンプルに保つ

Player 側のスクリプト例です。
移動やジャンプのロジックはすべてステート側に任せ、Player は「物理移動」と「アニメーション再生」だけに集中させます。


extends CharacterBody2D

@export var gravity: float = 900.0

func _physics_process(delta: float) -> void:
    # 重力だけここで管理し、x方向・ジャンプはステートに任せる構成もあり
    if not is_on_floor():
        velocity.y += gravity * delta

    move_and_slide()

このように、_physics_process() から「状態に応じた分岐」が消えているのがポイントです。
「今どの状態なのか」「状態ごとに何をするのか」は StateMachine と各 State に完全に委譲されています。

④ 敵キャラや動く床にもそのまま流用する

コンポーネント指向の醍醐味は「再利用」です。
例えば、敵キャラにも同じ StateMachine コンポーネントをそのまま使えます。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── StateMachine (Node)
      ├── IdleState
      ├── RunState
      └── JumpState

敵専用の PatrolStateChaseState を追加するのも簡単で、
「Enemy シーンの StateMachine の子に新しい State ノードをぶら下げるだけ」です。
プレイヤーと敵でまったく別の状態構成にしても、StateMachine コンポーネント自体は同じコードを共有できます。

動く床やギミックも同様に:

MovingPlatform (Node2D)
 ├── CollisionShape2D
 └── StateMachine
      ├── IdleState(停止中)
      └── MoveState(往復移動中)

という形で、「動作モード」を全部ステートとして切り出すと、
シーン構造もロジックもかなり見通しが良くなりますね。


メリットと応用

  • 巨大な if/elif/switch 文からの解放
    各状態が独立したスクリプトになるので、1ファイルあたりの責務が明確になります。
  • 継承ツリーではなく「状態ノードの合成」で差異を表現できる
    「この敵は JumpState がいらない」「このボスは AttackState を2種類持つ」などを、
    ノードの追加・削除だけで表現できます。
  • シーン構造が「状態一覧」になる
    エディタ上で StateMachine の子を見るだけで「このキャラが持っている状態」が一目でわかります。
  • テストやデバッグがしやすい
    一時的に initial_state_name を変えて、特定の状態からゲームを開始する、などが簡単です。

応用としては:

  • アニメーションの再生をすべてステート側に移して、AnimationPlayer / AnimationTree を状態ごとに制御する
  • ネットワーク同期用の「RemoteState」「LocalState」など、モード切り替えにも使う
  • UI の画面遷移(Title, InGame, Pause, Result など)をステートで管理する

改造案:グローバルな「AnyState」ハンドリングを追加する

「どの状態からでも受け付けたい入力」(例: Pause メニューなど)を処理したい場合、
StateMachine に「共通ハンドラ」を追加するのもアリです。


# StateMachine.gd の一部に追記

@export var global_pause_action: String = "ui_cancel"

func _unhandled_input(event: InputEvent) -> void:
    # どのステートでも共通で扱いたい入力
    if event.is_action_pressed(global_pause_action):
        # ここでポーズメニューを開くなど
        get_tree().paused = not get_tree().paused
        return

    # 通常のステートへのフォワード
    if forward_input and current_state:
        current_state.handle_input(event)

このように、StateMachine 自体もコンポーネントとして拡張していけるのが、
合成(Composition)ベースで設計する楽しさですね。
継承に頼らず、「必要な機能をノードとして足していく」スタイルで、
自分のプロジェクトに合ったステートマシンを育てていきましょう。