【Unity】Unityで敵の視界(FOV)を作る:距離・角度・遮蔽判定でプレイヤーを検知する

作成: 2026-07-12最終更新: 2026-07-13

ステルスゲームの「見つかる」を作ります。敵の視界を距離・角度・Raycastの遮蔽チェックの3段階で判定し、プレイヤーを検知する仕組みを解説。壁に隠れると見つからない視界コーンの実装から、発見時の状態遷移、Gizmosでの可視化まで、ステルスAIの検知ロジックを扱います。

ステルスゲームの緊張感は、 「敵に見つかるかもしれない」 というスリルから生まれます。物陰に隠れ、視線の死角を縫って進む——あの体験の心臓部が、敵の 視界(Field of View)判定 です。でも「見える/見えない」をどう実装すればいいのか、いざとなると悩みます。実は、視界判定は 距離・角度・遮蔽の3段階 に分けて考えると、驚くほどシンプルに作れます。

この記事では、敵の視界でプレイヤーを検知する仕組みを組み立てます。距離 で範囲を絞り、角度 で視野コーンを作り、Raycast で壁の遮蔽をチェックする——この3段階の検知ロジックと、発見時の状態遷移、そして Gizmosでの視界の可視化 までを解説します。NavMeshでAIパスファインディング の「追跡」と対をなす、"発見" に特化した1本です。

ステルス視界のイメージ。敵から扇形(コーン)の視界が広がり、その中にいるプレイヤーは検知される。壁の陰に入ったプレイヤーは、視界内でも遮蔽されて見つからない様子を示す

この記事でわかること

  • 視界判定の 3段階(距離 → 角度 → 遮蔽)
  • 角度 で視野コーンを作る(Vector3.Angle
  • Raycast で壁の遮蔽をチェックする
  • 発見時の 状態遷移(巡回 → 追跡)
  • Gizmos で視界を可視化してデバッグする
  • 発展:見られ続けると見つかる 警戒度(疑いメーター)

動作確認環境: Unity 2022.3 LTS / Unity 6

Sponsored

視界判定は3段階で考える

「敵にプレイヤーが見えているか」——これを一発で判定しようとすると難しく感じますが、 3つの条件をANDで重ねる と考えると、途端に明快になります。

視界判定の3段階の図。(1)距離:敵を中心とした円の範囲内か、(2)角度:敵の正面から視野角の内側か(扇形コーン)、(3)遮蔽:間に壁がないか(Raycast)。3つすべてを満たすと「見えた」となる流れを示す
段階判定内容
1. 距離プレイヤーが視界の 届く範囲内 か(近すぎず遠すぎず)
2. 角度プレイヤーが 視野角の内側 か(正面のコーンの中)
3. 遮蔽敵とプレイヤーの間に 壁がない か(Raycast)

この順番で判定するのがポイントです。 軽い判定から順に、ダメなら早期に打ち切る。まず距離(引き算1回で済む)、次に角度、最後にいちばん重いRaycast。「遠すぎるならそもそも角度もRaycastも見ない」と早期リターンすれば、大量の敵がいても軽く動きます。この3つすべてを満たしたときだけ、「プレイヤーが見えた」と判定します。

距離と角度でコーンを作る

まず「距離」と「角度」で、敵の前方に 扇形の視界コーン を作ります。ここが視界判定の骨格です。

距離と角度でコーンを作る図。敵の位置からプレイヤーまでの距離を測り(viewRadius以内か)、敵の正面ベクトルとプレイヤー方向の角度を測る(viewAngleの半分以内か)。この2つで前方に扇形のコーンができる様子を示す
using UnityEngine;

public class FieldOfView : MonoBehaviour
{
    [SerializeField] private float viewRadius = 8f;      // 視界の届く距離
    [SerializeField] private float viewAngle = 90f;      // 視野角(度)
    [SerializeField] private float eyeHeight = 1.6f;     // 敵の「目」の高さ
    [SerializeField] private float targetHeight = 1.2f;  // プレイヤー側の狙う高さ(胸)
    [SerializeField] private Transform player;
    [SerializeField] private LayerMask obstacleMask;     // 壁のレイヤー

    public bool CanSeePlayer()
    {
        // 視線は「足元→足元」ではなく「目→胸」で結ぶ
        Vector3 eye = transform.position + Vector3.up * eyeHeight;
        Vector3 targetPoint = player.position + Vector3.up * targetHeight;
        Vector3 toPlayer = targetPoint - eye;

        // (1) 距離:範囲外なら見えない
        float distance = toPlayer.magnitude;
        if (distance > viewRadius) return false;

        // (2) 角度:正面からの角度が視野角の半分を超えたら見えない
        float angle = Vector3.Angle(transform.forward, toPlayer);
        if (angle > viewAngle / 2f) return false;

        // (3) 遮蔽チェックへ(次の節)
        return !Physics.Raycast(eye, toPlayer.normalized, distance, obstacleMask);
    }
}

角度の判定が肝です。 敵の正面ベクトル(transform.forward)と、敵からプレイヤーへの方向の角度Vector3.Angle で測り、それが 視野角の半分以下 なら、プレイヤーはコーンの内側にいます。視野角90度なら、正面から左右45度ずつの範囲が視界、というわけです。距離と角度、この2つだけで「前方の扇形」が定義できます。

Raycastで遮蔽をチェックする

距離と角度の条件を満たしても、 間に壁があれば見えない はずです。ステルスゲームの「物陰に隠れる」を成立させるのが、この遮蔽チェックです。

遮蔽チェックの図。敵の目からプレイヤーへRaycastを撃つ。途中で壁に当たればプレイヤーは見えない(隠れている)。何にも遮られずプレイヤーに届けば見えている。壁の陰に入ると検知されない仕組みを示す

方法は Raycast です。 敵の目からプレイヤーへ向けて光線を撃ち、壁(障害物レイヤー)に当たるかどうか を調べます。

  • 壁に当たった → プレイヤーは壁の向こう。 見えない(隠れている)
  • 何にも当たらずプレイヤーに届いた → 間に遮る物はない。 見えている

前の節のコードの最後の行がこれです。Physics.Raycast(始点, 方向, 距離, obstacleMask) で、プレイヤーまでの間に 障害物レイヤーの壁があるか を調べ、当たれば false(見えない)、当たらなければ true(見える)を返します。これで、プレイヤーが柱や壁の陰に入った瞬間、敵の視界から消える——ステルスの基本挙動が完成します。

視線は「目→胸」で結ぶ: コードのeyeHeighttargetHeightがこの役です。始点を足元にすると、腰の高さの木箱を「すり抜けて」発見したり、逆に低い縁石で「隠れられて」しまったり、遮蔽の感覚が現実とズレます。「敵の目からプレイヤーの胸へ」で結ぶと、しゃがみや低い遮蔽物の駆け引きが自然になります。

Sponsored

Gizmosで視界を可視化する

視界は目に見えないので、そのままだと調整が大変です。そこで Gizmos を使い、シーンビューに視界コーンを描いて可視化します。これがステルスAI開発の必須テクニックです。

Gizmosで視界を可視化する図。シーンビューに、敵を中心とした視界の距離を表す円弧と、視野角を表す2本の境界線が描かれ、扇形のコーンが目に見える。プレイヤーを検知したらコーンの色が変わる様子を示す
// シーンビューに視界コーンを描く(実行しなくても見える)
void OnDrawGizmos()
{
    Gizmos.color = Color.yellow;
    // 視界の距離を円で表示
    Gizmos.DrawWireSphere(transform.position, viewRadius);

    // 視野角の左右の境界線
    Vector3 left = Quaternion.Euler(0, -viewAngle / 2f, 0) * transform.forward;
    Vector3 right = Quaternion.Euler(0, viewAngle / 2f, 0) * transform.forward;
    Gizmos.DrawLine(transform.position, transform.position + left * viewRadius);
    Gizmos.DrawLine(transform.position, transform.position + right * viewRadius);
}

OnDrawGizmos に描画コードを書くと、 ゲームを実行しなくてもシーンビューに視界が表示 されます。視界の距離を円で、視野角を2本の線で描けば、扇形のコーンが目で見えるようになります。これで「この壁だと死角ができるな」「視野角が広すぎるな」といった調整が、実際に見ながらできます。検知したらGizmosの色を赤に変えると、発見の瞬間も一目で分かります。

実践:見つかったら追いかける敵

視界判定ができたら、あとは 発見時にAIを動かす だけです。ステルスゲームの警備兵、メタルギア風の見張り、ホラーゲームの徘徊する敵——どれもこの検知ロジックが心臓部です。

実践の完成イメージ。巡回している敵の視界コーンにプレイヤーが入り、壁に遮られていなければ検知。敵が追跡状態に遷移してプレイヤーを追いかける。プレイヤーが物陰に隠れて見失うと、一定時間後に巡回へ戻る流れを示す

検知の結果を、AIの状態遷移につなげます。流れはこうです。

  1. 敵は普段 巡回 している
  2. 毎フレーム(または一定間隔で)CanSeePlayer() を呼ぶ
  3. true になったら 追跡状態へ遷移 し、NavMesh でプレイヤーを追いかける
  4. プレイヤーが物陰に隠れて false が続いたら、 一定時間後に巡回へ戻る
void Update()
{
    if (CanSeePlayer())
    {
        lastSeenTime = Time.time;
        state = State.Chase;           // 発見 → 追跡
        agent.SetDestination(player.position);
    }
    else if (state == State.Chase && Time.time - lastSeenTime > loseSightDelay)
    {
        state = State.Patrol;          // 見失って一定時間 → 巡回へ
    }
}

ポイントは2つです。 「発見はフラグ、行動は状態」CanSeePlayer() は見えるかを返すだけ。それを受けて追跡・巡回の状態を切り替える。検知と行動を分ける)と、 「見失いに猶予を持たせる」(隠れた瞬間に即あきらめると不自然。loseSightDelay の間は追い続け、それでも見つからなければ巡回に戻す。この「しつこさ」がAIらしさになる)。この検知ロジックは、ステートマシン の「巡回⇔追跡」の遷移条件にも、Behavior Tree の「見える?」の条件ノードにも、そのまま組み込めます。

うまく動かないときの3つの落とし穴: (1) obstacleMaskに壁のレイヤーを入れ忘れると、Raycastが何にも当たらず壁越しでも発見します。(2) 逆に敵自身やプレイヤーのレイヤーをマスクに含めると、Rayが体そのものに「遮蔽」として当たり、永遠に見つけられなくなります——マスクは「遮蔽物だけ」に絞るのが鉄則です。(3) プレイヤーが視野角の境界線上を出入りすると、検知のON/OFFが細かく震えます。次の「警戒度」方式にすると、この震えは自然に消えます。

発展:0か1でなく「警戒度」で見つける

一瞬視界をかすめただけで即発見——は、遊ぶ側には理不尽に感じます。「見られ続けると見つかる」に変えるのが警戒度(疑いメーター)です。仕組みは、検知が続いた時間を貯めるだけです。

[SerializeField] private float detectTime = 1.5f;  // 見え続けたら発見になる秒数
private float suspicion;  // 警戒度 0〜1

void Update()
{
    if (CanSeePlayer())
        suspicion += Time.deltaTime / detectTime;          // 見えている間は上がる
    else
        suspicion -= Time.deltaTime / (detectTime * 2f);   // 隠れるとゆっくり下がる
    suspicion = Mathf.Clamp01(suspicion);

    if (suspicion >= 1f)
    {
        lastSeenTime = Time.time;
        state = State.Chase;   // メーター満タンで初めて発見
        agent.SetDestination(player.position);
    }
}

頭上に「?」をsuspicionの量で出し分ければ、あのメタルギア風の駆け引きが完成です。プレイヤー側は「見られた!隠れろ!」という猶予のあるスリルを味わえて、境界ちらつき問題も同時に解決する——1つのメーターで三度おいしい定番テクニックです。

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

  • 判定は毎フレームでなくてよい: 視界判定を敵の数だけ毎フレーム行うと重くなります。0.1〜0.2秒に1回など、間隔を空けても体感はほぼ変わりません。InvokeRepeating やコルーチンで間引きましょう。
  • 2Dでも同じ考え方: 2Dゲームでも、距離・角度・Physics2D.Raycast の3段階は同じです。transform.right を正面にするなど、軸だけ読み替えます。
  • 音の検知と組み合わせる: 視界に加えて「足音が近いと気づく」を足すと、より本格的になります。視界(見る)と聴覚(聞く)は別の検知として実装し、どちらかで発見、とするのが定番です。

まとめ

  • 視界判定は 距離 → 角度 → 遮蔽(Raycast) の3段階をANDで重ねる
  • 軽い判定から順に行い、 ダメなら早期リターン して負荷を抑える
  • 角度は Vector3.Angle(正面ベクトルとプレイヤー方向)が視野角の半分以下 で内側
  • 遮蔽は Physics.Raycast で壁に当たるか。当たれば見えない(隠れている)
  • Gizmos でシーンビューに視界コーンを描き、見ながら調整する
  • 発見は フラグ、追跡・巡回は 状態。見失いには猶予を持たせる
  • 即発見が理不尽なら 警戒度——見えている間だけメーターを貯め、満タンで発見

まずは敵に視界コーンをGizmosで描き、その中にプレイヤーが入ったらデバッグログを出すところから始めてください。壁の陰に入ると検知が消える瞬間を見たら、もうステルスゲームの心臓部は完成です。あなたのゲームの見張りは、どんな死角を持っていますか?

さらに学ぶために