オブジェクトの動きを滑らかにするために、実際に補間はどのように機能しますか?


10

過去8か月間、同じような質問をいくつかしましたが、あまり喜びがなかったので、質問をより一般的なものにします。

OpenGL ES 2.0のAndroidゲームを持っています。その中に私は次のゲームループを持っています:

私のループは固定時間ステップの原理(dt = 1 / ticksPerSecond)で動作します

loops=0;

    while(System.currentTimeMillis() > nextGameTick && loops < maxFrameskip){

        updateLogic(dt);
        nextGameTick+=skipTicks;
        timeCorrection += (1000d/ticksPerSecond) % 1;
        nextGameTick+=timeCorrection;
        timeCorrection %=1;
        loops++;

    }

    render();   

私の統合は次のように機能します:

sprite.posX+=sprite.xVel*dt;
sprite.posXDrawAt=sprite.posX*width;

今、すべてが私が望むようにかなり機能します。オブジェクトを特定の距離(画面の幅など)で2.5秒で移動するように指定できます。また、ゲームループで許可するフレームスキップのため、ほぼすべてのデバイスでこれを実行でき、常に2.5秒かかります。

問題

ただし、問題は、レンダリングフレームがスキップされると、グラフィックが途切れるということです。それは非常に迷惑です。フレームをスキップする機能を削除すると、すべてが好きなようにスムーズになりますが、異なるデバイスでは異なる速度で実行されます。したがって、それはオプションではありません。

フレームがスキップする理由はまだわかりませんが、これはパフォーマンスの低下とは関係がないことを指摘したいと思います。コードを1つの小さなスプライトに戻し、ロジックはありません(必要なロジックは別として)スプライトを移動します)と私はまだスキップされたフレームを取得します。そして、これはGoogle Nexus 10タブレット上にあります(そして上記のように、デバイス間で速度を一定に保つためにフレームスキップが必要です)。

だから、私が持っている他の唯一のオプションは、内挿(または外挿)を使用することです。そこにあるすべての記事を読みましたが、それがどのように機能するかを理解するのに実際に役立ったものはなく、試行したすべての実装は失敗しました。

1つの方法を使用して物事をスムーズに移動させることができましたが、衝突をめちゃくちゃにしたため、実行できませんでした。レンダリングはレンダリング時にレンダリングメソッドに渡され、その中で処理が行われるため、同様の方法で同じ問題を予測できます。したがって、衝突が位置を修正した場合(キャラクターが壁のすぐ隣に立っている)、レンダラーはその位置を変更て壁に描画できます。

だから私は本当に混乱しています。レンダリングメソッド内からオブジェクトの位置を変更するべきではないと人々は言っていますが、オンラインのすべての例でこれが示されています。

私は正しい方向へのプッシュを求めていますこれらを何度も読んだので、人気のゲームループの記事(deWitters、Fix your timestepなど)にリンクしないください。私はしていない私のためのコードを記述するために誰を尋ねます。補間が実際にいくつかの例でどのように機能するかを簡単な言葉で説明してください。次に、アイデアをコードに統合し、必要に応じてより具体的な質問をします。(これは多くの人が悩んでいる問題だと思います)。

編集する

いくつかの追加情報-ゲームループで使用される変数。

private long nextGameTick = System.currentTimeMillis();
//loop counter
private int loops;
//Amount of frames that we will allow app to skip before logic is affected
private final int maxFrameskip = 5;                         
//Game updates per second
final int ticksPerSecond = 60;
//Amount of time each update should take        
private final int skipTicks = (1000 / ticksPerSecond);
float dt = 1f/ticksPerSecond;
private double timeCorrection;

そして、反対票の理由は...................?
BungleBonce 2014

1
時々言うことは不可能。これは、問題を解決しようとするときに良い質問があるはずのものをすべて持っているように見えます。簡潔なコードスニペット、試みたことの説明、研究の試み、問題の内容と知っておくべきことの明確な説明。
ジェシードーシー

私はあなたの反対票ではありませんでしたが、一部を明確にしてください。フレームがスキップされると、グラフィックが途切れると言います。それは明らかなステートメントのようです(フレームが欠落しているように見えます。フレームが欠落しているように見えます)。それで、スキップをよりよく説明できますか?何か奇妙なことが起こりますか?そうでない場合、フレームレートが低下するとスムーズなモーションが得られないため、これは解決できない問題である可能性があります。
セスバティン2014

おかげで、Noctrine、説明を残すことなく人々が反対票を投じたとき、それは本当に私を怒らせます。@SethBattin、申し訳ありません、はい、もちろん、そうです、フレームスキップがジャーキーさを引き起こしていますが、ある種の補間はこれを解決するはずです。私が間違っているとしたら、質問はどうなるでしょう。さまざまなデバイスで同じ速度でスムーズに実行するにはどうすればよいですか?
BungleBonce 2014

4
それらのドキュメントを注意深く読み直してください。それらは実際にはレンダリングメソッド内のオブジェクトの場所を変更しません。これらは、経過時間に基づいて、最後の位置と現在の位置に基づいてメソッドの見かけの位置を変更するだけです。
AttackingHobo 2014

回答:


5

モーションをスムーズに表示するために重要なことは2つあります。1つ目は、レンダリングするものが、フレームがユーザーに提示されたときの予想状態と一致する必要があること、2つ目は、ユーザーにフレームを提示する必要があることです。比較的一定の間隔で。フレームがT + 10msで表示され、次にT + 30msで表示され、次にT + 40msで表示されると、シミュレーションで実際に表示されているものが正しいとしても、ユーザーは混乱しているように見えます。

メインループには、定期的な間隔でのみレンダリングすることを保証するゲーティングメカニズムがないようです。したがって、レンダーの間に3つの更新を行う場合と、4つの場合を行う場合があります。基本的に、ループは可能な限り頻繁にレンダリングされます。シミュレーションの状態を現在の時間の前にプッシュするのに十分な時間をシミュレーションするとすぐに、次に、その状態をレンダリングします。ただし、更新またはレンダリングにかかる​​時間の変動や、フレーム間の間隔も変動します。シミュレーションのタイムステップは固定ですが、レンダリングのタイムステップは可変です。

おそらく必要なのは、レンダリングの直前の待機であり、これにより、レンダリング間隔の開始時にのみレンダリングを開始できます。理想的には、それが適応的である必要があります。更新/レンダリングに時間がかかりすぎて、間隔の開始が既に過ぎている場合は、すぐにレンダリングする必要がありますが、間隔の長さを増やして、一貫してレンダリングおよび更新して、間隔が終了する前の次のレンダリング。十分な時間がある場合は、間隔をゆっくり減らして(つまり、フレームレートを上げて)、再び速くレンダリングすることができます。

しかし、これがキッカーです。シミュレーションの状態が「現在」に更新されたことを検出した直後にフレームをレンダリングしない場合は、一時的なエイリアシングを導入します。ユーザーに提示されているフレームがわずかに間違ったタイミングで提示されており、それ自体がスタッターのように感じられます。

これが、読んだ記事で言及されている「部分的なタイムステップ」の理由です。そこには十分な理由があります。それは、物理タイムステップを固定レンダリングタイムステップの固定整数倍に固定しない限り、適切なタイミングでフレームを表示できないためです。あなたはそれらを発表するのが早すぎるか遅すぎるかのどちらかになります。固定されたレンダリングレートを取得し、物理的に正しいものを提示する唯一の方法は、レンダリング間隔が近づいたときに、固定された2つの物理タイムステップの中間になる可能性が高いことを受け入れることです。ただし、レンダリング中にオブジェクトが変更されるわけではありません。レンダリングでは、オブジェクトの場所を一時的に確立して、更新前と更新後の中間のどこかにオブジェクトをレンダリングできるようにする必要があるだけです。それは重要です-レンダリングのためにワールドの状態を変更しないでください。更新のみがワールドの状態を変更する必要があります。

したがって、それを疑似コードループに入れるには、次のようなものが必要だと思います。

InitialiseWorldState();

previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval

subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame

while (true)
{
    frameStart = ActualTime();

    //Render the world state as if it was some proportion 
    // between previousTime and currentTime
    // E.g. if subFrameProportion is 0.5, previousTime is 0.1 and 
    // currentTime is 0.2, then we actually want to render the state
    // as it would be at time 0.15. We'd do that by interpolating 
    // between movingObject.previousPosition and movingObject.currentPosition
    // with a lerp parameter of 0.5
    Render(subFrameProportion); 

    //Check we've not taken too long and missed our render interval
    frameTime = ActualTime() - frameStart;
    if (frameTime > renderInterval)
    {
        renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
    }

    expectedFrameEnd = frameStart + renderInterval;

    //Loop until it's time to render the next frame
    while (ActualTime() < expectedFrameEnd)
    {
        //step the simulation forward until it has moved just beyond the frame end
        if (previousTime < expectedFrameEnd) &&
            currentTime >= expectedFrameEnd)
        {
            previousTime = currentTime;

            Update();
            currentTime += fixedTimeStep;

            //After the update, all objects will be in the position they should be for
            // currentTime, **but** they also need to remember where they were before,
            // so that the rendering can draw them somewhere between previousTime and
            //  currentTime

            //Check again we've not taken too long and missed our render interval
            frameTime = ActualTime() - frameStart;
            if (frameTime > renderInterval)
            {
                renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
                expectedFrameEnd = frameStart + renderInterval
            }
        }
        else
        {
            //We've brought the simulation to just after the next time
            // we expect to render, so we just want to wait.
            // Ideally sleep or spin in a tight loop while waiting.
            timeTillFrameEnd = expectedFrameEnd - ActualTime();
            sleep(timeTillFrameEnd);
        }
    }

    //How far between update timesteps (i.e. previousTime and currentTime)
    // will we be at the end of the frame when we start the next render?
    subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}

これが機能するためには、更新されているすべてのオブジェクトが、オブジェクトがどこにあったかに関する情報をレンダリングで使用できるように、オブジェクトが以前どこにあったか、現在どこにあるかの知識を保持する必要があります。

class MovingObject
{
    Vector velocity;
    Vector previousPosition;
    Vector currentPosition;

    Initialise(startPosition, startVelocity)
    {
        currentPosition = startPosition; // position at time 0
        velocity = startVelocity;
        //ignore previousPosition because we should never render before time 0
    }

    Update()
    {
        previousPosition = currentPosition;
        currentPosition += velocity * fixedTimeStep;
    }

    Render(subFrameProportion)
    {
        Vector actualPosition = 
            Lerp(previousPosition, currentPosition, subFrameProportion);
        RenderAt(actualPosition);
    }
}

そして、ミリ秒単位でタイムラインをレイアウトしてみましょう。レンダリングの完了には3ms、更新には1msかかり、更新のタイムステップは5msに固定され、レンダリングのタイムステップは16ms [60Hz]で始まります(そのままです)。

0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33
R0          U5  U10 U15 U20 W16                                 R16         U25 U30 U35 W32                                 R32
  1. まず、時間0で初期化します(したがってcurrentTime = 0)。
  2. 1.0(100%currentTime)の比率でレンダリングし、時間0で世界を描画します
  3. それが完了すると、実際の時間は3になり、フレームが16まで終了するとは予想されないため、いくつかの更新を実行する必要があります
  4. T + 3:0から5に更新します(その後、currentTime = 5、previousTime = 0)
  5. T + 4:フレームが終了する前なので、5から10に更新します
  6. T + 5:フレームが終了する前なので、10から15に更新します
  7. T + 6:フレームが終了する前なので、15から20に更新します
  8. T + 7:まだフレーム終了の前ですが、currentTimeはフレーム終了を超えています。これ以上シミュレートする必要はありません。これを行うと、次にレンダリングする時間を超えてしまいます。代わりに、次のレンダリング間隔を静かに待ちます(16)
  9. T + 16:もう一度レンダリングする時が来ました。previousTimeは15、currentTimeは20です。したがって、T + 16でレンダリングする場合、5msの長いタイムステップを1ms通過します。つまり、フレーム全体の20%です(比率= 0.2)。レンダリングするとき、オブジェクトを以前の位置と現在の位置の間の20%の位置に描画します。
  10. 3.にループバックし、無期限に続行します。

シミュレーションのタイミングが早すぎるという別のニュアンスがあります。つまり、ユーザーの入力は、フレームが実際にレンダリングされる前に発生したとしても無視される可能性がありますが、ループがスムーズにシミュレーションされていると確信できるまでは心配する必要はありません。


注意:疑似コードは2つの点で弱いです。1つ目は、死のスパイラルのケースをキャッチしません(fixedTimeStepよりも更新に時間がかかります。つまり、シミュレーションがさらに遅れ、実質的に無限ループになります)。2つ目は、renderIntervalが再び短くなることはありません。実際には、renderIntervalをすぐに増やしたいが、実際のフレーム時間の許容範囲内で可能な限り、時間をかけて徐々にそれを徐々に短くしていきます。それ以外の場合、1つの不良/長い更新は、低いフレームレートで永遠にサドルします。
MrCranky 2014

この@MrCrankyに感謝します。確かに、私はループでレンダリングを「制限」する方法について長い間苦労してきました!それを行う方法を理解できず、それが問題の1つであるかどうか疑問に思いました。私はこれを適切に読んで、あなたの提案を試してみます、報告します!ありがとうございました:-)
BungleBonce

ありがとう、@ MrCranky、OK、私はあなたの答えを読んで読み直しましたが、理解できません:-(実装しようとしましたが、画面に何も表示されませんでした。本当にこれに苦労しています。previousFrameとcurrentFrame移動オブジェクトの以前の位置と現在の位置に関連していますか?また、「currentFrame = Update();」という行についてはどうですか?-この行を取得できません。これはupdate();を呼び出すことを意味します。それ以外の場合はupdateを呼び出していますか?それとも、単にcurrentFrame(位置)を新しい値に設定することを意味しますか?ご協力ありがとうございます!!
BungleBonce

はい、効果的に。previousFrameとcurrentFrameをUpdateとInitialiseWorldStateからの戻り値として配置する理由は、レンダリングが2つの固定された更新ステップの途中であるため、レンダリングが世界を描画できるようにするためです。すべての現在の位置だけでなく、描画したいオブジェクトだけでなく、以前の位置も。各オブジェクトに両方の値を内部的に保存させると、扱いにくくなります。
MrCranky 2014

しかし、時間をTにしたときの世界の現在の状態を表すために必要なすべての状態情報が単一のオブジェクトの下に保持されるように物事を設計することも可能です(ただし、はるかに困難です)。フレーム状態を更新ステップによって生成されたものとして扱うことができ、前のフレームを維持することは、これらのフレーム状態オブジェクトをもう1つ保持することです。ただし、実際にはおそらく実装するように、答えをもう少し書くように書き直す場合があります。
MrCranky 2014

3

誰もがあなたに言ってきたことは正しいです。レンダーロジックでスプライトのシミュレーション位置を更新しないでください。

このように考えると、スプライトには2つの位置があります。シミュレーションが最後のシミュレーションの更新時のものであるとシミュレーションで示され、スプライトがレンダリングされる場所。それらは2つの完全に異なる座標です。

スプライトは外挿位置でレンダリングされます。外挿位置は、レンダーフレームごとに計算され、スプライトのレンダリングに使用された後、破棄されます。これですべてです。

それ以外はよくご理解いただいているようです。お役に立てれば。


すばらしい@WilliamMorrison-これを確認してくれてありがとう、これが事実であることを本当に100%確信することはできませんでした。
BungleBonce 2014

これらの使い捨て座標を使用して、好奇心の強い@WilliamMorrisonは、スプライトが他のオブジェクトに「埋め込まれた」または「真上に」描画される問題を軽減する方法を明らかにします。レンダリング時にコリジョンコードも実行する必要がありますか?
BungleBonce 2014

私のゲームではそうです、それは私がやっていることです。私より良くしてください、それをしないでください、それは最善の解決策ではありません。これは、使用すべきでないロジックでレンダリングコードを複雑にし、冗長な衝突検出でCPUを浪費します。最後から2番目の位置と現在の位置の間を補間することをお勧めします。これは、悪い位置に外挿するのではなく問題を解決しますが、シミュレーションの1つ後ろのステップをレンダリングするので複雑になります。私はあなたの意見、あなたが取るアプローチ、そしてあなたの経験を聞くのが大好きです。
ウィリアムモリソン

ええ、それは解決するのが難しい問題です。この点について別の質問をしましたが、gamedev.stackexchange.com / questions / 83230 /…監視したり何かを寄付したりする場合は。さて、あなたのコメントであなたが提案したこと、私はまだこれをしていませんか?(前のフレームと現在のフレームの間の補間)?
BungleBonce 2014

結構です。現在、実際に外挿しています。シミュレーションから最新のデータを取得し、分数タイムステップ後にそのデータがどのように見えるかを推定します。代わりに、最後のシミュレーション位置と現在のシミュレーション位置の間を、レンダリングの分数タイムステップで補間することをお勧めします。レンダリングはシミュレーションより1タイムステップ遅れます。この性を保証あなたはシミュレーションを検証しませんでした状態でオブジェクトをレンダリングすることは決してないだろう(。シミュレーションが失敗しない限り、すなわちAの発射が壁に表示されません。)
ウィリアム・モリソンに
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.