ここまでのあらすじ
”憂鬱”の記事から話が続いているこのシリーズですが、ここまでの文脈のあらすじとしては、
1.”憂鬱”の記事で、Unityのゲームにクリーンアーキテクチャなどを適用するのは難しいのではないか?という話をした。
2.(ソーシャルゲームの場合)インゲームはダーティでも仕方ないけど、アウトゲームにはクリーンアーキテクチャなど適用可能であるという意見を見かけた。
3.インゲームとアウトゲームを完全に分けるという発想についての話をした。
4.アウトゲーム的な考え方の第一歩として、基本的なMVPパターンについての話をした。
この記事のシリーズは、ここから”Unityソルジャーが頑張って綺麗なアウトゲーム(インゲームのUIにも適用可能)の作り方を学ぶ”みたいな方向に話を広げる予定です。
そのモチベーションとしては、世間のUnityエンジニアの5割くらいがソシャゲ開発に従事してると思われる現状のニーズを鑑みて、大規模な商業ソシャゲアプリ開発に耐えうるアウトゲーム設計ができるようになれば、それは強みになるだろうからです。
また、アウトゲームの考え方を学ぶ過程で、ソシャゲじゃなくても普通のUnityゲームのシーンとかを綺麗に作るヒントも学べそうだなと考えていますので、ソシャゲを開発する予定が無い人にも役に立つ話になるかもしれません。
普通のUnityのゲーム開発の考え方とは文化が違う面があり、戸惑うかもしれませんが、一歩ずつ順を追って学べばそんなに難しい話にはならないと思います。
単に、インゲーム(普通のUnity)とアウトゲーム(クリーン)で完全に別の話をしてると分けて考えれば大丈夫でしょう。
Unityソルジャーも、エンジニアっぽいスキルを身に付けて、魔法戦士的な両刀使いにクラスチェンジを目指しましょう!
というわけで、今回の記事は、Unity世界と普通のC#世界を繋ぐ架け橋となる重要な存在であるところの”DIコンテナ“の話です。
前回の話で、”インゲームとアウトゲームを分ける”という考え方を書きましたが、「具体的にどうやるの?」という話がまだでした。DIコンテナがそのカギを握ってます。
アウトゲーム?DIコンテナ?興味な~いって人でも、ここから結構話が面白くなってくるはずなので、是非試しに読んでください!
DIコンテナとは一体何なのか?
そもそもDIって何なの?って話ですが、DIとはDependency Injectionの略で、日本語では依存性注入と呼ばれてます。
依存性注入?何やら難しそうな用語が出てきましたね。
しかし、実際は何も難しくないです。
DIとは、クラスのインスタンスの初期化時に、そのクラスが必要としてる他のインスタンスの参照をブチ込んであげる事です。
何のこっちゃ?Unityユーザーでも分かるように説明します。
Unityユーザーは誰でもDIを使ってる!?
例えば、あるGameObjectのコンポーネントに、別のコンポーネントを参照させたい時は、publicのフィールドを定義してから、
こんな風に、インスペクタで参照を突っ込んであげますよね?(画像の例だとButtonコンポーネントを突っ込んでる)
こうする事で、ゲームの開始時に、クラスのフィールドに指定したコンポーネントが実際に突っ込まれて、使用可能になります。
当たり前ですね。
こういう方法は、Unityの初心者でも上級者でも誰でも知ってる方法です。
要するに、これもDIの一種です。初期化時に、インスペクタで指定しておいた通りに勝手に参照を解決してくれてますからね。
複数のコンポーネントがあったり、色んなサブクラスがある時も、一体どの参照をブチ込めばいいのかを、マウスでドラッグするだけで簡単に指定したり差し替えたりできるんですから、つくづく便利なシステムだと思います。
つまり、Unityユーザーなら誰だってDIをバリバリ使いこなしてるという事になります。Unityのシステム自体に便利にDIをやってくれる仕組みが内蔵されてるという事です。
「じゃあ何でDIコンテナなんてもんが必要なの?」と思われるかもしれません。
普通のC#でのDI
ここで一旦、Unityから離れて普通のC#について考えてみましょう。
Unityならエディタのシーンとインスペクタがあるおかげで、自由自在に依存関係を指定して、解決する事が可能でした。
しかし、普通のC#にはそういうエディタはありません。ぜ~んぶコードから自前で依存関係を解決してやる必要があります。コンストラクタから参照を渡してあげる、いわゆる手動DIって事ですね。
でもそれって面倒くさいですよね。
そこんとこを何とかしたいと思って考案されたのがDIコンテナです(多分)。
DIコンテナとは、読んで字のごとく、”DIしてくれるコンテナ”です。
コンテナというからには、何かを格納するんでしょうけど、何を格納するかといえば、クラスのインスタンスです。
つまるところ、DIコンテナの中には、他で使いたいクラスのインスタンスが一杯詰まっており、新しいインスタンス生成時に、必要な参照をコンテナの中身から自動的にブチ込んでくれます。(どのインターフェースに対してどの実体をブチ込むのか、あらかじめコードでルールを記述しておく必要があります)
これによって、Unityエディタが無い普通のC#でも、簡単に依存関係を解決できます。
UnityでのDIコンテナの役割
ここまでで、Unityには元々DIの仕組みがある事と、普通のC#ではDIコンテナを使うという話をしました。
しかし、ZenjectやVContainerは、Unity向けのDIコンテナです。
どうして元々インスペクタで参照を解決できるUnity上でDIコンテナを使う必要なんてあるんですか?
それはつまり、Unity世界と普通のC#世界を繋げるためです。
Unity向けのDIコンテナは、普通のDIコンテナと同様、普通のC#を扱う事ができる事に加えて、Monobehaviourのサポートが追加されてます。
普通のC#クラスにMonobehaviourの参照をブチ込むみたいな事ができるわけです。
ある意味で、前回の記事で書いた、インゲームの世界とアウトゲームの世界を繋げるという話でもあります。
Unityの世界をView的な世界と見なせば、View世界とModel世界を繋げるという見方もできるかもしれません。
相当大雑把に分けると、この表みたいなイメージになります。
Unity世界 | 普通のC#世界 |
ダーティ | クリーン |
View | Model |
インゲーム(とUI) | アウトゲーム |
Monobehaviour | 普通のC# |
この分断された二つの世界を繋げるのがDIコンテナです。
ここからはMVPパターンで言うModelとかViewの用語が出てくるので、まだ読んでない人は前に書いたMVPの記事を読んでおくと理解がはかどります。
ModelをUnity世界から普通のC#世界に引っ越させる!?
二つの世界といっても、Unityユーザーには、まだあんまりピンと来ないかもしれません。
Unityを使ってると、Model的なデータでも、とにかく何でもかんでもMonobehaviourにして、シーンに置くなりプレハブにするなりが当たり前だからです。
Unityのシーンに3Dモデルを置いて、コンポーネントを貼ってシーンに配置するという方法は分かりやすくていいですよね。
が、しかし、例えば単なるゲームのコンフィグデータみたいなものまでMonobehaviourにしてシーン上に配置する必要ってあるんでしたっけ?
Monobehaviourのデメリット
まあ別に、それが悪いという事では全然無いのですが、コンフィグデータみたいなものをシーンに置いちゃうと、別のシーンのロードをしたら消えちゃいますよね。もちろんDontDestroyOnLoadにしとけばいいんですけど、それでも誰かが間違って勝手にDestroyしてしまうリスクはあるわけです。
また、コンフィグデータだの3Dキャラだのが一緒くたになってシーンに置かれてると、シーンが取っ散らかって他人から見るとワケ分らなくなりがちです。
ついでに言うと、Monobehaviourはnewで生成できないので、普通のC#と比べてテストが書きにくいという問題もあります。
この辺が、Monobehaviourである事のデメリットと言えます。
クリーン的なモノの見方で言えば、Unityのシーンなんて、色んなオブジェクトが動的に生成、消滅してて、シーン上にいる限り、どこから誰がいつどんな操作を受けるか分からない、不確定要素が多すぎるダーティでView的な世界だと言えます。
そんな危険な世界にModel的なデータなどを置いておくのは気が気じゃない!
そういった理由から、”必要が無ければMonobehaviourじゃなくて普通のC#で書こうぜ!”というモチベーションが出てきます。
ここんとこは、いつも何でもとりあえずMonobehaviourで書いちゃうUnityユーザーとしては文化の違いを感じるところです。
まあ、DIコンテナが登場する以前はUnity上で普通のC#なんて扱いにくくてあんま使わなかったわけですが。
VContainerの話
もう少し具体的に、VContainterの場合の話をしましょう。
前に書いたMVPの話では、ModelもPresenterもViewも、全部Monobehaviourとして書きました。
でもこれだと、新しいシーンを読み込んだ時に、どうやって新しいPresenterにModelの参照を渡せばいいでしょうか?
まあ、Presenterの初期化時にGameObject.Findとかで参照を取って来るのがUnityの作法ですよね。
でも、もしシーンの読み込み順の問題とかで必要なModelが存在しなければ、null参照エラーが起きますし、確実性が低いっちゃ低い方法ですよね。
ですから、ModelをMonobehaviourから普通のC#に置き換えて、DIコンテナに格納する形にすれば、諸々の懸念は解決します。
さらに、VContainerでは、普通のC#でありながら、MonobehaviourみたいにStartやUpdate関数を書けるようにする機能があるので、Presenterさえも普通のC#に置き換える事が可能になります。
詳しい話はまた別の記事に書くとして、VContainerのマニュアルのページだけ貼っておきます。こちらのページの”6. Inversion of Control (IoC)”の項目でMVPのMとPを普通のC#で書くヤツをやってます。
https://vcontainer.hadashikick.jp/getting-started/hello-world
普通のC#のデメリット
さて、これでシーンとして置く必要が無さそうなクラス、つまりModel的なクラスをUnity世界から普通のC#世界に追い出すことができましたね。
GameObjectの不安定な寿命問題から解放されました。
参照解決のトラブルも減りましたし、シーンもスッキリしましたし、Modelを安全に管理できるようになりましたし、テストも書きやすくなりました。
が、しかし、Monobehaviourでなくなった事は、メリットばかりではなく、デメリットもそれはそれで存在します。
何と言っても、インスペクタで簡単に値を設定できるのがUnityの便利な所だったのに、普通のC#だとそれができなくなります。これがつらい。
エディタのプレイモード時に、値を変更する事もできなくなります。
また、全てがMonobehaviourだった頃は、シーンだけ見てればゲームの全体の動きが追えたのに、普通のC#世界が入り込んでくると追えない部分も出てきてしまいます。
つまりMonobehaviourも普通のC#も一長一短という事で、使い分けが大事になって来るでしょう。
とは言え、シーンを追ってデバッグしないといけないような入り組んだ問題が起きるのは大抵インゲームでしょうから、単にインゲームにはDIコンテナ使わない(インゲームは全部Viewだ!と考える)。みたいなスタンスでもいいのかもしれません。
ScriptableObjectもあるよ
ちなみに、「Unityのシーン上には置きたくないけど、インスペクタで簡単に値を編集できる恩恵は受けたい…」みたいな時は、ScriptableObjectが使えます。
ソシャゲでサーバーから取ってこなきゃいけないようなデータはともかく、最初からアプリ内に埋め込んでても構わない設定値とかは、ScriptableObjectで持っててもいいんじゃないでしょうか。
ScriptableObjectもnewできないからテストしずらいという点に注意です。
VContainerでScriptableObjectを使う方法はこちら。
https://vcontainer.hadashikick.jp/registering/register-scriptable-object
まとめ
今回の記事では、UnityでのDIコンテナについて書きました。
UnityにはそもそもDI機能が備わってますが、それでもUnity用のDIコンテナを用いる理由は、Unity世界と普通のC#世界を繋ぐためだという事。
Unity世界から普通のC#世界に移行する事のメリットとデメリットも書きました。
DIコンテナの役割について、今回の記事の文脈以外の用途について書くのは省きましたが、たとえばUnityのインスペクタで手動で参照設定するのがダルいから自動で割り当てたいとかにも使えるようです。
今回の記事を読めば、前回紹介したXFLAG Tech Noteの内容も分かりやすくなるんじゃないかなと思います。
お詫び
前回の記事で、「アウトゲームの設計は簡単そう!」みたいな事を書いたのに対して、「簡単とは何だ!」とお気持ちを傷つけられた人がいるそうです。
すみませんでした。
私の文章能力の問題で誤解を与えてしまったようですが、私があの文章で実際に云わんとした事を、もっと順を追って説明すると、
まず、アウトゲームの設計の基盤を考える人がいるわけです。この基盤自体を設計するのは、非常に高度なソフトウェアアーキテクチャなどへの知識が求められ、簡単ではありません。これは大変難易度が高い。
それで、そのような高度なアウトゲームの設計を記事に書いてくれた偉大な方々がいたわけです。これは大変にありがたい事ですね。
そのおかげで、それらを真似すれば、我々がアウトゲームを作る時の敷居が下がったというわけで、簡単になったと言ったのはここの所であったわけです。
つまり、そのような記事でアウトゲームの作り方を学べるぞ!というウキウキとした気分と、記事を書いてくださった方々への圧倒的感謝の気持ちだけがあったのがあの文章だと弁明させていただきたい。
さて、私が今自分で書いたあの文章を読み返した時に、一番マナー違反くさいな~と感じたのは、金の話を持ち出してる所ですね。
twitterで「金を生み出すコードを書くのが一番大事!」みたいな事を言ってる人を見かけたので、そういう視点も取り入れてみようかなと思って書きました。
というのも、あの記事の「インゲームとアウトゲームを分けて考えよう」という話からの、「だからアウトゲームの設計について学ぼう」という結論に繋げるのは、ちょっとストーリーの繋がりが弱いと思ったので、金という分かりやすいモチベーションを語っておこうかなという事ですね。
実際に私が金に目がくらんでるとしたら、こんなブログ記事を書いてわざわざ情報を垂れ流すハズ無いですよね。
私が記事を書いているのは、それが妥当なアイデアだと感じたからです。
もしも間違ったアイデアに基づいて、生産性を浪費している人がいるとしたら、もったいない事ですからね。