はじめに
普段は企画職を中心に学んでいる私が、株式会社GENEROSITY様での臨地実務実習で行わせていただいた、XR技術を使ったサービスの企画発案から価値検証モデル(Minimum Viable Product, 通称:MVP)の開発内容を全3回に分けて記事にさせていただきます。
記念すべき第1回では、3Dシーンで行ったメイン体験のコアとなる要素を抜粋して説明していきたいと思います。
技術ブログの作成自体も初めての経験なので、拙い点は広い心と温かい目で見守ってくださると幸いです。
目次
概要
今回のゴール
環境
実装
準備
スクリプトの作成
実行
まとめ
概要
私は「他人に言いにくい本音を抱えているが、誰にも見られたくない」といった課題を解決するために、本音を一人で解消するためのスマホ向けアプリ「本音バルーン」を作成いたしました。
視覚的なカタルシス(爽快感やストレスの解消)を体験した人がより感じるようにするため、ARを活用したゲーミフィケーション要素を企画に取り入れました。
私が開発したMVPの全体の流れを動画にしたので、ぜひご覧ください!(0.75倍速推奨)
リリースまでは至っておらず、あくまでもMVPであることをご了承ください。
今回のゴール
ビジュアルやその他の要素(背景の風船の出現)を除いて、InputField
に入力された文字数で呼び出されるモーダルを変更します。今回は2種類のモーダルを変えることで、InputField
の文字数が次のシーンへ進むのに値するかしていないかを判断できるようにしたいと思います。
環境
OS
Windows11
Unityバージョン
Unity - 2022.3.49fi
テストデバイス
Google Pixel 6(Android 12)
Unity上のSimulatorは実装されていないのでGoogle Pixel 5で行っています
実装
準備
All templates から 3D
を選び、プロジェクトを作成しましょう。
プロジェクトの作成が完了したら、日本語を追加します。必要なければ飛ばしてください。
下記のjapanese_full.txt
のview rawを押します。書かれているものをCtrl + a で全選択し、Ctrl + cでコピーしましょう。
既存のフォントは日本語に対応していないため、対応している好みのフォントをネットから拾ってきてください。
フォントはProject
内のAssets
→TextMeshPro
→Fonts
あたりにドラッグ&ドロップして追加してください。
次に、画面上部のWindow
からTextMeshPro
内のFont AssetCreator
を選択します。
Source Font File
でフォントを選択して、先ほどコピーしておいた日本語をCustom Character List
にCtrl + vで張り付けましょう。あとはGenerate Font Atlas
を押して長めのダウンロードの後にSave as...
を選択したら完了です。
UnityScreenNavigatorのダウンロード
メインであるモーダルを呼び出すための画面遷移OSSライブラリ「Unity Screen Navigator」をダウンロードしましょう。
一度READMEを読むことをおすすめしますが、読まなくても今回行いたいことは全て出来るようにするので大丈夫です。
画面UIの用意
InputField
シーン内に必要なオブジェクトを配置していきます。
Hierarchy
を右クリック、UI
→Input Field - TextMeshPro
を選択するとHierarchy
にInputField(TMP)
が追加されます。Canvas
のサイズに合わせて好きに拡大しましょう。
見た目の変更は、InputField(TMP)
のInspector
にあるImage
から行えます。
中央の文字は、Placeholder
のInspector
にある'TextMeshPro - Text(UI)'のText Input
から変更できます。
InputField
には以下の写真のような調整がしてあります。内容としては、日本語対応させたフォントへの変更(Font Asset
)、入力可能な文字数の制限(Character Limit
)、Content Type
をCustom、Line Type
をMulti line Newlineにしました。細かな説明は省きますが、これによりスマートフォンでの文字入力操作に対応した設定が完了となりました。
Bottun
モーダルを呼び出すための必要なUIになります。一旦このままで放置。
ModalContainer
空のオブジェクトをHierarchy
上に作成してInspector
の下部分にあるAdd Component
からModalContainerスクリプトを加えてください。
ModalPrefabの作成
今回は2種類のモーダルを文字数に応じて出し分けたいため、2つのModalPrefabを作成します。
Prefabの内容は基本的に変わらないので作成自体は難しくないですが、画面UIを用意したシーンで行うとHierarchy
の管理が面倒くさいので別のシーンで作成することをおすすめします。
文字数が足りないときに呼び出されるModal1
と先へ進めるときに呼び出されるModal2
を作りました。
特徴としては、役割の違うボタンUIが2~3つ配置してあります。スクリプトの作成で紹介するボタンに役割を与えるOptionSelectorModalスクリプトがHierarchy
で一番上の親オブジェクトに付いています。一つ下のオブジェクトには、このPrefabをモーダルにするためのModalスクリプトが付いています。UIオブジェクトたちが子オブジェクトになる階層であれば完了です。
作成したモーダルはHierarchy
からAssets内のフォルダにドラッグ&ドロップしてPrefabにしましょう。
シーン内に残っているPrefab化したオブジェクトは、Hierarchy
から削除しても構いません。
注意
今回は簡易的にモーダルの呼び出しを行いたいため、シンプルで分かりやすいResourcesフォルダ内に格納したモーダルを呼び出すことにしました。
スクリプトの作成
モーダルの呼び出すために自作するスクリプトは以下の2つです。
こまめなデバッグをおすすめします。
OptionSelectorModalスクリプト
前述したように、モーダル内のボタンに役割を与えるスクリプトです。以下のブログを参考に作成しました。
ブログで作成したものと違う点としては、YesとNoのほかにExitとOKを加えたことです。ただ、今回のゴールではNoボタンにしか役割を与えていません。ここで与えられた役割を呼び出された先で実行します。
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を使えないので忘れないようにしましょう。
呼び出すモーダルを文字数で切り替えたいので保存する変数を加えます。
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を初期化させます。
理由としては、残ったデータが悪影響を及ぼさず、正常な状態を保つためです。
private void Awake()
{
// インスタンスのセットアップ。既にある場合は削除
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
private void Start()
{
// モーダルコンテナを取得し、初期化
_modalContainer = ModalContainer.Find("ModalContainer");
textLength = 0;
}
textLengthを管理しているメソッドです。これらの管理により、textLengthの値が様々なメソッドで扱われることを可能としています。
// テキスト量を設定するメソッド
public void SetTextLength(int length) => textLength = length;
// テキスト量を取得するメソッド
public int GetTextLength() => textLength;
// テキスト量をリセットするメソッド
public void ResetTextLength() => textLength = 0;
文字数に応じたモーダルを呼び出すためのボタンUIを制御するメソッドです。コルーチンにより表示するモーダルを制御します。もしコンソールにエラーが表示されていたなら、ほとんどがこのメソッドからです。
// 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;
}
最後に全体図
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にドラッグ&ドロップすれば動画と同じ実行が出来ていると思います。
もし、コンソールにエラーが表示されているのなら、その原因のほとんどがアタッチしたスクリプトの場所が違うかインスタンスが正しく参照されていないかのどちらかなので、確認してみてください。
まとめ
この記事では、テキストデータの文字数によって呼び出すモーダルを切り替えるところまでですが、実際の開発では先ほどのSceneControllerスクリプトをさらに改良して文字数によって呼び出すモーダルの追加とシーン移動時のローディング処理と特定の言葉が入力された時の特殊処理も行っております。
第2回、第3回の記事も合わせてご覧ください