前置き
前回までの記事で、MVPパターンとDIコンテナの役割について分かりました。
もう必要な知識は全部揃ってますので、あとは、実際にアウトゲームの設計をするだけです。
簡単です!
(前回のお詫びでも書きましたが、簡単というのは、自分自身と読者にそう言い聞かせて簡単だと思い込んでもらうために連呼してるだけで、何かをナメくさった発言とかでは無い事をあらかじめ断っておきます)
”設計をする”というと何やら難しそうですが、今回は実際には自分で設計はしません。既存の公開されている設計をそのまま真似させていただきます。
もんりぃさんがオープンソースで公開されてる”Clean Architecture For Unity”、略してCAFUというUnityアプリのテンプレートがあります。
https://github.com/umm/cafu_core
こちらは実際にキッズスターという会社のゲームアプリで実用されている設計であり、相当”固い”作りだと思われます。
ですので素人が下手にゼロベースで設計を考えるより、とりあえずこちらのCAFUをそのまま使ってみる方が安牌だと思います。
CAFUの設計についてはこちらのスライドなどで語られてます。
今回の記事では、このCAFUの設計をほぼそのまま利用させていただきつつ、自分なりに噛み砕いて解説してみます。
さらに、以前から紹介しているXFLAG Tech Noteですが、そちらで書かれているアイデアからも所々比較しつつ拝借させて頂こうかなと思ってます。
前提としてのクリーンな目線
前回の記事でも少し触れましたが、Unityソルジャーとクリーンなエンジニアの間には文化の違いがあるので、Unityソルジャーもアウトゲームでは割り切って一旦クリーンな目線を頭に入れてください。
まずクリーンな信条として、
「どこに何の処理が書いてるかすぐ分かるようにしろ!」
というものがあります。
どういう処理をするかによって、クラスの命名ルールを決めておくとか、処理毎にクラスを分けるとか、クラス名を見ただけで中身が想像できるようにするとかです。
まあ単に、整理整頓してねって事ですね。
何でそうする必要があるかって、チーム開発では、みんながてんでバラバラに好きな場所に好きな処理を書いて好きなクラス名を付けてたらマジにプロジェクトが意味不明になって取り返しがつかなくなるからです。
ルールが明確なら、コードレビューで何を見ればいいかもハッキリしてるので、時間のロスが減ります。
また、「なるべくテストを書け」という信条もあります。
何でテストを書くのか?っていうのは、想像してもらえばわかりますが、大規模開発でクラスが1000個とかある状況で、その全てがまったくテストされてなくて、何もかもがちゃんと動くか動かないかハッキリしない状態で置いとかれるって逆に怖くないですか?
あるいは、このクラスを修正したいけど、そうしたら他の1000個のクラスに影響出ちゃうなあ…って場合に、テスト書いてなくて修正の影響が確認できない場合、怖くて修正する気が失せませんか?
怖い話を聞いて、段々テストを書きたくなってきたかと思います。
というわけで、クラスを分けまくっとけば、それぞれのクラスのテストも書きやすくなります。
この辺の前提を頭に入れておく必要があります。
まあ、こんな風に一概にルール化できるのも、アウトゲームでやる事がほとんどが定型処理だからだと思います。
アウトゲームでやる事は、
・データの読み込み
・データの書き込み
・データをModelに変換してPresenterに提供
・WebRequestでAPIを叩く
こんな感じの処理に収まるでしょう。
個人的にはインゲームは定型処理に収まらないし、こうは行かないよな~とはやっぱ思いますが、インゲームとアウトゲームで、ソルジャー脳とエンジニア脳を切り替えて考えてください。
今回作る想定の画面
抽象的な設計の話をしていくわけですが、一応具体的な例もあった方が分かりやすいと思うので、このような”キャラクター詳細”画面(CharacterDetailScene)を作るイメージで話を進めていきます。
ステータス表示と、名前変更ボタンやキャラ強化ボタンがある感じです。
CAFU的な設計を考える
まず、特に何も考えずに作ったUnityゲームはこんな感じになりがちです。
全てがダーティなUnity世界に配置されており、カオスな状態です。
いや、ソルジャー的な作りをディスってる訳じゃないですよ。単にクリーン目線だとこういう風に見えているという事です。
アウトゲームについて、今まで記事に書いたMVPとDIコンテナの知識を踏まえると、こんな風に綺麗な感じにできます。ModelをUnity世界からクリーンな普通のC#世界に逃がせましたね。
ちなみにキャラクター詳細画面のModelはこんな感じになると思います。
1 2 3 4 5 6 7 8 |
public class CharacterDetailModel { public string name; //名前 public int hp; //HP public int mp; //MP public int atk; //ATK public int def; //DEF } |
ただし、ソシャゲを作る事を考えると、これだけだと不十分です。
今のままだと、要するに全部のロジックをPresenterに書くハメになりますよね?ほとんど何でもかんでもPresenterに書いちゃうのは、まだまだカオスすぎると言えます。
というわけで、”UseCase“というクラスを用意しました。Presenterに直接処理を書くのをやめて、Presenterが何かしたいと思ったらUseCaseを叩くようにします。
”キャラクター詳細画面”では、3つのUseCaseが必要になるでしょう。
・GetCharacterDetailModelUseCase→キャラクター詳細画面用のModelを取得する
・ChangeCharacterNameUseCase→キャラクターの名前を変更する
・CharacterPowerUpUseCase→キャラクターを強化する
(このように完全に一つの処理毎にUseCaseを用意するのは、XFLAGでのやり方で、これだとUseCaseのクラス数が膨れ上がる問題があります。CAFUではもう少し粒度が荒くなってて、一つのUseCaseが複数の処理を持ってる形になってます。ケースバイケースです)
いい感じになってきましたが、まだ分からない事があります。上の例の中の、GetCharacterDetailModelUseCaseですが、一体どこからModelのデータを持ってくればいいんでしょうか?
つまり、データの取り扱いについても考える必要があります。
データ層の構造はこんな感じです。
まず”Entity“ですが、これはデータが定義されてます。Modelと似てますが、Entityはサーバーから返ってきたJsonデータとか、マスターデータとかの、大元のデータが定義されてます。
例えばキャラクターデータのEntityはこんな感じでしょうか。
1 2 3 4 5 6 7 8 9 10 |
public class CharacterDataEntity { public string name; //名前 public int hp; //HP public int mp; //MP public int atk; //ATK public int def; //DEF public string likeFood; //好きな食べ物 public string dislikeFood; //嫌いな食べ物 } |
↑キャラクター詳細画面のModelとほとんど同じですが、好きな食べ物とかの、キャラクター詳細画面では必要ないデータも含まれてます。
“Model“というのは、それぞれの画面で必要なデータだけを、画面(View)ごとにEntityから抜粋して加工したものになります。
“Repository“は、Entityデータを持っています。という事は、Entityデータ毎にRepositoryが対になって存在する形になるかと思います。UseCaseからEntityデータくれと言われたら渡してあげます。EntityデータはDataStoreから持ってきます。
”DataStore”はデータの読み込み、書き込み先です。ScriptableObjectだったり、PlayerPrefsだったり、はたまたAPI叩いてサーバー上からデータをもらう、書き込むとかするパターンがあるかと思います。
RepositoryがEntityをScriptableObjectから持ってきたい時は、ScriptableObjectDataStoreを、PlayerPrefsから持ってきたい時はPlayerPrefsDataStoreを叩く形になります。
CAFUではさらに”Translator”というのも用意されています。EntityとModelを相互に変換するものだそうですが、すいませんがまだよく分かってないので今回は省きます。
ここまでで全体をまとめるとこうなります。
こうして図で見るとややこしそうに見えますが、ここまで説明した通り、MVPに加えてデータの取り扱い方を決めた程度で、そんなに難しい事はしてません。
この図で重要なのは、例えばUseCaseがRepositoryを通り越して直接DataStoreを触ったり、PresenterがUseCaseを通り越して直接Repositoryを触ったりしないようにする事です。
どうしてそれぞれのクラスの触れる範囲を制限するか?というと、もしPresenterが何でもかんでも触ってOKだったら、この設計を無視してPresenterに全てを書いてしまう輩が出てくるかもしれなくて、設計が崩壊するからです。
そもそも何でこういう設計になったのか?という説明をこの記事ではだいぶ省いていますが、クリーンアーキテクチャによる合理的な設計になってます。詳細はもんりぃさんのスライドなど参照してください。
それぞれのクラスが疎結合になってるので、例えば画面の見た目の仕様が途中で完全にひっくり返ったとしても、見た目の変更だけならドメイン層やデータ層にはダメージは及びません。
あるいは、あるEntityの保存先がローカルからサーバーに変更されたとしても、単にRepositoryが参照するDataStoreを差し替えれば済むので、ほぼノーダメージで済みます。
もしこれが決め打ちでUseCaseから直接PlayerPrefsとかを読み書きする作りだったらダメージがでかかったハズですよね。
でも、ちょっと疑問なのが、この設計だとEntityが変更されたら相当ダメージの影響範囲が大きい気がしますね。Entityは割とどこからでも触りますから。
結構サーバのAPIに仕様変更が入ってEntityの持ち方が変わる事ってありそうな気がしますが、どうなんでしょうか。
以前に”憂鬱”の記事では「ゲームにクリーンアーキテクチャを使うのは難しい」と書いたのに、今回の記事では使っとるやんけと思う人がいるかもしれません。
念押ししておきますが、私の今の考えでは、”アウトゲームは定型的な処理だから、仕様変更があっても設計自体が変わる事は起きないので、クリーンアーキテクチャが適用可能”と思われます。インゲームは無理そうという考えは変わってません。
キャラクター詳細画面表示の流れ
設計の全体像が掴めたので、キャラクター詳細画面表示の時の処理の流れを考えてみます。
ModelをViewに反映するまでの流れを図にするとこんな感じです。
まず、Viewは上に貼った画面の通りです。
Modelも上述したCharacterDetailModelとなります。
①Presenterは、まず画面に表示するModelを取得したいので、GetCharacterDetailModelUseCase.Runを叩いてModelを要求します。
②GetCharacterDetailModelUseCaseはCharacterDataRepositoryを叩いてCharacterDataEntityを要求します。
③CharacterDataRepositoryは、例えば今回はPlayerPrefsからデータを持って来るとして、PlayerPrefsDataStoreを叩いて、④⑤CharacterDataのEntityを取得して⑥⑦UseCaseに返します。
GetCharacterDetailModelUseCaseに戻ってきましたが、もらったEntityから必要なデータだけを抜粋して、⑧⑨CharacterDetailModelというModelに変換して、⑩Presenterに返します。
⑪Presenterは、もらったModelデータをViewに反映させます。
そしてPresenterは、”名前変更ボタン”が押されたらChangeCharacterNameUseCaseが叩かれ、”強化ボタン”が押されたらCharacterPowerUpUseCaseが叩かれるようにボタンのOnClickにAddListenerしておきます。
また、”強化”されたらキャラのパラメータが変化するはずで、これはModelの変化なので、Modelの変化にトリガーされて、Viewの表示が更新される処理もReactivePropertyにSubscribeされて仕込まれてるでしょう。”名前変更”も同様です。
まあ今回のキャラクター詳細表示画面の例で行くと処理の流れはこんな感じでしょう。
ローカルにキャッシュされたデータを取得するだけじゃなく、通信などを挟んでデータを取得する場合は、取得するまでに時間がかかりますから、画面遷移時にローディング表示などを挟んでデータ取得を待つ必要がありそうですね。
XFLAGとの違い
XFLAG Tech Noteでも、おおむねCAFUと同じような設計になってますが、ちょっとだけ違います。
XFLAGではRepositoryはDataStoreを叩きません。
その理由はPDFの32ページあたりから詳しく書かれているのですが、かいつまんで説明します。
もしもCAFUみたいにRepositoryが毎回DataStore(サーバーのAPIを叩く)を参照する作りの場合、画面を開くたびにAPIを叩いて通信が発生してデータを取り直してしまいます。
Webページならそういう作りでしょうが、ゲームの場合はイチイチ通信して毎回データを取り直すまでも無いです。
ですのでXFLAGの設計では、アプリの起動時に必要なデータを全てサーバから取得して、Repositoryにキャッシュしておきます。
そして、例えばキャラの名前の変更をした時は、UseCaseがAPIを叩いてサーバ側のデータを更新して、さらにその後RepositoryのEntityキャッシュも更新する。という流れで処理をします。
つまり、XFLAGではScriptableObjectとかPlayerPrefsのようなローカルデータは一切使用しない想定という事です。(多分)
まあアプリにデータを埋め込んでしまうと、もしそのデータを変更したくなった時はストアのアプリを更新するしかなくなるので、一切アプリにデータを埋め込まない作りもアリだと思います。
ですから、XFLAGではDataStoreは存在せず、UseCaseが直接APIClientを叩くという作りになります。
CAFUにはDataStoreがありますが、CAFUはガチなソシャゲだけでなく、もっとカジュアルなゲーム(例えばサーバと通信しないローカルだけのゲーム)での使用も想定しているという事だと思います。
ユニットテスト
クラスの役割を分けて、ルールを決めたので、すでにテストは書きやすくなってます。
実際にテストを書くには、テストしたいクラスがアクセスする周りのクラスにインターフェース切っておく必要があります。
例えばCharacterDetailUseCaseをテストしたければ、CharacterDataRepositoryを直接触らないで、ICharacterDataRepositoryみたいなインターフェースを切ってワンクッション置いておく必要があります。
つまりCharacterDetailUseCaseにMockCharacterDataRepositoryとMockApiClientを注入してテストするという感じです。
いちいちインターフェース切ってMock版のクラスを実装するのは面倒くさそうですが、品質のためにも是非やってください。(クリーン目線)
EntityやModelは変数しか置いてないのでテストは要りません。
ちなみに本番アプリではDIコンテナでインターフェースに対して本番クラスをぶち込むように指定します。
どのクラスで何をテストすればいいのかとか、詳しい事は上でリンク張ったもんりぃさんのスライドを見れば書いてあります。
今回の設計だとUseCaseのクラスの数が相当膨れ上がりそうな気がするのですが、Presenterのテストを書くとなると全てのUseCaseにインターフェース切ってMockを作る作業量がヤバそうです。
XFLAGでは”Model層以下は基本的にユニットテストを書くようにしています”との事で、という事は逆に言えば、Presenterとかのプレゼンテーション層はテスト書いて無さげで直接UseCaseの実体を触ってるようです。
それでもいいんじゃないかな。
もんりぃさんのスライドでも”Presenterレイヤーはテスト書く価値低め”と書かれてます。
まあ、どこまでインターフェース切るのか?どこまでテストするのか?とかは、アプリの種類、目的、規模感によっても判断が変わってくるでしょう。
まとめ
CAFUとXFLAG Tech Noteを参考にして、クリーンなアウトゲーム設計について話をしました。
CAFUはオープンソースですし、サンプルゲームのリポジトリもあるので、実際に自分でアウトゲームを作る時は参考になるのではないでしょうか。
ただ、個人的な感想ですが、CAFUのサンプルゲームのソースを流し見しても、UniRxが多用されてたりしてて正直よく分かりませんでした。
XFLAGはプロジェクトが公開されてるわけではないですが、記事に書かれてるソースコードは分かりやすいです。
もし私がアウトゲームを作るなら、CAFUの設計とXFLAGのソースを参考にしながらとりあえず自分なりにゼロから書いてみる感じかなと思います。
しかし、こうしてしっかり調べてみても、やっぱりこのようなアウトゲーム的な考え方でインゲーム作るのって難しいよな…という気がします。
CAFUのサンプルゲームのインゲーム(もぐら叩き)は実際にこの設計に従って作られてるので、「実際にやれてる例がここにありますが?」と言われるとまあそうなんですが、例えばFPSを開発するとして、このアーキテクチャのどこがどうゲームプレイに当てはまるのか、想像できないですよね。
ただし、インゲームでもUIについては普通にアウトゲームと同じように作れるかと思います。
さて、今回の記事では単なるデータを扱う場合の話でしたが、実際のゲームでは、画像をDLしてキャッシュしたい時はどうするの?とか、AssetBundleはどうするの?みたいな話も出てくるかと思われます。
私もアウトゲーム設計は理論としては分かってきまものの、実戦経験はまだまだなので、ハッキリした事は言えませんが、今回の設計のRepositoryとかEntityの中に納まるかもしれませんし、そうでなくても応用してアレンジしていただく形になるかと思います。
今回の記事では割と抽象的な設計の話に留まって、イマイチ具体的な話に踏み込めませんでしたが、その内に実際に設計に従ってサンプルプロジェクト的な物を作ってみたいと思います。