ラベル プログラミングテクニック の投稿を表示しています。 すべての投稿を表示
ラベル プログラミングテクニック の投稿を表示しています。 すべての投稿を表示

2011年6月23日木曜日

小規模ゲームの設計

先の投稿で、Game Coding Completeに触発されて書いた記事がありますが、簡単なゲームに使うには大規模すぎます。
あらゆる機能は、コスト0で手に入るものではありません。実行コストなり開発コストなりに必ず跳ね返ってきます。拡張性という機能も同じです。必ず天秤にかける必要があります。
とくにメッセージによるオブジェクト間の通信は、コンパイル時間が非常に遅いC++だからこそ必要なテクニックかもしれません。

かといって、闇雲にクラスを作ると関連爆発を起こして管理不能な状態に陥ります。特にゲームの場合、オブジェクト間の通信が複雑になりがちです。

この場合に使われるのは…そうです、Mediatorパターンです。
Mediatorパターンに関しては私が説明するよりも他を当たってもらったほうがよいので割愛しますが、Mediatorとなるクラスのインターフェイスについては一考の余地があります。

一人で開発する場合、Mediatorのインターフェイスが増えても管理するのは一人なので、一枚岩のインターフェイスでも特に問題はありません。
ですが、もし複数人で開発するのならば、特定の機能ごとに分割したインターフェイスを作ると後々分業が楽になると思います。例えばサウンド再生のインターフェイス、当たり判定のインターフェイス、などです。

2011年6月19日日曜日

ゲームの設計を考えてみる

ABCのLTで話すネタを考えてます。何にしようかな…

今回はゲームの設計です。Game Coding Completeという本に触発されました。
といっても私はJava歴3ヶ月という新米も新米なので、はずしてたらすいません。

一般的な話として、継承は注意深く行うべきです。
というのも、継承をするとスーパークラスにサブクラスが強力に依存してしまうからです。
といっても、設計とはコードを整理して、見通しをよくすることですが、その整理において、分類というのは強力なツールです。よって、継承は設計において重要です。適切に使えるなら大いに使うべきです。

そして、継承は、フラットに浅く行うべきです。深い階層構造は、柔軟性を損ねる上、適切な設計を難しくします。
ですが、ここで言語の表現能力という壁にぶちあたります。単一継承言語という壁です。多重継承、もしくはMixinをサポートしている言語だと継承関係をフラットに保つことは難しくありません。ですが、単一継承の場合、本来並列の継承関係にすべき場面でも、直列にせざるを得ません。

ゲームにおいて、仕様がころころ変わることはよくあります。というのも、実際に動かしてみないと、おもしろいかどうか分からないからです。私は一人で開発したことしかありませんが、それですら仕様変更はよくあります。これが、デザイナーがプログラマを指示する形だった場合、さらに変更が多くなるのは想像に難くありません。

例えば、Actorクラスを継承する、動かない障害物Obstacleクラスと、パスに沿って動く敵、Enemyクラスがあったとしましょう。途中、仕様変更でObstacleクラスの一部はパスに沿って動くようにするという仕様変更があったとします。
何も考えない場合、Obstacleクラスに動くコードを追加して、Obstacleクラスにパスに沿って動く機能を追加するでしょう。
ですが、ちょっと待ってください。パスに沿って動くコードはすでにEnemyクラスに存在するわけです。そのコードを改めて書くのはもったいなくありませんか?
それならばと、えいやっとコピペ……?いえ、これもまずいです。コピペの害悪については話すまでもないと思います。
ちょっとマシな回答は、こうです。Enemyクラスのパスに沿って移動するコードをActorクラスに下ろします。リファクタリングのレシピにも載ってます。めでたしめでたしですね。

……実はあんまりめでたくありません。これを繰り返すと、Actorクラスがどんどん膨れ上がることになるのです。1クラスに責務を詰め込むのはよくありません。さらに、Actorクラスへの変更はActorクラスを継承するすべてのクラスに波及します。

よって、さらにマシな回答は、Actorクラスを継承するPathActorというスーパークラスを作成することです。そして、Enemyクラス、Obstacleクラスともに、PathActorクラスを継承するように変更します。かくして深い継承関係ができあがりました。

今回のケースでは、これが現実的な回答に思えます。ですが、さらに機能要件が増えたらどうでしょう?それが継承関係に合致する保障はありません。
例えばクラスXを継承するクラスA、B、Cがあり、A・CそしてB・Cにそれぞれ別の機能追加要件があった場合、単一継承言語ではお手上げです。

ならどうすればよいのか?答えのひとつに、コンポジションがあります。ベースとなるオブジェクトに、コンポーネントという形で、機能ごとに実装されたオブジェクトをプラグインするわけです。
ベースとなるオブジェクトには、onMessage(Message msg)のように、メッセージを受信する単一のインターフェイスを用意します。実装は各コンポーネントにメッセージを丸投げするだけです。Messageに応答するか否かは、各コンポーネントに任せます。

以上です。Game Coding Completeを読んで思いましたが、実際ここまでの仕掛けが必要なのは相当大規模なゲームになると思います。
そして、この手のテクニックを見て思うのが、静的言語の硬直性です。この仕掛けは、動的メタプログラミング可能な言語では必要ありません。かといって静的言語の型チェックは捨てがたいのですが……。
私がActionScriptを好きなのは、動的言語と静的言語のハイブリッド言語だからかな、とぼんやりと思います。

2011年6月18日土曜日

オブジェクトプールに溺れる

前の投稿で、GC付き言語ではGCを回避しないと悲しいことになると書きました。私はまだメモリ環境の厳しいところでGC付き言語によるゲーム実装を行ったことはありませんが、話によると考えずに作り捨てすると、数秒ごとに2~300ms止るようです。これだと正直ゲームになりません。

で、ゴミを出さないようなゲーム設計について。オブジェクトプールです。
原理は簡単です。オブジェクトプールという対象オブジェクトの配列を用意して、空なら新しく作り、そして空ではないなら配列からオブジェクトを取り出し、初期化します。そして不要になったら配列に戻します。
これだけです。

コレだけなんですが…魔物が潜みます。ヒューマンエラーを起こしやすいんです。
配列に戻し忘れはまだいいです。GCが回収してくれます。
問題は配列に戻したのにまだ参照している状況です。これはDangling PointerならぬDangling Objectです。死んだと思ったオブジェクトがまだ生きてるので、再利用するとBomb!
……とはなりませんが、プログラムが不整合を起こします。これもメモリリークほどではないかもしれませんが、検出が難しい類のバグです。new/delete malloc/freeと同じく、注意深く行わないといけません。

以上が一般的な話。以下は私が考えた(多分一般的ではない)テクニックです。

Dangling Objectはそのままだと検出は難しいです。ですが、一枚レイヤーをかませれば(完全ではないものの)検出できます。そのレイヤーとは、ハンドル経由でオブジェクトにアクセスすることです。
ハンドルがオブジェクトだと本末転倒なので、ハンドルは数値になります。
ハンドル=>オブジェクト変換機をレジストリと表現しますと、まず、レジストリは、オブジェクトの要求があるとオブジェクトプールからオブジェクトを引き出して、ハンドルを割り振ります。この際、すでに登録されているオブジェクトとはかぶらないハンドルを割り振ります。ここでは1が割り振られたとします。
要求したオブジェクトが不要になると、オブジェクトをレジストリに返します。レジストリはオブジェクトをプールに戻し、登録を削除しします。
例えば、ハンドル1のオブジェクトは登録が削除されたのに、ハンドル1でオブジェクトをレジストリからもってこようとしたとしましょう。当然、ハンドル1のオブジェクトは存在しないので、例外を投げることが可能になります。
ですが、お察しの通りハンドル1にオブジェクトが割り振られる可能性は0ではありません。なので、ハンドルは十分に広くとる必要があります。完全ではないといったのはこの理由です。

もうひとつオブジェクトプールのテクニックがあります。
アクターのようにライフタイム長いオブジェクトはGCに任せても問題ないこともありますがが、計算の一時オブジェクトは断じて許容できません。これを放置しておくと、数秒でGCが走るストレスフルなゲームが出来上がります。かといって、一々オブジェクトプールに返す処理を書くのはめんどくさい(ヒューマンエラーがおきやすい)。ならばどうすればいいか。

答えは、毎フレームごとにまとめて回収する、です。

計算の一時オブジェクトは本当に一時オブジェクトで、計算結果をどこかに代入した後は使われることはありません。なので生成したオブジェクトをどこか配列に蓄積しておいて、その配列に入っているオブジェクトはフレーム毎にプールに格納します。
一時オブジェクトではない場合は、プールからとってくるのではなくて、生成してGCに任せます。
私の経験上、deleteするのを忘れることはあっても、newを忘れることはあまりないので、オブジェクトを生成する手段が二つあっても、使い分けはうまくいきます。


以上です。この手の話はネットではあまり見かけませんね。
ゲーム作るなら速いしメモリ管理も自由にできるC++使いますしね。私もオブジェクトプールで気をもむくらいならそっちのほうがいいと思います。