MVPパターンをUnityエディタ上で実装する仕組みを作ってみた

## はじめに

記事執筆時点では来週になりますが、個人制作ゲーム「**求む！洞窟探検家**」が 2024/10/17 リリースされます。

このゲームのUIは、Unityエディタでコンポーネントをポチポチ追加する形で実装しました。
少しユニークだと思いますので、この記事ではどのような機能を用意したのかと、その使い方を紹介します。今回は機能の実装部分については触れません。

## 動機

リリースしたスマホアプリは、コードの実装を変えて反映させる前に Appleさん、Googleさんの審査が必要になります。特にUIはあちこちの画面ごとに実装が必要で、少し変えたいということも多いです。これを、できるだけノーコードで、かつ、わかりやすく変更できるようにしたいと思いました。

## ライフゲージの例

![スクリーンショット 2024-10-12 14.35.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/532e12ef-debf-86ed-bfc6-29b05bf62729.png)
よくあるようなライフゲージです。このゲームは探検中の状態でセーブ＆復帰ができるため、ライフもセーブされます。

UIのイメージなどを配置し終わったら、まずMVPの P にあたる、**SaveDataPresenter**コンポーネントを適当なGameObjectにアタッチします。

![スクリーンショット 2024-10-12 15.11.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/d72fbe3b-8ee6-01be-15dc-3b6cbc1f074e.png)
ここでは詳しく触れませんが、このゲームのセーブデータは、テーブルデータを階層構造で複数保持し、実行中はその変更を監視できるリアクティブなデータ構造になっています。

SaveDataPresenterは、セーブデータの Data Path に記述したパスにあたるテーブルのColumn名のインデックス番目の値を監視し、その変更を Notify To で指定したビューへ通知します。

この例では、Player/Physical の life列 0番目 の変更を LifeGauge へ通知します。

![スクリーンショット 2024-10-12 15.23.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/128ab5ac-1f05-d988-196a-2ba0400e49d6.png)
LifeGauge には、MVPパターンの V にあたる、**ViewPositionFloat**コンポーネントをアタッチします。 
ViewPositionFloatは、float値が通知されると、アタッチしているGameObjectのRectTransformの位置を変更します。また、ゲームによくあるゲージがゆっくり伸縮するようなアニメーションもこのコンポーネントが処理します。

この例では、Rectの右側の位置を、通知された値の3.2倍に設定しています。

![life.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/291ea672-1ea6-8eb7-c81d-b26acddcb1a0.gif)
これだけで、どこかでダメージを受けても、何かで回復したりしても、アニメーション付きで正しいゲージが表示されます。

## ボリューム調整の例

![スクリーンショット 2024-10-12 16.17.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/eccf6338-be08-9d66-856f-45861b3fd494.png)
設定画面のスライダーでボリュームを調整します。

![スクリーンショット 2024-10-12 16.19.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/443b3dd5-9a9a-e0ef-7e73-ff820c6183d1.png)
このゲームでは、ボリューム調整など端末によって変えたいような設定は、セーブデータとは別に PlayerPrefs を使って保存しています。

先ほどと似ていますが、今回は **PlayerPrefPresenter**を使います。

この例では、volume_bgm というキーの値を SliderBGM へ通知します。

![スクリーンショット 2024-10-12 16.24.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/de831ae4-fb18-967d-d262-30c07596e30f.png)
Slider BGM には、**ViewSelectableInt**コンポーネントと、Sliderコンポーネントをアタッチしています。

ViewSelectableIntは、通知されたint値を、同じGameObjectにアタッチされているSelectable（Sliderの基本クラス）コンポーネントの値として設定します。

これにより、PlayerPrefsに保存されているボリュームの値をスライダーに反映させることができます。

さらに、PlayerPrefPresenter（と各PlayerPrefs値を管理するクラス）は、操作された値の通知を受け取る機能があり、**いまの設定だけで自動的に**スライダーの操作がPlayerPrefsへ反映されるようになります。

## その他の機能

![スクリーンショット 2024-10-12 16.51.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/9c6d7a58-a966-5572-6cd0-3babc74d79c3.png)
この他にも、通知されたstring値からアドレスを生成して非同期でリソースを読み込み、Imageコンポーネントの画像へ反映させる **ViewGraphicString** や、

![スクリーンショット 2024-10-12 16.55.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/30678a35-51b8-9be8-5a64-2aa7faf2cb97.png)
通知されたstring値をキーとして、テーブルから指定列の値を検索してビューへ通知する **OperatorKeyToFloat** や、

![スクリーンショット 2024-10-12 16.54.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/4c2cffb1-943c-b7c2-483e-c1cb86488b45.png)
通知された値に単純な計算を（複数段）加えてビューへ通知する **OperatorCalcFloat** や、

![スクリーンショット 2024-10-12 17.01.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/3e91cd5a-91a6-9890-7484-9b6f336a716c.png)
![スクリーンショット 2024-10-12 17.12.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530151/8532fb49-1ef4-595b-a293-d82bdb5c9ec2.png)
もっと複雑な計算を VisualScript で行う、**OperatorScriptFloat** なども用意しました。

## まとめ

特殊な計算が必要な場合は、C#コードで書いたメソッドをVisualScripting経由で呼び出すなどする場合もありましたが、ほとんどはこれらの組み合わせで実装することができました。

執筆時点ではリリース前ですが、これだけノーコードで実装していれば、アセットだけで柔軟に更新できるはずです（バグがなければ😅）

また、自分は個人で制作しましたが、単純なものは簡単に実装できるので、デザイナさんやプランナさんが実装するというチーム運用もできるかもしれません。

## 宣伝

〜 私たちと洞窟調査をしませんか？ 〜

『求む！洞窟探検家』は、シンプル操作の新体験2.8Dアドベンチャーです。
スマホ向けに App Store、Google Play Store で 10/17 リリースです！

公式サイト：https://sites.google.com/view/wanted-cave-explorer/

https://gamewith.jp/gamedb/article/game/show/12063/36893?from=ios

https://gamebiz.jp/news/393807