[Unity] アイテム取得について考える[ユーザビリティ]

## 春🌸

こんにちは、ゆきおです。
今日はUnityのアイテム取得の仕組み作りについて考えたことをまとめます。
ドアの開閉ロジックの実装ついでにアイテム取得ロジックも実装したのですが
今になって見返すと「あーもっとこうしたら良いかも」とか
ゲームをプレイしていて「これってこういう仕組みで再現できるな」とか
新たに気づきがあったのでアウトプットしてこうと思います。

## アイテム取得ロジックの現状
ということで半年前くらいにConnpassで発表するために作成してそのまんまの実装を書いていきます。
ドアの開閉と仕組みはほとんど一緒で、「キャラクターのレイキャストがItemタグのオブジェクトにヒットしたらUIでアナウンスして、Fキー押すと消える」という仕組みです。
そのうちここにアイテムウィンドウや、鍵でドアを開けるみたいな合わせ技もやっていこうと思います。

前もってドアの開閉やアイテム取得、NPCに話しかけるなどをインタラクションイベントという1つのイベントにまとめたので機能追加しやすい設計を心がけました。

**・レイキャストがそれぞれのタグをもったオブジェクトにヒットする**
**・UIでアナウンスする**
**・Fキーを押してそれぞれ対応するInteractメソッドを実行する**

こんな流れですね。よくあるゲームの実装を勝手にイメージして作りました。

![ezgif-5-a8a9e44c12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/76fd18d3-bde9-b573-fe54-8a7b1b5eaad3.gif)

## Itemクラス
```C#
using UnityEngine;

public class Item : MonoBehaviour, IInteractable
{
    public void Interact()
    {
        InteractItem();
    }

    public void InteractItem()
    {
        Debug.Log("Item is picked up.");
        // アイテム取得処理（ここではオブジェクトを非アクティブにします）
        gameObject.SetActive(false);
        // 他の処理をここに追加（例：インベントリにアイテムを追加する等）
    }
}

```
大した実装もないのですがインターフェースであるInteract()の中身を実装しています。
ここからさらに実装するのであれば**アイテムデータベース**というUnityのアイテム管理用データベースを作成して登録したり、アイテムインベントリを作成してデータベースに登録した画像やアイテムの説明文などをUIに表示したりと、色々とやることがあります。

## InteractManager
あんまり覚えてないのですがインタラクションに必要な関数を抜き出してみました
```C#
 void Update()
    {
        CheckForInteractableObjects();

        if (interactActionReference.action.triggered)
        {
            InteractWithObjects();
            HideInteractText();
        }
    }

    void InteractWithObjects()
    {
        Vector3 rayOrigin = transform.position + new Vector3(0, 1f, 0); // 程よい高さからレイを出す
        RaycastHit hit;

        if (Physics.Raycast(rayOrigin, transform.forward, out hit, interactRange))
        {
            string tag = hit.collider.tag;
            switch (tag)
            {
                case "Door":
                    hit.collider.GetComponent<Door>().Interact();
                    break;
                case "Item":
                    hit.collider.GetComponent<Item>().Interact();
                    break;
                default:
                    interactText.gameObject.SetActive(false);
                    break;
            }
        }
    }

    void CheckForInteractableObjects()
    {
        Vector3 rayOrigin = transform.position + new Vector3(0, 1f, 0);

        RaycastHit hit;
        if (Physics.Raycast(rayOrigin, transform.forward, out hit, interactRange))
        {
            string tag = hit.collider.tag;
            ShowInteractText(tag, hit.collider.gameObject);
        }
        else
        {
            HideInteractText();
        }
    }


    void ShowInteractText(string tag, GameObject hitObject)
    {
        string action = "";
        switch (tag)
        {
            case "Door":
                Door door = hitObject.GetComponent<Door>();
                action = door.isOpen ? "F:Close" : "F:Open";
                break;
            case "Item":
                action = "F:Pick Up";
                break;
        }
        interactText.text = action; // テキスト内容を設定
        interactTextObject.SetActive(true); // テキストオブジェクトをアクティブに
    }

    void HideInteractText()
    {
        interactTextObject.SetActive(false); // テキストオブジェクトを非アクティブに
    }
```
キャラクターからレイを発射し、当たったオブジェクトがタグを持っているかを検証したり
タグを持っているものにヒットすればタグを見て対応するUIを表示したり
Fキーを押したらそれぞれのInteractメソッドを呼び出したり
といった感じで実装しています。たぶん。

## 実際に動かしてみて
ドアオブジェクトとかNPCだったらレイを当てやすいのですがやはりアイテムとなると当たり判定を広く持たせないとレイを当てづらいなあと思いました。
なのでここはひとつ仕様を変えようと企んでいます。

とはいってもそんな難しいものでもなく、オブジェクト側にもキャストを付けようと思います。
レイキャストとは別に**スフィアキャスト**というものがありまして、オブジェクトを中心に半径何メートルとか指定して円状にヒットをチェックできるものです。

https://docs.unity3d.com/ja/2023.2/ScriptReference/Physics.SphereCast.html

ハンターハンターの「円」や呪術廻戦の「簡易領域」みたいな要領です。わかりやすい。

```C#
private void Update()
    {
        // SphereCastを使用してキャラクターの接近を検出
        RaycastHit hit;
        if (Physics.SphereCast(transform.position, detectionRadius, transform.forward, out hit, 0f, characterLayer))
        {
            // キャラクターが範囲内に入った場合
            if (!isCharacterInRange)
            {
                isCharacterInRange = true;
                ShowAppealUI();
            }
        }
        else
        {
            // キャラクターが範囲外に出た場合
            if (isCharacterInRange)
            {
                isCharacterInRange = false;
                HideAppealUI();
            }
        }
    }
```
こんなイメージです。円に入ったら「ココダヨー」みたいにUIを表示して見落としを防ぐ狙いです。

## 実装に至った経緯
これはバイオハザードをプレイしていてこういう仕組みなんかなあと思ったのがきっかけです。

バイオ1～3はアイテムをピカピカ光らせて見落としを防いでいました。
4以降はドロップしたアイテムから光の柱が出て、柱の色でアイテムやお金を区別して表示していましたね。

んで仕様が大きく変わった7やREシリーズからは、アイテムやドキュメントに接近すると何やら矢印が現れてそっちを向くと〇ボタンのUIが加わります。

![スクリーンショット 2024-04-16 181202.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/5c5825b7-daa4-d609-8d5c-59813e690167.png)

これはおそらくアイテム側が円状にレイを飛ばすことで存在をアピールして見落としを防いでおり、自然とそこに視線が誘導されキャラクターからのレイキャストで取得の可否を判定しているのではないかなと推測しました。

ちなみにドアの開閉方向についても前の記事で触れていますが、これもバイオシリーズをプレイしていて気づきました。

いつもゾンビが扉を突き破ろうとしてくるとき一生懸命ドアを押さえたりその辺の棚とかでバリケード作ってるけど引いて開くタイプだったらこうはならなくない…？

と思ったのがきっかけで「ドアを開ける方向」という点に気づきました。

バイオ7と8はFPSですので視野が限られています。
なのでこういう仕組みにすると良いんじゃね？みたいなことになったのかなあと勝手に思っています。

## おわり
以上、アイテム取得機能についていろいろと試行錯誤したお話でした。
ゲーム制作やワールド制作においてはイメージに近いゲームを実際にプレイしてみて、それをどんな実装で実現できるかをイメージして取り掛かるっていうのが大事だなと思った次第です。

CG制作においても、風景CGだったら実際に森や山に出向いたり、建物のテクスチャや建築様式など自分の目で確かめるといったロケも大事だという風に勉強しました。

そうやって頭抱えるのもモノづくりの醍醐味ですね。

ではまた。