[Unity]Starter AssetsのInput Systemを拡張して機能を追加する

## はじめに
こんにちは、ゆきおです。
今回はUnityのStarterAssetsにデフォルトで備わっているInputSystemを拡張してインタラクションを実装していこうと思います。
9/20にConnpassで発表する内容の備忘録として書いておきます。

## InputSystemにキーを登録
まずはFキーをインタラクションとして登録します
![スクリーンショット 2024-09-10 12.25.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/c6d8914a-c741-ed67-5d0a-1f50dd7224d1.png)
こんな感じで登録して保存します。
Input Systemは保存しないといけないので登録して満足しないように気をつけてください（自戒

## InputSystemのスクリプトを拡張
次にInputSystemのスクリプトの方にもインタラクションを書き加えます。
特に難しいことはなく、JumpやSprintと仕組みは同じなので書き方をマネすれば大丈夫です。
ここで定義したプロパティはコントローラーの方でアクセスして値をイジります。
```cs
//一部省略

[Header("Character Input Values")]
		public Vector2 move;
		public Vector2 look;
		public bool jump;
		public bool sprint;
		public bool interact; //追加

  #if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
		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)　//追加
        {
			InteractInput(value.isPressed);
		}
#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;
		}

		public void InteractInput(bool newInteractState) //追加
        {
			interact = newInteractState;
		}


```

## ThirdPersonControllerの拡張
次にコントローラーの拡張です。
今回はTPSテンプレートを使用しています。

ちなみにどこでInputSystemを取得して入力を検知しているかというと、Start()の中でInput Systemの内容を取得して_inputに格納しています
```cs
            _input = GetComponent<StarterAssetsInputs>();
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
            _playerInput = GetComponent<PlayerInput>();
#else
			Debug.LogError( "Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it");
#endif
```
これで設定したキーと、それに応じて呼ばれるイベントを検知できるようになります。

そしてMove()やCameraRotation()などの各メソッドでInputSystemスクリプトのmoveとかsprintとかlookとか各プロパティにアクセスして値や状態を動かしています。

```cs
float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;

if (_input.move == Vector2.zero) targetSpeed = 0.0f;
```

これらのメソッドをupdate()内で実行して毎フレーム変化をチェックします。

```cs
private void Update()
{
    _hasAnimator = TryGetComponent(out _animator);

    JumpAndGravity();
    GroundedCheck();
    Move();
}
```
WASDを押したのであればmoveイベントが発生してMove()が呼び出されるという仕組みです。
InputSystem ⇄ InputSystemのスクリプト ⇄ ThirdPersonController
という相互関係ですね。

同じ要領でinteract()というメソッドをupdate()で呼び出すようにしましょう。
そのために色々下準備をします。

## インターフェースを作る
まずはインターフェースを作りましょう。
なぜなら「インタラクション」というのは複数のケースが想定されます。
普通はスペースキーを押したらジャンプするようになっていますが、これはキーと役割が１：１ですよね。
しかし「インタラクション」とは一言で言っても「開ける」「話しかける」「調べる」とか色々あります。

Fは開ける、Cは話しかける、Vは調べるとかやってたらキリないですよね。ゲームパッドにそんなにいっぱいボタンはありません。

なのでFキーを押したら「開ける」「話しかける」「調べる」を出し分けなければなりませんので、各アクションが別々のinteract()を実装する必要があるのでインターフェースを作成するわけです。

```cs
public interface IInteractable
{
    void Interact();
}
```
C#のお作法で「I〇〇able」の形で記述します。

## Doorクラス
次に、今回インタラクションして動かしたいのはドアの開閉アニメーションなのでDoorクラスを作ります。

```cs
public class Door : MonoBehaviour, IInteractable
{
    private Animator animator;
    private bool isOpen = false;

    private void Awake()
    {
        animator = GetComponent<Animator>();
    }

    public void Interact()
    {
        ToggleDoor();
    }

    private void ToggleDoor()
    {
        isOpen = !isOpen;
        string animationName = isOpen ? "Open" : "Close";
        
        animator.Play(animationName);
        
        Debug.Log($"Door is now {(isOpen ? "open" : "closed")}");
    }
}
```
ここでインターフェースの中身を実装するのでIInteractableを継承します。
開いてるか閉まってるかの状態を見て開けるアニメーションを再生するか閉めるアニメーションを再生するかを決めるだけですね。

ドアアニメーションを作成してAnimatorのあるオブジェクトにこのスクリプトもアタッチします。
アニメーションの作り方などはこちらを参考にしてください
 
https://qiita.com/kazuyuki_11E0/items/bd3c6870b649d4d1891b
 
もしアイテム取得やNPCとの会話を実装したい場合は同じようにItemクラスやNPCクラスを作成してインターフェースを継承して実装するという流れです。

## インタラクションを管理するクラス
次はItem取得がある前提ですが、インタラクション関係のメソッドを置いておくInteractionManagerを作成しておきます。
ここを切り分けておかないとコントローラーがどんどん肥大化して読みにくくなるので分けました。

```cs
using UnityEngine;

public class InteractionManager : MonoBehaviour
{
    public float interactionRange = 2f;
    public LayerMask interactableLayers;

    public void HandleInteraction(Vector3 rayOrigin, Vector3 rayDirection)
    {
        Debug.Log($"HandleInteraction called. Range: {interactionRange}, Layers: {interactableLayers.value}");
        if (Physics.Raycast(rayOrigin, rayDirection, out RaycastHit hitInfo, interactionRange, interactableLayers))
        {
            Debug.Log($"Raycast hit: {hitInfo.collider.gameObject.name}, Tag: {hitInfo.collider.tag}");
            switch (hitInfo.collider.tag)
            {
                case "Door":
                    InteractWithDoor(hitInfo.collider.gameObject);
                    break;
                case "Item":
                    InteractWithItem(hitInfo.collider.gameObject);
                    break;
                default:
                    Debug.Log($"Interacting with unknown object: {hitInfo.collider.tag}");
                    break;
            }
        }
        else
        {
            Debug.Log("Raycast did not hit any object");
        }
    }

    private void InteractWithDoor(GameObject door)
    {
        Debug.Log($"Attempting to interact with door: {door.name}");
        Door doorComponent = door.GetComponent<Door>();
        if (doorComponent != null)
        {
            doorComponent.Interact();
        }
        else
        {
            Debug.LogError($"Door object {door.name} does not have a Door component");
        }
    }

    private void InteractWithItem(GameObject item)
    {
        Item itemComponent = item.GetComponent<Item>();
        if (itemComponent != null)
        {
           itemComponent.interact();
        }
        else
        {
            Debug.Log("Item object does not have an Item component");
        }
    }
}
```

レイキャストをしてヒットしたオブジェクトのタグを見て、それぞれのInteract()を呼び出すという仕組みにしました。
「話しかける」とかを追加実装したくなったらSwitch文内のケースを増やすのと、InteractWithメソッドを実装するだけなので拡張しやすくしています。

そしたらこれを ThirdPersonControllerで呼び出します。

## コントローラーの拡張
まずはプロパティを定義しておきます
```cs
[Header("Interaction")]
public Transform interactionRayOrigin;
private InteractionManager _interactionManager;
```
FPSですとキャラクターの視点＝カメラなのでカメラからレイキャストを発射すればいいですが、TPSの場合はキャラクター自身から発射しなければなりませんので、レイを出す位置を指定できるようにしておきます。

そしたらStart()内でIntaractionManagerを呼び出しましょう
```cs
_interactionManager = GetComponent<InteractionManager>();
```

次にFキーを押すことで呼び出すInteract()を実装します
先ほどIntaractionManagerで作成したメソッドを呼び出し、最後にはinteractの値を初期化します。
```cs
private void Interact()
{
    if (_input.interact && _interactionManager != null)
    {
        Vector3 rayOrigin = interactionRayOrigin.position;
        Vector3 rayDirection = _mainCamera.transform.forward;

        _interactionManager.HandleInteraction(rayOrigin, rayDirection);

        _input.interact = false;
    }
}
```

これをUpdate()内で呼び出しましょう。

```cs
private void Update()
{
    _hasAnimator = TryGetComponent(out _animator);

    JumpAndGravity();
    GroundedCheck();
    Move();
    Interact(); //追加
}
```

スクリプトは以上です。

## 最後に
DoorにはDoorスクリプトをアタッチし、タグをDoorに設定しておきます。
![スクリーンショット 2024-09-10 14.26.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/01142955-4273-504e-121c-c1fa50d7abf9.png)

またボックスコライダーをAddしコライダーの形を大体でいいのでドアに合わせます。
黄緑色の枠線がコライダーで、レイキャストがコイツに当たると情報を取得します。

![スクリーンショット 2024-09-10 14.26.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/0dc22519-367e-b078-9d6d-ee2f7bf3879a.png)

それとPlayerArmatureにInteractionManagerをアタッチしInteractable Layerを今はEverythingにしておきます。
また ThirdPersonControllerを拡張したことでInteractionRayOriginプロパティが追加されていますので、PlayerArmatureの中にあるPlayerCameraRootをアタッチしましょう。
これでキャラクターの胸の辺りからレイが出るのでインタラクト可能なオブジェクトにアクセスしやすくなります。

![スクリーンショット 2024-09-10 14.24.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/399de1af-aa48-b1a4-8c5d-9a4eed93d647.png)

![スクリーンショット 2024-09-10 14.24.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562437/66efad7b-17db-4f81-4d11-f2eb9a811fb1.png)

## 終わり
以上でドアのアニメーションをFキーで再生することができました。
InputSystemやインターフェースを活用することで、コントローラーが散らからずクリーンな実装を実現できます。
気が向いたらアイテムの取得についても実装していきたいと思います。

ゲーム制作などにお役立ていただけたら幸いです。