web-dev-qa-db-ja.com

androidゲームループとレンダリングスレッドでの更新

Androidゲームを作成していますが、現在、希望するパフォーマンスが得られていません。独自のスレッドにゲームループがあり、オブジェクトの位置を更新しています。レンダリングスレッドはこれらのオブジェクトをトラバースします。現在の動作は途切れ途切れ/不均一な動きのように見えます。説明できないのは、更新ロジックを独自のスレッドに配置する前に、glが呼び出す直前のonDrawFrameメソッドに配置したことです。その場合、アニメーションは完全にスムーズで、特にThread.sleepを介して更新ループを調整しようとすると、途切れたり不均一になったりします。更新スレッドを異常終了させて​​も(スリープなし)、アニメーションはスムーズになります。 .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でそれを調整していますが、彼のゲームは非常にスムーズに見えます。誰かが何かアイデアを持っているか、誰かが私が説明していることを経験したことがありますか?

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

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

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バッファーが更新されたばかりのバッファーと等しいことを検出した場合、更新スレッドのロックによって処理されます。それらが等しい場合、これは前のフレームがまだ描画されていないことを更新スレッドに示します。

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

ありがとう

19
user1578101

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

2つのスレッドが非同期で動作していると、このような問題が発生する可能性があります。このように見てください。アニメーションを駆動するイベントは、ハードウェアの「vsync」信号です。つまり、Android Surface Compositorが、データでいっぱいの新しい画面をディスプレイハードウェアに提供するポイントです。 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とsleepの2つのクロックが互いに打ち合うことになり、結果としてアニメーションが途切れます。その上、Thread.sleep()は、正確性や最小スリープ時間について特定の保証をしません。

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

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

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

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

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

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

更新:私は同様の質問に さらに長い答え を書きました。

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

17
fadden

問題の一部は、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;
    }
  }
}
1
Blackhex