[Unity]ドア開閉ギミックをスクリプトで制御する

こんにちは、ゆきおです。
最近はBlenderで自然風景を作成することにハマったりなんかして
Udemyで教材を探して森を作っていました。
CG制作というのはなかなかしんどい作業ですね…尊敬します。

![スクリーンショット 2023-08-10 192915.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/b3f7f9ca-4237-ad40-6ad3-a0eaa06abe14.png)

![スクリーンショット 2023-08-18 133602.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/df120bc6-9667-9183-ba15-d557772e975b.png)

というわけで今回はスクリプトを中心に、ドアの開閉ギミックを作っていきます。

## 実装
ドアに実装するスクリプトと、キャラクターに実装するスクリプトのおおきく2つに分けて実装します。
ドアにはアニメーションの再生を、キャラクターにはその実行命令を担っていただきます。
そしてInput SystemにFキーをインタラクションとして登録、それとインタラクション用にインターフェースも実装します。

だいーぶ前に書いたコードでいま見てみると色々思うところはあるのですがとりあえず概要だけ伝えられたらいいなと思っています。

まずは予めインターフェースを実装しておきます。
C#やほかの言語がどうなのかいまいち覚えていませんが、インターフェースを実装するときのお作法として「I+○○+able」という形で書くのが一般的かなと思います。
今回はInteractなので「**IInteractable**」というインターフェースを実装します。

```C#
public interface IInteractable
{
    void Interact();
}

```
こんだけですね。
なぜ実装するかというと「インタラクション」というのはあらゆるゲームにおいて複数存在し得ます。
開ける/閉じる、話しかける、アイテムを拾うなどなど
そう考えるとFキー1つでいろんなことをしなきゃならんので各アクションがこのInteract()を持ってきてそれぞれ実装した方がスマートかなと思いました。

それぞれのイベントをどう判別するかは後ほど書きます。

続いてドアのスクリプトです
``` C#
using UnityEngine;

public class Door : MonoBehaviour, IInteractable
{

    [SerializeField] Animator animator = default;
    public bool isOpen = default;

    public void Interact()
    {
        InteractDoor();
    }

    public void InteractDoor()
    {
        Debug.Log("InteractDoor called");

        if (!isOpen)
            Open();
        else
            Close();
    }

    public void Open()
    {
        isOpen = true;
        animator.Play("Open");
    }

    public void Close()
    {
        isOpen = false;
        animator.Play("Close");
    }
}

```
まずプロパティに前回作成したAnimatorを受け取り、ドアが開いているか閉じているかのフラグを設定します。
Unityではプロパティに **[Serialized Field]** を付けるか **public** にすることでエディタのインスペクターにプロパティが表示されるようになります。
そこにAnimatorをセットしたり、パラメータをエディタ側でいじったりすることができます。
ただ、値がどこからでも参照、アクセス出来るのはよくないので大体は[Serialized Field]+privateが一般的かなと思います。

先ほどのインターフェースを継承し、Interact()の中身を実装しています。
中身となるInteractDoor()は、開いているか閉じているかのフラグを見て前回作成したOpenとCloseアニメーションを再生するというだけです。
```C#
animator.Play("Open");
```
Playメソッドの引数にアニメーションの名前を文字列として渡すだけで再生できます。  

  
続いてInteractManagerを実装します。
Managerと付けるのは基本よろしくないらしいですが、インタラクションイベントを管理するクラスで操作キャラクターにアタッチします。

```C#
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;

public class InteractManager : MonoBehaviour
{
    public InputActionReference interactActionReference; // インスペクターからアサイン
    public float interactRange = 3f;
    public GameObject interactTextObject;
    public TextMeshProUGUI interactText;

    private void OnEnable()
    {
        interactActionReference.action.Enable();
    }

    private void OnDisable()
    {
        interactActionReference.action.Disable();
    }

    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); // テキストオブジェクトを非アクティブに
    }
}

```
何やら色々書いていますがOnEnableだのOnDisableだのはいったんシカトしてください。
・キャラクターからレイキャストを発する
・レイがヒットしたオブジェクトのタグをチェックし、F:PickUpやF:Openのようなガイドテキストを出し各Interact()メソッドを呼び出す
というのが主な仕組みです。

```C#
public InputActionReference interactActionReference;
```
InputSystemで作成したInteractイベントを渡します。

```C#
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;
            }
        }
    }
```
レイキャストはUnity組み込みのPhysicsクラスから呼び出すだけです。
レイを発射する位置や距離、当たった対象の情報を格納するRaycastHitを持ちます。

レイキャストがヒットしたオブジェクトのタグをみて、それぞれのInteract()を実行します。
この場合だとドアの開閉ギミックなのか、アイテムを拾うかですね。
アイテムに対してもDoorと同様、Itemクラスを作成してアイテム自体の挙動を実装しています。

このようにそれぞれのクラス、タグとレイキャストを組み合わせることで機能追加をしやすくしています。
もし「NPCに話しかける」とかFキーの役割を追加したいときはNPCクラスを作成しswitch文の中に追加するだけで良いので管理しやすいかなあと思って設計しました。
この形の設計はもっと練り上げれば応用が利きそうだなと思い、個人的にはいい収穫になりました。

## エディタ側の操作
ドアにはDoorスクリプト、キャラクターにはInteractManagerをドラッグ＆ドロップしてアタッチします。
それぞれAnimatorとInteractActionReferenceをアタッチします。

InteractActionReferenceをアタッチするためにStarterAssets内にあるInputSystemにInteractイベントを追加します。
![スクリーンショット 2024-04-03 165655.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/5ca5df55-3af6-956d-668b-9c6f2655da52.png)
Actionsの+から追加してFキーを割り当てます
これに加えてInputSystemのスクリプトを拡張します。
```C#
using UnityEngine;
using UnityEngine.Events;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif

namespace StarterAssets
{
	public class StarterAssetsInputs : MonoBehaviour
	{
		[Header("Character Input Values")]
		public Vector2 move;
		public Vector2 look;
		public bool jump;
		public bool sprint;

		[Header("Movement Settings")]
		public bool analogMovement;

		[Header("Mouse Cursor Settings")]
		public bool cursorLocked = true;
		public bool cursorInputForLook = true;

        public UnityEvent onInteract = new(); //追加

#if ENABLE_INPUT_SYSTEM
        public void OnMove(InputValue value)
		{
			MoveInput(value.Get<Vector2>());
		}

		public void OnLook(InputValue value)
		{
			if(cursorInputForLook)
			{
				LookInput(value.Get<Vector2>());
			}
		}

		public void OnJump(InputValue value)
		{
			JumpInput(value.isPressed);
		}

		public void OnSprint(InputValue value)
		{
			SprintInput(value.isPressed);
		}

        public void OnInteract(InputValue value)  //追加
        {
            if (value.isPressed) onInteract.Invoke();
        }
#endif


        public void MoveInput(Vector2 newMoveDirection)
		{
			move = newMoveDirection;
		} 

		public void LookInput(Vector2 newLookDirection)
		{
			look = newLookDirection;
		}

		public void JumpInput(bool newJumpState)
		{
			jump = newJumpState;
		}

		public void SprintInput(bool newSprintState)
		{
			sprint = newSprintState;
		}

		private void OnApplicationFocus(bool hasFocus)
		{
			SetCursorState(cursorLocked);
		}

		private void SetCursorState(bool newState)
		{
			Cursor.lockState = newState ? CursorLockMode.Locked : CursorLockMode.None;
		}
	}
	
}
```
ぶっちゃけこの辺はよくわかってないのでUnityEventという形で実装し、InteractManagerのInteractActionReferenceにアタッチしています。

![スクリーンショット 2024-04-03 165404.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/8ee2578d-a767-ed5c-f696-b025217eb170.png)
そのほかにも別途作成したテキストを設定しています。

あとすっかり忘れていましたがタグを設定してあげます。
![スクリーンショット 2024-04-03 171655.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/dea34e5f-25bc-4da7-4ae8-eaf5cc2da4e3.png)
インスペクター上部にTagという項目があるのでDoorを追加します。


## まとめ
これでドアの開閉ギミックはとりあえず完成です。
・アニメーションの作成
・スクリプトでアニメーションの再生
ものすごい単純に言うとこの２点だけで、アニメーションの再生方法に関してはゲームのシステムによって変化します。
今回はTPSでやってみたかったので色々めんどくさいことをしなければならなかったですが、
カジュアルな脱出ゲームで右クリックするだけとかだったらInputSystemは使わず、スクリプトの方で簡単に実装できます。
```C#
void Update()
{
    if (Input.GetMouseButtonDown(1))
    {
       if (!isOpen)
            Open();
        else
            Close();
    }
}
```
こんな感じでドア側のスクリプトでUpdate()関数内に定義するだけで終わりです。
作りたいゲームに合わせて設計しつつ、参考にしていただければ幸いです。

![ezgif-1-4c0b0b978b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/d227ff90-8204-a54d-9515-e7fdc081f538.gif)


今回はこんなところで、お疲れ様でした。