【Unreal Engine】EnumとSwitchで状態を管理する:Blueprintのステートマシン入門

作成: 2026-07-20

bIsAttackingとbIsDeadが同時にtrueになる。boolを増やす管理は組み合わせが爆発して破綻します。Blueprint Enumと Switch on Enum で状態を1つに絞る設計を図解し、巡回→追跡→探索→帰還の敵AIを組む実践つき。

キャラクターに攻撃を実装して bIsAttacking を足す。ダッシュを実装して bIsDashing を足す。死亡処理で bIsDead を足す。そのうち「死んでいるのに攻撃モーションが出る」「ダッシュ中に攻撃したら滑り続ける」といった挙動が出てきます。

原因は、 同時に成立してはいけない状態を、別々のboolで持っている ことです。UEでは Blueprint EnumSwitch on Enum を使って、状態を1つの変数に絞り込めます。この記事では、boolが破綻する仕組みと、Enumで状態を管理する設計、そして巡回する敵AIを組む手順を解説します。

複数の状態カードのうち1枚だけが光っているイメージ

この記事でわかること

  • boolを増やすと 組み合わせが2のN乗で爆発する
  • Blueprint Enum の作り方とDisplay Nameの設定
  • Switch on Enum で状態ごとに処理を分ける
  • 状態変更は SetState関数1本に集約 する
  • AnimBPのState Machineとの違い(ロジックと見た目)
  • 実践: 巡回 → 追跡 → 探索 → 帰還 の敵AIをEnumで組む

Sponsored

boolが増えると壊れる理由

状態をboolで持つのは、最初はうまくいきます。1つか2つなら、Branch で分ければ足りるからです。

問題は3つ目からです。bIsMovingbIsAttackingbIsDead の3つを持った時点で、変数が取りうる組み合わせは 2 × 2 × 2 = 8通り になります。

3つのboolが作る8通りの組み合わせ表。4通りは正しい状態、残り4通りはあり得ない状態
bIsMovingbIsAttackingbIsDead意味
falsefalsefalse待機
truefalsefalse移動
falsetruefalse攻撃
falsefalsetrue死亡
truetruefalse移動しながら攻撃?
truefalsetrue死んだまま移動
falsetruetrue死んだまま攻撃
truetruetrue全部同時

意味があるのは上の 4通りだけ です。残りの4通りは、起きてはいけない組み合わせです。ところがBlueprintは、それを止めてくれません。 「あり得ない状態を作らない」のは、すべて書く人の責任 になります。

boolが5つになれば2の5乗で 32通り 、そのうち正しいのは6通りです。フラグを1つ足すたびに、確認すべき組み合わせが倍になっていきます。

そして厄介なのは、この破綻が 書いた直後には出ない ことです。「死亡処理でbIsAttackingをfalseに戻し忘れた」という1行の抜けが、数日後に「たまに死体が攻撃する」というバグとして現れます。

Enumを使うと、この問題が構造ごと消えます。 状態変数が1つしかなく、そこに入る値も1つだけなので、 あり得ない組み合わせをそもそも作れません 。「戻し忘れ」というミスの余地がなくなります。

Sponsored

Enumを作る

Enum(列挙型) は、あらかじめ決めた選択肢の中から1つだけを選べる型です。

コンテンツブラウザで右クリック → BlueprintsEnumeration で作れます。名前は E_ を頭に付ける のが慣例です。ここでは E_EnemyState とします。

開いたら Add Enumerator ボタンで項目を足していきます。項目ごとに2つの欄があります。

内容
Display NameBlueprintのドロップダウンやピンに表示される名前
Descriptionマウスを乗せたときのツールチップ。空でも構わない
E_EnemyStateにPatrol・Chase・Search・ReturnHomeの4項目を並べたEnumアセットの図

Display Nameは後から変えても、それを使っているBlueprintは壊れません。表示名だけの変更として扱われるからです。

一番上の項目が既定値になります。 Enum型の変数を作ると、初期値は自動的にリストの先頭になります。「何もしていない状態」を一番上に置いておくと、初期値の設定漏れが事故になりません。

作ったEnumは、そのまま変数の型として使えます。BP_Enemy に変数 CurrentState を作り、型の検索欄に E_EnemyState と入れれば選べます。

状態変数は1つだけ持つ 。これがステートマシンの出発点です。

Switch on Enumで分岐する

Enum変数をグラフへドラッグしてピンから線を引き出し、switch と検索すると Switch on E_EnemyState というノードが出ます。

Switch on E_EnemyStateノードから4本の実行ピンが出て、それぞれの状態処理へつながる図

このノードは、Enumの項目ぶんだけ実行ピンを並べます。Selection ピンに繋いだ値と一致するピンだけが実行されます。

Branch を4つ連ねるのと結果は同じですが、読みやすさが違います。

Branchを連ねるSwitch on Enum
分岐の見え方条件が縦に長く伸びる取りうる値が全部ピンとして並ぶ
書き漏らし気づきにくい繋がっていないピンが目で分かる
項目を足したとき自分でBranchを追加するピンが自動で増える

Switch on Int と違い、Enum版は 選択肢がすべてピンとして表示されます 。「この状態のときの処理を書き忘れている」が一目で分かるのが、いちばんの利点です。

処理の書き方は、Godotの match やC#の switch と同じ発想です。 Switchの直後には状態ごとの入り口だけを置き、中身は関数へ逃がす と読みやすさが保てます。

Event Think(自作のイベント)
  → Switch on E_EnemyState(Selection: CurrentState)
      Patrol      → ThinkPatrol(関数)
      Chase       → ThinkChase(関数)
      Search      → ThinkSearch(関数)
      Return Home → ThinkReturnHome(関数)
Sponsored

状態変更はSetStateに集約する

ここからが設計の要点です。状態を変えるとき、 Set CurrentState をあちこちに置いてはいけません。

理由は、状態の切り替えには「その瞬間だけやりたいこと」が必ず付いてくるからです。追跡に入ったら移動速度を上げる、攻撃に入ったらモンタージュを再生する、巡回を抜けたらタイマーを止める。 Setノードだけを置くと、これらが抜けます。

そこで、状態変更専用の関数を1つ作ります。

SetState関数が、ExitState→変数を更新→EnterStateの順に必ず通ることを示したライフサイクル図
関数: SetState(入力: NewState / E_EnemyState)

  → Branch(Condition: CurrentState == NewState)
      True  → Return Node(同じ状態への再突入を防ぐ)
      False → ExitState(State: CurrentState)   ← 今の状態を抜ける処理
            → Set CurrentState(NewState)        ← ここだけが変数を書き換える
            → EnterState(State: CurrentState)   ← 新しい状態に入る処理

Enumどうしの比較は、Equal (E_EnemyState) ノードで行えます。Enum変数のピンから == と検索すれば出てきます。

EnterStateExitState の中身は、どちらも Switch on E_EnemyState です。

関数: EnterState(入力: State / E_EnemyState)
  → Switch on E_EnemyState(Selection: State)
      Patrol      → Set Max Walk Speed(200)→ 次の巡回地点へ Move To
      Chase       → Set Max Walk Speed(600)
      Search      → Set SearchTimer(0.0)
      Return Home → Set Max Walk Speed(200)→ HomeLocation へ Move To

関数: ExitState(入力: State / E_EnemyState)
  → Switch on E_EnemyState(Selection: State)
      Chase → Set LastSeenLocation(プレイヤーの現在位置を記録)
      その他 → 何もしない

この形の効果は3つあります。

  • 状態が変わる瞬間の処理が1箇所に集まる: 「追跡に入ったら速度を上げる」を書く場所が EnterState の1行に決まります
  • 同じ状態への再突入を弾ける: 先頭のBranchがあるので、毎フレーム SetState(Chase) を呼んでも速度設定は1回しか走りません
  • デバッグしやすい: SetState の中に Print String を1つ置くだけで、状態遷移の全履歴が画面に出ます(→ Print Stringデバッグ

状態を書き換えていいのは SetState の中だけ 。このルールを守るかどうかで、後から追える設計になるかが決まります。

Sponsored

AnimBPのState Machineとの違い

UEには「ステートマシン」と名の付くものがもう1つあります。Animation Blueprintの State Machine です。名前が同じなので混乱しますが、扱う対象がまったく違います。

左にBPのEnumで管理するロジックの状態、右にAnimBPのState Machineで管理する見た目の状態を並べ、値を渡して同期する流れを示した図
この記事のEnumステートマシンAnimBPのState Machine
置き場所Actor / Character の BlueprintAnimation Blueprint
管理するものゲームロジックの状態(追跡中か、攻撃中か)見た目の状態(どのアニメを再生するか)
遷移の条件距離、入力、HPなどロジック側から受け取った値
状態を切り替える人自分で書いた SetState遷移線に設定した条件式

役割分担ははっきりしています。 ロジックが決め、AnimBPが従う 。両方で判断すると、「ロジックは攻撃中なのに見た目は歩いている」というずれが起きます。

同期のさせ方は簡単で、AnimBPのEvent Graphから所有キャラクターの CurrentState を読み、AnimBP側の変数へ入れるだけです。AnimBPの遷移条件は、その変数を見るだけにします。

AnimBPの Event Blueprint Update Animation
  → Try Get Pawn Owner → Cast To BP_Enemy
  → Get CurrentState → Set AnimState(AnimBP側の変数、型は E_EnemyState)

AnimBP側の作り方は Animation BlueprintのState MachineとBlend Space で扱っています。Enumをそのまま遷移条件に使えるので、この2つは素直に噛み合います。

実践:巡回する敵AIをEnumで組む

ステルスゲームの警備兵、アクションRPGの雑魚敵、タワーディフェンスの巡回ユニット。 「普段は巡回し、見つけたら追い、見失ったら探して、諦めて戻る」 はジャンルを問わず出てくる基本の敵AIです。これを4つの状態で組みます。

動かすとこうなる

敵は決められた地点を往復しています。プレイヤーが一定距離まで近づくと速度を上げて追ってきます。走って離れると、最後に見た地点まで来て3秒立ち止まり、そのあと元の位置へ戻って巡回を再開します。

Patrol・Chase・Search・ReturnHomeの4状態を矢印でつないだ遷移図と、プレイヤーとの距離で切り替わる様子
遷移条件
Patrol → Chaseプレイヤーとの距離が 1200以下
Chase → Searchプレイヤーとの距離が 1800より大きい
Search → Chase探索中に距離が 1200以下 に戻る
Search → Return Home探索を始めて 3秒 経過
Return Home → Patrol元の位置との距離が 200以下
Return Home → Chase戻る途中で距離が 1200以下

再現条件

Third Person テンプレート で新規プロジェクトを作ります。

  1. コンテンツブラウザで右クリック → BlueprintsEnumerationE_EnemyState と名付け、Add EnumeratorPatrol / Chase / Search / ReturnHome の4項目をこの順で作る
  2. Character を親クラスにしたBlueprint BP_Enemy を作り、レベルに1体配置する
  3. レベルに NavMeshBoundsVolume を置き、床全体を覆うように広げる(これが無いと1歩も動きません。→ NavMeshの設定
  4. BP_Enemy の詳細パネルで AI Controller ClassAIController にする

BP_Enemy に次の変数を作ります。 初期値まで合わせてください 。ここがずれると結果が変わります。

変数名初期値役割
CurrentStateE_EnemyStatePatrol今の状態。書き換えるのは SetState だけ
HomeLocationVector(BeginPlayで代入)巡回の中心
PatrolTargetVector(BeginPlayで代入)今向かっている巡回地点
bPatrolFarBooltrue次に向かう巡回地点(trueで遠い方)
LastSeenLocationVector(0,0,0)最後にプレイヤーを見た位置
SightRangeFloat1200.0発見する距離
LoseRangeFloat1800.0見失う距離
SearchSecondsFloat3.0探索を続ける秒数
SearchTimerFloat0.0探索の経過時間
ThinkIntervalFloat0.2考える間隔(秒)

SightRange より LoseRange を大きくする のが重要です。同じ値にすると、境界線上で発見と見失いが交互に起きて、敵が小刻みに震えます。

判断はTickではなくタイマーで回す

距離の判定を Event Tick に書くと、毎フレーム(60fpsなら1秒に60回)計算することになります。敵AIの判断はそこまでの頻度を必要としません。 0.2秒に1回 で十分です。

Event BeginPlay
  → Set HomeLocation(GetActorLocation)
  → Set PatrolTarget(HomeLocation + (600, 0, 0))
  → SetState(NewState: Patrol)
  → Set Timer by Event
        Time: ThinkInterval(0.2)
        Looping: ✔
        Event: Think(赤いピンから引き出して Custom Event を作る)

これだけで負荷が 1/12 になります(→ Tickに頼らない設計)。

完成したThinkイベント

ThinkイベントからSwitch on E_EnemyStateで4分岐し、各状態が距離を測ってSetStateを呼ぶノードグラフ
Custom Event: Think
  → Switch on E_EnemyState(Selection: CurrentState)
      Patrol      → ThinkPatrol
      Chase       → ThinkChase
      Search      → ThinkSearch
      Return Home → ThinkReturnHome

距離は毎回使うので、先に純粋関数(Pure)として切り出します。

関数: GetDistanceToPlayer(Pure / 戻り値: Distance / Float)
  → Get Player Pawn(Player Index: 0) → Get Actor Location
  → Get Actor Location(self)
  → Vector Length(A - B)→ Return Node

あとは状態ごとの4つの関数です。

関数: ThinkPatrol
  → AI Move To(Pawn: self, Destination: PatrolTarget)
  → Branch(Vector Length(PatrolTarget - GetActorLocation) <= 200.0)  ← 巡回地点に着いたか
      True → Set bPatrolFar(NOT bPatrolFar)                          ← 次に向かう先を反転
           → Set PatrolTarget(Select:
                 bPatrolFar = true  → HomeLocation + (600, 0, 0)
                 bPatrolFar = false → HomeLocation)
  → Branch(GetDistanceToPlayer <= SightRange)
      True → SetState(Chase)

関数: ThinkChase
  → AI Move To(Pawn: self, Destination: プレイヤーの現在位置)
  → Branch(GetDistanceToPlayer > LoseRange)
      True → SetState(Search)

関数: ThinkSearch
  → Set SearchTimer(SearchTimer + ThinkInterval)
  → Branch(GetDistanceToPlayer <= SightRange)
      True  → SetState(Chase)
      False → Branch(SearchTimer >= SearchSeconds)
                True → SetState(ReturnHome)

関数: ThinkReturnHome
  → Branch(GetDistanceToPlayer <= SightRange)
      True  → SetState(Chase)
      False → Branch(Vector Length(HomeLocation - GetActorLocation) <= 200.0)
                True → SetState(Patrol)

ThinkPatrol だけ往復の仕掛けを補足します。巡回地点 PatrolTarget まで200以下に近づいたら、bPatrolFar を反転させ、Select ノードで行き先を HomeLocationHomeLocation + (600, 0, 0) の2点で入れ替えています。これで敵は到着するたびに反対側を目指し、2点の間を往復し続けます。Select は繋いだ型に合わせて姿を変えるノードで、boolを Index に繋ぐと2つの入力を選び分けます(検索は Select で出ます)。Branch を増やさずに書けるのが利点です。

そして EnterStateExitState です。状態が切り替わった瞬間の処理は、すべてここに集まります。

関数: EnterState(入力: State / E_EnemyState)
  → Print String(Append("→ ", 状態名))              ← 遷移が目で見える
  → Switch on E_EnemyState(Selection: State)
      Patrol      → Set Max Walk Speed(200)→ AI Move To(PatrolTarget)
      Chase       → Set Max Walk Speed(600)
      Search      → Set SearchTimer(0.0)→ AI Move To(LastSeenLocation)
      Return Home → Set Max Walk Speed(200)→ AI Move To(HomeLocation)

関数: ExitState(入力: State / E_EnemyState)
  → Switch on E_EnemyState(Selection: State)
      Chase → Set LastSeenLocation(Get Player Pawn → Get Actor Location)

Set Max Walk Speed は Character Movement Component のノードです(→ Character Movement Componentの設定)。

ExitStateChase に注目してください。 追跡を抜ける瞬間に、最後の目撃位置を記録しています 。ここに書いておけば、Search に入ったときには必ず値が入っています。「記録し忘れて、探索が原点へ向かう」という事故が構造的に起きません。

確認する

Playして、敵に近づいてから走って離れてください。画面左上のPrint Stringに、順にこう出れば成功です。

→ Patrol
→ Chase
→ Search
→ Return Home
→ Patrol

見た目でも確認できます。 近づくと敵の移動速度が明らかに上がり(200→600)、離れると立ち止まって3秒待ち、そのあと最初の位置へ戻って往復を再開します 。探索の3秒は、ThinkInterval 0.2秒 × 15回ぶんです。体感でも「一拍置いて諦める」動きになります。

うまくいかないときの切り分けです。

  • 敵がまったく動かない → NavMeshBoundsVolumeが無いか床を覆っていない。P キーで緑の可視化を出して確認する
  • → Patrol すら出ないSet Timer by EventLooping が入っていないか、BeginPlay から SetState を呼んでいない
  • ChaseとSearchが高速で交互に出るSightRangeLoseRange が同じ値になっている。差を600以上つける
  • Searchが永遠に終わらないSearchTimer に足しているのが ThinkInterval ではなく固定値になっているか、SetState を通さず直接 Set CurrentState している
  • 速度が変わらないSet Max Walk SpeedThink 側に書いている。速度の変更は状態に入った瞬間の処理なので EnterState へ置く
  • 戻る途中で止まるReturn Home → Patrol の判定距離200が、AI Move Toの停止距離より小さい。値を大きくして試す

ポイントは2つです。

  • 状態変数は1つ、書き換える場所も1つ: CurrentState に触っていいのは SetState だけ、というルールを守るだけで、「フラグの戻し忘れ」というバグの種類がまるごと消えます。守れているかは、Set CurrentState ノードをグラフ検索して1個しか出ないことで確認できます
  • 状態に入った瞬間の処理は EnterState へ: 速度変更・アニメ再生・タイマーのリセットを Think 側に書くと、毎回実行されて挙動が壊れます。「1回だけやりたいこと」は入口へ、「繰り返しやること」は Think へ、と置き場所で分けてください

敵が「隠れる」「仲間を呼ぶ」「回り込む」のように、条件の優先順位で行動を選ぶようになったら、Enumでは分岐が追えなくなります。そこからは Behavior Treeで敵AIを作る へ進んでください。状態と遷移の形のまま、もう一段しっかりした仕組みへ移りたい場合は StateTree入門 が近道です。

Sponsored

Enumを増やすときの注意

運用していると、必ず状態を足したくなります。押さえておく点が3つあります。

項目を追加すると、Switchノードにピンが自動で増えます。 ただし増えたピンは 何にも繋がっていませんSwitch on E_EnemyState を使っている場所すべてを開いて、新しいピンの行き先を書く必要があります。書き忘れると、その状態のときだけ何も起きません。

Switch on E_EnemyStateにStunnedを追加すると5本目のピンが増えるが、そのピンだけ線が繋がっていないことを示すビフォーアフター図

項目を削除すると、そのピンに繋がっていた処理が外れます。 グラフには残りますが、実行されなくなります。使っている場所を確認してから消してください。アセットを右クリック → Reference Viewer で、どのBlueprintが参照しているかを一覧できます。

状態が7個を超えたら、設計を疑ってください。 遷移の組み合わせが増えすぎて、追えなくなります。よくあるのは、本来2つに分けるべきものが混ざっているケースです。「移動の状態(待機/歩行/走行)」と「戦闘の状態(非戦闘/構え/攻撃中)」は独立して変わるので、 Enumを2つに分ける ほうが素直です。この場合、組み合わせは掛け算になりますが、それぞれのEnumの中では1つしか成立しないので、boolのときの問題は起きません。

おまけ:先に知っておくと良いこと

  • 状態の変化を外へ知らせるならEvent Dispatcher: 「敵が追跡に入ったらUIに警告を出す」のような連携は、EnterState からEvent Dispatcherを呼ぶ形にします。敵がUIを直接知らなくて済みます(→ Event Dispatcherで疎結合にする
  • 被弾は状態遷移のきっかけになる: ダメージを受けたら SetState(Hit)、HPが0になったら SetState(Dead)。ダメージ処理側から SetState を呼ぶだけで、攻撃中でも確実に中断されます(→ ダメージ処理の実装
  • 遷移の許可表を作れる: 「Deadからはどこへも行けない」のようなルールを増やしたくなったら、SetState の先頭に許可判定を1つ挟みます。判定表は Map<E_EnemyState, ...> で持つのが素直です(→ Array・Set・Mapの使い分け
  • Enumは分類にも使える: 状態管理だけでなく、アイテム種別や属性の表現にも便利です。ただし「炎属性かつ希少」のように 複数の性質を同時に持たせたい ものはEnumに向きません。そちらは Gameplay Tags の領分です
  • 何でも状態にしない: 開くだけの宝箱、押すだけのスイッチのように、状態が2つで分岐も少ないものは bIsOpened のboolで十分です。Enumが効くのは「排他的な状態が3つ以上あって、遷移条件が複数ある」場合です

まとめ

  • boolを増やす管理は、組み合わせが 2のN乗 で増えて破綻する
  • Blueprint Enum(右クリック → Blueprints → Enumeration)で選択肢を定義する
  • Switch on Enum は、取りうる値がすべてピンとして並ぶので書き漏らしが見える
  • 状態変更は SetState 1本に集約 し、ExitState → 変数更新 → EnterState の順を必ず通す
  • AnimBPのState Machineは見た目担当 。ロジックが決め、AnimBPが従う
  • 優先順位で行動を選ぶAIになったら Behavior Tree を検討する

判断基準は 「今の状態を1つに決められるか」 です。決められるならEnumが効きます。

いま作っているBlueprintの bIs で始まる変数を並べてみてください。同時にtrueになってはいけないものは、いくつありますか?