序文
どうもKutoです。
本記事はQiita Advent Calender2025 Unity の21日目の記事です。
今回は自分のプロジェクトで組んでみたカスタムMVPアーキテクチャの紹介をしたいと思います。
MVPアーキテクチャとは?
詳しくはこちらを見て頂きたいですが、簡単に言うと、UIに関連するコード(View)、データロジックに関連するコード(Model)、それらを仲介するコード(Presenter)の3層にコードを分けて実装するアーキテクチャです。
このアーキテクチャを扱うメリットとしては色々ありますが、大きくはUnityに依存するUI関連コードとゲームロジックが分離され、依存関係が明確なコードを書きやすいということがあります。
さてそんなMVPアーキテクチャですが、実際にはMVPアーキテクチャの中でもその実装方法やレイヤーの分け方に様々な流派があります。
例えば前述したUnity公式が出している書籍ではMVP全てをMonoBehaviorとし、またViewはSlider等の画面の構成部品一つ一つを指しています。
他に主流なMVPアーキテクチャとしてあるのが、MV(R)Pアーキテクチャ。
これはViewとModelの依存関係をUniRx等のオブザーバーパターンを用いることで一方向にし、拡張性を高めたものです。
そんなMVPアーキテクチャですが、今回ゲームを開発するにあたり一からMVPアーキテクチャを構築したので、まとめてみようと思います。
カスタムMVPアーキテクチャの概要
以下に作成したアーキテクチャの概要図を示します。
先ほど載せたMVPアーキテクチャの図に比べて、Modelの周りにRepositoryと付いたものがゴチャゴチャと付いているのが分かると思います。
AssetRepositoryは単にAddressableのラッパー的に使用しているUtilityクラスなのであまり気にしなくて良く、ModelRepositoryが今回の主役となります。
このModelRepositoryはModelの管理クラスとして実装しているのですが、まず簡単に作成するゲームを概要を示した後、その設計意図や詳細な実装について解説したいと思います。
ゲーム仕様
今回作成するゲームは、簡単に言うと弾幕ゲームになります。東方の弾幕シューティングゲームをイメージして頂けると近いかなと思います。
インゲームとして弾幕シューティングがあり、アウトゲームとしてタイトル画面やステージ選択画面、シナリオ画面などがある構成です。
またゲームは全てオフライン環境を前提しています。
よってデータはサーバー等から配信せず、全てローカルにScriptableObjectとして持って管理することにしました。
アーキテクチャの解説
アーキテクチャの選定
Unityで扱われるアーキテクチャは様々です。ざっと並べても以下のようなものがあるでしょう。
- MVP
- MVVM
- レイヤードアーキテクチャ
- オニオンアーキテクチャ
よって開発当初、この中からどのアーキテクチャを採用するかを考える必要がありました。
アーキテクチャを考える際の要素としては色々なものがあると思いますが、今回は主に以下を踏まえMVPアーキテクチャを採用しました。
- メンバー
- 少人数なので割と作業範囲は完全に分かれる
- Unityに熟練している訳ではない
- ゲームスケール
- そこまで大きいゲームでもない
- 仕様変更もあまり想定されない
少人数でクリーンアーキテクチャ的な設計は余りにも過剰ですからね。程々のアーキテクチャであるMVPがちょうど良いなと。
Modelの設計
MVPで実装することに決めた後、次にModelの管理方法について考えることにしました。
まず今回のゲームで考えられそうなER図をざっくりと書き出し、Modelがどのような処理や特性を持ち得るかを考えました。
今回の場合考えられるのは上記のようなものでしょう。
Enemyに関してはゲーム中に複数が同時に存在するため、複数インスタンスの高速なOnOffに対応する必要があります。
そのためModelの初期化処理(ScriptableObject等から必要なデータをロードし、必要なら整形する)をModelのコンストラクタに置くのではなく、別途Factoryクラスを使用して管理する必要がありました。
そこで以下のModelRepositoryを用意することにしました。
public class HogeModelRepository
{
public HogeModelRepository Instance { get; } = new();
HogeModel hogeModel;
public HogeModel Get()
{
hogeModel ??= new();
return HogeModel;
}
}
上記はざっくりとしたModelRepositoryの例です。単純なModelなら上記のように実装し、Enemy等プール処理等の特別な処理が必要であればこのクラスに処理を追加する想定です。
こうすることで初期化処理をカプセル化することもでき、Modelがデータロジックだけに責任を持つことが出来ます。
Enemyのような特殊処理がない場合でも、単一責任の原則に則したコードになり可読性や拡張性が高まるため、これはかなり大きいメリットです。
またPresenterはこのModelRepositoryに対してModelのインスタンスを要求します。
これによりModelのインスタンスが特定のPresenterに依存せず、複数のPresenterから同一のModelを弄ることが出来るようになりました。
ModelRepositoryをstaticにしているのは判断が別れるところかなと思います。
DI系ライブラリ(Zenjectなど)を利用することも検討はしたのですが、メンバーがUnityに慣れていない中でライブラリを多く入れたくなかった、そこまでModelは多く無いため、staticでも問題は生じないと考えていた、という理由で避けました。
実際今回においてはそこまで間違いでもなかったかなと思います。
まとめ
雑になってしまいましたが、以上が解説となります。
残りのView, Presenterは特に変なことはしていないので、他のMVPの資料を参照頂ければと思います。
小規模ゲームでDIを使わずにModelを管理する場合は割と使いやすい手法なのかなと思っていますので、是非参考にして頂ければ幸いです