エイバースの中の人

株、不動産、機械学習など。

カテゴリ: Android

最近、ライブラリの最適化を進めているので、Instrumentsでメトセラのプロファイルをかけてみました。

top


まずは最上位モジュールです。render関数が描画関数、stringByEvaluationJavaScriptFromStringがゲームのJavaScriptでの実行、その他がタッチイベントなどの通知関数です。これを見ると、40%が描画、30%がゲームエンジン、10%がイベントで消費されていることが分かります。

GameloopTouchでは6回、GameloopTextfieldでは1回、stringByEvaluationJavaScriptFromStringを呼び出しています。これより、stringByEvaluationJavaScriptFromString一回につき、ゲーム全体で1%程度のロスが生じることが分かります。できるだけstringByEvaluationJavaScriptFromStringの呼び出し回数を減らすことがパフォーマンス向上で重要そうです。

renderer


次に描画関数の内部です。意外なことにcomponentsSeparatedByCharactersInSetが12%も消費していて、実描画は17%に過ぎないことが分かります。また、フォントの準備に8%の時間を消費しています。ゲームエンジンでは描画情報を文字列でObjectiveCに渡すのですが、それを区切り文字でパースする部分だけで12%も消費しているようです。したがって、このパースをいかに効率化するかが課題になりそうです。

component


componentsSeparatedByCharactersInSetの内部です。6%は分割した文字列をnewしている部分です。たしかに毎フレーム大量に文字列を新規生成すると重そうです。

render_all


実描画関数の内部です。これまた意外なことに、5.0%が文字列をintに変換する部分で消費されています。OepnGLによる描画の負荷は10%程度のようです。これにフォント描画の8%を足しても、合計18%程度ですね。

ということでまとめ。JavaScriptでゲームエンジンを動かした場合、
・OpenGLによる描画負荷:18%程度
・ゲーム部分:30%程度
・コマンドのパース:22%程度
・JavaScript<>ObjectiveC通信:10%程度
程度の負荷バランスになるようです。これだと、OpenGLを高速化するよりも、その他の部分を見直した方が最適化がかかりそうですね。

とりあえず、stringByEvaluationJavaScriptFromStringの呼び出しを減らす、コマンドにステート記憶を付けてコマンドの総量を減らす、というのが有効そうです。後、NSStringで新規メモリを確保せずにある区間を切り出す方法をご存知の方いたら教えてください。

IMG_0410

iPhoneのOpenGLで文字を書くのAndroid版です。例によって、OpenGLにはフォント命令が無いので、自由に文字を書く場合は、テクスチャに文字を書いて、それをポリゴンで描画する必要があります。

基本は、プログラム全体で一度、

 m_texture.image = Bitmap.createBitmap(m_texture.width, m_texture.height, Config.ALPHA_8);

のように文字を書く先のBitmapを確保して、フレーム毎に

 Canvas canvas = new Canvas(m_texture.image);
 Paint paint = new Paint();
 paint.setTextSize(14);
 paint.setARGB(0xff, 0x00, 0x00, 0x00);
 canvas.drawText(line, 0, y+in_block_y+y_unit-4, paint);

とCanvasでBitmapに文字を描画して、

 GLUtils.texSubImage2D(GL10.GL_TEXTURE_2D, 0, 0, 0, m_texture.image);

と文字を書いたBitmapでテクスチャを更新してしまえばよいです。

ただし、一人で悔やむ: AndroidのView.onDrawで、改行する。で書かれているように、自動的に折り返して文字を描画する命令が無いので、paint.breakTextを使って一行ずつに切り分けて、一行ずつ描画する必要があります。

また、drawTextで指定する座標が、左上座標ではなく、文字のベースライン(下線)座標なので、そのままUV値を計算すると文字がずれるので注意です。ベースライン座標から上と下に何ピクセル存在するかは、FontMetricsを使って取得することができます。(参考:テキストの描画(FontMetrics) - Android Wiki*

 FontMetrics fontMetrics = m_paint.getFontMetrics();
 m_font_top_offset = (int)Math.ceil(0 + fontMetrics.top); //ベースライン上ピクセル(負数)
 m_font_bottom_offset = (int)Math.ceil(0 + fontMetrics.bottom); //ベースライン下ピクセル(正数)


ソースコードはhttp://www.abars.biz/blog/FontTexture.javaからどうぞ。

このコードでは、最初に1024x1024 pixelの巨大なテクスチャを確保します。その後、毎フレーム、先行して現在のフレームに必要な文字を全て描画し、texSubImage2Dでテクスチャを更新します。

現在のフレームに必要な文字を全て描画した後は、一般的なオブジェクトを描画しながら、必要なタイミングで必要な文字を描画します。一枚の巨大なテクスチャに文字が格納されているので、次のように、必要な文字に応じてUV座標を変更しながら描画します。

 no=mFontTexture.getTexture();
 sx=mFontTexture.getWidth();
 sy=mFontTexture.getHeight();
 offset=mFontTexture.getOffset();
 GraphicUtil.drawTexture(gl,x,y,sx,sy,no, 0,offset/1024.0f,sx/1024.0f, sy/1024.0f, r,g,b, 1.0f);
 mFontTexture.nextReadPoint();

今回の描画文字に対するuのレンジは0〜sx/1024、vのレンジはoffset/1024〜offset/1024+sy/1024です。

texSubImage2Dで1024x1024ピクセルの画像を転送するのにかかる時間は、XperiaArcで3〜5msec、Xoomで7〜18msecでした。PowerVRよりもTegra2の方が、テクスチャの転送には時間がかかるみたいですね。

XperiaArcとXoomの出来が思いのほかよく、また、メトセラへの海外のストアからの問い合わせもあったりしていて、アジア圏はAndroidが強くなりそうかなということで、メトセライズデストラクタのAndroidネイティブ版の開発を進めています。

IMG_0392

もともと、メトセライズデストラクタのiPhone版は次のようなフレームワークで動作しています。
 ゲームコア(JavaScript)->WebKit(インタプリタ)->コマンドリスト(文字列)->
 ネイティブフレームワーク(ObjectiveC)->OpenGL
ngCoreとかと同じ原理ですね。

ngCoreは下記スライドが分かりやすいです。



パフォーマンスグラフは最後にありますが、まだチューニングの余地はありそうな気がします。参考までにメトセラiPhone版は、iPhone4でHTML5版が5FPS、描画エンジンをOpenGLにすると18FPSと3.6倍くらいのパフォーマンスは出てました。

ということで、Android版への移植は、後半のネイティブフレームワークさえ書けばよいことになります。JavaScriptが生成したコマンドリストを解釈する部分ですね。ついでに、次回作でも使いまわせるように、iPhone版のフレームワークも整理しています。

初Androidということで、EclipseとAndroidSDKをインストールしてHelloWorldを動かしてみましたが、エミュレータが重すぎて、ビルド->テストの1サイクルに1分とかかかって終わっている感じなので、開発は全て実機でやることにしました。いろんな人から伝え聞く所によると、みんなそうやっているそうですw ここはiPhoneの方が楽。

AndroidのWebkitを使う場合は、htmlファイルやgifファイルなど、全てassetsフォルダに置きます。そうすると、webView.loadUrl("file:///android_asset/native_android.htm");というようなパスでアクセスすることができます。

AndroidのUI構築はインタフェースビルダーみたいなものはなくて、デフォルトのウィンドウに、ビューをどんどんアタッチしていく感じになります。例えば、webViewを使いたい場合は、onCreateで
 webView = new WebView(this);
 setContentView(webView);
とするような感じになります。

一つのウィンドウに複数のビューをアタッチしたい場合は、レイアウトクラスを使います。
 LinearLayout linearLayout=new LinearLayout(this);
 setContentView(linearLayout);
 linearLayout.addView(webView,480,400);//ビューサイズも指定できます
 linearLayout.addView(glSurfaceView);
みたいな感じです。

webViewでlocalStorageを使うには
 settings.setDomStorageEnabled(true);
 webView.getSettings().setDatabasePath("/data/data/org.infil00p.testcase/app_database");
と、保存先を指定してやる必要があります。(参考:Android WebView で HTML5 の Web Storage と Web SQL Database API を使う - nobnakの日記

また、htmlの読み込みが終了したことを知るためには、
 webView.setWebViewClient(new ViewClient());
 webView.setWebChromeClient(new ChromeClient());
の二つのクライアントを設定する必要があります。(参考:クラウドなアプリケーション構築 前編-WebView | Android Techfirm Lab

実行を開始するには
 webView.loadUrl("file:///android_asset/native_android.htm");
です。

JavaからJavaScriptをコールするには、次のようにします。
 mWebView.loadUrl("javascript:GameTask();");

iPhoneのWebkitと大きく違うのは、関数の戻り値を取得できないことです。そこで、JavaScript側からJavaの関数をコールして戻り値を伝えます。

 webView.addJavascriptInterface(new LinkInterface(), "android");
 
 public final class LinkInterface {
  public LinkInterface() {
  }
  public void setDisplayList(String dl) {
   glSurfaceView.setDisplayList(dl);
  }
 }
としておくと、javascriptからandroid.setDisplayList(dl);として関数が呼べます。(参考:Javascript 関数の戻り値を Android から取得する方法 - A Day In The Life

ただし、非同期で関数が呼ばれるので、Handler.postでUIスレッドに渡して同期化してあげましょう。

ここまで来れば、タイマーで一定間隔ごとにmWebView.loadUrl("javascript:GameTask();");を呼んでやって、javascriptからコールしたsetDisplayListでOpenGLの再描画を送ってやればよいです。

OpenGLはGLSurfaceViewで
 super.setRenderMode(RENDERMODE_WHEN_DIRTY);
としてやると、非同期更新モードにできます。なので、
 public synchronized void setDisplayList(String dl) {
  mMyRenderer.setDisplayList(dl);
  super.requestRender();
 }
と、JavaScriptからコールされた時だけ、requestRenderをしてやって、
 public synchronized void onDrawFrame(GL10 gl)
で描画します。

ちなみに、OpenGL本は



がオススメです。OpenGLはiPhoneよりもAndroidの方が記述量が少なくて楽ですね。

assetフォルダにある画像ファイルは、BitmapFactory.decodeStreamで読み込めます。下記関数の仲で、nameがassetフォルダ以下のファイル名です。loadTexture(gl, context,"aa.png",no);とかで読めます。

public void loadTexture(GL10 gl, Context context,String name,int no) {
int[] textures = new int[1];

InputStream in;
BufferedInputStream buf;
Bitmap bmp=null;
try {
in = context.getAssets().open(name);
buf = new BufferedInputStream(in);
bmp = BitmapFactory.decodeStream(buf);
} catch (Exception e) {
Log.e("Error reading file", e.toString());
}

if (bmp == null) {
return;
}

gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bmp, 0);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);

gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glBlendFunc(GL10.GL_SRC_ALPHA,GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnable(GL10.GL_BLEND);

bmp.recycle();

this.m_width[no]=bmp.getWidth();
this.m_height[no]=bmp.getHeight();
this.m_texture[no]=textures[0];
}

そんな感じで、とりあえず絵が出るくらいにはなりました。

IMG_0408

画面上半分がHTML5での描画、画面下半分がOpenGLでの描画です。後は、タッチイベントと、音と、回転系と、フォントですね。フォントはちょっと大変そうな予感がしていますので、うまくいったら記事にします。

後、iPhoneと違って、非正方形のテクスチャが使えている気がするんですが、Android全般そうなのでしょうか?XperiaArcだけ?正方形化しておいた方がいいのか迷ってます。

ついでに、メトセライズデストラクタiPhone版はこの機会にユニバーサルバイナリ化を進めています。XoomのFlashPlayer10.1で普通にメトセラPC版が動いて、思ったよりも遊び心地がよかったので、iPadでも遊びたいなと思って実装中という感じです。とりあえずiPhoneアプリをiPadに対応させてユニバーサルアプリにする方法 - What should we playを参考にさせて頂きながらですが、ランドスケープモードの時にUIがうまく回転してくれなくてはまりました。どうやら、
 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone){
  //iPhone mode
 }else{
  //ランドスケープモードの場合
  CGSize size = glView.bounds.size;
  glView.center = CGPointMake(size.height/2, size.width/2);
  glView.transform = CGAffineTransformRotate(CGAffineTransformIdentity, (M_PI / 2.0));
 }
と、手動で回転させないといけないようです。

IMG_0402

iPad版もこんな感じで軽くは動いていますが、ここから調整に結構時間がかかりそうです。

ということで、ゲームコアをJavaScriptで書いておいて、フレームワーク化すると、iPhoneでもAndroidでも簡単にマルチプラットフォーム展開できてオススメです。ぜひぜひ。

↑このページのトップヘ