ラベル Android の投稿を表示しています。 すべての投稿を表示
ラベル Android の投稿を表示しています。 すべての投稿を表示

2011年6月20日月曜日

GLSLの微妙なはまりどころ

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

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

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

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

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を問題なく叩けそうですね。