こんにちは。カバー株式会社エンジニアのKです。
前回↓はホロライブアプリにMVPというアーキテクチャパターンを導入した時の話を書きました。
今回は、MVP導入後に残っていた課題を解決するために更なるリアーキテクチャを行った話について書きます。
Unityプロジェクトの刷新
MVP導入後もしばらくホロライブアプリの開発・運用を続けており、新機能の実装などもそれなりにうまくいっていました。
その運用と並行して、ホロライブアプリをさらに強化し、新しいスタイルの配信をできるようにしたいという要望が社内で膨らんできていました。その一つが、「3Dのバーチャル空間から配信する」という方向性でした。
実際にその方向性でアプリを作ってみようということになり、自分がアプリの設計を担当することになりました。上記のようなコンセプトのアプリを実現するには、既存のホロライブアプリの枠を超えた新機能がいくつも必要だったので、ホロライブアプリのUnityプロジェクトで開発を続けるのではなく、新しいUnityプロジェクトとして開発をすることにしました。
「3D空間での配信」というアプリの大まかな方向性はあるものの、具体的にどのようなアプリを作れば新しく、面白い配信ができるアプリになるのかは未知の領域でした。どのような機能をつければ良いか、どのようなシステムが最適なのか、という部分について、作って運用しながら手探りで進めていく必要がありました。
作りながらアプリの在り方を模索し続ける、というスタイルで開発を進めるには、変化に耐えられるアーキテクチャにする必要があります。もし、アプリの基礎ができた後で新しい機能を追加したいというときに、基礎の部分を大きく変更しなければならないとすれば、開発を進めるのが苦しくなってしまうからです。後からの変更や改造に耐えられるように、アプリの基礎の構造をしっかり考えておくことが重要でした。
また、これまでのホロライブアプリを開発する中で、「こうすればもっと開発がしやすくなりそうだ」、という改善点をいくつか見つけていたので、それらも新しいプロジェクトに盛り込むことにしました。
Zenject導入
まず、前回も書いた通り、インスタンス同士の参照をUnityエディタ上で手動で管理するのが大変という課題がありました。手間がかかるのもありますが、手動なので操作ミスも出やすい状態でした。
また、従来のプロジェクトではほとんどのクラスがMonoBehaviourとして書かれていましたが、MonoBehaviourは余分な機能が多く、すべてのクラスをMonoBeheviourにしてしまうのは無駄が多い設計でした。このあたりの話は以前、弊社の技術ブログ↓に書いたのでよければご覧ください。
そこで、導入したのがZenjectというライブラリです。Zenjectは、インスタンスの生成と参照関係の構築を、あらかじめ書いておいた設定の通りに行ってくれるという機能があります。これを使うことで、手動ではなくプログラムで自動的に参照関係の構築を行ってくれます。
また、ZenjectはMonoBehaviourではない、C#の普通のクラスに対しても使用できます。これによって、必要な箇所以外のクラスを非MonoBehaviourで記述し、快適な開発ができるようになりました。また、[SerializeField]では使用できなかったインターフェースによる参照も使用できるようになり、インターフェースを活用した開発が可能になりました。
「クリーンアーキテクチャ」
上記で「変化に耐えられるアーキテクチャにする」必要があると書いていました。では、それはどのようなアーキテクチャなのでしょうか?
ホロルームのアーキテクチャを組むうえで主に参考にしたのが、以下の書籍です。
「Clean Architecture 達人に学ぶソフトウェアの構造と設計」
Robert C.Martin (著), 角 征典 (翻訳), 高木 正弘 (翻訳)
自分はこの本や「クリーンアーキテクチャ」について書かれているネット記事などを読み、以下のようなアーキテクチャで新しいプロジェクトを立ち上げることにしました。
各レイヤーの役割は以下のように分けました。
- Domain : プロダクトの関心の中心
- Entity : アプリで扱いたい事柄をモデル化したデータ構造や関数を置く場所
- UseCase : アプリケーションで実現したい動作を記述する場所。Data, Controller, Presentationレイヤーを接続する
- Control : 外部からの入力を扱う
- Controller : UseCaseに入力を渡すコードを記述する
- Communicator : 実際に外部とネットワークで通信したりデバイスに接続したりするコードを記述する
- Data : UseCaseがデータにアクセスするための機能を提供する
- Repository : Domainレイヤーで定義されたIRepositoryの実装を置く場所
- DataStore : 外部のライブラリを呼び出すなどして実際にデータのCRUDを行うコードを置く
- Presentation : ユーザーに情報を表示するためのレイヤー
- Presenter : UseCaseから受けた命令・イベントや、UseCaseのデータ構造をViewが利用しやすい形に変換する。表示の都合上必要な一時変数は子のレイヤーに置く。
- View : UnityのMonoBehaviourを継承してSceneを作り上げ、実際にユーザーに向けて情報を提示する
アーキテクチャを作るうえでクリーンアーキテクチャ本から参考にした主な点は以下です。
- 各モジュールはドメインに向かって依存するようにする。モジュールの相互依存はしない。
- モジュールの依存の方向とデータフロー(参照したい方向)が逆の場合は、依存先のモジュールにインターフェースを作って依存元でそれを実装する。【依存関係逆転の法則】
DomainとDataは完全に非MonoBehaviourで記述しました(Vector3など一部のUnityのクラスは使用)。PresentationとControlについては、Domainと連携する部分(PresenterとController)は非MonoBehaviourにし、ViewとCommunicatorはMonoBehaviourを含んで良いというルールでプログラムを作っていきました。
MVPで書いていた旧プロジェクトよりも複雑なアーキテクチャだったので、クラスをどのレイヤーに置くべきかなど悩みつつ開発を進めていきました。最初は勝手がわからずになかなか開発が思うように進まない時期もありましたが、徐々に慣れてきてスムーズに進むようになりました。
Assembly Definition
上記のアーキテクチャで開発を進めるうえで、UnityのAssembly Definitionという機能を利用しました。
Assembly Definitionを使うには、モジュール化したいフォルダにAssembly Definition Fileを設置します。このファイルにはそのモジュールが参照するモジュールを設定できます。参照先として含まれていないモジュールは、参照元のプログラムからアクセスできなくなります。この機能により、モジュール同士の依存関係を機械的に制約することができ、アーキテクチャのルール違反を未然に防ぐことができます。
上記のアーキテクチャでは、Presentation, Data, ControlがDomainに依存し、お互いは依存しないというルールがあります。この場合はPresentation, Data, ControlそれぞれのフォルダにAssembly Definition Fileを設置し、その中にDomainへの参照を設定しておくことでルールに必要な制約を与えることができます。
弊チームではこのAssembly Definitionを活用することで、アーキテクチャのルールを守って開発を進めることができました。また、モジュールごとにAssembly Definition Fileを設置しておくことで、他のプロジェクトなどでも使いたいモジュールをSDKとして切り出すということも比較的楽に行えるようになりました。
Assembly Definitionを記述するのは手間もかかるので、モジュールの粒度には気を付けつつ活用しましょう。
まとめ
今回は、3D配信のための新規プロジェクトを立ち上げるにあたって、どのようなアーキテクチャで、どんなツールを使って開発を進めたのかを書きました。「クリーンアーキテクチャ」の考え方を参考にアーキテクチャを設計し、そのアーキテクチャをUnityで実現するためにZenjectとAssembly Definitionを活用しました。
次回は、今までのアプリ開発・運用の経験に基づき、現時点でのUnity開発アーキテクチャに対する考えをまとめてみようと思います。
ありがとうございました。