こんにちは、カバー株式会社のKです。普段はタレントが配信に使用する「ホロライブアプリ」の開発に携わっています。
これまで約5年ほどUnityを使ったホロライブアプリの開発に携わり、必要に応じてリアーキテクチャも行ってきました。このシリーズでは、これまでホロライブアプリで採用していたアーキテクチャを振り返りつつ、Unityアプリのアーキテクチャを考察していきたいと思います。今回は初期(特にアーキテクチャが考えられていなかった時期)~ MVP導入までを書きます。
初期
私がホロライブアプリの開発に参画した当初は、あまりアーキテクチャを意識した設計にはなっていませんでした。Canvas、モデル、サーバーへのアクセスなど、全ての要素がMonoBehaviourに書かれており、シーン上に無秩序に配置されていました。
配信にはしっかりと使用できるほどの機能と安定性を備えていましたが、保守性、拡張性が低く、チーム開発が難しい構造になっていました。
自分が参画してからもしばらくはそのアーキテクチャで開発を行っていましたが、開発を進めるのが苦しくなってきたため、アプリの仕様を概ね理解したくらいのタイミングでリアーキテクチャを行うことにしました。
Model View Presenter
ホロライブアプリをリアーキテクチャをするうえで、目標とするアーキテクチャをModel View Presenter(MVP)に定めました。
MVPとは、プログラムをModel, View, Presenterの3つのモジュールに分割するアーキテクチャパターンです。各モジュールは以下の役割を持ちます。
- Model : UI表示に必要なデータの実体を保持する
- View : ユーザーにデータをUIとして表示する
- Presenter : ModelとViewを仲介し、両者がお互いに依存しなくても済むようにする
こちらはあくまで、自分がこう考えてモジュール分けを行ったという例になります。MVPについてより詳しく知りたい方は、ぜひ検索してみてください。
MVPでは、各モジュールのつながりは以下のようになっています。
PresenterがModelとViewの参照を持っており、これによってModelとViewはお互いを参照しないままでデータを受け渡すことができます。赤と青の矢印がデータフローを表しています。Modelの変化があればPresenterがViewに伝達し、Viewで操作が行われればPresenterがModelに伝達します。
ModelとViewの変化をPresenterが察知するにはUniRxを活用します。UniRxはUnity上でReactive Programmingを行うためのライブラリです。
Reactive Programmingについては説明を割愛しますが、簡単に説明すると、「データの変化に応じて行いたい処理を予め宣言しておくことで、データの変化に連動した処理が自動的に実行される」という仕組みです。
UniRxではIObservableが処理の起点(データの変化)になり、それをSubscribeすることで実際に行いたい処理を記述します。IObservableには様々な種類がありますが、よく使うのはSubjectとReactivePropertyです。
ホロライブアプリの場合は、ModelとViewにIObservableとメソッドを配置し、それらをPresenterで接続することでMVPアーキテクチャを実現しました。例えば、以下のようなコードになります。この例では、Modelにある背景色のReactivePropertyをPresenterでSubscribeし、Viewにある色を実際に反映させる関数(SetBackgroundColor())を呼び出しています。
public class BackgroundModel : MonoBehaviour
{
private ReactiveProperty<Color> _backgroundColor = new ReactiveProperty<Color>();
public IReadOnlyReactiveProperty<Color> BackgroundColor => _backgroundColor;
void Awake()
{
_backgroundColor.AddTo(this);
}
}
public class BackgroundPresenter : MonoBehaviour
{
[SerializeField] private BackgroundModel _model;
[SerializeField] private BackgroundView _view;
void Awake()
{
_model.BackgroundColor.Subscribe(color => _view.SetBackgroundColor(color)).AddTo(this);
}
}
public class BackgroundView : MonoBehaviour
{
[SerializeField] private Image _backgroundImage;
public void SetBackgroundColor(Color color)
{
_backgroundImage.color = color;
}
}
このような簡単な例だとMVPの3つにクラスを分ける利点は分かりにくいかもしれません。実際の開発ではより多くのパラメータやクラスを扱うので、データ処理と表示を別々のクラスに分けておくことで、プログラムがすっきりするなどのメリットが目立ってきます。
MVP導入後
改善点
上記のような方針でホロライブアプリのリアーキテクチャを行った結果、以下のような改善が見られました。
プログラム全体の見通しが良くなった
それぞれのモジュールが行う処理をプロジェクト全体で揃えたことにより、プログラムの見通しが良くなりました。「このような処理はきっとこのあたりのクラスに書かれているだろう」という予測を立てやすくなりました。
新しい機能の追加が容易になった
既存のコードを変更することなく、クラスを追加するだけで新機能の実装ができる場面が増えました。例えば前述の背景色の例で言うと、背景色を別のUI(背景色選択ボタンなど)に適用させたいときにモデルは変更せず、Viewを新たに追加してPresenterでつなぎ込むだけで実現する、というようなことが可能になりました。これは、以前のようにパラメータとViewのパーツが同じクラスに入っているとできなかったことです。
クラスの動作チェックが行いやすくなった
機能開発中に、Modelのみ、Viewのみの動作チェックを行いたい場合があります。そんな場合には、Presenterを動作チェック用のプログラムに取り換えることで、ModelとViewそれぞれ単体で動作させることができるようになりました。
課題点
一方で、以下のような課題も残りました。
Unityのインスペクタで参照を差し込むのが面倒
Model, View, Presenterは全てMonoBehaviourで作っており、それらを接続するには [SerializeField] としてシーン上でマウスのドラッグ&ドロップで参照をつなげる必要がありました。これは、新しいシーンを作ったり、動作テスト用のシーンを作るときに特に面倒でした。
インターフェースの活用ができない
C#にはインターフェースという機能がありますが、[SerializeField] による参照はインターフェースに対応していないため、この機能を活用することができませんでした。MonoBehaviourを継承した抽象クラスで似たようなことが可能ではあるものの、機能の差はあるのでインターフェースを使いたい場面は残りました。
ModelとViewの中身については改善できていない
プログラムをModelとViewに分けてある程度スッキリさせることには成功しましたが、Model、Viewそれぞれの中身についてはそのままだったので、複雑な処理を行っているクラスは大きいままのものが残っていました。例えば、モデルを動作させるためのスクリプト群や、コラボ相手とインターネット経由でパラメータを同期させる部分などが複雑で重いままでした。
まとめ
今回はホロライブアプリにおけるMVPの実装例について書きました。MVPで実装しなおした結果、UIキャンバス周りなどは開発体験・保守性が大きく向上しました。一方で、MVPだけでは解決しきれない課題も露わになってきました。
次回はさらに開発・運用しやすいアーキテクチャを目指してさらなるリアーキテクチャを行った時の話を書きます。
ありがとうございました。次回もよろしくお願いします。