ここで、いくつかの問題点がある。
1. GLSurfaceView#onDrawFrameはGLSurfaceView内部で定義された専用のスレッド(GLThread)から呼び出される。描画処理の実行タイミングについては、GLThread側で適切に処理されている(おそらくは液晶画面のリフレッシュレート依存)ようなので特に弄る必要は無い。むしろ、それを妨げてはならない。
2. GLSurfaceView#onDrawFrame内部で描画に関係のないコードを実行していると、当然その分だけ画面更新が遅延し、フレームレートが低下する。なので、描画処理と更新処理は分けた方が良い。
フレーム更新処理を60fpsで適切に動作させる手法として、以下のソースコード例を挙げる。
なお、記事用に即興で書き起こしたものなのでこのままだとコンパイルすら通らないかもしれないが、実際に使用する場合には適宜修正して欲しい。
public class Foo implements Runnable {
  Thread mRunning;  //!< 実行中
  float mMeasuredFps;  //!< 計測された秒間フレーム数
  @Override
  public void run() {
    Thread thisThread = mRunning = Thread.currentThread();
    long lastTime = System.nanoTime();
    long lastTimeBySeconds = lastTime;
    long frequency = (long)(Math.floor((double)TimeUnit.SECONDS.toNanos(1L) / 60.f));  // 60fps周期
    long frame = 0L;
    while (thisThread == mRunning) {
      long currTime = System.nanoTime();
      long elapsedTime = currTime - lastTime;
      if (elapsedTime > frequency) {
        // 一定周期毎にフレーム更新処理を行う
        lastTime = currTime;
        ++ frame;
        long elapsedTimeBySeconds = currTime - lastTimeBySeconds;
        if (elapsedTimeBySeconds >= TimeUnit.SECONDS.toNanos(1L)) {
          // 秒間隔でFPSを再計算する
          mMeasuredFps = (float)((double)TimeUnit.SECONDS.toNanos(frame) / elapsedTimeBySeconds);
          lastTimeBySeconds = lastTime;
          frame = 0L;
        }
        {
          // TODO ここにフレーム更新処理を記述する
        }
      } else {
        // 次の更新周期時刻までスレッドを休止しておく
        long intervalTimeNanoseconds = frequency - elapsedTime;
        TimeUnit.NANOSECONDS.sleep(intervalTimeNanoseconds);
      }
    }
  }
}
Thread#sleepメソッドだとナノ秒単位の指定が煩わしくなるが、TimeUnitを用いれば簡潔に書ける。
エミュレータ環境にてテストしてみたところ、作成途中の2Dブロック崩しゲームアプリにて更新/描画処理ともにおよそ60fpsでの動作が確認できた。
ただし、いくつかの注意事項がある。
1. フレーム処理中にGCを動作させてはならない。必要なインスタンスはあらかじめ作成しておくこと。
2. デバッグ実行の場合、LogCat等のロギング処理のためかフレームレートが著しく低下する。実際の速度を検証する場合はAndroid端末(エミュレータでもok)にインストールし、メニューから起動させること。
3. GLThreadの処理優先が通常のスレッド処理優先と同位らしく、更新処理スレッドでCPUを占有してしまうとGLThread側の処理が圧迫されてしまう。何らかの対処が必要。
0 件のコメント:
コメントを投稿