ホラーゲームの敵AIって、実は「プレイヤーを見たら追いかける」だけだと全然怖くないんです。見た瞬間に一直線で突っ込んでくる敵は、数回遭遇すればパターンが読めてしまう。本当に怖いのは「物音がしたから来てみたけど、姿が見えない」「追いかけてたのに急に見失った、まだそのへんにいるかもしれない」という、確信が持てない時間の方です。
これを、Unity標準のNavMeshAgentだけで作りました。追加のペイドアセットは一切使っていません。設計と、実装で踏んだ罠をまとめます。
敵の内部状態を Patrol → Investigate → Chase → Search の4つのステートマシンにしました。それぞれの役割はシンプルです。
大事なのはSearchがChaseと別状態であることです。見失った瞬間にPatrolへ戻すと、プレイヤーは「壁の陰に隠れた1秒後にはもう安全」と学習してしまい、緊張感が消えます。Searchを挟むことで「本当にもう大丈夫か分からない」時間が生まれます。
Chase/Investigateへの遷移は、2つの独立したセンサーから起こります。
聴覚のほうは、発音側(プレイヤーの足音など)と受信側(敵の耳)を疎結合にしたくて、静的なイベントバス(NoiseEmitter)を挟みました。発音側は「ここで音が鳴った」と投げるだけ、受信側は範囲内かどうかを自分で判定する。これで敵の数が増えても発音側のコードは変えなくて済みます。
// 発音側:座標と強さを投げるだけ
NoiseEmitter.Emit(transform.position, radius: 6f);
// 受信側(各Enemy):範囲内なら Investigate へ
void OnNoise(Vector3 pos, float radius) {
if (Vector3.Distance(pos, transform.position) <= radius)
stateMachine.ChangeState(EnemyState.Investigate, pos);
}
平常時のPatrol。左上のHUDで現在の状態をデバッグ表示している
Chaseの先、捕獲された瞬間。ここまで自動テストで実際に再現して確認している
デモシーンをエディタスクリプトで自動生成し、その場でNavMeshBuilder.BuildNavMesh()を呼んで検証しようとしたら、「No asset directory」という素っ気ないエラーで静かに失敗しました。原因は単純で、シーンが一度もファイルとして保存されておらずパスを持っていなかったためです。NavMeshのベイクデータはシーンと同じ場所に保存されるので、保存先が決まっていないと焼き込みようがない、というだけの話でした。
EditorSceneManager.SaveScene() で保存してパスを確定させる。エラーメッセージからは意図が読み取りにくいので、シーン生成→即ベイクという処理順を組むときは要注意です。Chase中にプレイヤーを捕獲した直後、ごく短い間だけSearch状態に落ちてから捕獲演出に入る、という癖があることも自動テストで見つかりました。これはバグというより、「捕獲判定」と「視認ロスト判定」がほぼ同じフレームで両方成立しうる設計上の副作用です。捕獲演出自体には影響しませんが、状態遷移をログで追っている人向けにREADMEに明記しています。見た目に影響しない挙動でも、隠さずドキュメントに書くのは、前作(Horror Interaction System)でも徹底したポリシーです。
このEnemy AI Systemは、ドア・引き出し・インタラクトを扱うHorror Interaction Systemとはコード上の依存関係が一切ありません。どちらか片方だけでも使えます。ただ、「ホラーゲームを個人で作っている人」という客層は完全に同じなので、片方を持っていればもう片方も欲しくなるはずだと思って作りました。