見出し画像

ホロライブアプリを育て続けるために:MonoBehaviour分離編

こんなきり!😈
カバー株式会社技術開発本部アバター配信チーム、プログラマーのKです。カバー株式会社では、ホロライブプロダクションのタレントがYouTube配信などで使用する「ホロライブアプリ」を内製で開発しています。私はその開発チームのリーダーをやっています。

ホロライブアプリは、プロダクションのタレントが日々の配信で使用するアプリです。タレントのやりたいことや、新しい表現を実現するため、日々アップデートを重ねています。絶え間ないアップデートを実現するためには、プログラムを変化に耐える構造に保っておくことが重要です。そうすることで、ホロライブアプリというプロダクトを育て続けることができるのです。

今回は、そんなホロライブアプリの持続的な開発を支える取り組みの一部をご紹介します。

記事後半はプログラミング経験のある方向けの難易度となっています。ご興味があればぜひお読みください!


ホロライブアプリの開発環境

この記事はホロライブアプリに関して初めての技術ブログです。まず、どのような開発環境でホロライブアプリを作っているか説明します。

ホロライブアプリはマルチプラットフォームゲームエンジンである「Unity」上で開発されています。Unityはゲームエンジンとして開発されましたが、優れたCG描画機能を備えているため、リアルタイムでCGを動かすソフトウェアの開発にも使用することができます。

ゲームエンジン「Unity」

プログラミング言語は「C#」を使っています。これはUnity上でデフォルトで開発に使用できる言語です。

ホロライブアプリはWindows版とiOS版が存在します。どちらもタレントの姿を映し出す機能を備えています。さらに、iOS版アプリをWindows版と連携させることで、iPhoneのフロントカメラで取得した高精度な顔の動きを、高いGPU性能を持ったWindowsPC上に送信できます。これによって、動きの精度と高度な描画処理を両立できます。

iOS版とPC版アプリを連携できます


Windows版とiOS版はどちらも同じUnityプロジェクト内で開発しています。ユーザーへ提供する際にそれぞれのプラットフォームにビルドし分けています。マルチプラットフォーム開発に対応しているUnityの利点を活かし、WindowsアプリとiOSアプリのプログラムのほとんどを共通化して効率よく開発を進めています。

Unityの「MonoBehaviour」

Unityでは、「MonoBehaviour」という機能を使って、手軽にゲームやソフトウェアを作ることができます。Unityの画面上に表示されているほとんどのオブジェクトは、このMonoBehaviourをベースにしたクラスを組み合わせて表現されています。MonoBehaviourはUnityにおいて最も基礎的で重要な機能です。

MonoBehaviourはたくさんの便利な機能を備えています。機能はあまりにも多いので全てをこの場で説明することは出来ません。ここでは本当に基礎的なものだけ紹介します。

Update関数

一般的に、ゲーム画面では1秒間に30回以上の画面更新が走ります。この画面更新を「フレーム」と言います。MonoBehaviourにはUpdate関数というものがあり、この中にフレーム内で行いたい処理を書くことで、ゲーム画面上のオブジェクトを動かしたり、ユーザーからのコントローラー入力を処理することなどができます。

ホロライブアプリの場合はUpdate関数の中に、キャラクターモデルの姿勢を制御する処理や、キャラクターモデルの表情を動かす処理が書かれています。

using UnityEngine;

public class SampleClass : MonoBehaviour{
	void Update(){ //これがUpdate関数
		//ここに書いたコードが毎フレーム実行されます
	}
}

SerializeField

MonoBehaviourを継承したオブジェクトには色々なパラメータを持たせることができます。例えば、モンスターのオブジェクトにHP(耐久力)や攻撃力を持たせるなどです。このパラメータをSerializeFieldとして記述することで、Unityでゲームを実行中にも管理画面からパラメータを確認&変更することができます。

ホロライブアプリでは、実行中にキャラクターモデルの表情のパラメータを確認するなどの場面で活用されています。

using UnityEngine;

public class SampleClass : MonoBehaviour{
	[SerializeField]
	private int HP; //耐久力
	
	[SerializeField]
	private int ATK; //攻撃力
}
Unityエディタ上ではこのように表示されます

他のMonoBehaviourとの連携

一つのオブジェクトに複数のMonoBehaviourを付けることが出来ます。また、同じオブジェクトに付いているMonoBehaviour同士は、簡単にお互いを参照して連携することが出来ます(GetComponentなどの関数を活用します)。

これにより、オブジェクトの機能を分割して作れます。機能の再利用が簡単になったり、プログラムがシンプルになったり、といった利点があります。

ホロライブアプリではキャラクターモデルの顔を動かすMonoBehaviourと、身体を動かすMonoBehaviourを分けて作るなどしています。

GameObjectは複数のMonoBehaviourを持ち、連携させることができます

MonoBehaviourの弱点

※すみません!ここから難易度が少し上がります。プログラミング経験者向けになります。

このように非常に便利なMonoBehaviourですが、弱点もあります。それは「便利すぎる」ということです。

「…便利すぎて何か問題が?」という声が聞こえてきそうですが、説明します。

例えば、MonoBehaviourにはGameObject.Findという関数があります(※正確にはUnityEngine名前空間の機能です)。これはゲームのシーン上にあるオブジェクトの名前を検索して、そのオブジェクトを取ってくるという関数です。シーン上に対象のオブジェクトがあれば、名前を指定するだけで取ってくることができるので、簡単なゲームを作る時には非常に便利な機能です。
しかし、名前で指定して取ってくるということは、名前が同じということ以外何も保証されていないということでもあります。意図していないオブジェクトが返ってくる危険性もあります。このような挙動は、仕様通りの動作を保証したいプロジェクトでは害になってしまうのです。

またMonoBehaviourでは、先述したUpdate関数や、初期化用のStart関数を利用することができます。これらの関数は、特定のタイミングでUnityから自動で呼ばれるので大変便利ですが、毎フレーム行うような処理が必要ないクラスでは不要です。

…不要なだけならまだ良いのですが、そのクラスでMonoBehaviourの機能を使わないのにMonoBehaviourをベースに実装すると、Updateで何かしている可能性を考えてデバッグする必要があるなど、開発中に心配することが増えてしまうのです。

このように、MonoBehaviourは色々と便利であるがゆえに、その機能を必要としないクラスにとっては害になってしまう場合もあります。特に、仕様通りの動作を保証したいプロジェクトや、複数人で開発するプロジェクトでは害が大きくなってしまいます。

MonoBehaviourの乱用で発生しがちな害

なので、業務で開発するようなUnityプロジェクトにおいては、どうしても必要な部分以外はできるだけMonoBehaviourを使わずに開発を進める、ということが重要になってきます。

Unityで普通のC#クラスを活用するために

上記の通り、会社のプロジェクトでMonoBehaviourを無闇に使うと開発し辛くなってくるので、必要のない部分についてはMonoBehaviourではないクラスとして記述した方が良いです。

では、どのようにしてMonoBehaviourではないクラスをUnityに組み込めば良いのでしょうか?

実は、素のUnityでは非MonoBehaviourなクラスを活用して開発することは難しいです。

Unityから普通のC#のコードを呼ぼうとすると、基本的にはゲームシーン上に配置されたMonoBehaviourからアクセスすることになります。状態を持たない単純な関数であれば、staticクラスのメソッドとして記述してMonoBehaviourからアクセスすることもできます。しかし、クラスに変数などの状態を持たせたり、その状態を管理したいという場合などには、staticクラスにするのは適していません。

staticでないクラスを使うには、どこかでインスタンス化される必要があります。そのクラスを使っているMonoBehaviourが一つしかない場合は、とりあえずそのMonoBehaviourのStart関数の中でインスタンス化して使えば大丈夫です。しかし、開発の中では、複数のMonoBehaviourが同じインスタンスにアクセスしたいという状況はよく発生します。

ここで、「誰がそのクラスをインスタンス化するのか?」ということが問題になってきます。

Aという非MonoBehaviourクラスの一つのインスタンスに、BとCという二つのMonoBehaviourからアクセスする場合を考えます。とりあえず、BのStart関数内でAをインスタンス化したとします。この場合、Bは自分自身のメンバ変数として定義されたAにアクセスすれば良いです。しかし、CがAを使いたい時は、Bを経由してアクセスする必要があります。CはAを使いたいだけなのに、わざわざBを経由してアクセスすることになります。Cが行いたい処理にBは関係ないにも関わらず、無駄にBと関わることになってしまうのです。これでは、プログラムが無駄に複雑になってしまうので、好ましくありません。

CはAを使いたいだけなのに、無駄にBを経由してしまう

これを解決するために、Dというクラスを用意します。Dは、Aの生成を行い、BとCにAを渡します。
DがAのインスタンスの生成と管理を行ってくれるので、BとCは無駄にお互いと関わる必要がなくなりました。BとCはどうやってAを生成して管理するかということを考えなくて済むようになりました。これで、BとCの関係が先ほどの例よりもすっきりしたのではないでしょうか?

BとCはお互いを知る必要が無くなり、独立して開発しやすい構造に

この例のように、インスタンスの生成と利用を分離するテクニックを、「依存性の注入(Dependency Injection)」と呼びます

Unityにおいても、依存性の注入をうまく行えば、普通のC#クラスを使いつつ快適に開発を進めることができます。しかし、素のUnityには普通のC#クラスに対して依存性の注入を行う仕組みが存在しません。

依存性の注入とZenject

では、Unityで普通のC#クラスの依存性の注入をするにはどうすればいいのでしょうか?

幸い、Unity上で普通のC#クラスに対する依存性の注入を実現してくれるライブラリがいくつか存在しています。ホロライブアプリの開発では、「Zenject」というライブラリを活用しています

Zenjectには「コンテナ (Container)」という仕組みがあります。アプリの開発者がこのコンテナにインスタンスの生成方法を登録おくことで、インスタンスが必要になったタイミングでZenjectが勝手に生成を行ってくれます。また、他のクラスからそれらのインスタンスに簡単にアクセスすることもできます。

ZenjectでピュアC#を活用して、開発しやすい構造を実現!

私たちのチームでは、Zenjectによる依存性の注入を活用することで、MonoBehaviourとその他のC#コードを分離して開発を進めています。

まとめ

今回は、私たちのチームの持続可能な開発に向けた取り組みの一部をご紹介しました。

ホロライブアプリはUnityで開発しています。Zenjectを使って、Unityの基礎機能であるMonoBehaviourとその他のC#を上手く分離し、Unityのメリットを活かしながら快適な開発ができるように工夫しています。

私たちのチームは、ホロライブアプリの運用・開発を行い、新しい機能をタレントに提供してファンの皆様に楽しんでいただくことを目標に活動しています。それを実現するためにも、今回ご紹介したような地道な工夫を積み重ねながら開発しています。


最後までご覧いただきありがとうございます。カバー株式会社ではエンジニアを積極採用しております。こちらの記事を読んで気になっていただけましたら、以下リンクよりご応募をお願いいたします。