【Unity】VisionCone (視界判定) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

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);
        }
    }
}

使い方の手順

ここからは、実際にシーンに配置して使う手順を具体的に見ていきましょう。例として「敵キャラがプレイヤーを見つけたら追いかける」ケースを想定します。

手順①:レイヤーの準備

  1. Unity上部メニューから「Edit > Project Settings > Tags and Layers」を開きます。
  2. Layerに以下のような名前を追加します(名前は任意ですが分かりやすく)。
    • Player(ターゲット用)
    • Obstacle(壁・障害物用)
  3. シーン内のプレイヤーのGameObjectに「Player」レイヤーを設定します。
  4. 壁やブロックなど、視界を遮りたいオブジェクトに「Obstacle」レイヤーを設定します。

手順②:敵オブジェクトに VisionCone を追加

  1. 敵キャラ用のGameObject(例: Enemy)をシーンに配置します。
  2. EnemyVisionCone コンポーネントを追加します(Add Component > VisionCone)。
  3. インスペクタ上で以下を設定します。
    • 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 をベースに、ぜひ自分のゲームに合った視界システムを組み立ててみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!