技術記事

Unity中規模開発でMVVMを選ぶ理由 — R3とExtenjectで現実的に組む

yoritech編集部2026年5月2日6分で読める
UnityMVVMR3UniRxExtenjectZenjectReactivePropertyC#

Unity中規模開発でMVVMを選ぶ理由 — R3とExtenjectで現実的に組む

Unityでデザインパターンが必要になる瞬間

Unityはプロトタイプを動かすまでの距離が短い。MonoBehaviour 1枚に Update / OnTriggerEnter / UI イベントを書けば、それだけで何かが画面で動く。問題は『動いてしまう』ことだ。コードは書いた量に比例して肥大化し、UI と状態とゲームロジックが同じクラスの中で混ざる。

そこから 3〜6ヶ月運用すると、UI を差し替えたいだけなのにロジックを壊し、テストしたいだけなのに PlayMode を起動しないと検証できず、リファクタしたいが依存関係が追えない、という詰みに入る。デザインパターンを検討する瞬間は『機能追加にかかる時間が、最初の1ヶ月より明らかに長くなった』と感じた時だ。逆に言えば、それより前にやると過剰になりやすい。

クリーンアーキテクチャやオニオンパターンが万能ではない理由

中堅以上のエンジニアなら、こう考えるのは自然だ。「クリーンアーキテクチャを入れて、UseCase 層と Repository 層を分離し、依存方向を外側から内側へ揃えればいい」。理屈としては正しい。しかし Unity の中小規模、特にチーム3人以下・運用1〜2年のレンジでは、これが裏目に出やすい。

問題は層数とコスト感の不一致だ。Entity / UseCase / Interface Adapter / Framework の4層を真面目に守ると、ボタン1つ押した時の値変更ですら、ViewModel 相当 → UseCase → Repository → Entity と4ファイル触ることになる。MonoBehaviour と Pure C# 層をまたぐためのインターフェイスとマッパーも増える。ROI が割れるのは、チームが小さくドメインルールがそこまで複雑でないケースだ。

『使う/使わない』ではなく『どこまで持ち込むか』を判断する観点として、(1) コードベースの規模、(2) 運用予定期間、(3) チーム人数とローテーション頻度、の3つを置くといい。これらが小さいうちは、層を減らしても破綻しにくい。

MVVMをUnityで採用する理由

弊社が中小規模Unity案件で多く採用しているのが MVVM パターンだ。Model / View / ViewModel の3層構成で、責務はこう分ける。

  • Model: ドメインデータと業務ルール。Unity 非依存の Pure C#。
  • ViewModel: View に出すための状態とコマンドを保持。Model を操作するが UI を直接触らない。
  • View: MonoBehaviour 側。ViewModel を観測して UI に反映するだけ。入力イベントは ViewModel に流す。

クリーンアーキテクチャと比べると層が1つ少なく、ファイル数の増加が穏やかだ。それでいて『UI とロジックの分離』『密結合の削減』『MonoBehaviour 非依存のテスタビリティ』という効きの大きい3つの効果が手に入る。

特にテスタビリティは大きい。Model と ViewModel を Pure C# に保てば、PlayMode を立ち上げずに NUnit のエディタテストで状態遷移をユニットテストできる。バグの相当数はロジック層に潜むので、これだけで早期検出の網が広がる。View 側はそのうえで PlayMode テストや手動 QA に回せばいい。

抽象度が『ちょうど良い』のも採用理由だ。MVP や MVC でも似た分離はできるが、状態を Observable として公開する MVVM は、Unity の UI 更新(毎フレーム描画ではなく値変更時だけ反応したい)と相性が良い。

R3とExtenjectでMVVMを実装する

具体ライブラリは『R3(旧 UniRx の後継)』と『Extenject(Zenject のメンテナンス継続フォーク、Unity 向け DI コンテナ)』を採用する。

R3 は UniRx の設計思想を継承しつつ、Subject 周りの内部構造を ImmutableArray ベースから刷新し、ゲームのような高頻度購読でも性能が出るよう再実装された Reactive Extensions 実装だ。バックプレッシャーは責務外として IAsyncEnumerable / System.Threading.Channels 側に委ねる方針を取る。ViewModel の状態は ReactiveProperty として公開し、View が Subscribe して UI に反映する。

public sealed class CounterViewModel
{
    public ReactiveProperty<int> Count { get; } = new(0);
    public void Increment() => Count.Value++;
}

View 側はこの ReactiveProperty を購読してテキストへ流すだけだ。値が変わった瞬間にしか UI 更新が走らないので、Update でのポーリングが消える。

Extenject は Unity 向けの DIコンテナで、SceneContext / ProjectContext / GameObjectContext というライフタイム単位を持つ。ProjectContext はアプリ全体で1つ、SceneContext はシーン単位、GameObjectContext は Prefab 単位、と切り分けると依存解決が素直になる。

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<CounterViewModel>().AsSingle();
    }
}

これで ViewModel をコンストラクタ注入でき、View 側は [Inject] で受け取るだけで Model や ViewModel を握れる。テストでは Container をモック差し替えに使う、という王道の流れが Unity 上で再現できる。

採用判断のラインを置くならこうだ。プロジェクトが3ヶ月以下の使い捨てなら、MVVM すら導入せず素朴に書いた方が速い。複数年運用・複数人チーム・ドメインが厚いなら、クリーンアーキテクチャまで引き上げる選択肢が出てくる。その間 — Unity 案件で最も多いゾーン — では、MVVM+R3+Extenject が現実的な落とし所になる。

『既存プロジェクトに後付けでMVVMを入れたい』『設計レビューだけ第三者に見てほしい』など、Unity の C# 実装まわりで足が止まっているなら、yoritech の Unity アプリ開発代行サービスでは成果物単位での実装請負に加えて、こうした段階的な設計の置き換えもご相談に乗れる。

この課題、yoritechが解決します

まずはお気軽にご相談ください。貴社の課題に合わせた最適なプランをご提案します。

まとめてお問い合わせ