2011年6月23日木曜日

小規模ゲームの設計

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

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

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

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

2011年6月20日月曜日

GLSLの微妙なはまりどころ

AndroidでのGLSLは困ったことに、コンパイル時にこけても、エラーログを吐きません。
デスクトップでコンパイルのテストして、持っていくという手段をとってますが、それでもコンパイルが通らなかったりします。
以下微妙にはまった点を。

フラグメントシェーダでは精度を指定する
変数はlowp, mediump, highpを指定しないといけません。ちなみにhighpはExtensionらしいです。

型チェックが厳しい
定数に1を指定したら、コンパイル時にこけました。Int型と判断されたためでしょう。変数がfloatの場合、1.0とfloatの定数を指定する必要があります。

これどうやって見つけたかというと、最小構成のシェーダにした後、じょじょに範囲を広げていって、こけている行を特定しました。
IDEのビジュアルデバッガを使っていると、こういう初歩的なデバッグテクニックを忘れがちになりますね。気をつけないと。

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++使いますしね。私もオブジェクトプールで気をもむくらいならそっちのほうがいいと思います。

Androidでのゲーム開発手段

ABCでのLT講演が決まりました。がんばります。

Androidでゲームを開発する手段はJava一択ではありません。
わたしの知ってる限りでゲーム開発で使えそうな手段を挙げます。

  1. Unity(C#, JavaScript)
  2. HTML5(JavaScript)
  3. Adobe AIR(Action Script)
  4. SDK(Java)
  5. NDK(C/C++)
  6. Corona SDK(Lua)
Android上でさわったことがあるのはHTML5のみですが、他の開発手段もおいおい触っていきたいと思ってます。とくにクロスプラットフォーム開発が可能ならAdobe AIRとCoronaSDKは触っておきたいなと。
とはいえ、Android上で開発したことないだけで、上に挙げている言語はすべて扱ったことはありますので、ある程度予想はつきます。
まずHTML5とAdobe AIRは描画が足を引っ張ることになると思います。
CoronaSDKは複雑なものを組もうとするならば、Lua自体の動作速度がネックになるかと。
VM上で動作するJavaとC#ですが、両方ともJITを搭載しているので、動作速度はそこまで気にならない印象があります。
ただし、SDKはゲーム用のミドルウェアを使わないと非常にしんどい思いをする・・・というかしてます今。
あとGC付き言語の宿命として、GC回避コード組まないとGC起動してアクションゲームだと悲しい思いをします。GC起動を回避するためにはオブジェクトプールしかないんですが、これがまた面倒で、どれくらい面倒かというと、オブジェクトプールするならC/C++で組んだほうがいいくらいです。
C++の場合GCとしてReference Count法を採用できるので、記述能力で劣るJavaよりも(私感)ますますC++でいいような気分になります。
ただし、Android2.3のGCはConcurrentGCといって、別スレッドでコンカレントにGCを走らせることが可能になります。私の知ってるアルゴリズムを採用してるなら、停止時間3ms程度というデータも納得いきます。
特にマルチコア環境では、スループットもレスポンスも改善するという夢のようなクライアントサイドGCなのです。GoogleGJ!

2011年6月16日木曜日

AndroidでOpenGL ES 2.0を学びたい

Androidでゲームを作りたい人なのでGLES20を学んでいます。
OpenGLの知識は1.1で止ったままなので、もはや別物と化してしまったGLES20は改めて学ぶ必要があるようです。
まず、GLES20のAPIを学ぶ上で罠っぽいところが結構ありまして…
  1. OpenGLはステートを内部に溜め込む
  2. ステートによって関数の意味が変わる
  3. 固定シェーダが無くなった
1はオブジェクト指向的に考えると巨大な一枚岩OpenGLオブジェクトが存在するイメージです。明示的に指定しない限り、勝手にステートを変更したりすることはありません。例えばZバッファの比較が不要な場所でOFFにしたあと、ONにするのを忘れると、Zバッファがオフのまま描画され続けることになります。
ならこまめにOnOff切り替えればいいじゃないかと思われる方もいらっしゃるでしょうが、パフォーマンス上の理由であまりお勧めできません。
GPUへの命令発行は可能な限り減らさなければならないのです。

問題は2です。慣れてないと非常に気持ち悪いです。例えばテクスチャアドレッシングモードを変更する(glTexParameteri)とき、対象となるテクスチャをバインド(glBindTexture)しないと変更できません。テクスチャネームを指定して直接変更…できたらいいんですけど、できません。
もうひとつ例を挙げると、Shaderに渡すAttribute変数を設定する関数(glVertexAttribPointer)があるのですが、この関数、VBOがバインドされている場合とそうでない場合では渡す引数の意味が変わります。通常はデータへのポインタを渡すのですが、VBOがバインドされている場合、VBOのオフセットを渡す必要があります。このあたり理解して無いと…それは後述します。

3はまぁ、時代の流れですね。固定シェーダがやってたことと同じシェーダを書くことはそんなに難しいものではないので、特に問題はないと思います。

あと、Androidの開発言語はJavaなわけですが、Javaオンリーで開発すると罠がいくつかあります。
  1. Android2.2のGLES20のAPI設計したヤツでてこい!><
  2. テクスチャ読み込みが回りくどい
まず1です。VBO設定関連のAPIが軒並み使用不可能です。先に説明したとおり、VBOをバインドすると設定関数の意味が変わるわけですが、たぶんそのことを知らなかったのでしょう、2.2ではVBOのオフセット指定することができません。なのでAndroid2.2ではVBOは作成できるけど使用できないというなんともぐんにょりなAPIに仕上がってます。ちなみに2.3では追加されてます。

もうひとつ、OpenGLにはデータチャンクを渡すAPIがあるのですが、これが大抵引数がjava.nio.Bufferなんです。それだけならまだ救いがあるのですが、DirectBufferかつNativeOrderという条件が加わります。
悪いことに、DirectBufferが必要となると、まずByteBufferが必要になるのですが、asXXXBuffer()による変換後のオブジェクトは、array()メソッドがサポートされていません。putで書き込む必要があります。
すなわち、頂点データを加工するには、Javaだと二倍のメモリ(そしてコピーのコスト)を払う必要があるわけです。かといって、メモリをけちって1要素ずつput()したら動作速度は目も当てられないことにになります。

2はもう、あからさまに回りくどいです。手順としては
  1. BitmapFactoryからリソースを読み込み、Bitmapを取得
  2. getPixels()でピクセル値を取得
  3. 色の並びをOpenGLに合わせる(Option)
  4. ネイティブオーダーかつダイレクトなByteBufferを作成(ByteBuffer.allocateDirect() ByteBuffer.order())する。
  5. ByteBufferにピクセル値を書き込む
  6. ビデオメモリに転送(glTexImage(), glTexSubImage())
  7. Bitmapを開放(recycle())
めんどくさい&メモリ食う(画像の3倍のメモリ)&遅い(ピクセル操作)と三拍子そろってます。
なのでNDKを使わざるを得ないのが実情かと。

追記:
Bitmapを直接読み込めるAPIありました…android.opengl.GLUtilsがそれです。
2.3が出回れば、JavaのみでOpenGLを問題なく叩けそうですね。