state-machine について
state-machine は、Godot 4.3 以降の有限ステートマシンを GDScript と C# で解説するスキルです。プロジェクトの複雑さに応じて、列挙型、ノード、リソース、階層型、並列型の設計を選ぶための指針を提供します。
主な機能
- 013種類の基本 FSM 方式を比較
- 02GDScript と C# の例
- 03階層型と並列型のパターン
- 04遷移とライフサイクルのチェックリスト
活用シーン
- 01キャラクター動作の方式選定
- 02エディター設定可能な AI ステートの設計
- 03大規模 FSM の関心事ごとの分割
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill state-machineインストール後、Claude Code で/state-machineを入力して読み込まれたか確認します。
同梱ファイルreferences/hierarchical-and-parallel.md
詳細
Godot 4.3+ のステートマシン
複雑さに応じて適切な FSM パターンを選びましょう。すべての例は、非推奨 API を使わずに Godot 4.3+ を対象としています。
関連スキル: 移動状態の統合には player-controller、AI の状態パターンには ai-navigation、リソースベースの状態設定には resource-pattern、FSM で駆動する AnimationTree の状態には animation-system、ステートマシンとしての会話フローには dialogue-system、キャスターの状態制御(詠唱中/スタン中)には ability-system、FSM と併用するビヘイビアツリーが必要な場合の LimboAI アドオンの HSM(
BTState)には limboai、GDScript のみで使える BT の代替には beehave を参照してください。
アドオンを選ぶ場合: このスキルでは、組み込みの FSM パターン(enum、ノードベース、リソースベース)を扱います。エージェントに完全なビヘイビアツリーが必要な場合は、代わりに limboai(C++ + HSM、Godot 4.6+)または beehave(GDScript のみ、Godot 4.1+)を参照してください。
1. アプローチの比較
| アプローチ | 複雑さ | 最適な用途 |
|---|---|---|
| Enum ベース | 低 | シンプルなオブジェクト、状態が 5 個未満 |
| ノードベース | 中 | 複雑な動作を持つキャラクター |
| リソースベース | 高 | データ駆動型またはエディターで設定可能な AI |
推奨する際は、採用しなかったアプローチと、判断を決めた要因(状態数、独立した関心事、デザイナーによる編集)を明示してください。「7 つの状態それぞれに独自の enter/exit ロジックがあり、1 つのスクリプトでは手に負えなくなるため、match を使う enum ではなくノードベース」のようにします。代替案を明示しない選択は、覆すことができません。
2. アプローチ 1:Enum ベース(最もシンプル)
状態数が少なく、重要な enter/exit ロジックがない場合に使用します。
GDScript
extends CharacterBody2D
enum State { IDLE, PATROL, CHASE, ATTACK }
@export var patrol_range: float = 200.0
@export var chase_range: float = 300.0
@export var attack_range: float = 50.0
@export var speed: float = 80.0
var current_state: State = State.IDLE
var patrol_target: Vector2 = Vector2.ZERO
@onready var player: Node2D = get_tree().get_first_node_in_group("player")
func _physics_process(delta: float) -> void:
match current_state:
State.IDLE:
_state_idle()
State.PATROL:
_state_patrol()
State.CHASE:
_state_chase()
State.ATTACK:
_state_attack()
move_and_slide()
func _state_idle() -> void:
velocity = Vector2.ZERO
if _player_in_range(chase_range):
current_state = State.CHASE
elif randf() < 0.005:
patrol_target = global_position + Vector2(randf_range(-patrol_range, patrol_range), 0.0)
current_state = State.PATROL
func _state_patrol() -> void:
var direction := (patrol_target - global_position)
if direction.length() < 4.0:
current_state = State.IDLE
return
velocity = direction.normalized() * speed
if _player_in_range(chase_range):
current_state = State.CHASE
func _state_chase() -> void:
if not is_instance_valid(player):
current_state = State.IDLE
return
if _player_in_range(attack_range):
current_state = State.ATTACK
return
if not _player_in_range(chase_range):
current_state = State.PATROL
return
velocity = (player.global_position - global_position).normalized() * speed
func _state_attack() -> void:
velocity = Vector2.ZERO
if not _player_in_range(attack_range):
current_state = State.CHASE
func _player_in_range(range: float) -> bool:
if not is_instance_valid(player):
return false
return global_position.distance_to(player.global_position) <= range
C# 相当
using Godot;
public partial class SimpleEnemy : CharacterBody2D
{
private enum State { Idle, Patrol, Chase, Attack }
[Export] public float PatrolRange { get; set; } = 200f;
[Export] public float ChaseRange { get; set; } = 300f;
[Export] public float AttackRange { get; set; } = 50f;
[Export] public float Speed { get; set; } = 80f;
private State _currentState = State.Idle;
private Vector2 _patrolTarget = Vector2.Zero;
private Node2D _player;
public override void _Ready()
{
_player = GetTree().GetFirstNodeInGroup("player") as Node2D;
}
public override void _PhysicsProcess(double delta)
{
switch (_currentState)
{
case State.Idle: StateIdle(); break;
case State.Patrol: StatePatrol(); break;
case State.Chase: StateChase(); break;
case State.Attack: StateAttack(); break;
}
MoveAndSlide();
}
private void StateIdle()
{
Velocity = Vector2.Zero;
if (PlayerInRange(ChaseRange))
{
_currentState = State.Chase;
}
else if (GD.Randf() < 0.005f)
{
_patrolTarget = GlobalPosition + new Vector2((float)GD.RandRange(-PatrolRange, PatrolRange), 0f);
_currentState = State.Patrol;
}
}
private void StatePatrol()
{
var direction = _patrolTarget - GlobalPosition;
if (direction.Length() < 4f) { _currentState = State.Idle; return; }
Velocity = direction.Normalized() * Speed;
if (PlayerInRange(ChaseRange)) _currentState = State.Chase;
}
private void StateChase()
{
if (!IsInstanceValid(_player)) { _currentState = State.Idle; return; }
if (PlayerInRange(AttackRange)) { _currentState = State.Attack; return; }
if (!PlayerInRange(ChaseRange)) { _currentState = State.Patrol; return; }
Velocity = (_player.GlobalPosition - GlobalPosition).Normalized() * Speed;
}
private void StateAttack()
{
Velocity = Vector2.Zero;
if (!PlayerInRange(AttackRange)) _currentState = State.Chase;
}
private bool PlayerInRange(float range) =>
IsInstanceValid(_player) && GlobalPosition.DistanceTo(_player.GlobalPosition) <= range;
}
Enum ベースから切り替えるタイミング:
- enter/exit ロジックが状態メソッド間で重複し始める
- アニメーションとの同期に明示的な enter/exit フックが必要になる
match/switchブロックが約 100 行を超える
3. アプローチ 2:ノードベース(キャラクターに推奨)
各状態を独立したノードにします。StateMachine ノードは、現在アクティブな状態に入力処理とプロセス呼び出しを委譲し、状態は名前を指定して遷移を開始します。
シーンツリー
Player (CharacterBody2D)
└── StateMachine (Node)
├── Idle (State)
├── Run (State)
├── Jump (State)
└── Attack (State)
State 基底クラス
GDScript(state.gd)
class_name State
extends Node
## Populated by StateMachine._ready()
var entity: CharacterBody2D
var state_machine: StateMachine
## Called when this state becomes active.
func enter() -> void:
pass
## Called when this state is deactivated.
func exit() -> void:
pass
## Mirrors _process. Return a state name string to transition, or "" to stay.
func update(delta: float) -> String:
return ""
## Mirrors _physics_process. Return a state name string to transition, or "".
func physics_update(delta: float) -> String:
return ""
## Mirrors _unhandled_input.
func handle_input(event: InputEvent) -> String:
return ""
C#(State.cs)
using Godot;
public partial class State : Node
{
/// Populated by StateMachine._Ready()
public CharacterBody2D Entity { get; set; }
public StateMachine StateMachine { get; set; }
public virtual void Enter() { }
public virtual void Exit() { }
public virtual string Update(double delta) => string.Empty;
public virtual string PhysicsUpdate(double delta) => string.Empty;
public virtual string HandleInput(InputEvent @event) => string.Empty;
}
StateMachine クラス
GDScript(state_machine.gd)
class_name StateMachine
extends Node
@export var initial_state: State
var current_state: State
var states: Dictionary = {}
func _ready() -> void:
for child in get_children():
if child is State:
states[child.name] = child
child.entity = owner as CharacterBody2D
child.state_machine = self
if initial_state:
current_state = initial_state
current_state.enter()
func _unhandled_input(event: InputEvent) -> void:
var next := current_state.handle_input(event)
if next:
transition_to(next)
func _process(delta: float) -> void:
var next := current_state.update(delta)
if next:
transition_to(next)
func _physics_process(delta: float) -> void:
var next := current_state.physics_update(delta)
if next:
transition_to(next)
func transition_to(state_name: String) -> void:
if not states.has(state_name):
push_error("StateMachine: unknown state '%s'" % state_name)
return
current_state.exit()
current_state = states[state_name]
current_state.enter()
C#(StateMachine.cs)
using System.Collections.Generic;
using Godot;
public partial class StateMachine : Node
{
[Export] public State InitialState { get; set; }
public State CurrentState { get; private set; }
private readonly Dictionary<string, State> _states = new();
public override void _Ready()
{
foreach (var child in GetChildren())
{
if (child is State state)
{
_states[state.Name] = state;
state.Entity = Owner as CharacterBody2D;
state.StateMachine = this;
}
}
if (InitialState != null)
{
CurrentState = InitialState;
CurrentState.Enter();
}
}
public override void _UnhandledInput(InputEvent @event)
{
var next = CurrentState.HandleInput(@event);
if (!string.IsNullOrEmpty(next)) TransitionTo(next);
}
public override void _Process(double delta)
{
var next = CurrentState.Update(delta);
if (!string.IsNullOrEmpty(next)) TransitionTo(next);
}
public override void _PhysicsProcess(double delta)
{
var next = CurrentState.PhysicsUpdate(delta);
if (!string.IsNullOrEmpty(next)) TransitionTo(next);
}
public void TransitionTo(string stateName)
{
if (!_states.TryGetValue(stateName, out var next))
{
GD.PushError($"StateMachine: unknown state '{stateName}'");
return;
}
CurrentState.Exit();
CurrentState = next;
CurrentState.Enter();
}
}
具体例:IdleState
GDScript(idle_state.gd)
class_name IdleState
extends State
func enter() -> void:
entity.get_node("AnimationPlayer").play("idle")
func physics_update(delta: float) -> String:
if not entity.is_on_floor():
return "Jump"
if Input.get_axis("move_left", "move_right") != 0.0:
return "Run"
return ""
func handle_input(event: InputEvent) -> String:
if event.is_action_pressed("jump") and entity.is_on_floor():
return "Jump"
if event.is_action_pressed("attack"):
return "Attack"
return ""
4. アプローチ 3:リソースベース(データ駆動型)
コードを変更せずに、デザイナーが Godot インスペクターで状態を設定する必要がある場合に使用します。
StateData リソース
class_name StateData
extends Resource
@export var state_name: String = ""
@export var animation_name: String = ""
@export var move_speed: float = 0.0
@export var can_transition_to: Array[String] = []
AI コントローラーで Array[StateData] をエクスポートします。デザイナーはインスペクターで各項目を設定できるため、動作の調整や状態の追加にコードの変更は必要ありません。実行時には can_transition_to を読み取って遷移を検証し、アクティブな各状態に応じて animation_name/move_speed を選択します。
using Godot;
[GlobalClass]
public partial class StateData : Resource
{
[Export] public string StateName { get; set; } = string.Empty;
[Export] public string AnimationName { get; set; } = string.Empty;
[Export] public float MoveSpeed { get; set; } = 0f;
[Export] public Godot.Collections.Array<string> CanTransitionTo { get; set; } = new();
}
AI コントローラークラスで Array[StateData] をエクスポートします([Export] public Godot.Collections.Array<StateData> States)。実行時には、StateName を使ってアクティブな StateData を検索し、AnimationName/MoveSpeed を読み取って動作を駆動します。TransitionTo の呼び出しを制御するには、CanTransitionTo を使用します。
5. 階層型および並列ステートマシン
フラットな FSM が約 8 個を超える状態に拡大した場合や、複数の関心事(移動 + 戦闘 + アニメーション)にまたがる場合は、階層型マシン(各状態がサブステートマシンを持つ。例:OnGround が Idle/Walk/Run を含む)または並列マシン(移動、戦闘、アニメーション用の独立した FSM を並行して実行)に分割します。どちらも状態数が乗算ではなく加算となる構成を保てます。
完全なシーンツリー、HierarchicalState 基底クラス、並列マシンを使ったキャラクターの例、「どちらを選ぶか」の比較表については、references/hierarchical-and-parallel.md を参照してください。それぞれ GDScript と C# の例があります。
6. 判断フローチャート
Start
│
▼
Fewer than 5 states?
├─ Yes ──────────────────────────────────► Enum-Based
└─ No
│
▼
Multiple independent concerns
(movement + combat + animation)?
├─ Yes ──────────────────────────────► Parallel State Machines
└─ No
│
▼
States naturally nest
(sub-states within states)?
├─ Yes ────────────────────────► Hierarchical State Machine
└─ No
│
▼
Designers need to configure
states in the Inspector?
├─ Yes ──────────────────► Resource-Based
└─ No ──────────────────► Node-Based
7. 実装チェックリスト
- 実際の複雑さに合ったアプローチ(enum/ノード/リソース)を選択した
- 採用しなかったアプローチと、その判断要因を明記した
- すべての状態に明示的な
enter()メソッドとexit()メソッド(または同等のもの)がある - すべての遷移を明示的に指定した — 状態間の暗黙的なフォールスルーがない
- 必要に応じて、アニメーションを
enter()で開始し、exit()でクリーンアップする - 1フレーム内で無限再帰を引き起こす可能性のある循環遷移ループがない
- 状態数が約8を超えるか、複数の関心事にまたがる場合、フラットなFSMを階層型または並列型に置き換える
- 並列ステートマシンが同じ状態(例:両方が速度を設定する)を変更しない — 1つのステートマシンにつき1つの関心事とする