これを扱うユースケースを想定します
FEHはゲームなので、ゲームとしての遊び方を想定するわけですね。
FEHのゲーム画面は次のような手順で進みます。
- マップ上にユニットを初期配置する
- ユーザのターンが始まる
- ユーザがユニットを操作する
- 操作が終わったらCPUのターンが始まる
- CPUがユニットを操作する
- 操作が終わったらユーザのターンが始まる
- ...
- 操作中に勝利・敗北条件を達成した場合、ゲームに勝利/敗北する(勝利条件は敵を全員倒す、や規定ターン数生存する、など)
状態遷移図にしたほうが早いですね!
これがモデルの一部になるか?はちょっと難しい問題です。なぜなら構造としてはinvariant なのですがユーザ/相手のターンにはインタラクションが発生するからです。
インタラクションはモデルの一部ではありません。正確には個々のインタラクションはモデルの一部(モデルに対する操作)ですが、インタラクションは管理しなければならず、インタラクションの管理はユーザや物理的インタフェースに依存するからです。例えば単なる将棋の対戦アプリならばユーザをモデル内に含み交互に操作することもできるでしょうが、局面を巻き戻した自分の駒と相手の駒をそっくり取り換えたりする(将棋を教えるときにはわりとあります)こともあるかもしれず、そういったことはユーザが何を目的としてモデルを操作するかによる、つまりユーザに依存しています。
また、ゲームの開始・中断・再開・終了はモデルの管理下ではありません。例えばゲーム開始前に駒を並べる手順なんてものは決められていないのでモデルになりませんし、ゲームを中断するときにどうするかはユーザ同士の取り決めに依存するでしょう。プロの対局なら封じ手を記録する必要がありますが友人同士で遊んでいるなら盤面を取っておけばいいだけですし、勝負をつけずにゲームを破棄することだってあるでしょう。
盤面を取っておくにしろ、物理的にそのままにしておくこともあれば盤面を記録することもあるでしょう。
というわけで、簡単なのは「ユーザのインタラクション(とか中断に備えての保存)」をモデルから切り離しユースケースにしてしまうということです。
ユースケースは…Gameでいいでしょう。とりあえずこのプログラムは遊ぶためのものなので。
なお、同じモデルを利用する他のユースケースも存在します。例えばゲームのシミュレータを作るとか。
ゲームのシミュレータは戦力の試算に役に立ちますし、ゲームを作る側にとってはバランスを保つために死活問題となります。
さてこれらのユースケースを実際に動かすために必要なもの、といえばもちろんユーザの操作を伝えるためのUIです。
UIはユースケースに依存します。例えばゲームのUIとシミュレータのUIは全く違ったものになるでしょう。
また、UIは実行環境にも依存します。一般的には環境が用意したUI用のAPIへアクセスする=依存することで構成されているからです。今回作っているGameはAndroid上のアプリなのでLibGDXという実行環境に依存します。一方、SimulatorはただのAndroidアプリなので一般的なAndroid用UIライブラリに依存しています。
実際にはユーザの操作はライブラリを介してゲームやシミュレータに伝えられます。
Simulatorもあるときのクラス図も同時に描けます。
同時に描けるだけで意味はあんまし無くてSim側はこんなこともできるよってだけの話なので次からは描きません。
盤に対する操作を作る
さて、GameUIとなんだか適当な名前で呼んでいるこいつは具体的には何でしょうか?
こいつは現時点では「ゲームを管理するもの」「盤面を操作するもの」です。まずこの二つは管理対象が別なので別クラスに分けます。ゲームを管理するのはゲームUIでいいとして盤面を操作するものはおそらく盤面に依存するでしょう。なのでこれはもう盤面UIですね。
LibGDXは手抜きでクラスとして書いていましたが、そろそろ具体的にClickListenerを使う事にしましょう。
LibGDXのClickListenerを継承した盤UIオブジェクトを作ります。盤UIは盤の形状をした画像を表示し、画面へのタップ・ドラッグを盤への操作に変換します。
同様にゲーム自体を制御するGameUIを操作アイコンや設定アイコン、時には別画面やウィンドウとして作り配置します。
CleanArchitectureに対応させた場合、これはUIなのかコントローラなのか難しいところですが、LibGDXに依存している=外部へ依存しているのでUIでありコントローラではないことにしておきます。LibGDXの渡してくるパラメータをこちらでコントロールできませんし、仕様が変わったときに影響を受けるからです。
ここではUIにコントローラ用のコードが混ざっていますが、例えばLibGDXのClickListenerが気に入らないときに「自前のClickListenerを作ってLibGDXのタッチ位置を読み取る低レベルAPIからそれを叩くようにする」ときはそのClickListenerとそれを実装したオブジェクトは立派なコントローラ(でありそれを叩く低レベルAPIを利用したコードはUI)でしょう。
余談ですがこのClickListenerなどFWの提供するインタフェースを継承したオブジェクトは、CleanArchitectureの図ではUIですが(特に昔の)MVCではコントローラです。
例えばWebアプリのFWでMVCなどで開発するときにコントローラを作りますが、初代Strutsなど昔のFWではFWの提供するインタフェースを継承したActionとかを作ってコントローラと呼んでいました。Action自体をテストするのはHttpRequestを用意・処理するのが大変なので、直接コードを書かずにHttpRequestを解析した結果をパラメータとして受け取るメソッドを別に書いていた、つまりUIとコントローラを分けて書いていた人も多いのではないでしょうか?
このとき、盤UIへの操作、特に「動かす」については「動かしてる途中」が存在することがあります。これについては長くなるので以前書いた記事をご覧ください。
この「移動中」は操作する人により存在することもしないこともある、つまりユースケースの一部です。例えばCPUが操作してる最中とか、人でもGameUIをコマンドラインにするとか、いっそ物理的なコマを操作してその結果をGameに渡すなんて時には移動中は存在しません。ただ「移動中のモデル」はユーザに依存しないため、一度作ってしまったら盤モデルに統合してしまってよいかと思います。
これで盤を操作することができるようになりました!
でも盤を操作した結果がどこにもありません!というわけで盤の状況や駒の動きをユーザに伝える何かが必要となります。しかし、盤は何物にも依存しないので盤から直接何かに伝えるわけにはいきません。なので駒を「Gameの駒」とし、移動結果や戦闘結果をイベントとして扱い、イベントを受け取るListener/Observerを作成し、ゲーム側で受け取ります。
Listenerはイベントを見て画面表示、ゲームなのでキャラのアニメーションを作成します。LibGDXはActorという仕組みで2Dのキャラクターを管理しており、立ち絵や攻撃モーションを表示させることができます。なおGameの駒への操作は今は役に立ちませんが操作対象が盤ではなく駒の時に必要となります。例えば枡内に複数の駒が存在し得るときとか枡自体が無いときとか。
CleanArchitectureに対応させる
とこうなります。外部から内部へ依存していることがわかるでしょうか?
Controllersが無かったりPresentersあたりがしょぼい(実はあまり作りこんでない)ですがここはWebアプリにするときは別クラスになったり、LibGDXが無いプラットフォームでに対応するときはモーション管理はPresenterで共通化しモーション作成はUIでそれぞれプラットフォームに合わせた実装にするなんてときに効いてくるはずです。
なお、Clean ArchitectureではUI/Controllerがアクセスする先はUseCaseがinterfaceを用意することになっていますが、このケースではアプリのinterfaceはUseCaseが用意しますが将棋盤のinterfaceは将棋盤が、つまりEntitiesが用意しています。これは将棋盤が「動きだけ」をモデル化したものであり盤UIはその動きだけを指示する、つまりUseCaseを挟む必要が無いからです。気になるなら盤への操作をユースケースとして切り離すこともできます。
なおこの(LibGDXに依存した)盤UIはEntities(ゲーム)に依存していないため、ゲーム内容にかかわらずLibGDXで将棋盤上のゲームを作る際には何度でも再利用できます。
モジュール構成と依存(GradleのDependency)
さて、依存が一方通行という事はビルドのDependencyも一方通行という事です。
現在Android上のモジュールはこうなってます。
・android:Androidアプリとしての起動用コードだけ(LibGDXが生成)
・battlefield:Entities。Gameの駒とEventListener
・board:Entities。盤モデル・駒など盤上の要素
・boardlibgdx:UI。盤モデルへのアクセスを提供するLibGDXコード
・core:UI。アプリに依存するLibGDXコード(表示とか)とアプリの初期化
・fehbs:Androidアプリとして作成したシミュレータ。今回は関係ない。
・fehsbattlemodel:駒の能力。ファイアーエムブレムヒーローズの戦闘モデル
・repos:リポジトリ。シミュレータから使っているがゲームからはまだ未使用。
それぞれの build.gradle からKotlinやJUnitへの依存を消したものは
board:
dependencies {
}
fehsbattlemodel:
dependencies {
}
battlefield:
dependencies {
implementation project(":board")
implementation project(":fehsbattlemodel")
}
boardlibgdx:
dependencies {
implementation project(":board")
implementation "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
}
core:
dependencies {
implementation project(":board")
implementation project(":boardlibgdx")
implementation project(":battlefield")
implementation project(":fehsbattlemodel")
implementation "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
}
実はGradleよく知らないのでDependenciesが具体的に何をやってるのかはわかってないのですが依存は整理されてますね!まぁもっとCoreからゲーム本体を引っぺがさないといけないのですが!
重要なのは依存が外から中へ向かう事
ゲーム開発「入門」において鬼門になるのはゲームの動きのロジックと画面表示のロジックが入り混じり、分離できなくなることです。
例えば「あるキャラの実際の位置」を変えるロジックと「画面上の位置」を変えるロジックとが混ざりあうことがあり、その結果キャラが他のキャラや地形をすり抜けることがあります。
開発手順としても分離されてないゲームは実際に動かすしかテストする方法がありませんので遅々として開発が進まないなんてこともしょっちゅうです。
プロのゲームプログラマならきっとうまく切り分けてるでしょうし関係ないのでしょう。多分。
ですが、世の中のゲーム作成やツールの入門記事はだいたいゲームのロジックと表示位置のロジックが入り混じっています!1画面しかないとかタップだけとかシンプルなゲームを作ってるうちは問題になりませんが、一定規模の大きさのゲームを作るときや分業をするときに大問題になります(なりました)。
このように依存を一方通行にすることで、より外側のコードが内側のコードを破壊することを防ぐことができます。例えば中心にある盤上の移動を管理するコードは外側で画面上の位置を操作しても壊れることはありません。表示はおかしくなっても操作やゲーム性には影響が出ないはずです。
おまけ・Unityではどうなるか?
ところで今までKotlin+LibGDXでゲームを設計する話をしてきましたが、個人がゲームを作るなら普通Unityですよね。ではこの設計を(将棋盤ではなく3Dモデルを扱うゲームとして)Unityに適用すると…こうなるかと思います。Unitiyのクラス名とかは知らないのでなんとなくこんな感じって表記ですが。
将棋盤上で移動や他の駒との衝突判定をするのと同じことをUnityの3D空間はやってくれるわけですね!自分で作るのはユースケース(Unityではゲーム進行を管理する系のコード)とController(Unityのオブジェクト)とEntities(ただしほとんどの入門記事ではオブジェクト内に直接書かれる)とモーション管理(これもオブジェクト内直接が多い。Unityがリソースを管理してくれる仕組みがあるはずだけど詳しく知らない)です。自分で書くのはゲームの内容だけで細かいことはもう全部Unityでいいんじゃないかなみたいな!
とはいえ、UnitiyオブジェクトはUIかつコントローラかつユースケースからの影響も受けそうだし画面上のオブジェクトでもあるのでCleanArchitectureそのままってわけにはいかなさそうですね。ゲーム内容に依存する部分をDDDにするくらいがちょうどよさそうかな?
まぁ私はUnityはかじった程度でしか知らないので細かいところは間違ってるでしょうし全然違うかもしれないので指摘もらえたら修正するなりこの段落を消すなりします。
※Unityには将棋盤の枡のような「物理的に衝突しない移動制限」や「経路探索」はなく自分で作る必要があるので多分2Dは3D空間よりむしろ手間がかかると思われます。まぁ私が作った将棋盤モデルは(当然)言語に依存しないため移植したらすぐでしょうが。