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.gdres://components/state_machine/StateMachine.gdres://components/state_machine/states/IdleState.gdres://components/state_machine/states/RunState.gdres://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 をアタッチ
PlayerはCharacterBody2Dで、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
敵専用の PatrolState や ChaseState を追加するのも簡単で、
「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)ベースで設計する楽しさですね。
継承に頼らず、「必要な機能をノードとして足していく」スタイルで、
自分のプロジェクトに合ったステートマシンを育てていきましょう。
