ステルスゲームの緊張感は、 「敵に見つかるかもしれない」 というスリルから生まれます。物陰に隠れ、視線の死角を縫って進む——あの体験の心臓部が、敵の 視界(Field of View)判定 です。でも「見える/見えない」をどう実装すればいいのか、いざとなると悩みます。実は、視界判定は 距離・角度・遮蔽の3段階 に分けて考えると、驚くほどシンプルに作れます。
この記事では、敵の視界でプレイヤーを検知する仕組みを組み立てます。距離 で範囲を絞り、角度 で視野コーンを作り、Raycast で壁の遮蔽をチェックする——この3段階の検知ロジックと、発見時の状態遷移、そして Gizmosでの視界の可視化 までを解説します。NavMeshでAIパスファインディング の「追跡」と対をなす、"発見" に特化した1本です。
この記事でわかること
- 視界判定の 3段階(距離 → 角度 → 遮蔽)
- 角度 で視野コーンを作る(
Vector3.Angle)- Raycast で壁の遮蔽をチェックする
- 発見時の 状態遷移(巡回 → 追跡)
- Gizmos で視界を可視化してデバッグする
- 発展:見られ続けると見つかる 警戒度(疑いメーター)
動作確認環境: Unity 2022.3 LTS / Unity 6
視界判定は3段階で考える
「敵にプレイヤーが見えているか」——これを一発で判定しようとすると難しく感じますが、 3つの条件をANDで重ねる と考えると、途端に明快になります。

| 段階 | 判定内容 |
|---|---|
| 1. 距離 | プレイヤーが視界の 届く範囲内 か(近すぎず遠すぎず) |
| 2. 角度 | プレイヤーが 視野角の内側 か(正面のコーンの中) |
| 3. 遮蔽 | 敵とプレイヤーの間に 壁がない か(Raycast) |
この順番で判定するのがポイントです。 軽い判定から順に、ダメなら早期に打ち切る。まず距離(引き算1回で済む)、次に角度、最後にいちばん重いRaycast。「遠すぎるならそもそも角度もRaycastも見ない」と早期リターンすれば、大量の敵がいても軽く動きます。この3つすべてを満たしたときだけ、「プレイヤーが見えた」と判定します。
距離と角度でコーンを作る
まず「距離」と「角度」で、敵の前方に 扇形の視界コーン を作ります。ここが視界判定の骨格です。

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 です。 敵の目からプレイヤーへ向けて光線を撃ち、壁(障害物レイヤー)に当たるかどうか を調べます。
- 壁に当たった → プレイヤーは壁の向こう。 見えない(隠れている)
- 何にも当たらずプレイヤーに届いた → 間に遮る物はない。 見えている
前の節のコードの最後の行がこれです。Physics.Raycast(始点, 方向, 距離, obstacleMask) で、プレイ ヤーまでの間に 障害物レイヤーの壁があるか を調べ、当たれば false(見えない)、当たらなければ true(見える)を返します。これで、プレイヤーが柱や壁の陰に入った瞬間、敵の視界から消える——ステルスの基本挙動が完成します。
視線は「目→胸」で結ぶ: コードの
eyeHeightとtargetHeightがこの役です。始点を足元にすると、腰の高さの木箱を「すり抜けて」発見したり、逆に低い縁石で「隠れられて」しまったり、遮蔽の感覚が現実とズレます。「敵の目からプレイヤーの胸へ」で結ぶと、しゃがみや低い遮蔽物の駆け引きが自然になります。
Gizmosで視界を可視化する
視界は目に見えないので、そのままだと調整が大変です。そこで Gizmos を使い、シーンビューに視界コーンを描いて可視化します。これがステルスAI開発の必須テクニックです。

// シーンビューに視界コーンを描く(実行しなくても見える)
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の状態遷移につなげます。流れはこうです。
- 敵は普段 巡回 している
- 毎フレーム(または一定間隔で)
CanSeePlayer()を呼ぶ trueになったら 追跡状態へ遷移 し、NavMesh でプレイヤーを追いかける- プレイヤーが物陰に隠れて
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で描き、その中にプレイヤーが入ったらデバッグログを出すところから始めてください。壁の陰に入ると検知が消え る瞬間を見たら、もうステルスゲームの心臓部は完成です。あなたのゲームの見張りは、どんな死角を持っていますか?
さらに学ぶために
- Raycast完全ガイド ——遮蔽チェックの土台
- NavMeshでAIパスファインディング ——発見後の「追跡」の実装
- ステートマシン設計パターン ——巡回⇔追跡の状態管理
- Behavior Treeで敵AIを設計する ——「見える?」を条件ノードに組み込む
- Unity公式ドキュメント:Physics.Raycast ——遮蔽判定の一次情報