【第１回】UnityScreenNavigatorを使って呼び出すモーダルをInputFieldの文字数で変更する【初心者向け】

# はじめに

普段は企画職を中心に学んでいる私が、株式会社GENEROSITY様での臨地実務実習で行わせていただいた、XR技術を使ったサービスの企画発案から価値検証モデル(Minimum Viable Product, 通称：MVP)の開発内容を全３回に分けて記事にさせていただきます。
記念すべき第１回では、3Dシーンで行ったメイン体験のコアとなる要素を抜粋して説明していきたいと思います。
技術ブログの作成自体も初めての経験なので、拙い点は広い心と温かい目で見守ってくださると幸いです。

# 目次

[概要](#概要)
[今回のゴール](#今回のゴール)
[環境](#環境)
[実装](#実装) 
[準備](#準備)
[スクリプトの作成](#スクリプトの作成)
[実行](#実行)
[まとめ](#まとめ)

# 概要
私は「他人に言いにくい本音を抱えているが、誰にも見られたくない」といった課題を解決するために、本音を一人で解消するためのスマホ向けアプリ「本音バルーン」を作成いたしました。
視覚的なカタルシス(爽快感やストレスの解消)を体験した人がより感じるようにするため、ARを活用した[ゲーミフィケーション](https://www.youtube.com/watch?v=IXjtRpQKtsI)要素を企画に取り入れました。
私が開発したMVPの全体の流れを動画にしたので、ぜひご覧ください！(0.75倍速推奨)
リリースまでは至っておらず、あくまでもMVPであることをご了承ください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/WVW-WBDzyF0?si=qD9Yadt14TehdPpy" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 今回のゴール

ビジュアルやその他の要素(背景の風船の出現)を除いて、`InputField`に入力された文字数で呼び出されるモーダルを変更します。今回は2種類のモーダルを変えることで、`InputField`の文字数が次のシーンへ進むのに値するかしていないかを判断できるようにしたいと思います。

![TrueBalloon_PT-PT.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/efd72339-bd94-c462-92e1-3e8deddd84fe.gif)


# 環境

#### OS
Windows11
#### Unityバージョン
Unity - 2022.3.49fi
#### テストデバイス
Google Pixel 6(Android 12)

:::note warn
Unity上のSimulatorは実装されていないのでGoogle Pixel 5で行っています
:::

# 実装
## 準備
All templates から `3D` を選び、プロジェクトを作成しましょう。

![スクリーンショット 2024-11-08 135730.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/b0d00631-acf3-c7cd-c7d6-6aa07ccca736.png)

プロジェクトの作成が完了したら、日本語を追加します。必要なければ飛ばしてください。
下記の`japanese_full.txt`のview rawを押します。書かれているものをCtrl + a で全選択し、Ctrl + cでコピーしましょう。

https://gist.github.com/kgsi/ed2f1c5696a2211c1fd1e1e198c96ee4

既存のフォントは日本語に対応していないため、対応している好みのフォントをネットから拾ってきてください。
フォントは`Project`内の`Assets`→`TextMeshPro`→`Fonts`あたりにドラッグ＆ドロップして追加してください。
次に、画面上部の`Window`から`TextMeshPro`内の`Font AssetCreator`を選択します。
`Source Font File`でフォントを選択して、先ほどコピーしておいた日本語を`Custom Character List`にCtrl + vで張り付けましょう。あとは`Generate Font Atlas`を押して長めのダウンロードの後に`Save as...`を選択したら完了です。

![スクリーンショット 2024-11-08 150142.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/2d815e6b-f017-fd51-48a3-4ee9f2814c3f.png)

### UnityScreenNavigatorのダウンロード
メインであるモーダルを呼び出すための画面遷移OSSライブラリ「Unity Screen Navigator」をダウンロードしましょう。
一度READMEを読むことをおすすめしますが、読まなくても今回行いたいことは全て出来るようにするので大丈夫です。

https://github.com/Haruma-K/UnityScreenNavigator

### 画面UIの用意
#### InputField
シーン内に必要なオブジェクトを配置していきます。
`Hierarchy`を右クリック、`UI`→`Input Field - TextMeshPro`を選択すると`Hierarchy`に`InputField(TMP)`が追加されます。`Canvas`のサイズに合わせて好きに拡大しましょう。
見た目の変更は、`InputField(TMP)`の`Inspector`にある`Image`から行えます。
中央の文字は、`Placeholder`の`Inspector`にある'TextMeshPro - Text(UI)'の`Text Input`から変更できます。
![スクリーンショット 2024-11-08 143041.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/a9e06b82-ee18-b03f-6610-2207b181ceaf.png)

`InputField`には以下の写真のような調整がしてあります。内容としては、日本語対応させたフォントへの変更(`Font Asset`)、入力可能な文字数の制限(`Character Limit`)、`Content Type`をCustom、`Line Type`をMulti line Newlineにしました。細かな説明は省きますが、これによりスマートフォンでの文字入力操作に対応した設定が完了となりました。

![スクリーンショット 2024-11-08 143102.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/22ea5f63-5d61-bc48-1206-4ede942498e2.png)

#### Bottun
モーダルを呼び出すための必要なUIになります。一旦このままで放置。

![スクリーンショット 2024-11-08 155808.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/47b41173-db29-0706-175e-af5aca951da7.png)

#### ModalContainer

空のオブジェクトを`Hierarchy`上に作成して`Inspector`の下部分にある`Add Component`から**ModalContainerスクリプト**を加えてください。

:::note warn
注意
ModalContainerオブジェクトはCanvas内のHierarchyの一番下に配置しましょう！
表示させるModalPrefabが既に配置されているオブジェクトに隠れてしまいます！
![スクリーンショット 2024-11-11 150617.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/d5efc76d-6a49-29b0-b926-087c88225493.png)
![スクリーンショット 2024-11-11 150601.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/98bd40b8-d589-9a72-29e3-446a65f8ad4b.png)

:::

### ModalPrefabの作成

今回は2種類のモーダルを文字数に応じて出し分けたいため、2つのModalPrefabを作成します。
Prefabの内容は基本的に変わらないので作成自体は難しくないですが、画面UIを用意したシーンで行うと`Hierarchy`の管理が面倒くさいので別のシーンで作成することをおすすめします。

文字数が足りないときに呼び出される`Modal1`と先へ進めるときに呼び出される`Modal2`を作りました。
特徴としては、役割の違うボタンUIが2~3つ配置してあります。スクリプトの作成で紹介するボタンに役割を与える**OptionSelectorModalスクリプト**が`Hierarchy`で一番上の親オブジェクトに付いています。一つ下のオブジェクトには、このPrefabをモーダルにするための**Modalスクリプト**が付いています。UIオブジェクトたちが子オブジェクトになる階層であれば完了です。

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/51259fee-1467-c957-9fab-3c6579e3c51a.png)

作成したモーダルは`Hierarchy`からAssets内のフォルダにドラッグ＆ドロップしてPrefabにしましょう。
シーン内に残っているPrefab化したオブジェクトは、`Hierarchy`から削除しても構いません。

:::note warn
注意
今回は簡易的にモーダルの呼び出しを行いたいため、シンプルで分かりやすい**Resources**フォルダ内に格納したモーダルを呼び出すことにしました。
:::

## スクリプトの作成

モーダルの呼び出すために自作するスクリプトは以下の2つです。

:::note info
こまめなデバッグをおすすめします。
:::

### OptionSelectorModalスクリプト
前述したように、モーダル内のボタンに役割を与えるスクリプトです。以下のブログを参考に作成しました。
ブログで作成したものと違う点としては、YesとNoのほかにExitとOKを加えたことです。ただ、今回のゴールではNoボタンにしか役割を与えていません。ここで与えられた役割を呼び出された先で実行します。

https://qiita.com/Miyakovsky/items/6e111de23ceaabd86e2f#%E5%AE%9F%E8%A3%85

```php:OptionSelectorModal
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using UnityScreenNavigator.Runtime.Core.Modal;

public class OptionSelectorModal : Modal
{
    [SerializeField]
    private Button _yesButton = null;

    [SerializeField]
    private Button _noButton = null;

    [SerializeField]
    private Button _exitButton = null;

    [SerializeField]
    private Button _okButton = null;

    public Button.ButtonClickedEvent OnClickYesButton => _yesButton.onClick;
    public Button.ButtonClickedEvent OnClickNoButton => _noButton.onClick;
    public Button.ButtonClickedEvent OnClickExitButton => _exitButton.onClick;
    public Button.ButtonClickedEvent OnClickOkButton => _okButton.onClick;

    private void Awake()
    {
        // 各ボタンにイベントを追加
        _yesButton.onClick.AddListener(OnYesButtonClicked);
        _noButton.onClick.AddListener(OnNoButtonClicked);
        _exitButton.onClick.AddListener(OnExitButtonClicked);
        _okButton.onClick.AddListener(OnOkButtonClicked);
    }

    private void OnDestroy()
    {
        // モーダル破棄時にイベントリスナーを削除
        _yesButton.onClick.RemoveListener(OnYesButtonClicked);
        _noButton.onClick.RemoveListener(OnNoButtonClicked);
        _exitButton.onClick.RemoveListener(OnExitButtonClicked);
        _okButton.onClick.RemoveListener(OnOkButtonClicked);
    }

    private void OnYesButtonClicked()
    {
        SceneManager.LoadScene("sceneName");
    }

    private void OnNoButtonClicked()
    {
        ModalContainer.Find("ModalContainer").Pop(true);
    }

    private void OnExitButtonClicked()
    {
        SceneManager.LoadScene("sceneName");
    }

    private void OnOkButtonClicked()
    {
        SceneManager.LoadScene("sceneName");
    }
}

}
```
これにより、`Inspector`上でモーダル内のボタンを参照させることが出来るようになりました。

### SceneControllerスクリプト

**SceneController**スクリプトでは、元のシーンにあるボタンUIを押すと現在のInpuField内の文字数に応じて決められたモーダルが呼び出される機能を作成します。

**using UnityScreenNavigator.Runtime.Core.Modal;** と **private ModalContainer _modalContainer = null;** がないとモーダルを呼び出す機能が使えないので**絶対に**忘れないでください。
また、**using TMPro;** も忘れるとTextMeshProを使えないので忘れないようにしましょう。
呼び出すモーダルを文字数で切り替えたいので保存する変数を加えます。
```php:SceneController
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityScreenNavigator.Runtime.Core.Modal;

public class SceneController : MonoBehaviour
{
    [SerializeField]
    private ModalContainer _modalContainer = null;  
    public GameObject inputGameObject;
    public Button button;
    public TMP_InputField inputField;

    // テキストの文字数を保存する変数
    private int textLength;

    // シングルトンインスタンスの設定
    public static SceneController Instance { get; private set; }

```

`Start`メソッドでは、シーン開始時にModalContainerを取得し、textLengthを初期化させます。
理由としては、残ったデータが悪影響を及ぼさず、正常な状態を保つためです。

```php:SceneController
private void Awake()
    {
        // インスタンスのセットアップ。既にある場合は削除
        if (Instance == null) Instance = this;
        else Destroy(gameObject);
    }

    private void Start()
    {
        // モーダルコンテナを取得し、初期化
        _modalContainer = ModalContainer.Find("ModalContainer");
        textLength = 0;
    }

```

textLengthを管理しているメソッドです。これらの管理により、textLengthの値が様々なメソッドで扱われることを可能としています。

```php:SceneController

 // テキスト量を設定するメソッド
    public void SetTextLength(int length) => textLength = length;

    // テキスト量を取得するメソッド
    public int GetTextLength() => textLength;

    // テキスト量をリセットするメソッド
    public void ResetTextLength() => textLength = 0;
```

文字数に応じたモーダルを呼び出すためのボタンUIを制御するメソッドです。コルーチンにより表示するモーダルを制御します。もしコンソールにエラーが表示されていたなら、ほとんどがこのメソッドからです。

```php:SceneController
// NextButtonクリック時に呼ばれる
    public void OnClick_NextButton()
    {
        StartCoroutine(NextButton());
    }

    // 入力フィールドの文字数に応じてモーダルを表示するコルーチン
    private IEnumerator NextButton()
    {
        string inputText = inputField.text;  // 入力フィールドからテキストを取得
        textLength = inputText.Length;       // テキストの長さを取得

        // テキストの長さに応じて表示するモーダルを変更
        if (textLength >= 6 && textLength < 100)
        {
            // 6文字以上で100文字未満の場合にModal2を表示
            yield return ShowModalWithLoading("Modal2");
        }
        else if (textLength >= 0 && textLength < 6)
        {
            // 0文字以上で6文字未満の場合にModal3を表示
            PlaySound(checkSound);
            yield return ShowModalWithLoading("Modal1");
        }
    }

    // モーダルを表示するコルーチン
    private IEnumerator ShowModalWithLoading(string modalName)
    {
        // モーダルを非同期で表示し、完了を待機
        var handle = _modalContainer.Push(modalName, true, onLoad: x =>
        {
            var OptionSelectorModal = (OptionSelectorModal)x.modal;
        });

        // モーダルが閉じるまで待機
        yield return handle;
    }
 
```

最後に全体図

```php:SceneController
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityScreenNavigator.Runtime.Core.Modal;

public class SceneController : MonoBehaviour
{
    [SerializeField]
    private ModalContainer _modalContainer = null;  
    public GameObject inputGameObject;
    public Button button;
    public TMP_InputField inputField;

    // テキストの文字数を保存する変数
    private int textLength;

    // シングルトンインスタンスの設定
    public static SceneController Instance { get; private set; }

    private void Awake()
    {
        // インスタンスのセットアップ。既にある場合は削除
        if (Instance == null) Instance = this;
        else Destroy(gameObject);
    }

    private void Start()
    {
        // モーダルコンテナを取得し、初期化
        _modalContainer = ModalContainer.Find("ModalContainer");
        textLength = 0;
    }

    // テキスト量を設定するメソッド
    public void SetTextLength(int length) => textLength = length;

    // テキスト量を取得するメソッド
    public int GetTextLength() => textLength;

    // テキスト量をリセットするメソッド
    public void ResetTextLength() => textLength = 0;

    // NextButtonクリック時に呼ばれる
    public void OnClick_NextButton()
    {
        StartCoroutine(NextButton());
    }

    // 入力フィールドの文字数に応じてモーダルを表示するコルーチン
    private IEnumerator NextButton()
    {
        string inputText = inputField.text;  // 入力フィールドからテキストを取得
        textLength = inputText.Length;       // テキストの長さを取得

        // テキストの長さに応じて表示するモーダルを変更
        if (textLength >= 6 && textLength < 100)
        {
            // 6文字以上で100文字未満の場合にModal2を表示
            yield return ShowModalWithLoading("Modal2");
        }
        else if (textLength >= 0 && textLength < 6)
        {
            // 0文字以上で6文字未満の場合にModal3を表示
            PlaySound(checkSound);
            yield return ShowModalWithLoading("Modal3");
        }
    }

    // モーダルを表示するコルーチン
    private IEnumerator ShowModalWithLoading(string modalName)
    {
        // モーダルを非同期で表示し、完了を待機
        var handle = _modalContainer.Push(modalName, true, onLoad: x =>
        {
            var OptionSelectorModal = (OptionSelectorModal)x.modal;
        });

        // モーダルが閉じるまで待機
        yield return handle;
    }
}

```
# 実行
最後に**SceneController**スクリプトを元のシーンのボタンUIにドラッグ＆ドロップすれば動画と同じ実行が出来ていると思います。
もし、コンソールにエラーが表示されているのなら、その原因のほとんどがアタッチしたスクリプトの場所が違うかインスタンスが正しく参照されていないかのどちらかなので、確認してみてください。

![TrueBalloon_PT-PT.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/efd72339-bd94-c462-92e1-3e8deddd84fe.gif)

# まとめ

この記事では、テキストデータの文字数によって呼び出すモーダルを切り替えるところまでですが、実際の開発では先ほどの**SceneController**スクリプトをさらに改良して文字数によって呼び出すモーダルの追加とシーン移動時のローディング処理と特定の言葉が入力された時の特殊処理も行っております。

![TrueBalloon_PT01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/ad96debb-faca-f14a-89ab-bcd7bf8434e7.gif)

第2回、第3回の記事も合わせてご覧ください


https://qiita.com/Hikari_Fukazawa/private/c8595e35a681521b3a4f

https://qiita.com/Hikari_Fukazawa/private/5390f9d25f486df5909e