ゲーム開発 に所謂なアプリケーション設計パターンをおいそれと適用するのは難しい

アドベントカレンダーでもなんでもない記事 0日目です。

Unityで長らくゲーム開発をやっているけれど、Web界隈などで色々と発達しているアプリケーション設計パターンをおいそれと持ち込めば良いわけではないと感じているので、それについて考えてみようと思う。

ここでいう設計パターンていうのは、たとえばUIとかをつくるフレームワークの競争で発達してきた MVC派生 や ReactとかのElmアーキテクチャに影響を受けたものたち、はたまた、Webサーバ(HTTPサーバ) を書くときに 「良し」とされている 、DDD的な考え方の上での、抽象レイヤと実装レイヤの分け方を教条化するクリーンアーキテキクチャとかなんかそういうの。

追記: ゲームでも「ドメインロジックとプレゼンテーションの分離」はした方が良いと思っている。全体としては狭義でのMVPとかは自分もやってる。

こういった者達は、先人のアイデアや言葉の整理 がよくできていてとてもおもしろいし、考え方はどこでも有用なのだけれど、出自はよく理解しておく必要がある。 UIやHTTPサーバは、どう考えても、「アプリケーションを書くにあたって全部一緒なこと」を一般化しやすい環境だと思う。

UI は結局のところ、(アプリケーショ開発者の視点では) 人間が操作したタイミングでしか 画面の状態が変わらないことが大半なので、サーバとかからやってきた抽象的なデータの粒度と、UIの状態の粒度がほぼほぼ一致している。

f:id:hadashia:20201223115626p:plain
UIでは、データと見た目の状態の粒度がだいたい一緒

だから「データを UIコンポネにどうマッピングするか?」という問題を一般化して、ここを簡単にすると生産性が上がる。フレームワークがここを考えてくれてここだけ自動化してくれる。

HTTP鯖は、開発者の視点では、おおざっぱに言えば HTTPリクエストを受けて HTTPレスポンスを返す関数を書くことが目的なので、状態管理についてあまり悩む必要はないし、HTTPもRDBも、標準化されていたり、安定している有り物の実装が使えるので、実装よりのパーツをいちからつくる必要がない。

f:id:hadashia:20201222144247p:plain
HTTP鯖は、有り物の実装に依存するものを切り離すと綺麗

HTTP鯖がアプリケーション毎に大きく違うのは、HTTPの仕様でもなく、I/O先の実装でもなく、もっと抽象的な真ん中のところだけが違うので、「レイヤをきってアプリ固有の部分を隔離」していく構成は保守性が高いですよね? と言われるとそんな雰囲気はある。

ところがゲーム開発の場合は、こういった特徴を一般化してフレームワークに追いやれるか? というと そういうわけでもない。

「ゲーム」と一口に言っても色々で、アクションゲーム的なものとRPG的なものではかなり中身の性格は違っているし、 ゲームでは「ドメインロジックが主」というよりもむしろ、「プロジェクト固有のViewコンポネを自前でつくっていく」ことの方にどう考えても重みがある。

f:id:hadashia:20201222145721g:plain
マリオのgif

たとえば今僕は、↑に 、マリオのgif画像のimgタグを貼ってみたのだが、このときに与えたデータは「src="{画像のURL}"」ここだけ。 imgタグという既製品のViewコンポーネントは、外から見たら「画像URLを与えるとそのとおりに画像を出してくれる」という、データ → 見た目 の粒度が 1:1 のものとして扱うことができる。UI のプログラミングはだいたいこのマッピングを考えていく。

だけども、gifがパラパラ漫画よろしくアニメーションしているということは、imgタグの実装の内部では、「再生されてからの経過時間」という、もっと細かい粒度のデータの管理がされていて、「経過時間に対応する現在のフレームを表示する」という複雑な制御がおこなわれているはずだ。外からは見えないだけで。

ゲ開発で、そのゲーム固有の色々なものをつくる、ということは、imgタグそのものを実装する、という姿が近いとおもっている。つまり、人間に理解しやすい抽象化されたデータよりも、もっと細かくて複雑なフレーム毎の見た目の変化をプログラムしていくことになることが多い。

Viewコンポネの実装の詳細はいつも複雑なので、それを外から見るともっと抽象的で扱いやすいインターフェイスにしていく必要がある(imgタグしかり) のはゲームも別に変わらない。違うのは 部品そのものを自作することがゲーム開発の主な関心事になってくること。HTMLであれば基本は 標準化されたパーツを組み合わせて集約させたものを扱うのだけど。

( ちなみに、gifアニメがわかりやすい例かなと思って出してみましたが、スプライトアニメーションをその都度ゲームごとに実装するという意味ではなくて、Viewコンポネの内部に閉じ込めておいて良い細かい遷移を自作する必要があるよねという主張です)

f:id:hadashia:20201223115652p:plain
ゲームではレイヤ毎に粒度が違う部品を自作

ゲームでは、部品そのものを自作することが多い関係上、Viewコンポネに対してのデータのマッピングを土台にする MVVM とか React のようなものを爆誕させても、その先のViewへの適用方法を自前で書く必要がでてきたりするのでかなりの手間になる。 また、Viewあたりの部品を自前で書くことに比重がある関係上、ドメインモデルだけの保守性を上げても全体として保守性が上がるかは疑問だし、 レイヤ間の依存関係を完全分離する(絶対に抽象のみにしか依存させない)ことは意義に対しての高い買い物になりがち。

クリーンアーキテクチャ

人々がいつものように、

みたいなあたり前のコードを書いていたところに、ある日、ボブおじさんがやってきて、

と、制御の方向はなにも変えなくて良いんだけど、依存方向だけこのように「逆」になるんだぜ。みたいなことを言いに来た。という事件のことをクリーンアーキテクチャと呼ぶ。

Clean Coder Blog

制御フローやレイヤの分け方については特に何も主張しておらず、「依存性ルール」だけに注目してそれを図解してみた、という部分だけが骨子で、そこに主な意味がある。DDD的な構成が全くわからない人はクリーンアーキテクチャを調べるのではなくてDDDを調べてみるべき。そもそもの全体の制御フローをどうすべきかの指針がまずあって、その上で「依存性ルール」に反しているところを「逆転」させる、これがこの理論の使い方。

巷でみかけるよくある混乱:

  • 全体の制御フローや所有関係や分離方法になにも指針がないなかで「依存性ルール」の図だけ眺めて、「クリーンアーキテクチャって謎」となっている
  • 元記事では、このような、所有関係に対して 「依存だけ逆転」させる手法として、「インターフェイスだけ所有させる」という、OOPの古典的な方法のみですべての例を構成しているので、なんだかレイヤ境界すべてにインターフェイスを切ったり型を別でつくっとけば良いんしょ?てなっている

これはどちらも勘違いです。

インターフェイスをつかった「逆転」はたとえば以下のようなことになる。

人々がいつものように書いていた状態:

// ドメイン

class AnimalService
{
    readonly SqlAnimalRepository repository;

    public void DoSomething()
    {
        var doggo = repository.Find("犬");
        // ... なんかやる
    }    
}
// 実装依存部分

class SqlAnimalRepository
{
    public Animal Find(string name) => ...
}

依存性だけを逆転させた場合:

// ドメイン

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(的な)
  • 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」というのは、遷移する状態全てではなくて、「アプリケーション固有の状態」のことだからである。

しかし、なんか強力なフレームワークとか足回りを前提とした設計パターンをあてはめることはゲームでは単に手間になる。

MVP 的な構成をまずつくって、

  • M と V の世界は完全に分ける。
  • M のところは DDD的なレイヤードアーキテクチャ的なのをつくるなりなんなり責任分割してく
  • V コンポネ自体は、親子関係やら 細かすぎる複雑な制御ならなんやら、外から隠蔽してよいところは隠蔽してブラックボックスにする。(マリオのgifのように)
  • Presenter/Controller は それらの世界をつなげる

これだけをベースにプロジェクトに合わせて分け方を変えていくことを基本すれば十分であろう。

抽象レイヤを多重にするというレイヤード(クリーン)アーキテクチャの考え方は、抽象データのやりとり部分のみに適用させれば良いし、

MVC派生の、「ドメインとプレゼンテーションの分離」は全体に適用させれば良い。

UI = f(state) という考え方の、外から見た場合のViewの状態遷移を隠蔽するのもくそイケているであろう。

ただしViewへのマッピングの自動化はあきらめた方が生産性も保守性も柔軟性も高い。

そんなところじゃないでしょうか。