ガンズターン 公式サイト

楽しいことに、まじめです。 ——ガンズターンアプリ研究所公式サイト

Androidへの移植に苦戦ちう……(Android & OpenGL)

Pocket

いつの間にか10日経っていました……

お疲れ様です。ガンズターンアプリ研究所のRyosukeです。
毎日、本当に暑いですねー (~_~;)

暑すぎて、自宅ではエアコンつけないと全然仕事が手につかないです。

かといって、ずっとつけっぱなしにしていると今度は寒い。
我が家のエアコンは、アパートに最初から備え付けのものなんですが、けっこう年代物であるため、微調整が利きません。

28度設定にしていても、吹き出てくる風はけっこう冷たくて、1時間もつけているとだんだん寒くなってきます。
かといって、こまめにつけたり消したりするのは電気代的には一番まずいエアコンの使い方らしいので、なんとも悩ましい限りです。

まあ、一番怖いのは熱中症になることなので、基本的には暑くなりすぎるまえにケチケチせずにエアコンつけることにしています。

自宅で仕事している時は基本的に1人なので、もしそんな状況で倒れちゃったら目もあてられませんからね。

さて。

リジェクト続きの「ぱずもぐ!」iOS版ですが、実は先週末の木曜日頃に再度のリジェクトを頂いてしまいました。

理由は「広告関係」です。
すでに再申請を終えてますので、その結果が出次第、詳しくお伝えできればと思います。
(とりあえず今は「お察しください」としか言えません…… ( ; ; ))

iOS版の申請が無事通るまでの間に、Android版を完成させちゃおうというつもりで、このところずっとAndroid Studioと格闘しています。

ようやく「Java」特有(というかAndroid特有?)の考え方に慣れてきまして、なんとか「動く」ところまで出来上がりました。
(当初、1週間程度で移植完了するつもりだったのに、すでに取り掛かってから2週間以上経ってます……)

しかしまあ難しいですね。
Android Java上でOpenGLを使うのと、Objective-C上でOpenGLを使うことの間には予想以上の差がありました。

たくさん思い通りにならないことがありましたので、そのうちのいくつか(とくに困った部分)を、メモ代わりにここに書き記しておきたいと思います。
(本当はもっと詳しく、1つ1つ掘り下げて記事にしていきたいのですが、とりあえず)

1. AndroidでのOpenGLは最低2つのマルチスレッドで動く

はい。これ。個人的に、一番厄介な問題です。(今の所)
わたし、これまでマルチスレッドには一度も挑戦してきませんでした。
その必要性を感じるほど、規模の大きなゲームを自作したことはなかったので。

「ぱずもぐ!」においても、基本的にゲーム内部の処理はシリアルに動作させてます。
(HTTP通信に関わる部分だけは一応マルチスレッド化して非同期通信にしてますが)

描画に関する部分なんてとくに、処理の順番を間違えるととんでもない見栄えになるので、マルチスレッド化するなんてことはまったく考慮に入れてなかったわけです。

そんなわけで、iOS版ではコレクションに対する追加や削除も、いたるところで好き勝手にやりたい放題やっていました。
どんなタイミングでコレクションをいじくったって、基本的にシングルスレッドですから、不都合は生じてなかったんですね。

けれど、Androidではそうはいきませんでした。

AndroidでOpenGLを使うには、GLSurfaceViewというViewを使う必要があるんですが、こいつが曲者だったんです。

GLSurfaceViewはGLThreadという名前のスレッドを自動的に作成します。
つまり、AndroidでOpenGLを使おうとすると、自動的にマルチスレッド(mainスレッドとGLThreadは最低限生成されてしまう)なアプリになってしまい、これまでiOS版で好き勝手にやってきたようなことが一切通用しなくなってくるのです。

このことが、わたしにとっては非常に厄介な問題となって立ちはだかるのでした。

以下に、具体的な例を示しますね。

2. OpenGLに対する操作は全てGLThread内で処理しなければならない

GLThreadは、アプリ全般で行われるOpenGLに関する処理を、全てそのスレッドで請け負います。
逆にいうと、GLThread以外のスレッドからOpenGLに関する処理を行っても、正しく動作しません。

例えば、glGenTextures()というOpenGL関数をGLThread以外で呼び出すと、本来なら新たに生成されたtextureIdが返ってくるはずが、常に「0(= GL_FALSE)」しか返ってきません。
この状況に陥った時、わたしは最初、「またテクスチャのサイズ間違えたかな?(縦も横も2の2乗じゃないとよくこういう状況になるので)」と思ったんですが、どうもそういうわけじゃない。

そこで、AndroidStudio上でLogCatを確認してみると、以下のようなエラーが頻発していました。

call to OpenGL ES API with no current context (logged once per thread)

似たような状況に陥っていた人はたくさんいたらしく、インターネットで調べたら対策はなんとかわかったんですが、それがまためんどくさい……。
なんと、GLThread以外でOpenGLを操作する部分は全て「GLSurfaceView」のインスタンスメソッドである「queueEvent(Runnable)」に渡さないといけないんだそうです。

つまり、以下のような感じ。(あくまで例のために、即興で作成したコードで動作確認してないので、ちゃんとコンパイルできるかどうかも怪しいコードですので、あしからず)

//元々のソースコード(GLThread以外で実行されるという前提)
GL10 gl = getGL(); //GL10オブジェクトを取得するメソッドと思ってください。
int textureIds[] = new int[1];
gl.glGenTextures(1, textureIds, 0);
int textureId = textureIds[0]; //ここでtextureIdを取得したと思っても、中身は常に0になっている。

↓

//変更後のソースコード
final GL10 gl = getGL();
final int textureIds[] = new int[1];
GLSurfaceView glView = getGLView(); //GLSurfaceViewを取得するメソッドと思ってください。
glView.queueEvent(new Runnable() {
    @Override
    public void run() {
        gl.glGenTextures(1, textureIds, 0);
        int textureId = textureIds[0];
    }
});

……いかがですか?
もともとが4行のシンプルなコードだったのに、なんと10行にもなってしまいました。
しかもですね。ここでは簡略化して記載してるのでわかりづらいかもしれませんが、queueEventにわたされた処理は、以降に記載された処理との順序が保証されません。
マルチスレッドで、別のスレッドに処理を渡しているわけですから、あたりまえですね。

例えば以下のように書いてしまったら、アウトです。

//変更後のソースコード
final GL10 gl = getGL();
final int textureIds[] = new int[1];
GLSurfaceView glView = getGLView(); //GLSurfaceViewを取得するメソッドと思ってください。
glView.queueEvent(new Runnable() {
    @Override
    public void run() {
        gl.glGenTextures(1, textureIds, 0);
    }
});
int textureId = textureIds[0]; //←このコードが実行される時点で、glGenTexturesが実行されてる保証はない。

上記コメントにも書いた通り、「int textureId = textureIds[0]」が実行される時点では、glGenTexturesが実行されている保証がありません。おそらく、ほとんどの場合はtexureIds[0]の初期化されていない不定な値が代入されてしまうことでしょう。

かといって、先に書いたように「Runnable」の中に「int textureId = textureIds[0]」を書いてやったとしても、果たしてどうやって、無名オブジェクトの中で生成された変数の値を外部が取得するのか、ていう問題が新たに発生します。
(上記コードのままでは、せっかく生成したtextureIdの情報をどこにも渡せずに終わってしまいます)

つまるところ、一番の正解例は、元々のソースコードの処理をまるごとメソッド化しておいて、queueEventの中でそのメソッドを実行し、メソッドの実行結果はメンバ変数か何かに保持しておくっていう形にしておくのがよさそうです。

しかしまあ、めんどくさいですね。

1つ1つ処理を考えていくと非常にめんどくさいので、わたしは大雑把に、アプリの処理の大元を司っているメインのアクティビティの中で、以降の処理を丸ごと「queueEvent」の中に突っ込むという荒技でしのいでます。

それでとりあえず動いていますが、描画処理が異様に重たいので、おそらく正解例ではないのでしょう。なにせ、描画に直接関係ない処理まで全てひっくるんで、GLThreadのqueueEventに投げちゃってるんですから。

一通り動くようになったら、チューニングの段階で、もっと細かい単位でqueueEventに投げるという改良をしたいと思ってます。

3. コレクション(ArrayList等)を好き勝手にいじれなくなる

これもまた、個人的には悩まされた現象の一つです。
iOS版の「ぱずもぐ!」では、NSMutableArrayとかNSMutableDictionaryとかをかなり多用していまして、基本的にシングルスレッドなので、for文を回してる間の排他処理なんかを実装する必要もありませんでした。

けれどAndroidでは、GLThreadとmainが別スレッドで動作するために、同じコレクションに対する操作が重複して複数走ってしまうことも考慮しないといけません。
実際、なんの工夫もなくiOS版のコードをただJavaに移しただけの状態では、「ConcurrentModificationException」という例外が発生しまくって、デバッグどころの騒ぎじゃありませんでした。

解決方法は簡単です。

ずばり、コレクションに対する「add」、「set」、「remove」等の処理を行う部分は、必ず「synchronized(コレクション自身のオブジェクト){}」で囲ってやるというだけです。

ただ、簡単なんですが、非常にめんどくさいです。

「ぱずもぐ!」ではコレクションに対する上記操作を行っている箇所が色んなところにちらばって記述されており、その全ての箇所で「synchronized()」を書くのはかなりの労力を要しました。
(実際には、add等をするためのメソッドを別に作成し、synchronizedはその中に記述、外部からは全てそこにアクセスするように修正したんですが、それでも大変でした)

しかもこれ、状況によって発生したりしなかったりするので、一通り動くことを確認した今でも、まだ修正漏れがあるんじゃないかと恐ろしく思ってます。

マルチスレッドだと、「運が悪いと動かない」みたいな処理が随所に発生してしまうので、まずそれを見つけるのが大変です。

似たような不具合は、おそらくまだ残ってると思いますのでリリース前にしっかりデバッグしたいと思います。

4. イテレータ使ったら遅かった……

これは、OpenGLに特有の話というわけではないんですが、今回初めて知ったので残しておきます。

先ほども書きましたが、Android版「ぱずもぐ!」はまだまだ描画処理が「非常に重たい」です。
しかし、今日一日である程度「劇的に」改善しました。

いったい何をどうしたかというと、頻繁に行う「コレクションの全要素に対する処理(反復処理)」を行う際、イテレータ(Iterator)を使うことをやめました。

そもそも使ってなかったんですが、最初に「ConcurrentModificationException」が起きた時、イテレータを使ってないことが原因じゃないかと思って、アプリ内の全ての反復処理をイテレータを使うものに置き換えてしまいました。

速度的にもイテレータを使った反復処理の方が有利、というような記事をどこかで読んだことがありまして、それを鵜呑みにしていたんです。

ただ、これ、例えばゲームアプリ内で、毎フレーム描画するたびに行う反復処理中ではイテレータ使わないほうがいいですね。

というのも、確かに「イテレータを使った反復処理」自体は高速で、「for (int i=0; i<array.size(); i++)」とした場合と遜色ない速度で動作するんですが、しかしボトルネックは反復処理に要する時間とは別のところから発生していました。

言わずと知れた「ガーベジコレクション」です。

これまた、iOS版の開発時にはお目にかかることのなかった概念です。

イテレータを使った処理では、毎回、反復処理を行う前後でメモリの確保、解放が行われるため、それだけたくさん「ガーベジコレクション」の動作する要因を作ります。

OpenGLでゲーム作っていて、どうにも動作が遅いなあと思った時、LogCatに「D/dalvikvm﹕ GC_CONCURRENT」とかっていうメッセージが多発していたら、まず「ガーベジコレクション」を動作させない工夫をすべきです。(ということを今日一日で身にしみて学習しました)

ガーベジコレクションの動作を抑止する方法については色んなものがありますが、インターネットで検索するといくらでも出てくるので、それらが参考になると思います。

というか、ぶっちゃけ、自分もまだ「イテレータ」を使わないようにしただけで、まだまだ大量に「ガーベジコレクション」が動作してる状況なので、これからもっと勉強していきたいと思ってます。

5. というわけで、Android版も鋭意制作中です

久しぶりの記事なので、サクッと書いて終わらせるつもりが、気づけば長文になっていました。

iOS版の審査ですが、おそらく結果が出るのは今週末ぐらいなので、下手したらAndroid版のほうが先にリリースされちゃうかも知れません。(とはいえ、まだまだまともに動かない状態なので、あと1〜2週間ぐらいかかっちゃいそうですが)

次の審査こそは何事もなく通りますように。 (>人<;)

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

トラックバックURL: http://gunsturn.com/2015/07/27/studying_java_2/trackback/