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

作成: 2025-12-06

enumとmatch文を使ったシンプルなステートマシンの実装。複雑なキャラクターの行動を整理整頓する設計パターン

概要

ゲーム開発が進むと、キャラクターのロジックはどんどん複雑になります。「プレイヤーは通常状態、歩き状態、ジャンプ状態、攻撃状態、ダメージ状態がある」「敵は巡回状態、追跡状態、攻撃状態、待機状態がある」。これらをif文やbool型の変数(is_jumping, is_attackingなど)だけで管理しようとすると、コードはすぐにスパゲッティのように絡み合い、バグの温床となります。

この問題を解決するための古典的かつ非常に強力なデザインパターンが、ステートマシン(状態機械) です。

ステートマシンとは?

ステートマシンとは、オブジェクトが取りうる「状態(ステート)」と、ある状態から別の状態へ「遷移(トランジション)」する条件を明確に定義することで、複雑な振る舞いを整理整頓するモデルです。

ステートマシンの基本要素:

  1. ステート(状態): オブジェクトの現在の振る舞いの種類。例:「待機(Idle)」「移動(Move)」「攻撃(Attack)」。各状態は、その状態でいる間、毎フレーム何をするべきか(_process_physics_processに相当する処理)を知っています
  2. トランジション(遷移): ある状態から別の状態へ移るきっかけ。例:「プレイヤーが範囲内に入ったら、『待機』から『追跡』へ」「HPが0になったら、どの状態からでも『死亡』へ」

このモデルを使うことで、「ジャンプ中に攻撃はできるか?」「ダメージ中は移動できるか?」といった複雑なルールを、状態と遷移の組み合わせとして明確に設計できます。

Godotでのシンプルな実装例

Godotでステートマシンを実装する方法は様々ですが、ここではenum(列挙型)とmatch文を使った最もシンプルで分かりやすい実装を紹介します。

まず、キャラクターが取りうる状態をenumで定義します。

# Enemy.gd
extends CharacterBody2D

# 状態をenumで定義
enum State { IDLE, WANDER, CHASE, ATTACK }

# 現在の状態を保持する変数
var current_state = State.IDLE

# プレイヤーへの参照(追跡用)
var player

func _ready():
    # 初期状態を設定
    change_state(State.IDLE)

func _physics_process(delta):
    # 毎フレーム、現在の状態に応じた処理を実行
    match current_state:
        State.IDLE:
            idle_state(delta)
        State.WANDER:
            wander_state(delta)
        State.CHASE:
            chase_state(delta)
        State.ATTACK:
            attack_state(delta)

次に、状態を変更するための中心的な関数change_stateと、各状態ごとの処理を行う関数を定義します。

# 状態を変更する関数
func change_state(new_state):
    current_state = new_state
    # 新しい状態に応じた初期設定などを行う
    match new_state:
        State.IDLE:
            # アイドルアニメーション再生など
            $AnimatedSprite2D.play("idle")
            # 一定時間後にWANDER状態へ
            $Timer.start(2.0)
        State.WANDER:
            # ランダムな方向へ移動開始
            pass
        # ... 他の状態の初期設定 ...

# 各状態のフレームごとの処理
func idle_state(delta):
    # アイドル中の処理(例:プレイヤーを探す)
    if can_see_player():
        change_state(State.CHASE)

func wander_state(delta):
    # うろつき中の処理
    # ... 移動処理 ...
    if can_see_player():
        change_state(State.CHASE)

func chase_state(delta):
    # 追跡中の処理
    # ... プレイヤーに向かって移動 ...
    if distance_to_player() < 50:
        change_state(State.ATTACK)
    elif not can_see_player():
        change_state(State.IDLE)

func attack_state(delta):
    # 攻撃中の処理
    # ... 攻撃アニメーションと当たり判定 ...
    # 攻撃が終わったら追跡に戻る
    if not $AnimatedSprite2D.is_playing():
        change_state(State.CHASE)

# タイマーがタイムアウトしたときの処理
func _on_timer_timeout():
    if current_state == State.IDLE:
        change_state(State.WANDER)

この構造のポイントは、_physics_processが非常にシンプルになることです。メインループは現在の状態に応じて適切な関数を呼び出すだけで、各状態のロジックはその関数内にカプセル化されています。状態遷移のロジックも各ステート関数内に記述されているため、どこで状態が変わるかが明確になります。

より高度な実装へ

このenummatch文による方法は、シンプルで分かりやすい反面、状態が増えるとファイルが長大になる欠点があります。より大規模なプロジェクトでは、各状態を個別のクラス(State.gdを継承したIdleState.gd, ChaseState.gdなど)として実装し、current_state変数にそのクラスのインスタンスを保持する方法が取られます。これにより、各状態のロジックを完全に別のファイルに分離でき、保守性がさらに向上します。

まとめ

ステートマシンは、複雑な振る舞いを整理するための強力な設計ツールです。キャラクターのロジックがif文のネストで複雑になってきたと感じたら、それはステートマシンを導入する絶好のタイミングです。

  • 状態(State)enumで定義する
  • 遷移(Transition) のロジックを各状態の処理関数内に記述する
  • _physics_processでは、現在の状態に応じた関数を呼び出すだけにする

この基本を押さえるだけで、あなたのゲームのAIやプレイヤーキャラクターのコードは、驚くほどクリーンで、拡張しやすいものになるでしょう。