Unity製アプリを作る時のアーキテクチャをどこまで区切るか、あるいはレイヤードアーキテクチャの一指針
はじめに
Unityでゲーム開発を行う際、アーキテクチャ設計は長期的な開発効率と保守性に大きく影響します。
もしUnity製アプリのコードベースをインゲームとアウトゲームに分けた時、後者のアウトゲーム側は同じイベント駆動型のWebやネイティブアプリで流行している設計パターンをUnityに輸入 することで、保守性を高めることができる。というアイデアに異論がある人は少ないと思います。
インゲーム、アウトゲームってなに?
(※一旦ここはイベント駆動のUI設計部をアウトゲーム、毎フレームのTickやUpdateにしたがって状態遷移する部分をインゲーム、とします)
こんな感じです。
-
アウトゲーム: メニュー画面、設定画面、ショップ画面など
- イベント駆動(ボタンクリック、画面遷移)
- WebアプリのUIパターンが適用しやすい
-
インゲーム: バトル画面、プレイヤー操作、ゲームロジック
- フレーム駆動(Update、FixedUpdate)
- Unity固有の設計パターンが必要
同様にViewとロジックはライフサイクルが同期していないので分離する(Viewの寿命はロジック、あるいはモデルより短い)について異論がある人はいないと思います。
そしてViewが直接Modelを扱うのではなく、Viewと寿命が同期しているPresenterを用意して、ModelとのやりとりやViewへの反映はPresenterが行う、という分け方をする、しますよね???
これがよく聞くMVP(Model-View-Presentation)パターンというやつで、ここに落ち着くことが多い と思います。
しかし、MVPパターンと言っても「Modelってどう作るの?」という最後の(?)問題が残されています。
今回は、UnityScreenNavigatorサンプルプロジェクトで採用されている「クリーンアーキテクチャに近い多層設計によるMVPパターン」と、より軽量な「Service+DIパターン」を大体示しておきます。
多層設計アプローチ(UnityScreenNavigatorサンプル)
大体これです。DIを使うかどうかはお好みで。
良い点
- どの層を今触っているか、という点に自覚的にコードが書ける
- asmdefを適切に切ることで循環参照を書けないように、というのがやりやすい
- 同種の設計に慣れているメンバーの認知負荷が低い
困る点
- ボイラープレート(冗長な記述)が増えてしまう
- 同種の設計に慣れていないメンバーの認知負荷が高い
Service+DIパターン
大体シーン、あるいはプロジェクト単位でServiceクラスがBindされていて、UI側が各地からServiceにアクセスする作り方です。
重要なこととして、別にこのパターンを使うとあなたの会社は赤字になって倒産する、とかこれを使うとあなたの給料が下がる、とかは無いです。
これはHogeManagerというclassがstaticで定義されていてUIやゲーム内のあちこちから参照されてる、という使い方や発想はそのままで、DIやMVPを組み込んだ形になります。
良い点
- 誰もが一度は通る(?)⚪︎⚪︎Manager方式と近いため理解しやすい
- 多層設計アプローチよりコード分量が少ない
困る点
- 本質的にstaticなGameManagerとおなじ(多くのゲームのSoundManagerはそれでいいかも)
- モデルレイヤーを使う側(UI含むView側)のコードをレビュー時に注視しないとひどい使い方をされやすい
多層設計アプローチで書くときのいくつかの指針
僕がリードをやってよい、というシチュエーションでは以下のような指針にする、かも。
かなり乱暴な代わりに、一定の秩序が保てると思います。
UseCaseの作成は必須にするの?
必須にします。簡単なものならRepository不要、とかUseCase不要っていうルールにすると簡単ってどこまで??って言われるので。
まあそれは半分冗談ですが、テストコードを書く時にUseCase層でシナリオテストを書く、という感じで指針を決められる点や、自分が詳しく無い領域のコードを読む時にUseCaseから辿る、という使い方ができるためです。
Presenterが参照できるモデルはどの層?Repositoryを読んでいい?
だめです。UIについてはPresenterは絶対にUseCase層経由でモデルにアクセスしてください。
つまり場合によってはRepositoryがreadとwriteをするだけのクラスで、UseCaseはRepositoryをラップしたreadとwriteをするだけ、というほぼコピペみたいなことになりますが、それでもUseCase層経由でモデルにアクセスしてください。
UseCaseとPresenter,Viewのn:m問題について
- PresenterとViewは1:1をルール付けします
- UseCaseとPresenterは1:nを許容します
- UseCaseとRepositoryはn:mを許容します
美しいビジネスアプリケーションは1:1で全部書けるかもしれませんが、大体のUnity案件はこのくらいの指針が良いと思います。(UseCaseとRepositoryは1:nで良いことも多いです)
ModelとRepositoryとUseCase全部書くの面倒臭いしServiceクラスを作っていい?
だめです!!!簡単なやつでもダメです!!!
ギリギリ許されるのは、あなただけのブランチ、あるいはFeatureFlag内でProductionCodeに入っていないものだけです。
(プロジェクトによっては許容すべきだと思います)
どちらのアプローチを選ぶの?
一般論としては、こんな感じです。が、正直最近はiOSやAndroidのアプリなどを触る時もUseCase層やRepositoryがある感じのプロジェクトが多いので、それに合わせる意味でも多層設計を選んでみるのが良いんじゃ無いかなあと思ったりします。
多層設計を選ぶべき場合
- ✅ チームが5人以上
- ✅ 開発期間が1年以上
- ✅ 複雑なビジネスロジック
- ✅ 高いテストカバレッジが必要
Service+DIを選ぶべき場合
- ✅ 小規模チーム(3人以下)
- ✅ プロトタイプ開発
- ✅ 単純なゲームロジック
- ✅ 高速な機能実装が優先
移行戦略というか典型的な設計例
- Phase 0: staticなManagerがいっぱいいる
- Phase 1: staticなManagerクラスをDI化
- Phase 2: Presenter層の導入(これがService+DI)
- Phase 3: UseCase層の分離(RepositoryはUseCaseの概念に押し込めれるでしょ!!というやつ)
- Phase 4: Repository層の抽象化(これが多層設計の話。UseCase層のテストを書く時にRepository層をモックにしてテストできる!)
Discussion