[Unity]CineMachineカメラを使ったメニュー画面を作りたい開拓

#なんかこう...かっこいいメニュー画面にしたい！
## アイデア出し
口で言うのは簡単だけどなかなか難しい。
とりあえずいろんなゲームのUI動画を見てきました。
その中でこれいいなと思うのがあったので、
今回はそれを作っていきたいと思います。
動画は以下になります。

[オーバーウォッチUI演出動画](https://www.youtube.com/watch?v=lNxprI3-qqo)

YouTubeより引用

時間帯としては0:00~0:25のボタンを押したらカメラが移動する。
こんなのを作りたいと思います。

## 動作環境
OS:Windows10
Unity:2020.3.21f1
Cinemachine:2.8.3


## 作り方
### 簡単な前提知識
・Cinemachine
・InputSystem

今回作っていく上で、以下アセットを使用していきます。

https://assetstore.unity.com/packages/3d/environments/japanese-apartment-126497

### メニューリストの作成
メニュー画面を作りたいってことならまずはメニューで表示するメニューリストが必要ですよね。
ということで作ってみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/0c537aea-4612-e379-5344-42f3340c5a5b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/2c3cae34-b405-157e-515c-57577f23a757.png)
今回のメニュー画面はButtonを選択している際に上キーを押したときにButton(2)が選択されるようなループするメニューを作りたいのでループする処理も入れていきます。


ボタン長押しで次々と選択物を移動させたい場合は
Buttonで使用できるNavigationを使用して現在選択しているボタンを可視化するのも良いかと思います。
簡単にできるのはこちらの方だと思います。
今回はボタンの長押し処理を入れたくない。ボタンが押されたタイミングを判定したい。
という理由から見送りました。

https://docs.unity3d.com/ja/2018.4/Manual/script-SelectableNavigation.html

```C#:ILoopMenu.cs
namespace Title {
    public interface ILoopMenu
    {
        /// <summary>
        /// SetSelectedGameObjectを１つ下へ移動させる
        /// </summary>
        void downSelectedGameObject();

        /// <summary>
        /// SetSelectedGameObjectを１つ上へ移動させる
        /// </summary>
        void upSelectedGameObject();
    }
}
```

```C#:LoopMenu.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using Title;

public class LoopMenu : MonoBehaviour, ILoopMenu
{
    // 起動時にデフォルトで選択しておくリストアイテムのインデックスの番号を指定
    [SerializeField] private int _defaltSetListItemsIndex = 0;

    // リストアイテム達の親のlistItemsのGaneObject
    [SerializeField] private GameObject _listItems;

    private List<GameObject> _gameObjectsList = new List<GameObject>();

    private int _listItemsIndex;

    private void Awake()
    {
        if (_defaltSetListItemsIndex > _listItems.transform.childCount - 1 
        ||  _defaltSetListItemsIndex < 0) {
            Debug.LogError("_defaltSetListItemsIndexの値が正常ではありません");
            _defaltSetListItemsIndex = 0;
        }
        // リストで使用するgameObjectをすべてリストへ格納する
        for (var i = 0; i < _listItems.transform.childCount; ++i)
        {
            _gameObjectsList.Add(_listItems.transform.GetChild(i).gameObject);
        }
        // 一番上のアイテムをデフォルトでセットするよう設定
        if (_gameObjectsList[_defaltSetListItemsIndex] != null)
        {
            EventSystem.current.SetSelectedGameObject(_gameObjectsList[_defaltSetListItemsIndex]);
            _listItemsIndex = _defaltSetListItemsIndex;
        }
    }

    public void downSelectedGameObject()
    {
        if (++_listItemsIndex >= _gameObjectsList.Count)
            _listItemsIndex = 0;
        EventSystem.current.SetSelectedGameObject(_gameObjectsList[_listItemsIndex]);
    }

    public void upSelectedGameObject()
    {
        if (--_listItemsIndex < 0)
            _listItemsIndex = _gameObjectsList.Count - 1;
        EventSystem.current.SetSelectedGameObject(_gameObjectsList[_listItemsIndex]);
    }
}

```

#### 解説
_defaltSetListItemsIndexでデフォルトで選択しておくindexの番号を設定します。
_defaltSetListItemsIndexの値がおかしかったらとりあえずindex:0を入れておくようにしました。
**EventSystem.current.SetSelectedGameObject**で引数に与えたオブジェクトを選択状態にしてくれます。
**downSelectedGameObject()**,**upSelectedGameObject()**でそれぞれキーが押されたときに上に、下に、選択を１つずらす処理になります。

これでメニューリストの処理は完成しました。

### CineMachineを使ったカメラの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/0d0ca993-e668-a9b7-0da4-e1b648874f78.png)
雑にアセット(オブジェクト)を配置していきます。
次に適当なオブジェクトにフォーカスを合わせてアップにして描画するVirtualCameraを3台作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/945c873c-5e06-8730-4910-e1e979710d58.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/8ecfaa24-3c69-8078-6599-aae1841d8ee7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/b4ce5ed8-988f-2701-a38c-a9a75a9cbff4.png)
構成はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/21947f10-6e15-5944-1cdd-dd4029a99d79.png)

こんな感じで設定します。
そしたら、これらを十字キーが押されたときにそれぞれ対応したカメラに切り替わるようにしたいので以下のようにスクリプトを書きます。

```C#:IVirtualCameraSelector.cs
namespace Title {
    public interface IVirtualCameraSelector
    {
        /// <summary>
        /// 次のCinemachineカメラを描画先にセットする
        /// </summary>
        void nextTopPriorityVcam();

        /// <summary>
        /// 前のCinemachineカメラを描画先にセットする
        /// </summary>
        void previousTopPriorityVcam();
    }
}
```

```C#:IVirtualCameraSelector.cs
using UnityEngine;
using Cinemachine;
using Title;

public class VirtualCameraSelector : MonoBehaviour, IVirtualCameraSelector
{
    // バーチャルカメラ一覧
    [SerializeField] private CinemachineVirtualCamera[] _virtualCameraList;

    // 非選択時のバーチャルカメラの優先度
    private int _unselectedPriority = 0;

    // 選択時のバーチャルカメラの優先度
    private int _selectedPriority = 10;

    // 選択中のバーチャルカメラのインデックス
    private int _currentCamera = 0;

    // バーチャルカメラの優先度初期化
    private void Awake()
    {
        // バーチャルカメラが設定されていなければ、何もしない
        if (_virtualCameraList == null || _virtualCameraList.Length <= 0)
            return;

        // バーチャルカメラの優先度を初期化
        for (var i = 0; i < _virtualCameraList.Length; ++i)
        {
            _virtualCameraList[i].Priority =
                (i == _currentCamera ? _selectedPriority : _unselectedPriority);
        }
    }

    private void Update()
    {
        // バーチャルカメラが設定されていなければ、何もしない
        if (_virtualCameraList == null || _virtualCameraList.Length <= 0)
            return;
    }

    public void nextTopPriorityVcam()
    {
        // 以前のバーチャルカメラを非選択
        var vCamPrev = _virtualCameraList[_currentCamera];
        vCamPrev.Priority = _unselectedPriority;

        // 追従対象を順番に切り替え
        if (++_currentCamera >= _virtualCameraList.Length)
            _currentCamera = 0;

        // 次のバーチャルカメラを選択
        var vCamCurrent = _virtualCameraList[_currentCamera];
        vCamCurrent.Priority = _selectedPriority;
    }
    public void previousTopPriorityVcam()
    {
        // 以前のバーチャルカメラを非選択
        var vCamPrev = _virtualCameraList[_currentCamera];
        vCamPrev.Priority = _unselectedPriority;

        // 追従対象を順番に切り替え
        if (--_currentCamera < 0)
            _currentCamera = _virtualCameraList.Length - 1;

        // 次のバーチャルカメラを選択
        var vCamCurrent = _virtualCameraList[_currentCamera];
        vCamCurrent.Priority = _selectedPriority;
    }
}
```

#### 解説
_virtualCameraListに使用するvirtualCameraを遷移させたい順番でセットします。
VirtualCamera.Priority で描画させるVirtualCameraの優先度がセットできるのでこの優先度をキー入力があった際に対応するVirtualCameraに割り当てることでカメラの切り替えを行っています。

### 完成
作成したメニューリストと、CineMachineをTitleController.csを作成して２つを合体させたいと思います。

```C#:ITitleController.cs
namespace Title
{
    public interface ITitleController
    {
        /// <summary>
        /// 選択するメニューインデックスを１つ下に移動させる
        /// </summary>
        void downCursorPanel();

        /// <summary>
        /// 選択するメニューインデックスを１つ上に移動させる
        /// </summary>
        void upCursorPanel();
    }
}

```

```C#:TitleController.cs
using UnityEngine;
using UnityEngine.InputSystem;

namespace Title
{
    [RequireComponent(typeof(VirtualCameraSelector))]
    [RequireComponent(typeof(LoopMenu))]
    public class TitleController : MonoBehaviour, ITitleController
    {
        private IVirtualCameraSelector virtualCameraSelector = null;
        private ILoopMenu loopMenu = null;

        private void OnValidate()
        {
            virtualCameraSelector = this.GetComponent<IVirtualCameraSelector>();
            loopMenu = this.GetComponent<ILoopMenu>();
        }

        private void Update()
        {
            if (Keyboard.current.sKey.wasPressedThisFrame)
            {
                downCursorPanel();
            }
            if (Keyboard.current.wKey.wasPressedThisFrame)
            {
                upCursorPanel();
            }
        }

        public void downCursorPanel()
        {
            virtualCameraSelector.nextTopPriorityVcam();
            loopMenu.downSelectedGameObject();

        }
        public void upCursorPanel()
        {
            virtualCameraSelector.previousTopPriorityVcam();
            loopMenu.upSelectedGameObject();
        }
    }
}
```

#### 解説
VirtualCameraSelector、LoopMenuをそれぞれTitleController.cs内からキー入力がされたときに対応するメソッドを呼び出します。
キー入力の判定部分はとりあえず今のところ仮ですが、InputSystemから判定できる形にする予定です。

### 完成

これでカーソルを移動させつつ、カメラを移動したカーソルに対応する位置を写せるような動作を作成することができました！
![unityカメラとUIの同期.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272154/c664b515-12dd-b3d3-9128-9c075600962c.gif)