yotiky Tech Blog

とあるエンジニアの備忘録

Unityと協調するためのアーキテクチャ『MVP4U』

これまでいくつかの記事でアーキテクチャデザインパターンを見てきた。Unity独自の事情なども徐々に咀嚼し、ある程度動きそうなアーキテクチャを開発チームにアウトプットできたので、ここらで共有しておこうと思う。まだ適用し始めた段階なので、今後いくつかのプロジェクトで試してみて内容を精査していければと考えている。

編集履歴

  • 2018/11/28 サンプルプロジェクトを掲載

目次

序章

UWP / Web / UnityのMVx

MVxはView を分離するためのアーキテクチャデザインパターン。いくつかのプラットフォームにおいてどういう構成となるか比較してみる。

f:id:yotiky:20181116001505p:plain

それぞれのViewを見ていくと、

UWP(WPF)は、XAMLというマークアップ言語の部分をViewとして捉えがちだが、Viewの裏に持つFrameworkとMVVM Frameworkが強力な土台となってなりたっている。ただ、実装者から見ると、XAMLとそれをカスタマイズする機能を実装すれば良い設計となっているので、主にXAML≒Viewとして扱われる事が多い。

Web(JavaScript)は、大きくフロントエンドとバックエンド(サーバーサイド)でMVxの構成を取る。その上でさらにフロントエンド内でもMVxを取る二重構成。
フロントエンドに注目した場合、HTML+CSSが極めてピュアなView(フロントヤード)、JavaScriptによる機能実装部分がView以外(バックヤード)ということができる。

Unityは、コンポーネント指向、つまり機能を部品として実装し、組み合わせることで全体を構成する設計思想を取っている。コンポーネント指向に対し、"Viewをどう捉えるか"が難しいという問題を持っている。

UnityのMVPを考える

UnityはUWP(WPF)とWeb(JavaScript)の中間のような構成になっている。
UWP(WPF)ほど実装者に露出する部分が高度に設計された作りになっていないし、そうするための強力なFrameworkも整っていない。また、Webのフロントヤードほど「ピュアなViewとそれ以外」のような設計がされているわけでもない。このため、「Unityのコンポーネント指向に歩み寄りつつ、如何にViewとして実装者が扱いやすい形を定義するか」が肝となる。

MVP4U (MVP for Unity)

f:id:yotiky:20181116002113p:plain

Unityでは、Rxを組み合わせてMVPを構築することが流行っており、PresenterはViewとModelを参照し、逆方向には戻り値やRxで依存関係を一方向に担保する。
MVP4Uでは、Viewの部分に「Componentを調和・調整する役目」として"FrontOrchestrator"を定義する。詳細は次の通り。

f:id:yotiky:20181116002130p:plain

Hierarchyでは、最上位にあたるRootと一階層目にParent、その配下にそれぞれ3DモデルなどのGameObjectが配置される。
RootはSceneに唯一のScenePresenterを持ち、ParentではViewとPresenterをそれぞれ1つ持つ。ScenePresenterには下位のPresenterをすべて登録し、エントリーポイントとしてPresenterの初期化とその順序を制御する役割を持つ。

MVP4UにおけるViewの目的は、「Componentを束ね、Presenterが必要とする情報、操作を公開する」こと。紐づくParent配下で、Script側から操作する必要があるComponentを参照する。自身に機能として処理は持たない。これを許容すると、Viewは使い勝手が良い立ち位置なので処理が集約してしまい、コードが肥大化することが予想される。機能は各Componentに持たせることで適切な責務を分担する。

FrontOrchestratorは、自身のComponentで処理が完結しないComponent同士を連携させたい時に調整する役割を持つ。
MVPを導入し始めた頃に、「Viewとして公開し、PresenterでView同士を紐づける」ことをやりがちだがこれが該当する。PresenterはModelとの橋渡しなので、View同士のコネクションのために無理にPresenterを経由する必要はないので、ここでその辺を吸収するわけだ。

XR

UWP/Web/Unity(XR)のView

f:id:yotiky:20181116002155p:plain

UnityのとくにXRアプリにおいては、MVx パターンが適用しにくい。なぜか。
他のアプリ(UWP/Web/uGUIなど)では"画面"という単位が存在し、画面遷移することでアプリのシナリオが進行する。この"画面"を単位としてViewを定義することで、ViewとViewの境目が明確になる。一方、XRには"空間"しか存在しない。1つの空間を区切る単位が存在しないため、無理に区切ろうとすると結局1つの空間が1つのViewになってしまう。

シナリオの進行

UnityはGame Loopでプログラムが駆動している。1 ループの一巡で、必要な処理が開始して終わるまでがFrameと言う単位になり、ループを繰り返してタイムラインを形成する。
f:id:yotiky:20181116002222p:plain
f:id:yotiky:20181116002226p:plain

アプリを作成するには、Game Loopとは別にアプリのタイムライン、シナリオを組み立ててそれを進行させる必要がある。また画面があるアプリでは、"画面のフレーム(枠)"に対し「状態を入れ替える」ことで遷移するイメージができるが、画面のないXRでは、空間に存在する物の「状態を変化させる」イメージが近いのかも知れない。

XRでの適用例

構成

MVP4UをXRの事情に合わせて適用する例を紹介する。

f:id:yotiky:20181116002321p:plain

シナリオの進行を管理する部分を、アプリのエンジンとして通常のMVP構成とは別けて考える。
シナリオの章となるChapter2を遷移する単位とし、ScenarioはChapterのつながりが書かれた台本となる。シナリオの進行役にはDirectorを配置する。ディクレターに渡す台本を替えると異なるストーリーが展開される、みたいな作り。状態をチャプターを跨いで共有したい場合は、シナリオの文脈ということで、SenarioContextに記録して共有する。
Hierarchyでは、RootにあたるSpaceと、ParentにあたるChapterを持つことになる。HierarchyのChapterはScenarioのChapterとほとんどの場合は1:1の関係である。

フロントヤードでは、Unityのお作法に近いかたちで実装していけば良い。
ComponentやFrontOrchestratorは、Startで初期化したり、gameobjectを直接いじったり、Observableが必要なら使えば良い。FrontOrchestratorがプレーンなクラスで実装できるならその方が良いが、必要があればMonoBehaviourを使っても良いだろう。フロントヤードで初期化の順序を制御したくなったらViewで纏めて調整することもできる。

Chapterの定義

HierarchyのChapterの配下には、各チャプターで登場する3Dモデルが定義される。
空間において同じ登場人物(3Dモデル)は唯一であり、「状態を入れ替える」のではなく「状態を変化させる」と考えると自然にも思える。また、 GameObjectを何かで区切ってそれを跨ぐことは、Unityに対して不自然となるため調和が乱れる。(コストがかかる)Chapterは、"同じ登場人物(3Dモデル)が被らないことを前提に区切れる単位"が最適解となりそう。

f:id:yotiky:20181116002436p:plain

エンジンとして定義したディレクターが、シナリオのチャプターを切り替えることで、アプリのストーリーが展開していく。

f:id:yotiky:20181116002453p:plain

Engineの初期化とScenarioの進行

このあたりはサンプルで実装した詳細なので参考程度に。
エンジン部分の初期化はエントリーポイントであるScenePresenterが全体初期化時に一緒に担当する。

f:id:yotiky:20181116002515p:plain

Scenarioの進行では、各Presenterと対になるUseCasesがScenarioDirectorに遷移を依頼。
ScenarioDirectorは、遷移実行前後のフックポイントを持っているので、それをUseCases / Presenterへと伝播させ、それぞれ必要な遷移前処理 / 遷移後処理を実行できるようになっている。

f:id:yotiky:20181116002529p:plain

遷移へのアプローチ

画面遷移の手段として、①1つのSceneで処理する方法、②Sceneを切り替える方法、③対象をPrefab化しInstantiateして入れ替える方法、を耳にする。
①は、これまでに説明してきたものとなる。②は、ロード時間やデータの共有の手間をペイできるほどのストーリーがまだ出てきていないので保留しているが、HierarchyのRoot直下に一層追加されて、バックヤードも対になる層を持たせてその層で新たなContextが共有されるようなイメージではある。③は、ステージを完全に分断(=3Dモデルが被らない)できるストーリーであれば、バックヤードはそのまま応用が効きそうだし、有効そうである。だが、こちらもまだ使うほどの場面には出くわしていない。

サンプルプロジェクト

構成

最後に参考までにサンプルで実装したアプリの構成を載せておく。
レイヤーの定義を決めかねているので、MVP+αな感じでパッケージだけ分けてある。UI以外は一旦Modelとしてまとめちゃってる状態。
ここではDIもテストも考慮していないので、疎結合のためのInterfaceは切っておらず、規約としてしか使っていない。(IViewくらい)
Model内にModelがあるが、サンプルではドメイン相当となる処理がほとんどないので使っていない。UIには、Sound系やAnimation系なんてのも出てくるのかなとぼんやり。

f:id:yotiky:20181116002544p:plain

github.com


  1. イベント関数の実行順 - Unity マニュアル

  2. サンプルではenumとして各章を定義した