概要
ゲーム開発が進むと、キャラクターのロジックはどんどん複雑になります。「プレイヤーは通常状態、歩き状態、ジャンプ状態、攻撃状態、ダメージ状態がある」「敵は巡回状態、追跡状態、攻撃状態、待機状態がある」。これらをif文やbool型の変数(is_jumping, is_attackingなど)だけで管理しようとすると、コードはすぐにスパゲッティのように絡み合い、バグの温床となります。
この問題を解決するための古典的かつ非常に強力なデザインパターンが、ステートマシン(状態機械) です。
ステートマシンとは?
ステートマシンとは、オブジェクトが取りうる「状態(ステート)」と、ある状態から別の状態へ「遷移(トランジション)」する条件を明確に定義することで、複雑な振る舞いを整理整頓するモデルです。
ステートマシンの基本要素:
- ステート(状態): オブジェクトの現在の振る舞いの種類。例:「待機(Idle)」「移動(Move)」「攻撃(Attack)」。各状態は、その状態でいる間、毎フレーム何をするべきか(
_processや_physics_processに相当する処理)を知っています - トランジション(遷移): ある状態から別の状態へ移るきっかけ。例:「プレイヤーが範囲内に入ったら、『待機』から『追跡』へ」「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が非常にシンプルになることです。メインループは現在の状態に応じて適切な関数を呼び出すだけで、各状態のロジックはその関数内にカプセル化されています。状態遷移のロジックも各ステート関数内に記述されているため、どこで状態が変わるかが明確になります。
より高度な実装へ
このenumとmatch文による方法は、シンプルで分かりやすい反面、状態が増えるとファイルが長大になる欠点があります。より大規模なプロジェクトでは、各状態を個別のクラス(State.gdを継承したIdleState.gd, ChaseState.gdなど)として実装し、current_state変数にそのクラスのインスタンスを保持する方法が取られます。これにより、各状態のロジックを完全に別のファイルに分離でき、保守性がさらに向上します。
まとめ
ステートマシンは、複雑な振る舞いを整理するための強力な設計ツールです。キャラクターのロジックがif文のネストで複雑になってきたと感じたら、それはステートマシンを導入する絶好のタイミングです。
- 状態(State) を
enumで定義する - 遷移(Transition) のロジックを各状態の処理関数内に記述する
_physics_processでは、現在の状態に応じた関数を呼び出すだけにする
この基本を押さえるだけで、あな たのゲームのAIやプレイヤーキャラクターのコードは、驚くほどクリーンで、拡張しやすいものになるでしょう。