【Unity】キャラクターの視線判定を行う

こじゃらこじゃら

キャラの視界に入っているかどうかの判定を行うにはどうすればいいの?

このはこのは

やり方はいくつかあるけど、ベクトルの内積計算で判定できるわ。

キャラクターの視界に入ったかどうかを判定する方法の紹介です。

本記事の方法を実践すると、次のような範囲にターゲットが入っているかどうかを判定できるようになります。

実現方法は一通りではありませんが、本記事では汎用的で処理が軽いベクトルの内積を用いて判定する方法を紹介します。

UCL UCL

この作品はユニティちゃんライセンス条項の元に提供されています

動作環境
  • Unity 2021.2.14f1

スポンサーリンク

前提条件

次のように、キャラクターとターゲットがシーン上に配置されているものとします。

例では、ユニティちゃんの視界にターゲット(キューブ)があるかどうかを判定するものとして解説を進めます。

視線判定の計算式

次のように、ターゲット位置が円錐部分の領域に入っていたら、見えている判定とします。

オブジェクトの位置P、長さ1に正規化された向き\vec{d}ターゲットの位置Q視野角\theta最大距離Dとします。

ターゲット位置Q視界に入る条件は次式で表せます。

見えていると判定する条件式
\frac{\vec{P Q}}{|\vec{P Q}|} \cdot \vec{d} > \cos{\frac{\theta}{2}}

かつ

0 < |\vec{P Q}| < D

ただし、|\vec{d}| = 1

サンプルスクリプト

指定されたターゲットが視線の範囲内に存在しているかどうかを判定するサンプルスクリプトです。

SightCheckerExample.cs
using UnityEngine;

public class SightCheckerExample : MonoBehaviour
{
    // 自分自身
    [SerializeField] private Transform _self;

    // ターゲット
    [SerializeField] private Transform _target;

    // 視野角(度数法)
    [SerializeField] private float _sightAngle;

    // 視界の最大距離
    [SerializeField] private float _maxDistance = float.PositiveInfinity;

    #region Logic

    /// <summary>
    /// ターゲットが見えているかどうか
    /// </summary>
    public bool IsVisible()
    {
        // 自身の位置
        var selfPos = _self.position;
        // ターゲットの位置
        var targetPos = _target.position;

        // 自身の向き(正規化されたベクトル)
        var selfDir = _self.forward;
        
        // ターゲットまでの向きと距離計算
        var targetDir = targetPos - selfPos;
        var targetDistance = targetDir.magnitude;

        // cos(θ/2)を計算
        var cosHalf = Mathf.Cos(_sightAngle / 2 * Mathf.Deg2Rad);

        // 自身とターゲットへの向きの内積計算
        // ターゲットへの向きベクトルを正規化する必要があることに注意
        var innerProduct = Vector3.Dot(selfDir, targetDir.normalized);

        // 視界判定
        return innerProduct > cosHalf && targetDistance < _maxDistance;
    }

    #endregion

    #region Debug

    // 視界判定の結果をGUI出力
    private void OnGUI()
    {
        // 視界判定
        var isVisible = IsVisible();

        // 結果表示
        GUI.Box(new Rect(20, 20, 150, 23), $"isVisible = {isVisible}");
    }

    #endregion
}

上記スクリプトを適当なゲームオブジェクトにアタッチし、各種パラメータをインスペクターから設定すると機能するようになります。

実行結果

スクリプトの解説

条件式に使うコサイン内積の計算は以下部分で行っています。

// cos(θ/2)を計算
var cosHalf = Mathf.Cos(_sightAngle / 2 * Mathf.Deg2Rad);

// 自身とターゲットへの向きの内積計算
// ターゲットへの向きベクトルを正規化する必要があることに注意
var innerProduct = Vector3.Dot(selfDir, targetDir.normalized);

Mathf.Cos()メソッドに指定する角度は弧度法(ラジアン)のため、Mathf.Deg2Rad定数を掛けて単位変換しています。

2つのベクトルの内積は、Vector3.Dot()メソッドで計算できます。

参考:Mathf-Cos – Unity スクリプトリファレンス

参考:Vector3-Dot – Unity スクリプトリファレンス

条件式の不等式の判定処理は次の部分で行っています。

// 視界判定
return innerProduct > cosHalf && targetDistance < _maxDistance;

2Dゲームにおける視界判定

次のような扇状の範囲にターゲットが存在しているか判定する方法も紹介します。

判定式は3D版のものをそのまま流用できます。2Dの場合はz軸成分を0にすることで実現できます。

サンプルスクリプト 

2D版の視線判定のサンプルスクリプトです。xy平面上で判定するものとします。扱うベクトル型がVector2になった以外は3Dの場合と一緒です。

SightCheckerExample2D.cs
using UnityEngine;

public class SightCheckerExample2D : MonoBehaviour
{
    // 自分自身
    [SerializeField] private Transform _self;

    // ターゲット
    [SerializeField] private Transform _target;

    // 視野角(度数法)
    [SerializeField] private float _sightAngle;

    // 視界の最大距離
    [SerializeField] private float _maxDistance = float.PositiveInfinity;

    #region Logic

    /// <summary>
    /// ターゲットが見えているかどうか
    /// </summary>
    public bool IsVisible()
    {
        // 自身の位置
        Vector2 selfPos = _self.position;
        // ターゲットの位置
        Vector2 targetPos = _target.position;

        // 自身の向き(正規化されたベクトル)
        // この例では右向きを正面とする
        Vector2 selfDir = _self.right;
        
        // ターゲットまでの向きと距離計算
        var targetDir = targetPos - selfPos;
        var targetDistance = targetDir.magnitude;

        // cos(θ/2)を計算
        var cosHalf = Mathf.Cos(_sightAngle / 2 * Mathf.Deg2Rad);

        // 自身とターゲットへの向きの内積計算
        // ターゲットへの向きベクトルを正規化する必要があることに注意
        var innerProduct = Vector2.Dot(selfDir, targetDir.normalized);

        // 視界判定
        return innerProduct > cosHalf && targetDistance < _maxDistance;
    }

    #endregion

    #region Debug

    // 視界判定の結果をGUI出力
    private void OnGUI()
    {
        // 視界判定
        var isVisible = IsVisible();

        // 結果表示
        GUI.Box(new Rect(20, 20, 150, 23), $"isVisible = {isVisible}");
    }

    #endregion
}

実行結果

さいごに

オブジェクトが視界に入っているかどうかの判定方法の1つとして、円錐状や扇状の範囲にターゲットが存在しているかどうかをチェックする方法を紹介しました。

今回はターゲットの1点の座標で判定していましたが、大きさを考慮する場合は複数の点のどれかが視界に入っているかどうかという判定処理に変更すれば実現可能です。

参考サイト

スポンサーリンク