キャラクターに攻撃を実装して bIsAttacking を足す。ダッシュを実装して bIsDashing を足す。死亡処理で bIsDead を足す。そのうち「死んでいるのに攻撃モーションが出る」「ダッシュ中に攻撃したら滑り続ける」といった挙動が出てきます。
原因は、 同時に成立してはいけない状態を、別々のboolで持っている ことです。UEでは Blueprint Enum と Switch on Enum を使って、状態を1つの変数に絞り込めます。この記事では、boolが破綻する仕組みと、Enumで状態を管理する設計、そして巡回する敵AIを組む手順を解説します。
この記事でわかること
- boolを増やすと 組み合わせが2のN乗で爆発する
- Blueprint Enum の作り方とDisplay Nameの設定
- Switch on Enum で状態ごとに処理を分ける
- 状態変更は SetState関数1本に集約 する
- AnimBPのState Machineとの違い(ロジックと見た目)
- 実践: 巡回 → 追跡 → 探索 → 帰還 の敵AIをEnumで組む
boolが増えると壊れる理由
状態をboolで持つのは、最初はうまくいきます。1つか2つなら、Branch で分ければ足りるからです。
問題は3つ目からです。bIsMoving・bIsAttacking・bIsDead の3つを持った時点で、変数が取りうる組み合わせは 2 × 2 × 2 = 8通り になります。

| bIsMoving | bIsAttacking | bIsDead | 意味 |
|---|---|---|---|
| false | false | false | 待機 |
| true | false | false | 移動 |
| false | true | false | 攻撃 |
| false | false | true | 死亡 |
| true | true | false | 移動しながら攻撃? |
| true | false | true | 死んだまま移動 |
| false | true | true | 死んだまま攻撃 |
| true | true | true | 全部同時 |
意味があるのは上の 4通りだけ です。残りの4通りは、起きてはいけない組み合わせです。ところがBlueprintは、それを止めてくれません。 「あり得ない状態を作らない」のは、すべて書く人の責任 になります。
boolが5つになれば2の5乗で 32通り 、そのうち正しいのは6通りです。フラグを1つ足すたびに、確認すべき組み合わせが倍になっていきます。
そして厄介なのは、この破綻が 書いた直後には出ない ことです。「死亡処理でbIsAttackingをfalseに戻し忘れた」という1行の抜けが、数日後に「たまに死体が攻撃する」というバグとして現れます。
Enumを使うと、この問題が構造ごと消えます。 状態変数が1つしかなく、そこに入る値も1つだけなので、 あり得ない組み合わせをそもそも作れません 。「戻し忘れ」というミスの余地がなくなります。
Enumを作る
Enum(列挙型) は、あらかじめ決めた選択肢の中から1つだけを選べる型です。
コンテンツブラウザで右クリック → Blueprints → Enumeration で作れます。名前は E_ を頭に付ける のが慣例です。ここでは E_EnemyState とします。
開いたら Add Enumerator ボタンで項目を足していきます。項目ごとに2つの欄があります。
| 欄 | 内容 |
|---|---|
| Display Name | Blueprintのドロップダウンやピンに表示される名前 |
| Description | マウスを乗せたときのツールチップ。空でも構わない |

Display Nameは後から変えても、それを使っているBlueprintは壊れません。表示名だけの変更として扱われるからです。
一番上の項目が既定値になります。 Enum型の変数を作ると、初期値は自動的にリストの先頭になります。「何もしていない状態」を一番上に置いておくと、初期値の設定漏れが事故になりません。
作ったEnumは、そのまま変数の型として使えます。BP_Enemy に変数 CurrentState を作り、型の検索欄に E_EnemyState と入れれば選べます。
状態変数は1つだけ持つ 。これがステートマシンの出発点です。
Switch on Enumで分岐する
Enum変数をグラフへドラッグしてピンから線を引き出し、switch と検索すると Switch on E_EnemyState というノードが出ます。

このノードは、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(関数)
状態変更はSetStateに集約する
ここからが設計の要点です。状態を変えるとき、 Set CurrentState をあちこちに置いてはいけません。
理由は、状態の切り替えには「その瞬間だけやりたいこと」が必ず付いてくるからです。追跡に入ったら移動速度を上げる、攻撃に入ったらモンタージュを再生する、巡回を抜けたらタイマーを止める。 Setノードだけを置くと、これらが抜けます。
そこで、状態変更専用の関数を1つ作ります。

関数: 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変数のピンから == と検索すれば出てきます。
EnterState と ExitState の中身は、どちらも 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 の中だけ 。このルールを守るかどうかで、後から追える設計になるかが決まります。
AnimBPのState Machineとの違い
UEには「ステートマシン」と名の付くものがもう1つあります。Animation Blueprintの State Machine です。名前が同じなので混乱しますが、扱う対象がまったく違います。

| この記事のEnumステートマシン | AnimBPのState Machine | |
|---|---|---|
| 置き場所 | Actor / Character の Blueprint | Animation 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 | プレイヤーとの距離が 1200以下 |
| Chase → Search | プレイヤーとの距離が 1800より大きい |
| Search → Chase | 探索中に距離が 1200以下 に戻る |
| Search → Return Home | 探索を始めて 3秒 経過 |
| Return Home → Patrol | 元の位置との距離が 200以下 |
| Return Home → Chase | 戻る途中で距離が 1200以下 |
再現条件
Third Person テンプレート で新規プロジェクトを作ります。
- コンテンツブラウザで右クリック →
Blueprints→Enumeration。E_EnemyStateと名付け、Add Enumeratorで Patrol / Chase / Search / ReturnHome の4項目をこの順で作る - Character を親クラスにしたBlueprint
BP_Enemyを作り、レベルに1体配置する - レベルに NavMeshBoundsVolume を置き、床全体を覆うように広げる(これが無いと1歩も動きません。→ NavMeshの設定)
BP_Enemyの詳細パネルで AI Controller Class をAIControllerにする
BP_Enemy に次の変数を作ります。 初期値まで合わせてください 。ここがずれると結果が変わります。
| 変数名 | 型 | 初期値 | 役割 |
|---|---|---|---|
CurrentState | E_EnemyState | Patrol | 今の状態。書き換えるのは SetState だけ |
HomeLocation | Vector | (BeginPlayで代入) | 巡回の中心 |
PatrolTarget | Vector | (BeginPlayで代入) | 今向かっている巡回地点 |
bPatrolFar | Bool | true | 次に向かう巡回地点(trueで遠い方) |
LastSeenLocation | Vector | (0,0,0) | 最後にプレイヤーを見た位置 |
SightRange | Float | 1200.0 | 発見する距離 |
LoseRange | Float | 1800.0 | 見失う距離 |
SearchSeconds | Float | 3.0 | 探索を続ける秒数 |
SearchTimer | Float | 0.0 | 探索の経過時間 |
ThinkInterval | Float | 0.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イベント

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 ノードで行き先を HomeLocation と HomeLocation + (600, 0, 0) の2点で入れ替えています。これで敵は到着するたびに反対側を目指し、2点の間を往復し続けます。Select は繋いだ型に合わせて姿を変えるノードで、boolを Index に繋ぐと2つの入力を選び分けます(検索は Select で出ます)。Branch を増やさずに書けるのが利点です。
そして EnterState と ExitState です。状態が切り替わった瞬間の処理は、すべてここに集まります。
関数: 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の設定)。
ExitState の Chase に注目してください。 追跡を抜ける瞬間に、最後の目撃位置を記録しています 。ここに書いておけば、Search に入ったときには必ず値が入っています。「記録し忘れて、探索が原点へ向かう」という事故が構造的に起きません。
確認する
Playして、敵に近づいてから走って離れてください。画面左上のPrint Stringに、順にこう出れば成功です。
→ Patrol
→ Chase
→ Search
→ Return Home
→ Patrol
見た目でも確認できます。 近づくと敵の移動速度が明らかに上がり(200→600)、離れると立ち止まって3秒待ち、そのあと最初の位置へ戻って往復を再開します 。探索の3秒は、ThinkInterval 0.2秒 × 15回ぶんです。体感でも「一拍置いて諦める」動きになります。
うまくいかないときの切り分けです。
- 敵がまったく動かない → NavMeshBoundsVolumeが無いか床を覆っていない。
Pキーで緑の可視化を出して確認する → Patrolすら出ない →Set Timer by EventのLoopingが入っていないか、BeginPlayからSetStateを呼んでいない- ChaseとSearchが高速で交互に出る →
SightRangeとLoseRangeが同じ値になっている。差を600以上つける - Searchが永遠に終わらない →
SearchTimerに足しているのがThinkIntervalではなく固定値になっているか、SetStateを通さず直接Set CurrentStateしている - 速度が変わらない →
Set Max Walk SpeedをThink側に書いている。速度の変更は状態に入った瞬間の処理なので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入門 が近道です。
Enumを増やすときの注意
運用していると、必ず状態を足したくなります。押さえておく点が3つあります。
項目を追加すると、Switchノー ドにピンが自動で増えます。 ただし増えたピンは 何にも繋がっていません 。Switch on E_EnemyState を使っている場所すべてを開いて、新しいピンの行き先を書く必要があります。書き忘れると、その状態のときだけ何も起きません。

項目を削除すると、そのピンに繋がっていた処理が外れます。 グラフには残りますが、実行されなくなります。使っている場所を確認してから消してください。アセットを右クリック → 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になってはいけないものは、いくつありますか?