ゲーム開発に所謂なアプリケーション設計パターンを適用するのは難しい
ゲーム開発ひいてはクライアントサイドの開発において「クリーン」かどうかは正直けっこうどうでもよく、設計すべき一番のポイントは「制御フロー」にあります。
じゃあ具体的にどういう設計がいいのか、ていうのは、やはりjs界隈みたいにフレームワークになっていないとなかなか伝わらないようです。そういうのもそのうちつくってみたいなあと最近思ってます。
以下、以前ブログに書いたものの転載。
--
Unityで長らくゲーム開発をやっているけれど、Web界隈などで色々と発達しているアプリケーション設計パターンをおいそれと持ち込めば良いわけではないと感じているので、それについて考えてみようと思う。
ここでいう設計パターンていうのは、たとえばUIとかをつくるフレームワークの競争で発達してきた MVC派生 や ReactとかのElmアーキテクチャに影響を受けたものたち、はたまた、Webサーバ(HTTPサーバ) を書くときに 「良し」とされている 、DDD的な考え方の上での、抽象レイヤと実装レイヤの分け方を教条化するクリーンアーキテキクチャとかなんかそういうの。
こういったもの達は、先人のアイデアや言葉の整理 がよくできていてとてもおもしろいし、考え方はどこでも有用なのだけれど、出自はよく理解しておく必要がある。 UIやHTTPサーバは、どう考えても、「アプリケーションを書くにあたって全部一緒なこと」を一般化しやすい環境だと思う。
UI は結局のところ、(アプリケーショ開発者の視点では) 人間が操作したタイミングでしか 画面の状態が変わらないことが大半なので、サーバとかからやってきた抽象的なデータの粒度と、UIの状態の粒度がほぼほぼ一致している。
https://gyazo.com/bd83dbf2e68f34001b4b112eda3ebd5b
UIでは、データと見た目の状態の粒度がだいたい一緒
だから「データを UIコンポネにどうマッピングするか?」という問題を一般化して、ここを簡単にすると生産性が上がる。フレームワークがここを考えてくれてここだけ自動化してくれる。
HTTP鯖は、開発者の視点では、おおざっぱに言えば HTTPリクエストを受けて HTTPレスポンスを返す関数を書くことが目的なので、状態管理についてあまり悩む必要はないし、HTTPもRDBも、標準化されていたり、安定している有り物の実装が使えるので、実装よりのパーツをいちからつくる必要がない。
https://gyazo.com/17a26028e8d09aa7e6dac36f9bad3569
HTTP鯖は、有り物の実装に依存するものを切り離すと綺麗
HTTP鯖がアプリケーション毎に大きく違うのは、HTTPの仕様でもなく、I/O先の実装でもなく、もっと抽象的な真ん中のところだけが違うので、「レイヤをきってアプリ固有の部分を隔離」していく構成は保守性が高いですよね? と言われるとそんな雰囲気はある。
ところがゲーム開発の場合は、こういった特徴を一般化してフレームワークに追いやれるか? というと そういうわけでもない。
「ゲーム」と一口に言っても色々で、アクションゲーム的なものとRPG的なものではかなり中身の性格は違っているし、 ゲームでは「ドメインロジックが主」というよりもむしろ、「プロジェクト固有のViewコンポネを自前でつくっていく」ことの方にどう考えても重みがある。
https://gyazo.com/bf7bbdec95b0c1fcf80bf2fef47e9337
マリオのgif
たとえば今僕は、↑に 、マリオのgif画像のimgタグを貼ってみたのだが、このときに与えたデータは「src="{画像のURL}"」ここだけ。 imgタグという既製品のViewコンポーネントは、外から見たら「画像URLを与えるとそのとおりに画像を出してくれる」という、データ → 見た目 の粒度が 1:1 のものとして扱うことができる。UI のプログラミングはだいたいこのマッピングを考えていく。
だけども、gifがパラパラ漫画よろしくアニメーションしているということは、imgタグの実装の内部では、「再生されてからの経過時間」という、もっと細かい粒度のデータの管理がされていて、「経過時間に対応する現在のフレームを表示する」という複雑な制御がおこなわれているはずだ。外からは見えないだけで。
ゲ開発で、そのゲーム固有の色々なものをつくる、ということは、imgタグそのものを実装する、という姿が近いとおもっている。つまり、人間に理解しやすい抽象化されたデータよりも、もっと細かくて複雑なフレーム毎の見た目の変化をプログラムしていくことになることが多い。
Viewコンポネの実装の詳細はいつも複雑なので、それを外から見るともっと抽象的で扱いやすいインターフェイスにしていく必要がある(imgタグしかり) のはゲームも別に変わらない。違うのは 部品そのものを自作することがゲーム開発の主な関心事になってくること。HTMLであれば基本は 標準化されたパーツを組み合わせて集約させたものを扱うのだけど。
https://gyazo.com/e0c607ba4325f167e8f4bd50bf6fd828
ゲームではレイヤ毎に粒度が違う部品を自作
ゲームでは、部品そのものを自作することが多い関係上、Viewコンポネに対してのデータのマッピングを土台にする MVVM とか React のようなものを爆誕させても、その先のViewへの適用方法を自前で書く必要がでてきたりするのでかなりの手間になる。 また、Viewあたりの部品を自前で書くことに比重がある関係上、ドメインモデルだけの保守性を上げても全体として保守性が上がるかは疑問だし、 レイヤ間の依存関係を完全分離する(絶対に抽象のみにしか依存させない)ことは意義に対しての高い買い物になりがち。
クリーンアーキテクチャ
人々がいつものように、
ドメインロジック → DB操作
みたいなあたり前のコードを書いていたところに、ある日、ボブおじさんがやってきて、
制御フロー : ドメインロジック → DB操作
依存関係: ドメインロジック ← DB操作
と、制御の方向はなにも変えなくて良いんだけど、依存方向だけこのように「逆」になるんだぜ。みたいなことを言いに来た。という事件のことをクリーンアーキテクチャと呼ぶ。
クリーンアーキテクチャの骨子そこだけなので、このURLだけ理解しておけば充分だと思う。
制御フローやレイヤの分け方については特に何も主張しておらず、「依存性ルール」だけに注目してそれを図解してみた、という部分だけが骨子で、そこに主な意味がある。DDD的な構成が全くわからない人はクリーンアーキテクチャを調べるのではなくてDDDを調べてみるべき。そもそもの全体の制御フローをどうすべきかの指針がまずあって、その上で「依存性ルール」に反しているところを「逆転」させる、これがこの理論の使い方。
「ユースケース」みたいな言葉をクラス名にしているコードは、その辺をよく理解していない可能性がある。
巷でみかけるよくある混乱:
全体の制御フローや所有関係や分離方法になにも指針がないなかで「依存性ルール」の図だけ眺めて、「クリーンアーキテクチャって謎」となっている
元記事では、このような、所有関係に対して 「依存だけ逆転」させる手法として、「インターフェイスだけ所有させる」という、OOPの古典的な方法のみですべての例を構成しているので、なんだかレイヤ境界すべてにインターフェイスを切ったり型を別でつくっとけば良いんしょ?てなっている
これはどちらも勘違いです。
インターフェイスをつかった「逆転」はたとえば以下のようなことになる。
人々がいつものように書いていた状態:
code:cs
// ドメイン
class AnimalService
{
readonly SqlAnimalRepository repository;
public void DoSomething()
{
var doggo = repository.Find("犬");
// ... なんかやる
}
}
// 実装依存部分
class SqlAnimalRepository
{
public Animal Find(string name) => ...
}
依存性だけを逆転させた場合:
code:cs
// ドメイン
interface IAnimalRepository
{
Animal Find(string name);
}
class AnimalService
{
readonly IAnimalRepository repository;
public void DoSomething()
{
var doggo = repository.Find("犬");
// ... なんかやる
}
}
// 実装依存部分
class SqlAnimalRepository : IAnimalRepository
{
public Animal Find(string name) => ...
}
制御フロー : AnimalService → AnimalRepository
依存関係: IAnimalRepository ← AnimalRepository
となったので、逆転ができた。 という感じで、抽象的なレイヤの方に 実装してほしいインターフェイスだけ置く。というのが クリーンアーキテクチャまわりからの提案なのだけど、これ自体は普通に汎用性が高い考え方だとおもう。
ただし、Viewが何かを直接所有しないようにする方法とかは、イベントとか使えばそれで終わりなので、もうちょい最新のプログラミング機能つかえばええやんいいかんげんにせえよボブ。とは誰もが内に秘めている感想だろう。
ゲーム開発ではどうか、と考えたとき、ゲーム開発で Viewレイヤとの依存を(dllレベルで)完全に切り離すことが本当に保守性につながるかはよく考えたほうが良くて、Unityを使っているならUnityEngineに依存した自作コードが8割なので、それを分離したからといってその8割の保守性の高まりは感じないし、 自作の実装依存パーツを大量につくっているなか、さらにそれぞれに全てインターフェイスとかを切っていくのは単に手間である。
依存をどこまで綺麗にするかを完璧にやっても割に合わなすぎるので、もっと軽いノリのバランスに普通はなるであろう。
クリーンアーキテクチャは、エンタープライズjavaウェブアプリケーションのような、「実装依存」のパーツが常にありものを使用できる環境において、人々に依存性ルールを啓蒙するために整理された理論なのだから。
ちなみに、上記のblog中では、
Controller → 入力に反応して ドメインロジックに指令を出す
Presenter → ドメインロジックの変化に反応してViewに指令を出す
という言葉の定義がされているようだが、これは GUI文脈の MVC/MVP の違いみたいな話とはまた別なので さらなる言葉の多義性を生んだ。混乱を呼びすぎだぞボブ。
MVVM
MVVMのような設計パターンは、どちらかというと設計パターンというよりも、まずはじめに Viewコンポネの自動化バインディングが爆誕し、その上での構成に名前がついた。という印象がある。
RxSwift 界隈とかのように、必ずしも全自動バインディングがない中でも、
Model
ViewModel
ドメインモデルを非同期に整形/読み換えて Viewよりのデータをつくる
Controller(的な)
ViewModel をどこのViewにマッピングするか仲介する
View
みたいなやつは一般にはよくやられているようだ。
ただ、ゲームにおいては、「Viewのためにデータを加工する」なViewModel があったところで、本質的にはゲームは 「状態をViewの状態へ写す」というモデル化をしきれないケースがあまりにも多い気がするし (UIじゃないから)、なによりも Viewのパーツを自前でつくっている関係上、中心的な作業とは別にさらに抽象的なレイヤを重ねることになること、 ViewModelをかませて そこからViewのマッピングの面倒も見ないといけなくなること は、本当に良い買い物なのか考える必要がある。
もう一つ気になる点は、制御フローが双方向になるという点。あるイベントからものすごくたくさんのオブジェクトが変更される可能性のあるゲームというソフトウェアで双方向バインディングってすっごく複雑になりません?
Elmアーキテクチャ派生
React や Flutter や、SwiftUI、あたらしい.NET の MAUI とかは、MVVMという考え方を投げ捨て、 Viewっていうのは「View = f(state)」という純粋関数なのだ、と言い出した。外からは状態のかたまりではなく、「状態を引数に与えたらなんかその通りになる何か」と読み替えるとめっちゃ全体がシンプルになるっす。という考え方が持ち込まれたわけである。
Webフロントエンドやアプリ とかの、UIをプログラミングする環境では、現在このやりかたが完全に主流というかこれまでのものより改善した結果がこれだよね感を出している。
個人的にもReact とかめっちゃ良いと思うのだけど、MVVMと同じような理由でゲーム用のこういうフレームワークが出てくることはあまりないだろう (UIは別としても)
ただし、この「制御フローが単方向」という制約はとても使えるパターンだと感ずるところ。
落としどころ
「ドメインロジックとプレゼンテーションの分離」はゲームであっても普通にやっといた方が良いであろう。抽象的なデータと Viewの境界は疎結合になっていないとまじで困る。 「キャラクターのデータ」 と、「画面に適用させたい対象」との関連性は、1:1 ではないし、まだ画面には出てないけどデータを参照したい、みたいなケースは死ぬほどあるからである。 つまるところMVCが言っている「Model」というのは、遷移する状態全てではなくて、「アプリケーション固有の状態」のことだからである。
しかし、なんか強力なフレームワークとか足回りを前提とした設計パターンをあてはめることはゲームでは単に手間になる。
Viewへのマッピングの自動化はあきらめた方が生産性も保守性も柔軟性も高い。
重要なことは、MとVを分けること。それからイベント発火からの制御フローを明確にすること。