『Game Programming Patterns ― ソフトウェア開発の問題解決メニュー』を読み終わったのでその感想とメモです。

(Amazon へのリンクです)

読んだ動機

コード設計に関する本は、他にもデザインパターンやリファクタリングの本なども思い浮かんだのですが、デザインパターンは具体的な課題があって、それを解決するために見るカタログのようなものだと思っていて、適用範囲が限定的だと感じています。そして何よりデザインパターンの本はつまらないので、それよりはもっと汎用的に適用できる考え方を学べる本が良いと考えました。一方、リファクタリングの本も今回の質問に対する答えとしてはやや具体的すぎると感じたので見送りました (実はリファクタリングの本はまともに読んだことがないので実際は違うかもしれません。そのうち読みたい)。

「プリンシプル オブ プログラミング」読了 - nhiroki’s weblog

引用文では「リファクタリングの本」と言ってるけど、実際にはデザインパターンの本もまともに読んだことがなかったので、今回読みました。

感想・まとめ

  • 面白い、かつゲームに限らず汎用的に使える内容だった。「デザインパターンの本はつまらない」と言ってしまって申し訳ない!
  • この本の良い所はゲームという取っ付き易い題材でパターンが紹介されていることと、UML を使わずにパターンが解説されてることだと思う。よくデザインパターン系の本や記事でクラス図が出てくるけど、あれ見ても全然頭に入ってこないし、最初からコード見せてくれた方が理解しやすいと思う。
  • この本で紹介されているパターンに一貫しているのは、いかにしてゲームのビヘイビアをコードからデータに移動するかということ。著者はこれを「データ駆動型」と呼んでいる。これによりゲームデータの修正によるコード変更をなくし、生産性を高めることができる。前にゲームエンジンを作っている人の話を聞いたときに「ゲームデータはほぼ JSON です」みたいな話があまりピンと来なかったんだけど、本書を読んでなんとなく分かってきた。

読書メモ on Twitter

読書メモ

本書で紹介されているパターンを一覧にするとこんな感じ。

パターン一覧

Part II は GoF 本で紹介されているパターンの見直し、Part III は処理の流れに関するパターン、Part IV はプログラムの振る舞いを変えるためのパターン、Part V はいろいろなものを時間的・空間的に分離するためのパターン、そして Part VI は処理を最適化するためのパターンが紹介されている。

Part I イントロダクション

第 1 章 アーキテクチャ、実行速度、ゲーム

本章ではまだデザインパターンには踏み込んでないけど、ゲームに限らずソフトウェア開発に関する筆者の優れた知見が述べられてて参考になる。例えば、

良いデザインとは、何かを変更した時にまるでプログラム全体がその変更を予想して作られていたかのように思えるデザイン

本書は全体を通してそのようなデザインをするためのパターンが紹介されている。ただし最後の最適化のパートは例外で、変更に強いデザインを多少犠牲にしてでも最適化を実現するためのパターンが紹介されている。

本章では他には「プログラミングの面白さはその複雑性にある」という話がなるほど感が強かった。本質的に複雑だからこそ単純さを追い求める楽しさがあるのかもしれない。

Part II デザインパターン再訪

第 2 章 コマンド (Command)

四人組も後に「コマンドパターンは、コールバックのオブジェクト指向における代替品である」と語っています。

ある意味では、コマンドパターンはクロージャを持たない言語でクロージャをエミュレートする方法なのです。

コマンドがクロージャの代替品、という見方はなるほどと思った。

第 3 章 フライウェイト (Flyweight)

インスタンス依存のデータとインスタンス非依存のデータに分離して、非依存なデータはインスタンス間で共有するパターン。

第 4 章 オブサーバ (Observer)

オブザーバパターンの仕組み・注意点・使い所がとても分かりやすくまとまってて、是非このパターンを使う前に読んでほしい。コードレビューしてるとオブザーバの使い方が未熟で自分の足を撃ち抜いてるのを時々見かけるので・・・

無効リスナー問題 (Lapsed Listener Problem) って知らなかった。不必要なリスナーが残り続けることでメモリリークが発生する問題。

第 5 章 プロトタイプ (Prototype)

プロトタイプパターン、言語処理系におけるプロトタイプ (Self と JavaScript)、そして JSON によるゲームデータの管理におけるプロトタイプと委任について。データ管理におけるプロトタイプについてはそういう視点が自分にはなかったので学びがあった。

筆者曰く、プロトタイプパターンが最善の解決法だと感じられたことは一度もなかったらしいんだけど、その理由があまり明確に述べられていないように感じた。後の節でプロトタイプベースの言語は使いにくいという話はあるけど。

第 6 章 シングルトン (Singleton)

本章ではシングルトンパターンを紹介しつつ、それをいかに避けるか解説している。シングルトンはインスタンスを一つに制限し、グローバルアクセスを提供するという二つのことをしているので、本当に欲しいのはどちらかを意識すべき。可能ならばシングルトンより static クラスを使うべきだし、グローバルアクセスが欲しければ基底クラスの static メンバにするとかサービスロケータのパターンを使うべき、とのこと。他にも色々解説されてる。

グローバル変数が引き起こす問題のリストをざっと見れば、シングルトンパターンがそのいずれも解決してくれないことに気付きます。理由はシングルトンがグローバル変数だからです――ただクラスにカプセル化されているだけです。

第 7 章 ステート (State)

有限状態機械、並行状態機械、階層的状態機械、プッシュダウンオートマトン、など。わりと単純な状態管理手法の紹介。

ステートパターンの最終目標は、ある状態に関する振る舞いとデータを単一のデータ構造にカプセル化することです。

自分が使う範囲だと、カプセル化までしなくてもとりあえずステートを列挙型にするだけでだいぶコードがシンプルになる。ブール型でステート管理するのが 2, 3 個くらいになったら列挙型を使うことを検討するようにしてる。

「ゲーム AI の複雑なステート管理がどうなってるのか気になる」というツイートをしたら @chibi_e さんが本を紹介してくれました。

Part III シーケンスのパターン

第 8 章 ダブルバッファ (Double Buffer)

ダブルバッファの仕組み、バッファの粒度、切り替え方、画像以外への応用など。ダブルバッファというと、ちらつきなく画像を切り替える手法という印象が強かったけど、それ以外の応用例も載っていて興味深かった。

第 9 章 ゲームループ (Game Loop)

ゲームループの仕組み、実時間とゲーム時間を合わせる方法、プラットフォームのイベントループやゲームエンジンのループとの協調、消費電力の削減など。ブラウザのイベントループの話もちょろっと出てきて親近感を覚えた。

ゲーム作りは人間の本能のようなものです。計算が可能な機械をつくるたびに、その機械で動くゲームが作られてきたのですから。

至言だ。

第 10 章 更新メソッド (Update Method)

ゲームループとオブジェクトのビヘイビアの分離、update() 呼び出し中のオブジェクトリストの変更、時間進行が可変の場合への対応、update() の実装場所など。

Part IV ビヘイビアのパターン

第 11 章 バイトコード (Bytecode)

呪文や敵データなどをゲームエンジンを触らずに編集するために仮想マシンを導入する話。バイトコードの仕組みと実装、スタックマシンとレジスタマシン、VM 内での値の保持方法 (タグ付き形式など)、バイトコードの生成ツールなど。

ゲームデータを外部から与えるために言語処理系を組み込むっていう発想がなかったので面白かった。ゲームエンジンにスクリプト言語が組み込まれてるのはそういうことなのか。

第 12 章 サブクラスサンドボックス (Subclass Sandbox)

派生クラスが多い基底クラスがあるときに、派生クラスとシステムコンポーネントの結合度を下げてコードを共有する手法について。ブラウザ内では継承が多用されてるので、このパターンと同じようなことはよくやる。

第 13 章 型オブジェクト (Type Object)

モンスターなどを定義するときに、継承を使った階層構造をコンパイル時に作るのではなく、型を意味するオブジェクトを包含させることで実行時に型システムを構築する手法。これにより、ゲームデータの変更による再コンパイルを防ぐことができる。

Part V 分離のパターン

第 14 章 コンポーネント (Component)

モノリシックなクラスを継承ではなく包含を使って分離する方法とコンポーネント間の通信方法について。継承だと菱形継承が起きやすいけど、包含ならそれを避けられる。本書では記述がないけど、コンポーネント化した方がユニットテストもやりやすいよね。

第 15 章 イベントキュー (Event Queue)

コンポーネントをコード的にも時間的にも分離するパターン。グローバルキューとローカルキュー、同期と非同期、スレッド親和性、キューの実装、リクエストのマージ、シングルキャスト・ブロードキャスト・ワークキュー、リーダーとライターの対応数など。

キューってとても興味深いデータ構造だと思ってて、いずれキューイングのパターンとかを自分なりにまとめられたら良いなって思ってます。

第 16 章 サービスロケータ (Service Locator)

あらゆるコンポーネントで使うサービスのインスタンスを見つける手法。インスタンスの設定方法 (外部から登録、コンパイル時もしくは実行時に設定)、サービスが見つからない場合の対応 (NULL インスタンスとか)、サービス提供のスコープなど。

シングルトンとの違いについて少し考えました。シングルトンはグローバルアクセスと遅延生成を提供するのに対し、サービスロケータはグローバルアクセスとインスタンスの外部注入を可能にする。あと前者はクラス毎に実装されるけど、後者はロケータ自身が汎用的なシングルトンになる、とかかな?

Part VI 最適化のパターン

第 17 章 データ局所化 (Data Locality)

データ配置を工夫しキャッシュ効率を上げる方法。これまでの分離のパターンとは二律背反になるので性能が特に重要となる場所で使う。ポインタ参照を避けたデータ配置、仮想関数の排除、よくアクセスするデータの切り出し (ホット/コールド分離) など。

第 18 章 ダーティフラグ (Dirty Flag)

処理や通信を必要になるまで遅延することで性能を稼ぐ方法。遅延が長くなり過ぎたり、処理自体に時間がかかる場合もあるので、いつどのように遅延実行するか検討する必要がある。あとダーティかどうか指定する粒度も重要。

第 19 章 オブジェクトプール (Object Pool)

オブジェクトが頻繁に生成・解放されるとフラグメンテーションが起きる。固定長ブロックのメモリプールを作ってオブジェクトを再利用することでこれを防げる。フリーリストは未使用オブジェクトに埋め込むことでメモリを節約できる。

メモリ管理については昔読んだ「省メモリプログラミング ― メモリ制限のあるシステムのためのソフトウェアパターン集」という本がとても良かった。また読みたいから是非電子書籍化して欲しい。

(Amazon へのリンクです)

第 20 章 空間分割 (Spatial Partition)

オブジェクトを位置に基づいて管理するパターン。セルをまたぐ場合の処理、平坦な構造と階層的な構造、固定的な分割と適応的な分割、オブジェクトの情報の保存方法など。空間分割に関するデータ構造は馴染みがなかったので面白かった。