19

Android ゲームを作成していますが、現在、希望するパフォーマンスが得られません。オブジェクトの位置を更新する独自のスレッドにゲームループがあります。レンダリング スレッドは、これらのオブジェクトを走査して描画します。現在の動作は、途切れ途切れ/不均一な動きのように見えます。私が説明できないのは、更新ロジックを独自のスレッドに配置する前に、gl 呼び出しの直前に onDrawFrame メソッドにあったということです。その場合、アニメーションは完全にスムーズでしたが、Thread.sleep を介して更新ループを調整しようとすると、途切れ途切れ/不均一になるだけです。更新スレッドが暴走する (スリープしない) ことを許可しても、アニメーションはスムーズですが、Thread.sleep が関係している場合のみ、アニメーションの品質に影響します。

問題を再現できるかどうかを確認するために、スケルトン プロジェクトを作成しました。以下は、更新ループとレンダラーの onDrawFrame メソッドです: 更新ループ

    @Override
public void run() 
{
    while(gameOn) 
    {
        long currentRun = SystemClock.uptimeMillis();
        if(lastRun == 0)
        {
            lastRun = currentRun - 16;
        }
        long delta = currentRun - lastRun;
        lastRun = currentRun;

        posY += moveY*delta/20.0;

        GlobalObjects.ypos = posY;

        long rightNow = SystemClock.uptimeMillis();
        if(rightNow - currentRun < 16)
        {
            try {
                Thread.sleep(16 - (rightNow - currentRun));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

そして、ここに私のonDrawFrameメソッドがあります:

        @Override
public void onDrawFrame(GL10 gl) {
    gl.glClearColor(1f, 1f, 0, 0);
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
            GL10.GL_DEPTH_BUFFER_BIT);

    gl.glLoadIdentity();

    gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
    gl.glTranslatef(transX, GlobalObjects.ypos, transZ);
    //gl.glRotatef(45, 0, 0, 1);
    //gl.glColor4f(0, 1, 0, 0);

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

    gl.glVertexPointer(3,  GL10.GL_FLOAT, 0, vertexBuffer);
    gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, uvBuffer);

    gl.glDrawElements(GL10.GL_TRIANGLES, drawOrder.length,
              GL10.GL_UNSIGNED_SHORT, indiceBuffer);

    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}

レプリカ アイランドのソースを調べたところ、彼は別のスレッドで更新ロジックを実行し、Thread.sleep で調整していますが、彼のゲームは非常にスムーズに見えます。誰かが私が説明していることについて何か考えを持っているか、誰かが経験したことがありますか?

---編集: 2013 年 1 月 25 日 ---
考える時間があり、このゲーム エンジンを大幅に滑らかにしました。私がこれをどのように管理したかは、実際のゲーム プログラマーにとって冒涜的または侮辱的である可能性があるため、これらの考えを自由に修正してください。

基本的な考え方は、更新、描画...更新、描画...のパターンを維持しながら、時間のデルタを比較的同じに保つことです(多くの場合、制御できません)。私の最初の行動方針は、レンダラーが許可されたという通知を受けた後にのみ描画されるように、レンダラーを同期することでした。これは次のようになります。

public void onDrawFrame(GL10 gl10) {
        synchronized(drawLock)
    {
        while(!GlobalGameObjects.getInstance().isUpdateHappened())
        {
            try
            {
                Log.d("test1", "draw locking");
                drawLock.wait();
            } 
            catch (InterruptedException e) 
            {
                e.printStackTrace();
            }
        }
    }

更新ロジックを終了したら、drawLock.notify() を呼び出し、レンダリング スレッドを解放して、更新したものを描画します。これの目的は、更新、描画、更新、描画などのパターンを確立するのに役立つことです。

それを実装すると、かなりスムーズになりましたが、それでも時々動きがジャンプすることがありました. いくつかのテストの後、ondrawFrame の呼び出しの間に複数の更新が発生していることがわかりました。これにより、1 つのフレームに 2 つ (またはそれ以上) の更新の結果が表示され、通常よりも大きなジャンプが発生していました。

これを解決するために私がしたことは、2 つの onDrawFrame 呼び出しの間の時間デルタをある値 (たとえば 18 ミリ秒) に制限し、余分な時間を残りに格納することでした。この残りは、処理できる場合、次の数回の更新で後続の時間デルタに分配されます。このアイデアは、すべての突然のロング ジャンプを防止し、本質的に、複数のフレームにわたるタイム スパイクをスムーズにします。これを行うと、素晴らしい結果が得られました。

このアプローチの欠点は、しばらくの間、オブジェクトの位置が時間の経過とともに正確ではなくなり、その差を補うために実際に速度が上がることです。しかし、それはよりスムーズで、速度の変化はあまり目立ちません.

最後に、最初に作成したエンジンにパッチを適用するのではなく、上記の 2 つのアイデアを念頭に置いてエンジンを書き直すことにしました。おそらく誰かがコメントできるスレッド同期の最適化を行いました。

現在のスレッドは次のようにやり取りします:
-更新スレッドは現在のバッファーを更新し (更新と描画を同時に行うためのダブル バッファー システム)、前のフレームが描画されている場合は、このバッファーをレンダラーに渡します。
- 前のフレームがまだ描画されていない、または描画中の場合、更新スレッドは、レンダリング スレッドが描画したことを通知するまで待機します。
-レンダリング スレッドは、更新が発生したことを更新スレッドから通知されるまで待機します。
-レンダー スレッドが描画するとき、最後に描画した 2 つのバッファーを示す "最後に描画された変数" を設定し、前のバッファーが描画されるのを待っていた場合は更新スレッドに通知します。

少し複雑かもしれませんが、マルチスレッドの利点を考慮して、フレーム n-1 が描画されている間にフレーム n の更新を実行できると同時に、レンダラーが長い時間。さらに説明すると、この複数の更新シナリオは、lastDrawn バッファーが更新されたばかりのバッファーと等しいことが検出された場合、更新スレッドのロックによって処理されます。それらが等しい場合、これは前のフレームがまだ描画されていないことを更新スレッドに示します。

これまでのところ、私は良い結果を得ています。誰かコメントがあれば教えてください。私がしていることについて、正しいか間違っているかについて、あなたの考えを聞かせてください。

ありがとう

4

2 に答える 2

17

(Blackhex からの回答はいくつかの興味深い点を提起しましたが、これらすべてをコメントに詰め込むことはできません。)

非同期で動作する 2 つのスレッドを持つことは、このような問題につながります。このように見てください。アニメーションを駆動するイベントは、ハードウェアの「vsync」信号です。つまり、Android サーフェス コンポジターがデータでいっぱいの新しい画面をディスプレイ ハードウェアに提供するポイントです。vsync が到着するたびに、データの新しいフレームが必要です。新しいデータがない場合、ゲームは途切れ途切れに見えます。その期間に 3 フレームのデータを生成した場合、2 フレームは無視され、バッテリー寿命を浪費するだけです。

(CPU をフル稼働させると、デバイスが熱くなり、サーマル スロットリングが発生して、システム内のすべての動作が遅くなり、アニメーションが途切れる可能性があります。)

ディスプレイとの同期を維持する最も簡単な方法は、すべての状態の更新をonDrawFrame(). 状態の更新を実行してフレームをレンダリングするのに 1 フレームより長くかかることがある場合は、見栄えが悪く、アプローチを変更する必要があります。すべてのゲーム ステートの更新を 2 番目のコアに単純に移行するだけでは、それほど効果はありません。コア #1 がレンダラー スレッドで、コア #2 がゲーム ステートの更新スレッドである場合、コア #1 はコア #2 が状態を更新している間、アイドル状態になります。その後、コア #2 がアイドル状態になっている間、コア #1 が実際のレンダリングを再開します。これには同じくらいの時間がかかります。フレームごとに実行できる計算量を実際に増やすには、2 つ (またはそれ以上) のコアを同時に動作させる必要があります。http://developer.android.com/training/articles/smp.htmlその道を進みたい場合)。

フレームレートを管理するために使用しようとするとThread.sleep()、通常はうまくいきません。vsync 間の期間がどれくらいか、次の vsync が到着するまでの長さを知ることはできません。デバイスごとに異なり、一部のデバイスでは可変の場合があります。基本的に、2 つのクロック (vsync とスリープ) が相互に競合することになり、結果として途切れ途切れのアニメーションになります。その上、Thread.sleep()精度や最小スリープ時間について特定の保証を行うものではありません。

私は実際にはレプリカ島のソースを調べていませんがGameRenderer.onDrawFrame()、ゲーム ステート スレッド (描画するオブジェクトのリストを作成する) と GL レンダラー スレッド (リストを描画するだけ) の間の相互作用を見ることができます。彼らのモデルでは、ゲームの状態は必要に応じてのみ更新され、何も変更されていない場合は、以前の描画リストを再描画するだけです。このモデルは、イベント ドリブン ゲーム、つまり、何かが発生したときに画面上のコンテンツが更新される場合 (キーを押す、タイマーが起動するなど) に適しています。イベントが発生すると、最小限の状態の更新を行い、必要に応じて描画リストを調整できます。

別の見方をすると、レンダリング スレッドとゲーム ステートは、厳密に結び付けられていないため、並行して機能します。ゲーム ステートは、必要に応じて更新を実行するだけで、レンダー スレッドはすべての vsync をロックして、見つけたものを描画します。どちらの側も長時間何かを閉じ込めておかない限り、目に見えて干渉することはありません。唯一の興味深い共有状態は、ミューテックスで保護された描画リストであるため、マルチコアの問題は最小限に抑えられます。

Android Breakout ( http://code.google.com/p/android-breakout/ ) の場合、ゲームにはボールが連続して跳ね返ります。そこで、ディスプレイが許す限り頻繁に状態を更新したいので、前のフレームからの時間差分を使用して状態の変化を vsync から駆動し、どれだけ進んだかを判断します。フレームごとの計算は小さく、レンダリングは最新の GL デバイスではかなり簡単なので、すべて 1/60 秒に簡単に収まります。ディスプレイの更新がはるかに高速 (240Hz) の場合、時々フレームがドロップする可能性があり (これも気付かれる可能性は低い)、フレームの更新で 4 倍の CPU を消費することになります (これは残念なことです)。

何らかの理由でこれらのゲームのいずれかが vsync を逃した場合、プレーヤーは気付く場合と気付かない場合があります。状態は、固定期間の「フレーム」の事前設定された概念ではなく、経過時間によって進みます。したがって、たとえば、ボールは、2 つの連続するフレームのそれぞれで 1 単位、または 1 つのフレームで 2 単位移動します。フレームレートとディスプレイの応答性によっては、これが表示されない場合があります。(これは重要な設計上の問題であり、ゲームの状態を「ティック」の観点から想像すると頭が混乱する可能性があります。)

これらは両方とも有効なアプローチです。重要なのは、 が呼び出されるたびに現在の状態を描画し、onDrawFrameできるだけ頻繁に状態を更新しないことです。

たまたまこれを読んだ人への注意: を使用しないでくださいSystem.currentTimeMillis()。使用された質問の例は、SystemClock.uptimeMillis()実時間ではなく単調な時計に基づいています。それ、またはSystem.nanoTime()、より良い選択です。(私はcurrentTimeMillis、モバイル デバイス上で突然前後にジャンプする可能性がある に対して、ささやかな十字軍に参加しています。)

更新:同様の質問に対してさらに長い回答を書きました。

更新 2:一般的な問題について、さらに長い回答を書きました(付録 A を参照)。

于 2013-01-15T21:41:42.953 に答える
1

問題の一部は、Thread.sleep() が正確でないことが原因である可能性があります。実際の睡眠時間を調査してみてください。

アニメーションをスムーズにするために最も重要なことは、2 つの連続するアニメーション更新スレッド呼び出しの間の連続するレンダリング スレッド呼び出しでアニメーションを線形補間する補間係数 (アルファと呼ばれる) を計算することです。つまり、更新間隔がフレームレートに比べて高い場合、アニメーションの更新ステップを補間しないことは、更新間隔のフレームレートでレンダリングしているようなものです。

編集: 例として、これは PlayN のやり方です:

@Override
public void run() {
  // The thread can be stopped between runs.
  if (!running.get())
    return;

  int now = time();
  float delta = now - lastTime;
  if (delta > MAX_DELTA)
    delta = MAX_DELTA;
  lastTime = now;

  if (updateRate == 0) {
    platform.update(delta);
    accum = 0;
  } else {
    accum += delta;
    while (accum >= updateRate) {
      platform.update(updateRate);
      accum -= updateRate;
    }
  }

  platform.graphics().paint(platform.game, (updateRate == 0) ? 0 : accum / updateRate);

  if (LOG_FPS) {
    totalTime += delta / 1000;
    framesPainted++;
    if (totalTime > 1) {
      log().info("FPS: " + framesPainted / totalTime);
      totalTime = framesPainted = 0;
    }
  }
}
于 2012-12-29T10:52:12.663 に答える