【Godot】ステートマシンで管理するAIとプレイヤーの状態

作成: 2025-12-06最終更新: 2026-07-07

Godotでプレイヤーや敵AIの状態を整理するステートマシンを、enum/match方式から状態クラス方式まで、実用例で解説します。

敵AIに「待機」「巡回」「追跡」「攻撃」を足したら、if 文がどんどん増える。プレイヤーに「移動」「ジャンプ」「攻撃」「被ダメージ」を足したら、is_attackingis_damaged が同時にtrueになって変な挙動になる。

こうした状態管理の混乱を整理するのが ステートマシン です。難しい設計パターンに見えますが、最初は「今の状態は1つだけ」と決めるだけでも効果があります。

複数の状態カードを矢印でつないだステートマシンのイメージ

この記事でわかること

  • bool管理が破綻しやすい理由
  • enummatch で作る小さなステートマシン
  • 敵AIの Idle / Patrol / Chase / Attack の実装
  • 状態クラス方式へ移行する判断基準
Sponsored


ステートマシンは「今の状態を1つに決める」設計

ステートマシンは、キャラクターや敵が取りうる状態と、状態が切り替わる条件を整理する考え方です。

敵AIのIdle、Patrol、Chase、Attackの状態遷移図

敵AIなら、たとえば次のように考えます。

  • Idle: その場で待つ。
  • Patrol: 決められた範囲を歩く。
  • Chase: プレイヤーを見つけて追いかける。
  • Attack: 距離が近いので攻撃する。

重要なのは、敵が同時に PatrolAttack にはならないことです。今どの状態なのかを1つに決め、状態ごとの処理を分けます。


bool地獄を避ける

状態管理を bool だけで始めると、最初は簡単です。

var is_jumping := false
var is_attacking := false
var is_damaged := false

ただし状態が増えるほど、「攻撃中にダメージを受けたら?」「ダメージ中にジャンプ入力が来たら?」「死亡中に攻撃アニメーションが終わったら?」のような組み合わせが爆発します。

boolの組み合わせが絡む状態管理と、Stateで整理した状態管理の比較

ステートマシンでは、現在の状態を1つだけ持ちます。

enum State {
    IDLE,
    RUN,
    ATTACK,
    DAMAGE,
}

var current_state: State = State.IDLE

これだけで、「今は攻撃中なのか、被ダメージ中なのか」が明確になります。


実践:enumとmatchで敵AIを作る

まずは小さく、enummatch で敵AIを作ります。

# Enemy.gd
extends CharacterBody2D
class_name Enemy

enum State {
    IDLE,
    PATROL,
    CHASE,
    ATTACK,
}

@export var speed: float = 80.0
@export var attack_range: float = 32.0

var current_state: State = State.IDLE
var player: Node2D
var patrol_direction := Vector2.RIGHT

@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var idle_timer: Timer = $IdleTimer

func _ready() -> void:
    # 初期状態もchange_state()を通して、入場処理をそろえる
    change_state(State.IDLE)

func _physics_process(delta: float) -> void:
    # ここでは状態ごとの処理を呼び分けるだけにする
    match current_state:
        State.IDLE:
            _state_idle(delta)
        State.PATROL:
            _state_patrol(delta)
        State.CHASE:
            _state_chase(delta)
        State.ATTACK:
            _state_attack(delta)

このコードの関係を図にすると、current_state を見て、match が対応する状態関数へ振り分けているだけです。

current_stateをmatchで読み、_state_idleや_state_chaseなど状態ごとの関数へ処理を振り分ける図

_physics_process() は交通整理だけにします。状態ごとの処理は別関数へ逃がすと、読みやすさが保てます。

func _state_idle(delta: float) -> void:
    velocity = Vector2.ZERO

    # プレイヤーを見つけたら追跡へ移る
    if can_see_player():
        change_state(State.CHASE)

func _state_patrol(delta: float) -> void:
    velocity = patrol_direction * speed
    move_and_slide()

    if can_see_player():
        change_state(State.CHASE)

func _state_chase(delta: float) -> void:
    if player == null:
        # 追う対象がいなければ巡回へ戻す
        change_state(State.PATROL)
        return

    var direction := global_position.direction_to(player.global_position)
    velocity = direction * speed
    move_and_slide()

    if global_position.distance_to(player.global_position) <= attack_range:
        # 攻撃範囲に入ったら攻撃状態へ
        change_state(State.ATTACK)
    elif not can_see_player():
        change_state(State.PATROL)

func _state_attack(delta: float) -> void:
    velocity = Vector2.ZERO

    if player == null:
        change_state(State.PATROL)
        return

    if global_position.distance_to(player.global_position) > attack_range:
        change_state(State.CHASE)

状態が増えても、どの関数に処理を書くべきかが明確になります。


状態遷移はchange_stateに集約する

状態を変えるときは、current_state = State.CHASE と直接書かず、専用関数を通します。

change_state()を通して「抜ける(_exit_state)→状態を更新→入る(_enter_state)」を必ず同じ順で行う遷移ライフサイクルの図
func change_state(next_state: State) -> void:
    if current_state == next_state:
        return

    # 出る処理、状態更新、入る処理を必ず同じ順番にする
    _exit_state(current_state)
    current_state = next_state
    _enter_state(current_state)

状態に入った瞬間の処理と、抜ける瞬間の処理を分けます。

func _enter_state(state: State) -> void:
    # 状態に入った瞬間だけ必要な処理をまとめる
    match state:
        State.IDLE:
            sprite.play("idle")
            idle_timer.start(1.5)
        State.PATROL:
            sprite.play("walk")
        State.CHASE:
            sprite.play("run")
        State.ATTACK:
            sprite.play("attack")

func _exit_state(state: State) -> void:
    # 状態を抜ける時の片付けをまとめる
    if state == State.IDLE:
        idle_timer.stop()

この形にしておくと、「攻撃状態に入った瞬間だけアニメーションを再生する」「待機状態を抜けたらTimerを止める」といった処理を一箇所にまとめられます。

func _on_idle_timer_timeout() -> void:
    # Timerの通知でも、現在状態を確認してから遷移する
    if current_state == State.IDLE:
        change_state(State.PATROL)

Timerやシグナルから状態を変える場合も、必ず change_state() を通します。


状態クラスへ分けるタイミング

enum + match は小さく始めるには最適です。ただし、状態ごとの処理が長くなってきたら、状態をファイルに分ける方法を検討します。

enumとmatchで小さく始め、複雑化したらStateクラスへ分ける比較図

状態クラス方式では、各状態を IdleState.gdChaseState.gd のように分けます。

# State.gd
class_name State
extends Node

var owner_enemy: Enemy

func enter() -> void:
    pass

func exit() -> void:
    pass

func physics_update(delta: float) -> void:
    pass

ChaseState は追跡だけを担当します。

# ChaseState.gd
extends State

func enter() -> void:
    owner_enemy.sprite.play("run")

func physics_update(delta: float) -> void:
    if owner_enemy.player == null:
        owner_enemy.change_state("Patrol")
        return

    var direction := owner_enemy.global_position.direction_to(owner_enemy.player.global_position)
    owner_enemy.velocity = direction * owner_enemy.speed
    owner_enemy.move_and_slide()

    if owner_enemy.can_attack_player():
        owner_enemy.change_state("Attack")

クラス方式はファイルが増えるぶん、最初は少し重いです。状態が4個程度で処理も短いなら、enum + match の方が読みやすいことも多いです。


AnimationTreeとの関係

Godotの AnimationTree にもStateMachineがあります。これは主に アニメーションの状態 を管理する機能です。

この記事のステートマシンは、敵AIやプレイヤー操作など ゲームロジックの状態 を管理します。実制作では、ロジック側の状態が変わったタイミングで、AnimationTree側のアニメーションも切り替えます。

func _enter_state(state: State) -> void:
    match state:
        State.IDLE:
            animation_tree["parameters/playback"].travel("Idle")
        State.CHASE:
            animation_tree["parameters/playback"].travel("Run")
        State.ATTACK:
            animation_tree["parameters/playback"].travel("Attack")

アニメーション側の詳しい制御は、 AnimationTreeとステートマシン につながります。


よくあるつまずき

状態変数を直接書き換える

current_state = State.ATTACK をあちこちに書くと、アニメーション再生やTimer停止などの入口/出口処理が抜けます。状態変更は change_state() に集約します。

1つの状態関数が巨大になる

_state_chase() が100行を超えるなら、索敵、移動、攻撃判定をヘルパー関数へ分けます。それでもつらければ状態クラス方式へ移行します。

何でもステートマシンにする

スイッチのON/OFFや、一度だけ開く宝箱のような単純な状態に大きなステートマシンを作る必要はありません。状態が2つだけで分岐も少ないなら、is_opened のようなboolで十分です。

状態とアニメーションを混同する

Attack 状態だからといって、必ず1つの攻撃アニメーションだけとは限りません。コンボ攻撃、武器種、向きによってアニメーションが分かれることもあります。ロジックの状態とアニメーションの状態は、必要に応じて分けて考えましょう。


おまけ:ビヘイビアツリーを検討する場面

ステートマシンは「今の状態が何か」を整理するのに向いています。一方、敵AIが「隠れる」「回復する」「仲間を呼ぶ」「プレイヤーの背後に回る」のように、優先順位で行動を選ぶようになると、状態遷移だけでは複雑になります。

その場合は ビヘイビアツリー という考え方が候補になります。これは「条件を見て、どの行動を優先するか」を木構造で決める設計です。Godot学習パスでは、後続の ビヘイビアツリーでAIを設計 で扱います。


まとめ

ステートマシンは、プレイヤーや敵AIの状態を整理するための基本パターンです。

  • is_attackingis_damaged などのboolが増えたら導入を検討する。
  • まずは enummatch で小さく始める。
  • 状態変更は change_state() に集約する。
  • 状態ごとの処理が大きくなったらStateクラスへ分ける。
  • AnimationTreeのStateMachineとは役割を分けて考える。

判断基準は、 「今の状態は1つに決められるか?」 です。決められるならステートマシンが効きます。逆に、複数の条件を優先順位で選ぶAIになってきたら、ビヘイビアツリーも視野に入れましょう。