【Unity3D】Rayを使って壁越しにプレイヤーを検知しない敵を作ってみる

# はじめに
Unityの有料アセットである[Behavior Designer](https://assetstore.unity.com/packages/tools/visual-scripting/behavior-designer-behavior-trees-for-everyone-15277?locale=ja-JP) でプレイヤーを検知する敵を作っていましたが、ジャンプしている間は検知してくれなかったので、壁がないときにのみプレイヤーを検知する処理を勉強も兼ねて作ってみました。

※Unityのバージョンは2019.4.18f1を使用しています。

# Ray について
Unityの機能の中にRayというものがあります。 これは、指定した位置と方向から光線を飛ばす機能で、光線に当たったオブジェクトの情報を取得したり衝突判定を行うことができます。Rayの当たり判定は[Raycast](https://docs.unity3d.com/ja/current/ScriptReference/Physics.Raycast.html)という関数で調べることができ、RaycastHitという構造体にRayと接触したオブジェクトの情報が格納されます。

# 実装の手順
1. プレイヤーの座標の取得
1. 敵の位置からプレイヤーの位置へRayを飛ばす
1. 最初にRayが接触したオブジェクトを調べる

という手順で進めていきます。

## プレイヤーの座標の取得
まず、プレイヤーの座標を取得するために、OnTriggerStay()でコライダーと接触したオブジェクトを取得します。その後、コライダーからタグを調べて、"Player"という名前のタグがついていれば、そのオブジェクトの座標をプレイヤーの座標として利用します。
プレイヤーの座標を取得する他のやり方としては、Find()でプレイヤーのオブジェクトを見つけたり、public変数などでインスペクターにオブジェクトを直接入れる方法でも良いと思います。

## 敵の位置からプレイヤーの位置へRayを飛ばす
Rayを飛ばす位置については敵の視点となる位置(transform.position)を指定するだけです。方向を決めるためには、Rayを飛ばす先（プレイヤーの座標）から、Rayを飛ばす元（敵の座標）を引いて正規化する必要があります（参考：[ひとつのオブジェクトから別のオブジェクトへの向きと距離](https://docs.unity3d.com/ja/2019.4/Manual/DirectionDistanceFromOneObjectToAnother.html)）。
Rayを飛ばす位置と方向が決まったらnewで新しいRayを作成します。

## 最初にRayが接触したオブジェクトを調べる
Raycastを使う方法とRaycastAllを使う方法があります。
### Raycastを使う方法
Raycastは、「Rayがオブジェクトに当たったかどうか」というbool値を返します。また、Rayがオブジェクトに当たった場合は、out修飾子を用いることでRayが当たったオブジェクト（RaycastHit）を取得することができます。複数のオブジェクトにRayが当たった場合は、RaycastHitには最初に当たったオブジェクトが格納されます。
よって、RaycastHitに入ったオブジェクトを調べ、それがプレイヤーオブジェクトだった場合は、敵とプレイヤーとの間に物体がないということになるので「プレイヤーを見つけた」と判断します。

### RaycastAllを使う方法
Raycastが「Rayがオブジェクトに当たったかどうか」のbool値を返すのに対して、[RaycastAll](https://docs.unity3d.com/ja/current/ScriptReference/Physics.RaycastAll.html)は「Ray がヒットしたオブジェクト（RaycastHit）全てのリスト」を返す関数です。そこでLINQのFirst()メソッドを使ってRaycastAllの最初の要素を調べ、それがプレイヤーだった場合に「プレイヤーを発見した」という判定を行うようにすると、壁がないときにのみプレイヤーを検知するようになります。

# ソースコード
Raycastを使う方法で記述しています。RaycastAllを使う方法も後述しています。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyView : MonoBehaviour
{
    Ray ray;
    RaycastHit hit;
    Vector3 direction;   // Rayを飛ばす方向
    float distance = 10;    // Rayを飛ばす距離

    private void OnTriggerStay(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            // Rayを飛ばす方向を計算
            Vector3 temp = other.transform.position - transform.position;
            direction = temp.normalized;

            ray = new Ray(transform.position, direction);  // Rayを飛ばす
            Debug.DrawRay(ray.origin, ray.direction * distance, Color.red);  // Rayをシーン上に描画

            // Rayが最初に当たった物体を調べる
            if (Physics.Raycast(ray.origin, ray.direction * distance, out hit))
            {
                if (hit.collider.CompareTag("Player"))
                {
                    Debug.Log("プレイヤー発見");
                }
                else
                {
                    Debug.Log("プレイヤーとの間に壁がある");
                }
            }
        }
    }
}
```

## RayCastAllを使う場合
基本的には上記のスクリプトと同じなので、変更点だけを記載します。
LINQを使うためのusingディレクティブ

```
using System.Linq;
```
を先頭に書いておき、OnTriggerStayの中の

```
if (Physics.Raycast(ray.origin, ray.direction * distance, out hit))
```
を消して

```
hit = Physics.RaycastAll(ray).First();
```

を書くと同じように動作します。

# プロジェクトの設定
簡単なプロジェクトを作って動作確認をしました。
床をPlane, 壁（青い直方体）と敵（緑の立方体）をCube, プレイヤー（赤いカプセル）をCapsuleで作成しました。
また、Rayを飛ばす際にプレイヤーより先に床や敵自身のオブジェクトにRayが当たると正しく判定されないことがあるので、これら二つのオブジェクトのLayerを"Ignore Raycast"に変更しました。LayerMaskの設定は、デフォルトでは"Ignore Raycast"に指定されたものをRayの接触判定の対象外とします。Raycast内でLayerMaskを指定する方法もあるようです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1322453/ee94753b-9e3e-fbb6-e3bf-2c9498190275.png"  width=50%>
## プレイヤーオブジェクトの設定
プレイヤーオブジェクトは、タグを"Player"としておき、子要素にMainCameraを持たせます。また、矢印キーで移動できるようにスクリプトも書いてアタッチしておきます。
参考までにプレイヤーのスクリプトも載せておきます。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] private float speed = 0.1f;

    void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.position += Vector3.forward * speed;
        }
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            transform.position += Vector3.left * speed;
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            transform.position += Vector3.back * speed;
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            transform.position += Vector3.right * speed;
        }
    }
}
```

## 敵オブジェクトの設定
敵オブジェクトは、子要素に空のゲームオブジェクトを持たせて、そこに視界の範囲となるコライダーと、EnemyViewスクリプト（上記のスクリプト）をアタッチします。
コライダーは3Dで使えるものであれば何でもよいと思いますが、今回は敵の視界を表すので円錐のような形のコライダーを使いたいと思い、[コーンコライダー](https://assetstore.unity.com/packages/tools/conecollider-62755)というアセットをお借りしました。使い方は[こちらの記事](https://baba-s.hatenablog.com/entry/2018/01/07/223900)などで紹介されています。
# 動作確認
左がシーンで右がゲーム画面です。
プレイヤーがコライダー内に入ったときに、壁があるときは壁との接触を、壁がないときはプレイヤーとの接触を取れていることが確認できました。
![動作結果.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1322453/4ffced28-d9dc-4081-6201-9768b500db7e.gif)

# おわりに
今回は、Rayを使って壁越しにプレイヤーを検知しない方法を書いてみました。
記事の作成自体初めてで、読みづらかったり分かりにくい部分もあったかもしれません。質問等ございましたらコメントの方でお願いします。