Godot 4 でステート管理をしようとすると、ついこんな構成になりがちですよね。

  • Player.gd に state 変数を持たせて match state: で分岐しまくる
  • 「Idle」「Run」「Jump」…と増えるたびに、巨大な _physics_process() がさらに肥大化
  • 敵ごと・プレイヤーごとに似たようなステート処理をコピペして地獄

また、Godot のチュートリアルでも「プレイヤーシーンを継承して敵キャラを作る」みたいなパターンがよく出てきますが、継承ベースでステートを管理すると:

  • 「このキャラだけ特殊なステートを追加したい」ときに継承チェーンがどんどん深くなる
  • 共通ロジックを親クラスに寄せるほど、個別キャラのカスタマイズがやりづらくなる

そこで今回は、「継承より合成」の考え方で、どんなアクター(プレイヤー、敵、動く床など)にもポン付けできる汎用 StateMachine コンポーネント を用意します。
子ノードとして IdleRun の「ステートノード」をぶら下げておき、今アクティブなステートだけに処理を回すコンポーネントです。

【Godot 4】ステート爆発を優しく制御!「StateMachine」コンポーネント

このコンポーネントの役割はシンプルです。

  • 子ノードのステートを一括管理(例: Idle, Run, Jump
  • 現在アクティブなステートだけ_process / _physics_process 相当の処理を委譲
  • ステート切り替え時に enter() / exit() を自動呼び出し
  • 状態遷移 API を統一(change_state("Run") のように呼ぶだけ)

「StateMachine 自体は Node1つ」としてアタッチするだけでOKなので、プレイヤーでも敵でも動くギミックでも、同じコンポーネントを使い回せます。


フルコード: StateMachine.gd


extends Node
class_name StateMachine
## シンプルなステートマシンコンポーネント
## 子ノードを「ステート」として管理し、アクティブなステートだけ処理を行う

## 最初に有効にしたいステート名(子ノード名と一致させる)
@export var initial_state_name: StringName = "Idle"

## ステート切り替え時にログを出すかどうか(デバッグ用)
@export var debug_log: bool = false

## 物理フレームでステートを更新するかどうか
## プレイヤーや敵の移動など、物理挙動に絡む処理なら true 推奨
@export var use_physics_process: bool = true

## 通常のフレームでステートを更新するかどうか
## アニメーションや UI など、物理と関係ない処理用
@export var use_process: bool = false

## 現在のステートノード
var current_state: Node = null
## ステート名 → ノード の辞書
var _states: Dictionary = {}

## ステートノードが実装すべき「インターフェース」の名前一覧
## これらは必須ではないが、あれば StateMachine が自動で呼び出す
const FUNC_ENTER := "enter"
const FUNC_EXIT := "exit"
const FUNC_UPDATE := "update"
const FUNC_PHYSICS_UPDATE := "physics_update"

func _ready() -> void:
    # 子ノードをすべてステートとして登録
    _collect_states()
    # 初期ステートに切り替え
    if initial_state_name != StringName(""):
        change_state(initial_state_name)

func _process(delta: float) -> void:
    if not use_process:
        return
    if current_state and current_state.has_method(FUNC_UPDATE):
        current_state.call(FUNC_UPDATE, delta)

func _physics_process(delta: float) -> void:
    if not use_physics_process:
        return
    if current_state and current_state.has_method(FUNC_PHYSICS_UPDATE):
        current_state.call(FUNC_PHYSICS_UPDATE, delta)

## 子ノードを走査してステートを登録
func _collect_states() -> void:
    _states.clear()
    for child in get_children():
        # ノード名をステート名として使用
        var state_name: StringName = child.name
        _states[state_name] = child
    if debug_log:
        print("[StateMachine] Registered states: ", _states.keys())

## 外部からステートを切り替えるためのメインAPI
## 例: state_machine.change_state("Run")
func change_state(target_state_name: StringName) -> void:
    if not _states.has(target_state_name):
        push_warning("[StateMachine] State '%s' not found under %s" % [target_state_name, get_path()])
        return

    var new_state: Node = _states[target_state_name]
    if new_state == current_state:
        return  # 同じステートなら何もしない

    if debug_log:
        print("[StateMachine] %s -> %s" % [
            current_state if current_state else "<none>",
            target_state_name
        ])

    # 現在のステートの exit() を呼ぶ
    if current_state and current_state.has_method(FUNC_EXIT):
        current_state.call(FUNC_EXIT)

    current_state = new_state

    # 新しいステートの enter() を呼ぶ
    if current_state and current_state.has_method(FUNC_ENTER):
        current_state.call(FUNC_ENTER)

## 現在のステート名を返す(なければ空文字)
func get_current_state_name() -> StringName:
    if not current_state:
        return StringName("")
    return current_state.name

## ステートノードを動的に追加したあとなどに再スキャンしたい場合に呼ぶ
func refresh_states() -> void:
    _collect_states()
    # 現在のステート参照を更新(名前が変わった場合などに備える)
    var current_name := get_current_state_name()
    if current_name != StringName("") and _states.has(current_name):
        current_state = _states[current_name]

ステート用のベースクラス(任意だが推奨)

ステート側の実装を揃えたい場合は、簡単なベースクラスを用意しておくと楽です。必須ではありませんが、例として載せておきます。


extends Node
class_name State
## StateMachine が呼び出すことを想定したベースステートクラス
## 必須ではないが、継承しておくと補完が効いて便利

var owner_actor: Node = null  ## プレイヤー本体などを参照させたい場合に使う

func enter() -> void:
    # ステートに入ったときの初期化処理
    pass

func exit() -> void:
    # ステートから抜けるときの後始末
    pass

func update(delta: float) -> void:
    # _process 相当の処理
    pass

func physics_update(delta: float) -> void:
    # _physics_process 相当の処理
    pass

実際のステート(Idle, Run など)はこの State を継承してもいいですし、同じメソッド名を持つ普通の Node でも動きます。


使い方の手順

例: 2D プレイヤーキャラクターに StateMachine を組み込む

プレイヤーのシーン構成はこんな感じにします。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── StateMachine (Node)  ← ここに StateMachine.gd をアタッチ
      ├── Idle (Node)    ← Idle ステート用ノード
      └── Run (Node)     ← Run ステート用ノード

手順①: StateMachine.gd を用意して、Node にアタッチ

  1. StateMachine.gd を上記コードで作成します。
  2. プレイヤーシーンを開き、子ノードとして Node を追加し、名前を StateMachine にします。
  3. そのノードにスクリプトとして StateMachine.gd をアタッチします。
  4. インスペクタで initial_state_nameIdle に設定し、use_physics_process = true にしておきます。

手順②: ステートノード(Idle / Run)を作成

次に、StateMachine の子としてステートを作ります。

  1. StateMachine の子として Node を追加し、名前を Idle にします。
  2. 同様に Run ノードも追加します。
  3. それぞれにスクリプトをアタッチし、physics_update などを実装します。

例として、単純な 2D 移動プレイヤーの Idle / Run ステートを作ってみます。


# Player.gd (CharacterBody2D にアタッチ)
extends CharacterBody2D

@export var move_speed: float = 200.0

var state_machine: StateMachine

func _ready() -> void:
    state_machine = $StateMachine

# Idle.gd (StateMachine/Idle ノードにアタッチ)
extends Node
# extends State としてもOK(class_name State を使う場合)

var actor: CharacterBody2D

func _ready() -> void:
    # プレイヤー本体を取得(親の親が Player という前提)
    actor = get_parent().get_parent() as CharacterBody2D

func enter() -> void:
    # Idle に入ったときに速度をゼロにするなど
    if actor:
        actor.velocity = Vector2.ZERO

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

    var input_dir := Input.get_axis("ui_left", "ui_right")
    if input_dir != 0.0:
        # 入力があれば Run ステートへ
        var sm := get_parent() as StateMachine
        sm.change_state("Run")

# Run.gd (StateMachine/Run ノードにアタッチ)
extends Node

var actor: CharacterBody2D

func _ready() -> void:
    actor = get_parent().get_parent() as CharacterBody2D

func enter() -> void:
    # Run に入ったときの初期処理(アニメ再生など)
    pass

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

    var input_dir := Input.get_axis("ui_left", "ui_right")
    if input_dir == 0.0:
        # 入力がなくなったら Idle に戻る
        var sm := get_parent() as StateMachine
        sm.change_state("Idle")
        return

    # 移動処理
    actor.velocity.x = input_dir * actor.move_speed
    actor.move_and_slide()

ポイントは:

  • StateMachine 自体は プレイヤーのロジックを一切知らない
  • 各ステートは get_parent().get_parent() などで「所有者(プレイヤー本体)」を取ってくるだけ
  • ステート間の遷移は get_parent() as StateMachine 経由で change_state() を呼ぶだけ

これでプレイヤー側の _physics_process() はほぼ空のまま、ステートごとにロジックを分割できます。

手順③: 敵キャラや動く床にもそのまま流用

同じ StateMachine.gd を、敵キャラにもアタッチしてみましょう。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── StateMachine (Node)
      ├── Patrol (Node)
      └── Chase (Node)

Enemy.gd は「速度」「ターゲット参照」など最低限だけ持たせ、パトロールや追跡ロジックは Patrol.gd / Chase.gd に閉じ込めます。
StateMachine のコードはプレイヤーと完全に共通です。

手順④: ノードをまたいだステート遷移も簡単

例えば、動く床が「Stopped」「Moving」ステートを持っていて、プレイヤーが乗ったら Moving に遷移させたい場合:

MovingPlatform (Node2D)
 ├── CollisionShape2D
 └── StateMachine
      ├── Stopped
      └── Moving

プレイヤー側のスクリプトから:


func _on_body_entered(body: Node) -> void:
    if body is Node2D and body.has_node("StateMachine"):
        var sm := body.get_node("StateMachine") as StateMachine
        sm.change_state("Moving")

のように、コンポーネントとしての StateMachine にだけ依存する形で、他のオブジェクトのステートを安全に切り替えられます。


メリットと応用

この StateMachine コンポーネントを使うことで、次のようなメリットがあります。

  • ノード階層が浅いままステート管理ができる
    • プレイヤー本体は CharacterBody2D のまま、StateMachine は子ノード1つだけ
    • ステート追加は StateMachine の子ノードを増やすだけなので見通しが良い
  • ロジックの分割が明確
    • 「Idle のときに何が起こるか」「Run のときに何が起こるか」がスクリプト単位で分かれる
    • 巨大な match state: とはお別れできます
  • 再利用性が高い
    • StateMachine.gd はプレイヤーでも敵でもギミックでも共通で使える
    • 例えば「Damage」「Dead」ステートを別シーンにコピペするだけで流用可能
  • 継承地獄からの脱出
    • 「PlayerBase」→「EnemyBase」→「FlyingEnemy」…みたいな継承チェーンではなく
    • 「必要なノードに StateMachine コンポーネントを追加する」だけで済む

また、レベルデザインの観点でも:

  • シーンツリーを眺めるだけで「このオブジェクトがどんなステートを持っているか」が一目で分かる
  • 特定のステートだけを複製して別オブジェクトに持っていく、といった作業がしやすい

改造案: グローバルイベントでステート遷移する

例えば、「どこからでも特定のステートに飛びたい(ゲームオーバー時など)」という要件が出てきたら、StateMachine にシンプルなシグナル連携を追加できます。


# StateMachine.gd の一部に追記

@export var listen_global_bus: bool = false
@export var global_bus_path: NodePath = NodePath("/root/GlobalBus")
@export var global_event_name: StringName = "force_state"

func _ready() -> void:
    _collect_states()
    if initial_state_name != StringName(""):
        change_state(initial_state_name)
    if listen_global_bus:
        _connect_global_bus()

func _connect_global_bus() -> void:
    if not has_node(global_bus_path):
        push_warning("[StateMachine] GlobalBus not found at %s" % global_bus_path)
        return
    var bus := get_node(global_bus_path)
    if bus.has_signal(global_event_name):
        bus.connect(global_event_name, Callable(self, "_on_global_force_state"))

func _on_global_force_state(target_state: StringName) -> void:
    change_state(target_state)

これで、例えば GlobalBus ノードから emit_signal("force_state", "Dead") を呼ぶだけで、ゲーム内の複数の StateMachine を一斉に「Dead」ステートへ飛ばす、といったことも簡単にできます。

こんな感じで、StateMachine を「1つの巨大な親クラス」ではなく、「どのノードにも足せるコンポーネント」として育てていくと、プロジェクト全体の設計がかなり楽になりますね。ぜひ自分のゲーム用にカスタマイズしてみてください。