Godot 4 でステート管理をしようとすると、ついこんな構成になりがちですよね。
- Player.gd に
state変数を持たせてmatch state:で分岐しまくる - 「Idle」「Run」「Jump」…と増えるたびに、巨大な
_physics_process()がさらに肥大化 - 敵ごと・プレイヤーごとに似たようなステート処理をコピペして地獄
また、Godot のチュートリアルでも「プレイヤーシーンを継承して敵キャラを作る」みたいなパターンがよく出てきますが、継承ベースでステートを管理すると:
- 「このキャラだけ特殊なステートを追加したい」ときに継承チェーンがどんどん深くなる
- 共通ロジックを親クラスに寄せるほど、個別キャラのカスタマイズがやりづらくなる
そこで今回は、「継承より合成」の考え方で、どんなアクター(プレイヤー、敵、動く床など)にもポン付けできる汎用 StateMachine コンポーネント を用意します。
子ノードとして Idle や Run の「ステートノード」をぶら下げておき、今アクティブなステートだけに処理を回すコンポーネントです。
【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 にアタッチ
StateMachine.gdを上記コードで作成します。- プレイヤーシーンを開き、子ノードとして
Nodeを追加し、名前をStateMachineにします。 - そのノードにスクリプトとして
StateMachine.gdをアタッチします。 - インスペクタで
initial_state_nameをIdleに設定し、use_physics_process = trueにしておきます。
手順②: ステートノード(Idle / Run)を作成
次に、StateMachine の子としてステートを作ります。
StateMachineの子としてNodeを追加し、名前をIdleにします。- 同様に
Runノードも追加します。 - それぞれにスクリプトをアタッチし、
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つの巨大な親クラス」ではなく、「どのノードにも足せるコンポーネント」として育てていくと、プロジェクト全体の設計がかなり楽になりますね。ぜひ自分のゲーム用にカスタマイズしてみてください。
