[Unity]3Dホラーゲーム制作の為に作成した機能(1) 視界真ん中にオブジェクトを捉えた時のアウトライン表示

この記事は3Dホラーゲームを作る際、どのように機能を実装したのかをザックリまとめたものです。

# 出来るもの
![スクリーンショット 2023-12-01 063646.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631350/0becb659-35b7-0ee0-dacd-63b17e7ed608.png)


## やってる事
1.プレイヤーカメラからレイを毎フレーム飛ばす
2.当たったオブジェクトに該当スクリプトがあるか判定
3.該当スクリプト内にある関数を起動し、アウトラインのWidthを変えてアウトラインを表示
4.レイでスクリプトのあるオブジェクトが検出出来なくなったら、アウトラインのWidthを変えてアウトライン非表示

## プレイヤーの配置
以下のアセットをインポートし、プレイヤーをワールドに配置
[StarterAssets - FirstPerson](https://assetstore.unity.com/packages/essentials/starterassets-firstperson-updates-in-new-charactercontroller-pac-196525)

## アウトラインを表示する為のアセット
以下のアセットをインポート。
[Quiqk Outline](https://assetstore.unity.com/packages/tools/particles-effects/quick-outline-115488)

## スクリプト
メインカメラ側に、レイを飛ばして該当スクリプト(IPlayerSelectRayReceive)を検出して各関数を起動する「PlayerSelectRay」を追加する
![スクリーンショット 2023-12-01 063645.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631350/a64e700a-ff40-1bb9-4b58-9bd03f101398.png)

```C#:PlayerSelectRay.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// プレイヤーカメラから視点の先にレイを飛ばして
/// IPlayerSelectRayReceiveが継承元にあるスクリプトを取ってきて
/// 各タイミングで関数を叩く
/// </summary>
public class PlayerSelectRay : MonoBehaviour
{
    public IPlayerSelectRayReceive selectReceiveObj;

    private Camera playerCamera;
    [SerializeField]
    private float distance = 1.0f;

    // Update is called once per frame
    void Update()
    {
        // 準備
        playerCamera = gameObject.GetComponent<Camera>();
        Vector3 rayStartPosition = playerCamera.transform.position;
        Vector3 rayDirection = playerCamera.transform.forward.normalized;
        RaycastHit raycastHit;

        // Rayを飛ばす（out raycastHit でHitしたオブジェクトを取得する）
        bool isHit = Physics.Raycast(rayStartPosition, rayDirection, out raycastHit, distance);
        Debug.DrawRay(rayStartPosition, rayDirection * distance, Color.red);

        // 何かヒットした時
        if (isHit)
        {
            IPlayerSelectRayReceive tempReceive = raycastHit.collider.gameObject.GetComponent<IPlayerSelectRayReceive>();

            // 最初にヒットしたObjに受信側スクリプトがあるか
            if(tempReceive != null)
            {
                if (selectReceiveObj == null)
                {
                    tempReceive.FirstHit();
                    selectReceiveObj = tempReceive;
                }
                else
                {
                    // ヒット中
                    selectReceiveObj.HitNow();
                }
            }
            else
            {
                // 保持中のreceiveObjがある場合
                if (selectReceiveObj)
                {
                    selectReceiveObj.NotHit();
                    selectReceiveObj = null;
                }
            }
        }
        // 何もヒットしなかった時
        else
        {
            // 保持中のreceiveObjがある場合
            if (selectReceiveObj)
            {
                selectReceiveObj.NotHit();
                selectReceiveObj = null;
            }
        }
    }
}
```

オブジェクト側に持たせてRayで検出させ、「ヒット始め」「ヒット中」「ヒット終わり」の各関数を起動してもらう継承元スクリプト「IPlayerSelectRayReceive」

```C#:IPlayerSelectRayReceive.cs
using UnityEngine;

/// <summary>
/// プレイヤーがオブジェクトに視線を当てた時用の各発火スクリプト
/// </summary>
public class IPlayerSelectRayReceive : MonoBehaviour
{
    /// <summary>
    /// プレイヤーの視線の当たり始め
    /// </summary>
    public virtual void FirstHit(){}

    /// <summary>
    /// プレイヤーの視線が当たってる最中
    /// </summary>
    public virtual void HitNow(){}

    /// <summary>
    /// プレイヤーの視線の当たり終わり
    /// </summary>
    public virtual void NotHit(){}
}
```
IPlayerSelectRayReceiveを継承して各関数の中にOutlineの表示非表示処理を書いた「RaySelectOutline」を、アウトライン表示させたいオブジェクトに追加
![スクリーンショット 2023-12-01 064144.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631350/6a627363-423c-0c5b-7136-b25f11b9b4df.png)

```C#:RaySelectOutline.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// プレイヤーの視線がオブジェクトに当たった時に
/// そのオブジェクトの全ての子のアウトラインを表示するスクリプト
/// </summary>
public class SelectRayOutline : IPlayerSelectRayReceive
{
    private Outline outline;
    private List<Material> target_material = new List<Material>();
    [SerializeField]
    Color outlineColor = Color.cyan;

    void Start()
    {
        GetMaterials();

        // アウトラインを追加して各種設定
        outline = this.gameObject.AddComponent<Outline>();
        outline.OutlineColor = outlineColor;
        outline.OutlineMode = Outline.Mode.OutlineVisible;
        outline.OutlineWidth = 0.0f;
    }

    void GetMaterials()
    {
        // このスクリプトの子のMeshRendererを全部拾う
        MeshRenderer[] meshRenderers = this.GetComponentsInChildren<MeshRenderer>();

        // 各MeshRendererの中にあるマテリアルをリスト変数に全部入れる
        foreach (MeshRenderer mesh in meshRenderers)
        {
            foreach (Material material in mesh.materials)
            {
                target_material.Add(material);
            }
        }
    }

    public override void FirstHit()
    {
        SwitchOutline(true);
    }

    public override void HitNow(){}

    public override void NotHit()
    {
        SwitchOutline(false);
    }

    void SwitchOutline(bool isEenable)
    {
        if (isEenable)
        {
            if (outline.OutlineMode != Outline.Mode.OutlineAll)
            {
                outline.OutlineWidth = 2.0f;
            }
        }
        else
        {
            outline.OutlineWidth = 0.0f;
        }
    }
}
```
# 完成
![Outline.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631350/ced66c57-909e-ed64-39a2-387b31e71842.gif)