【Unity】ノベルゲーム用アセットFungusをVRで使えるようにしてみた

# 【Unity】ノベルゲーム用アセットFungusをVRで使えるようにしてみた

Unityでのノベルゲーム製作で有用なアセットに、`Fungus`があります。
私はOculus用のVRゲームを開発しており、その中で会話ウィンドウを出したくなりました。
しかし残念ながら、`Fungus`はVRに対応していません。そこで、`Fungus`をVRでも使えるように機能追加してみました。

## 作ったもの

こんな感じで会話ウィンドウが出るようになります。

![FungusVR2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/abcb31ed-a3d8-607b-94dc-b6774113955f.gif)

## やらなければいけないこと

会話ウィンドウをVRで使えるようにします。具体的には以下の機能を実装します。

- 会話ウィンドウを3D空間上に発生させる。
  - Fungusのデフォルトの会話ウィンドウは、画面前面に表示されるようになっています。
- 発言者の口から会話ウィンドウを発生させ、プレイヤーの目の前に移動する。
  - 発言者から発せられたように見せることができます。
- プレイヤーの頭の動きに追随させる。
  - 好みによりますが、空間上に会話ウィンドウが固定されているより見やすいと感じました。

## 環境

- Unityのバージョン: 2019.2.18f1(64-bit)
- Fungusのバージョン: v3.12.0
- Oculus Integrationのバージョン: 12.0

## 作り方

以下の流れで作成します。

- VR用の会話ウィンドウを作成する
- VR用のSayコマンドを作成する
- 会話を次の状態に進めるスクリプトを作成する
- SayDialogの設定を行う

## VR用の会話ウィンドウを作成する

VR用の会話ウィンドウ、`SayDialogForVR`コンポーネントを作成します。
`SayDialog`コンポーネントとの違いは、会話ウィンドウが3D空間上に配置されること、発言者の位置から会話ウィンドウが発生し、定位置に移動することです。

```C#:Assets/Fungus/Scripts/Components/SayDialogForVR.cs

using UnityEngine;
using System;

namespace Fungus
{
    public class SayDialogForVR : SayDialog
    {
        Vector3 prevPosition = Vector3.zero;
        Quaternion prevRotation = Quaternion.identity;
        Vector3 prevScale = Vector3.zero;
        Vector3 toScale;
        public float speed = 0.05f;
        public Vector3 localPosition = new Vector3(0.0f, -0.222f, 2.3732f);        
        public Transform mainCamera;
        
        Vector3 GetDialogPosition()
        {
            return mainCamera.position
                + mainCamera.right * localPosition.x
                + mainCamera.up * localPosition.y
                + mainCamera.forward * localPosition.z;                
        }

        Quaternion GetDialogRotation()
        {
            return Quaternion.LookRotation(GetDialogPosition() - mainCamera.position);
        }

        public void SetDialogTransform(Transform from)
        {
            prevPosition = from ? from.position: GetDialogPosition();
            prevRotation = GetDialogRotation();
            prevScale = from ? Vector3.zero : toScale;
        }

        protected override void Start()
        {
            toScale = transform.localScale;
            mainCamera = mainCamera ?? Camera.main.transform;
            base.Start();
        }

        protected override void UpdateAlpha()
        {
            transform.position = Vector3.Lerp(prevPosition, GetDialogPosition(), speed);
            transform.rotation = Quaternion.Lerp(prevRotation, GetDialogRotation(), speed);
            transform.localScale = Vector3.Lerp(prevScale, toScale, speed);
            prevPosition = transform.position;
            prevRotation = transform.rotation;
            prevScale = transform.localScale;
            base.UpdateAlpha();
        }
    }
}

```

## VR用のSayコマンドを作成する

Fungusでは自作コマンドを作成できます。
VR用のSayコマンド、`SayForVR`コマンドを作成しましょう。先ほど作成した、`SayDialogForVR`を呼び出すコマンドです。

```C#:Assets/Fungus/Scripts/Commands/SayForVR.cs
using UnityEngine;

namespace Fungus
{
    [CommandInfo("Narrative", 
                 "SayForVR", 
                 "Writes text in a dialog box.")]
    [AddComponentMenu("")]
    public class SayForVR : Say
    {
        // 子オブジェクトから、指定のタグが付いたTransformを探す。
        protected Transform FindTagInChildren(Transform t, string tagName)
        {
            if (t == null)
            {
                return null;
            }

            if (t.tag == tagName)
            {
                return t;
            }

            foreach (Transform child in t)
            {
                Transform s = FindTagInChildren(child, tagName);

                if (s)
                {
                    return s;
                }
            }

            return null;
        }

        public Transform talker;

        public override void OnEnter()
        {
            Transform from = FindTagInChildren(talker, "Head") ?? talker;
            ((SayDialogForVR)setSayDialog).SetDialogTransform(from);
            base.OnEnter();
        }
    }
}
```


## 会話を次の状態に進めるスクリプトを作成する

会話を次に進めるコンポーネントは、SayDialogについている`DialogInput`コンポーネントなのですが、このコンポーネントは、UIへの入力検知に`StandaloneInputModule`を使用していることを前提としています。
通常、Hierarcy上に`Canvas`等のUIオブジェクトを配置したとき、同時に`EventSystem`というGameObjectが配置されます。  
このEventSystemには`StandAloneInputModule`というコンポーネントがくっついています。これによりUIオブジェクトがキー入力、マウス入力を検知できるようになるわけですが、Oculusでは、入力に`Oculus Touch`コントローラを使うため、この`StandAloneInputModule`を`OVRInputModule`に置き換えて使うはずです。
そこで、`OVRInputModule`を使用するように書き換えた、`DialogInputForVR`を作成します。

### コード

以下のコードでは、Oculus Touchコントローラの、ABXYのいずれかのボタンを押したときに、会話ウィンドウが次に進むように設定しています。デバッグ用に、キーボードのEnter、Space、Zも有効化しています。

```C#:Assets/Fungus/Scripts/Components/DialogInputForVR.cs
using UnityEngine;
using UnityEngine.EventSystems;

namespace Fungus
{
    public class DialogInputForVR : DialogInput
    {
        protected override void Update()
        {
            if (EventSystem.current == null)
            {
                return;
            }

            if (writer != null && writer.IsWriting)
            {
                // ここを好きなキーに設定する
                if (OVRInput.GetDown(OVRInput.Button.One)
                || OVRInput.GetDown(OVRInput.Button.Two)
                || OVRInput.GetDown(OVRInput.Button.Three)
                || OVRInput.GetDown(OVRInput.Button.Four)
                || Input.GetKeyDown(KeyCode.Return)
                || Input.GetKeyDown(KeyCode.Z)
                || Input.GetKeyDown(KeyCode.Space))
                {
                    SetNextLineFlag();
                }
            }

            if (nextLineInputFlag)
            {
                var inputListeners = gameObject.GetComponentsInChildren<Fungus.IDialogInputListener>();
                for (int i = 0; i < inputListeners.Length; i++)
                {
                    var inputListener = inputListeners[i];
                    inputListener.OnNextLineEvent();
                }
                nextLineInputFlag = false;
            }
        }
    }
}

```

### 参照の追加

上記のスクリプトはAssets/Fungus/Script/Componentsに作りましたが、その場合、Fungus.csprojにOculus.VRのアセンブリ参照を追加する必要があります。そうしないと`OVRInput`がないといわれてコンパイルエラーになります。

- Unity Editor上でAssets/Fungus/Fungus.asmdefを選択してください。
- InspectorでAssembly Definition ReferencesにOculus.VRを追加してください。

![assembly.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/d95b7f70-7fdd-abc3-6593-53f7e6504758.png)

Fungus.csprojではなく、Oculus.VRが参照されている別のプロジェクト(Assembly-CSharp.csproj)でスクリプトを作成してもよいです。

## SayDialogの設定を行う

Funugsでの会話ウィンドウである`SayDialog`を、VRで使えるように設定変更します。

- SayDialogを配置します。
  - Tools(画面上部のメニューバー) -> Fungus -> Create -> SayDialog で、SayDialogが配置されます。
  - Hirarchy上の場所はどこでもよいです。
  - Rect Transformは、WidthとHeight、Scaleが重要です。ほかはどうでもいいです。
  - 私はこんな感じに設定しました。
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/1e195b3e-681d-30be-c9f2-0a83d0569b4c.png)

- SayDialogの設定を変更します。
  - InspectorからCanvasコンポーネントを探します。
  - Render ModeをWorld Spaceに変更します。
  - ![canvas.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/6ab815b9-1809-9948-2a0f-096f4fca0ca4.png)

- Say Dialogコンポーネントを外します。
- Say Dialog For VRコンポーネントをつけます。  
  - Main Cameraに、CenterEyeAnchorをセットします。
  - Speedはとりあえずデフォルトでいいと思います。
      - 発言者の位置から目的の位置まで移動する際の速度です。大きいほど早く移動します。
  - Local Positionもデフォルトでいいと思います。
      - Main Cameraから見た相対位置です。位置が良くない場合は、いい感じの位置になるようにこの位置を調整してください。
  - その他、Continue Button、Dialog Canvas等の設定項目は、もともとSay Dialogコンポーネントに設定されていたものを設定してください。
  - ![SayDialogForVR.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/929f69a0-70c9-57ad-0216-21ad57f6a41b.png)
  
- Dialog Inputコンポーネントを外します。
- Dialog Input For VRコンポーネントをつけます。
  - 設定変更は必要ありません。
  - ![DialogInputForVR.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/f16db3a1-ab89-810c-c00f-20f6743c65f9.png)

## 使い方

### SayForVRコマンド

Flowchart上で、SayForVRコマンドを作成します。冒頭のgifだとこんな感じです。

![FungusInspector.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/503ddfa1-8990-92c2-798c-6fbcfbed3325.png)

通常のSayコマンドと同じように使えますが、`Talker`というパラメータが一つ増えているはずです。
ここに、発言者のTransformを入れましょう。
すると、発言者の口(頭?)から会話ウィンドウが出てきたように見えるはずです。

発言者が自分以外の場合の例です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/0680d5ca-5ff9-ab0c-12ca-10e33bccd8bb.png)

発言者が自分の場合の例です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/b865b8dd-a5bb-e68c-c3bb-4fcee5bb2d8a.png)

`Talker`を設定しなかった場合は、会話ウィンドウがその場に発生しますので、地の分の時に使えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/6022c998-2e15-d862-f4a1-4fe52490f436.png)

### 3Dモデルの頭の位置のTransformを探し出すのがめんどくさい

3Dモデルの頭の位置は、モデルによってはHierarchyの階層の深いところにあると思います。例えば今使用している3Dモデルはこんな深いところに頭のTransformがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/b3ca3974-8eb9-2ccb-8bd0-01fc83c135c3.png)
毎回これを探し出して、SayForVRコマンドで指定してやらないといけないのはめんどくさいです。そんな時は、頭のTransformを持つGameObjectに、`Head`タグをつけましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574610/61df7ccb-ccbc-a470-46bc-65357b8509ad.png)
SayForVRコマンドの中で、デフォルトで`Head`というタグのついたTransformを、子階層のTransformを探してくる仕様になっています。
これにより、3Dモデルの一番親階層のTransformをTalkerに指定してやればよくなるので、あまりめんどくさくないですね。



## まとめ

Fungusの主要な機能である、`SayDialog`をVRでも使用できるようにしました。
Menuも需要があれば対応してみます。