Unityの学習を進めていくと、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの入力処理、移動、アニメーション、敵のAI、視界判定……全部が1つのスクリプトに詰め込まれていくと、だんだん「どこを触ればいいのか」「何がバグっているのか」が分からなくなってきます。
特に「敵がプレイヤーを見つけたかどうか」のような視界判定は、AIロジックとごちゃまぜになりやすい代表例です。
そこでこの記事では、視界判定だけに責務を絞ったコンポーネント 「VisionCone」 を用意して、
- 「見えているかどうか」の判定
- 壁に遮られていないかのチェック(
Physics2D.Raycast)
を1つの再利用可能な部品として切り出してみます。敵AIや警備ロボット、監視カメラなど、いろいろなオブジェクトにポン付けできる便利コンポーネントにしていきましょう。
【Unity】2Dステルスの必需品!「VisionCone」コンポーネント
ここでは、2Dゲームで「親オブジェクトからターゲットが見えているか」を判定する VisionCone コンポーネントを作成します。
- 視野角(どのくらいの角度まで見えるか)
- 視界距離(どのくらいの距離まで見えるか)
- 壁(レイヤー)による遮蔽
をパラメータとして調整できるようにしつつ、「見えている / 見えていない」 をプロパティとイベントで簡単に利用できる形にします。
フルコード:VisionCone.cs
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 2D用 視界判定コンポーネント。
/// ・親オブジェクトの向き(Transform.up or right)
/// ・視野角、視界距離
/// ・Raycast2D による遮蔽物チェック
/// によって「ターゲットが見えているか」を判定します。
///
/// 責務は「見える/見えないの判定」のみ。
/// 実際の追跡AIやアニメーション再生は、別コンポーネントから
/// IsTargetVisible やイベントを参照して実装しましょう。
/// </summary>
public class VisionCone : MonoBehaviour
{
// ====== インスペクタで設定するパラメータ ======
[Header("ターゲット設定")]
[Tooltip("視界に入っているかを判定したいターゲットの Transform。例: プレイヤー")]
[SerializeField] private Transform target;
[Header("視界パラメータ")]
[Tooltip("視界の最大距離(ユニット)。この距離を超えるターゲットは見えないと判定します。")]
[SerializeField] private float viewDistance = 5f;
[Tooltip("視野角(度)。左右合わせた角度。例: 90 なら正面45度ずつ。")]
[Range(0f, 360f)]
[SerializeField] private float viewAngle = 90f;
[Tooltip("親オブジェクトのどの軸方向を「前」とみなすか。2Dなら Right か Up が多いです。")]
[SerializeField] private ForwardAxis forwardAxis = ForwardAxis.Right;
[Header("レイヤーマスク")]
[Tooltip("壁や障害物として扱うレイヤー。Raycast がこれに当たると遮蔽されたとみなします。")]
[SerializeField] private LayerMask obstacleLayer;
[Tooltip("ターゲットが所属するレイヤー。Raycast がこれに当たった場合、視界内にいるとみなします。")]
[SerializeField] private LayerMask targetLayer;
[Header("デバッグ表示")]
[Tooltip("Sceneビュー上で視界のギズモを描画するかどうか。")]
[SerializeField] private bool drawGizmos = true;
[Tooltip("ターゲットが見えているときの線の色。")]
[SerializeField] private Color visibleColor = Color.green;
[Tooltip("ターゲットが見えていないときの線の色。")]
[SerializeField] private Color notVisibleColor = Color.red;
[Tooltip("視界の扇形の色(半透明推奨)。")]
[SerializeField] private Color coneColor = new Color(1f, 1f, 0f, 0.1f);
// ====== 公開プロパティ / イベント ======
/// <summary>現在ターゲットが見えているかどうか。</summary>
public bool IsTargetVisible { get; private set; }
/// <summary>ターゲットが見えるようになったときに呼ばれるイベント。</summary>
public UnityEvent onBecameVisible;
/// <summary>ターゲットが見えなくなったときに呼ばれるイベント。</summary>
public UnityEvent onBecameInvisible;
// 内部状態(前フレームの可視状態)
private bool _wasVisible;
/// <summary>
/// どの軸を前方向として扱うかの選択肢。
/// 2DのTopDownなら Up、横スクロールなら Right が使いやすいです。
/// </summary>
private enum ForwardAxis
{
Right,
Up
}
private void Update()
{
// 毎フレーム視界判定を更新
bool currentlyVisible = CheckVisibility();
// プロパティを更新
IsTargetVisible = currentlyVisible;
// 状態遷移を検出してイベントを発火
if (IsTargetVisible && !_wasVisible)
{
// 今フレームから見えるようになった
onBecameVisible?.Invoke();
}
else if (!IsTargetVisible && _wasVisible)
{
// 今フレームから見えなくなった
onBecameInvisible?.Invoke();
}
// 前フレームの状態を保存
_wasVisible = IsTargetVisible;
}
/// <summary>
/// 実際の視界判定ロジック。
/// - ターゲットが設定されているか
/// - 距離が範囲内か
/// - 視野角の内側か
/// - Raycast2D で壁に遮られていないか
/// を順番にチェックします。
/// </summary>
private bool CheckVisibility()
{
// ターゲットが未設定なら常に false
if (target == null)
{
return false;
}
// 位置と方向を計算
Vector2 origin = transform.position;
Vector2 toTarget = (Vector2)target.position - origin;
float distanceToTarget = toTarget.magnitude;
// 1. 距離チェック
if (distanceToTarget > viewDistance)
{
return false;
}
// 2. 視野角チェック
Vector2 forward = GetForwardDirection2D();
// ベクトルの角度を求める(0〜180度)
float angleToTarget = Vector2.Angle(forward, toTarget.normalized);
// 視野角は左右で半分ずつなので / 2
if (angleToTarget > viewAngle * 0.5f)
{
return false;
}
// 3. Raycast2D で遮蔽物チェック
// Raycast の対象レイヤーは「障害物 + ターゲット」を含める
int combinedMask = obstacleLayer | targetLayer;
// origin から target 方向へ Ray を飛ばす
RaycastHit2D hit = Physics2D.Raycast(origin, toTarget.normalized, viewDistance, combinedMask);
if (hit.collider == null)
{
// 何にも当たらない = ターゲットも障害物もない
return false;
}
// ターゲットレイヤーに当たったら「見えている」
bool hitIsTarget = (targetLayer & (1 << hit.collider.gameObject.layer)) != 0;
if (hitIsTarget)
{
// 先にターゲットに当たった = 壁に遮られていない
return true;
}
// 先に障害物に当たった = 壁に遮られている
return false;
}
/// <summary>
/// 2Dでの前方向ベクトルを取得するヘルパー。
/// ForwardAxis の設定に応じて Transform.right or Transform.up を返します。
/// </summary>
private Vector2 GetForwardDirection2D()
{
switch (forwardAxis)
{
case ForwardAxis.Up:
return transform.up;
case ForwardAxis.Right:
default:
return transform.right;
}
}
// ====== デバッグ用ギズモ描画 ======
private void OnDrawGizmosSelected()
{
if (!drawGizmos)
{
return;
}
if (!Application.isPlaying)
{
// エディタ上で再生前でも forward をざっくり描くために Transform.right を使用
DrawVisionGizmos(transform.right, false);
}
else
{
DrawVisionGizmos(GetForwardDirection2D(), IsTargetVisible);
}
}
/// <summary>
/// 視界の扇形と、ターゲットへのラインを描画します。
/// Sceneビューでのみ表示され、実行には影響しません。
/// </summary>
private void DrawVisionGizmos(Vector2 forward, bool isVisible)
{
Vector3 origin = transform.position;
// 視界の扇形を描画
Gizmos.color = coneColor;
int segments = 24; // 扇形の分割数(多いほど滑らか)
float halfAngle = viewAngle * 0.5f;
float angleStep = viewAngle / segments;
Vector3 prevPoint = origin;
for (int i = 0; i <= segments; i++)
{
float currentAngle = -halfAngle + angleStep * i;
// forward を中心に Z軸回転させて方向を作る
Quaternion rot = Quaternion.AngleAxis(currentAngle, Vector3.forward);
Vector3 dir = rot * (Vector3)forward;
Vector3 point = origin + dir.normalized * viewDistance;
if (i > 0)
{
Gizmos.DrawLine(prevPoint, point);
}
prevPoint = point;
}
// 中心線(正面方向)を描画
Gizmos.color = isVisible ? visibleColor : notVisibleColor;
Gizmos.DrawLine(origin, origin + (Vector3)forward.normalized * viewDistance);
// ターゲットへのラインを描画
if (target != null)
{
Gizmos.color = isVisible ? visibleColor : notVisibleColor;
Gizmos.DrawLine(origin, target.position);
}
}
}
使い方の手順
ここからは、実際にシーンに配置して使う手順を具体的に見ていきましょう。例として「敵キャラがプレイヤーを見つけたら追いかける」ケースを想定します。
手順①:レイヤーの準備
- Unity上部メニューから「Edit > Project Settings > Tags and Layers」を開きます。
- Layerに以下のような名前を追加します(名前は任意ですが分かりやすく)。
- Player(ターゲット用)
- Obstacle(壁・障害物用)
- シーン内のプレイヤーのGameObjectに「Player」レイヤーを設定します。
- 壁やブロックなど、視界を遮りたいオブジェクトに「Obstacle」レイヤーを設定します。
手順②:敵オブジェクトに VisionCone を追加
- 敵キャラ用のGameObject(例:
Enemy)をシーンに配置します。 EnemyにVisionConeコンポーネントを追加します(Add Component > VisionCone)。- インスペクタ上で以下を設定します。
- Target:プレイヤーの Transform をドラッグ&ドロップ。
- View Distance:例として
6など、ゲームに合わせて調整。 - View Angle:例として
90(正面45度ずつ)。 - Forward Axis:2D横スクロールなら
Right、TopDownならUpなど。 - Obstacle Layer:レイヤー選択から「Obstacle」をチェック。
- Target Layer:レイヤー選択から「Player」をチェック。
- Draw Gizmos:Sceneビューで視界を確認したい場合はチェックON。
手順③:敵AIと連携させる(シンプルな例)
実際の挙動(追いかける・止まる・攻撃するなど)は、別コンポーネントとして分離するのがおすすめです。例えば、以下のような簡単な追跡コンポーネントを作り、VisionCone と組み合わせます。
using UnityEngine;
/// <summary>
/// 非常にシンプルな 2D 追跡コンポーネント。
/// VisionCone が「見えている」ときだけターゲット方向に移動します。
/// </summary>
[RequireComponent(typeof(VisionCone))]
public class SimpleChaser2D : MonoBehaviour
{
[Header("移動設定")]
[SerializeField] private float moveSpeed = 2f;
[Tooltip("追跡対象。通常は VisionCone と同じターゲットを指定します。")]
[SerializeField] private Transform target;
private VisionCone _visionCone;
private void Awake()
{
_visionCone = GetComponent<VisionCone>();
}
private void Update()
{
if (target == null)
{
return;
}
// VisionCone がターゲットを視認しているときだけ移動
if (_visionCone.IsTargetVisible)
{
Vector2 direction = (target.position - transform.position).normalized;
transform.position += (Vector3)(direction * moveSpeed * Time.deltaTime);
}
}
}
このように、「視界判定(VisionCone)」と「移動ロジック(SimpleChaser2D)」を分離しておくと、
- 別の敵には違う移動ロジックを割り当てる
- 同じ VisionCone を監視カメラや動く床のトリガーにも使い回す
といったことが簡単になります。
手順④:監視カメラや動く床にも応用
例えば、以下のような使い方ができます。
- 監視カメラ:VisionCone の
onBecameVisibleイベントに、「アラームを鳴らす関数」を登録。 - 動く床:プレイヤーが視界に入ったときだけ床が動き出す、といったギミック。
- タレット:ターゲットが見えたときだけ弾を撃つ。
イベントの設定は、インスペクタ上で On Became Visible / On Became Invisible に任意のメソッドをドラッグ&ドロップするだけなので、コードを書かずにレベルデザイン側で調整しやすくなります。
メリットと応用
VisionCone コンポーネントを導入することで、以下のようなメリットがあります。
- 責務が明確:視界判定だけに集中しており、AIロジックや演出と分離されている。
- プレハブ化しやすい:敵プレハブに
VisionConeを1つ持たせておけば、どのシーンでも同じロジックを再利用可能。 - レベルデザインが楽:視野角・距離・レイヤーをインスペクタから調整するだけで、敵の「強さ」や「見つかりやすさ」を簡単に調整できる。
- デバッグしやすい:Sceneビューに視界の扇形とラインが表示されるので、「なぜ見えていないのか」が一目で分かる。
さらに、VisionCone は「ターゲットが見えているか」の情報を提供するだけなので、
- 追跡AI
- 警戒状態管理(パトロール / 追跡 / 見失い)
- アニメーション切り替え
- サウンド再生
などをそれぞれ別のコンポーネントで組み合わせていく、コンポーネント指向な設計にしやすくなります。
改造案:視界に入っている時間を計測する
「一瞬でも見えたらアウト」ではなく、「一定時間見られ続けたら発見」といったゲームデザインもよくあります。その場合は、VisionCone を直接いじるのではなく、別コンポーネントとして「視認時間」を計測するクラスを追加するときれいに分離できます。
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// VisionCone と組み合わせて、
/// 「一定時間以上見られたらイベントを発火」するコンポーネント。
/// </summary>
[RequireComponent(typeof(VisionCone))]
public class DetectionTimer : MonoBehaviour
{
[SerializeField] private float requiredVisibleTime = 2f;
public UnityEvent onDetected;
private VisionCone _visionCone;
private float _visibleTimer;
private void Awake()
{
_visionCone = GetComponent<VisionCone>();
}
private void Update()
{
if (_visionCone.IsTargetVisible)
{
_visibleTimer += Time.deltaTime;
if (_visibleTimer >= requiredVisibleTime)
{
onDetected?.Invoke();
// 一度発見したらタイマーをリセット or 無効化するかはゲーム仕様次第
_visibleTimer = 0f;
}
}
else
{
// 見失ったらタイマーをリセット
_visibleTimer = 0f;
}
}
}
このように、小さな責務ごとにコンポーネントを分けていくと、プロジェクトが大きくなっても管理しやすくなります。VisionCone をベースに、ぜひ自分のゲームに合った視界システムを組み立ててみてください。




