Wie funktioniert die Interpolation tatsächlich, um die Bewegung eines Objekts auszugleichen?

10

Ich habe in den letzten 8 Monaten ein paar ähnliche Fragen gestellt, ohne wirklich Freude zu haben, also werde ich die Frage allgemeiner stellen.

Ich habe ein Android-Spiel, das OpenGL ES 2.0 ist. Darin habe ich die folgende Spielschleife:

Meine Schleife arbeitet nach einem festen Zeitschrittprinzip (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();   

Meine Integration funktioniert folgendermaßen:

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

Jetzt funktioniert alles so, wie ich es gerne hätte. Ich kann angeben, dass sich ein Objekt in 2,5 Sekunden über eine bestimmte Entfernung (z. B. Bildschirmbreite) bewegen soll, und genau das wird es tun. Auch aufgrund des Frame-Überspringens, das ich in meiner Spieleschleife zulasse, kann ich dies auf so ziemlich jedem Gerät tun, und es dauert immer 2,5 Sekunden.

Problem

Das Problem ist jedoch, dass beim Überspringen eines Renderrahmens die Grafik stottert. Es ist extrem nervig. Wenn ich die Möglichkeit zum Überspringen von Frames entferne, ist alles reibungslos, wie Sie möchten, läuft jedoch auf verschiedenen Geräten mit unterschiedlichen Geschwindigkeiten. Es ist also keine Option.

Ich bin mir immer noch nicht sicher, warum der Frame überspringt, aber ich möchte darauf hinweisen, dass dies nichts mit schlechter Leistung zu tun hat. Ich habe den Code direkt auf 1 winziges Sprite zurückgeführt und keine Logik (abgesehen von der erforderlichen Logik) Bewegen Sie das Sprite) und ich bekomme immer noch übersprungene Frames. Und dies ist auf einem Google Nexus 10-Tablet (und wie oben erwähnt, muss ich Frames überspringen, um die Geschwindigkeit auf allen Geräten trotzdem konstant zu halten).

Die einzige andere Möglichkeit, die ich habe, ist die Verwendung von Interpolation (oder Extrapolation). Ich habe jeden Artikel gelesen, aber keiner hat mir wirklich geholfen zu verstehen, wie es funktioniert, und alle meine versuchten Implementierungen sind fehlgeschlagen.

Mit einer Methode konnte ich die Dinge reibungslos in Gang bringen, aber es war nicht praktikabel, weil es meine Kollision durcheinander brachte. Ich kann das gleiche Problem bei jeder ähnlichen Methode vorhersehen, da die Interpolation zur Renderzeit an die Rendermethode übergeben wird (und innerhalb dieser angewendet wird). Wenn Collision die Position korrigiert (Zeichen steht jetzt direkt neben der Wand), kann der Renderer seine Position ändern und in die Wand zeichnen .

Ich bin also wirklich verwirrt. Die Leute haben gesagt, dass Sie die Position eines Objekts niemals innerhalb der Rendering-Methode ändern sollten , aber alle Online-Beispiele zeigen dies.

Ich bitte um einen Push in die richtige Richtung. Bitte verlinken Sie nicht auf die beliebten Game-Loop-Artikel (DeWitters, Fix your timestep usw.), da ich diese mehrmals gelesen habe . Ich bin nicht zu fragen jemand meinen Code für mich zu schreiben. Erklären Sie bitte in einfachen Worten, wie Interpolation mit einigen Beispielen tatsächlich funktioniert. Ich werde dann versuchen, irgendwelche Ideen in meinen Code zu integrieren, und bei Bedarf spezifischere Fragen stellen. (Ich bin sicher, dass dies ein Problem ist, mit dem viele Menschen zu kämpfen haben).

bearbeiten

Einige zusätzliche Informationen - Variablen, die in der Spielschleife verwendet werden.

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
quelle
Und der Grund für die Ablehnung ist ...................?
BungleBonce
1
Manchmal unmöglich zu sagen. Dies scheint alles zu haben, was eine gute Frage haben sollte, wenn versucht wird, ein Problem zu lösen. Prägnantes Code-Snippet, Erklärungen zu dem, was Sie versucht haben, Rechercheversuche und eine klare Erklärung, was Ihr Problem ist und was Sie wissen müssen.
Jesse Dorsey
Ich war nicht Ihre Ablehnung, aber bitte klären Sie einen Teil. Sie sagen, die Grafik stottert, wenn ein Frame übersprungen wird. Das scheint eine offensichtliche Aussage zu sein (ein Frame wird übersehen, es sieht so aus, als würde ein Frame übersehen). Können Sie das Überspringen besser erklären? Passiert etwas Seltsameres? Wenn nicht, ist dies möglicherweise ein unlösbares Problem, da Sie keine gleichmäßige Bewegung erzielen können, wenn die Framerate sinkt.
Seth Battin
Danke, Noctrine, es ärgert mich nur sehr, wenn Leute abstimmen, ohne eine Erklärung zu hinterlassen. @ SethBattin, sorry, ja natürlich, Sie haben Recht, das Überspringen von Frames verursacht das Ruckeln, aber eine Interpolation sollte dies klären, wie ich oben sage, ich hatte einige (aber begrenzte) Erfolge. Wenn ich mich irre, wäre die Frage wohl, wie ich es auf verschiedenen Geräten reibungslos und mit der gleichen Geschwindigkeit zum Laufen bringen kann.
BungleBonce
4
Lesen Sie diese Dokumente erneut sorgfältig durch. Sie ändern die Position des Objekts in der Rendering-Methode nicht. Sie ändern nur den scheinbaren Ort der Methode basierend auf ihrer letzten Position und ihrer aktuellen Position basierend darauf, wie viel Zeit vergangen ist.
AttackingHobo

Antworten:

5

Es gibt zwei Dinge, die entscheidend dafür sind, dass die Bewegung reibungslos erscheint. Das erste ist offensichtlich, dass das, was Sie rendern, dem erwarteten Status zum Zeitpunkt der Präsentation des Rahmens für den Benutzer entsprechen muss. Das zweite ist, dass Sie dem Benutzer Bilder präsentieren müssen in einem relativ festen Intervall. Das Präsentieren eines Rahmens bei T + 10 ms, dann eines weiteren bei T + 30 ms und eines weiteren bei T + 40 ms erscheint dem Benutzer als ruckelnd, selbst wenn das, was für diese Zeiten tatsächlich angezeigt wird, gemäß der Simulation korrekt ist.

In Ihrer Hauptschleife scheint es keinen Gating-Mechanismus zu geben, um sicherzustellen, dass Sie nur in regelmäßigen Abständen rendern. Manchmal führen Sie also 3 Aktualisierungen zwischen den Renderings durch, manchmal 4. Grundsätzlich wird Ihre Schleife so oft wie möglich gerendert, sobald Sie genug Zeit simuliert haben, um den Simulationsstatus vor die aktuelle Zeit zu verschieben dann rendern Sie diesen Zustand. Aber jede Variabilität, wie lange das Aktualisieren oder Rendern dauert, und das Intervall zwischen den Frames variieren ebenfalls. Sie haben einen festen Zeitschritt für Ihre Simulation, aber einen variablen Zeitschritt für Ihr Rendering.

Was Sie wahrscheinlich brauchen, ist eine Wartezeit kurz vor dem Rendern, um sicherzustellen, dass Sie das Rendern immer erst zu Beginn eines Renderintervalls starten. Im Idealfall sollte dies anpassungsfähig sein: Wenn das Aktualisieren / Rendern zu lange gedauert hat und der Beginn des Intervalls bereits verstrichen ist, sollten Sie sofort rendern, aber auch die Intervalllänge erhöhen, bis Sie konsistent rendern und aktualisieren können und trotzdem darauf zugreifen können das nächste Rendern vor Ablauf des Intervalls. Wenn Sie genügend Zeit haben, können Sie das Intervall langsam verkürzen (dh die Bildrate erhöhen), um wieder schneller zu rendern.

Aber, und hier ist der Kicker: Wenn Sie den Frame nicht sofort rendern, nachdem Sie festgestellt haben, dass der Simulationsstatus auf "jetzt" aktualisiert wurde, führen Sie zeitliches Aliasing ein. Der Rahmen, der dem Benutzer präsentiert wird, wird etwas zur falschen Zeit präsentiert, und das an sich wird sich wie ein Stottern anfühlen.

Dies ist der Grund für den "Teilzeitschritt", der in den von Ihnen gelesenen Artikeln erwähnt wird. Es gibt es aus einem guten Grund, und das liegt daran, dass Sie die Frames einfach nicht zum richtigen Zeitpunkt präsentieren können, wenn Sie Ihren Physik-Zeitschritt nicht auf ein festes ganzzahliges Vielfaches Ihres festen Rendering-Zeitschritts festlegen. Sie präsentieren sie entweder zu früh oder zu spät. Die einzige Möglichkeit, eine feste Renderrate zu erhalten und dennoch etwas physikalisch Korrektes zu präsentieren, besteht darin, zu akzeptieren, dass Sie sich zum Zeitpunkt des Rendering-Intervalls höchstwahrscheinlich auf halbem Weg zwischen zwei Ihrer festen Physik-Zeitschritte befinden. Dies bedeutet jedoch nicht, dass die Objekte während des Renderns geändert werden. Nur, dass das Rendering vorübergehend festlegen muss, wo sich die Objekte befinden, damit sie irgendwo zwischen dem Ort, an dem sie sich vor und dem Ort nach dem Update befanden, gerendert werden können. Das ist wichtig - ändern Sie niemals den Weltzustand für das Rendern, nur Aktualisierungen sollten den Weltzustand ändern.

Um es in eine Pseudocode-Schleife zu bringen, brauche ich etwas mehr wie:

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);
}

Damit dies funktioniert, müssen alle zu aktualisierenden Objekte wissen, wo sie sich zuvor befanden und wo sie sich jetzt befinden, damit das Rendering das Wissen darüber verwenden kann, wo sich das Objekt befindet.

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);
    }
}

Lassen Sie uns eine Zeitleiste in Millisekunden erstellen, die besagt, dass das Rendern 3 ms dauert, das Aktualisieren 1 ms dauert, Ihr Aktualisierungszeitschritt auf 5 ms festgelegt ist und Ihr Renderzeitschritt bei 16 ms [60 Hz] beginnt (und bleibt).

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. Zuerst initialisieren wir zum Zeitpunkt 0 (also currentTime = 0)
  2. Wir rendern mit einem Anteil von 1,0 (100% currentTime), der die Welt zum Zeitpunkt 0 zeichnet
  3. Wenn dies abgeschlossen ist, beträgt die tatsächliche Zeit 3, und wir erwarten nicht, dass der Frame bis 16 endet. Daher müssen wir einige Updates ausführen
  4. T + 3: Wir aktualisieren von 0 auf 5 (also danach currentTime = 5, previousTime = 0)
  5. T + 4: noch vor dem Frame-Ende, also aktualisieren wir von 5 auf 10
  6. T + 5: noch vor dem Frame-Ende, also aktualisieren wir von 10 auf 15
  7. T + 6: noch vor dem Frame-Ende, also aktualisieren wir von 15 auf 20
  8. T + 7: noch vor dem Frame-Ende, aber currentTime liegt direkt hinter dem Frame-Ende. Wir möchten nicht weiter simulieren, da dies uns über die Zeit hinaus treiben würde, die wir als nächstes rendern möchten. Stattdessen warten wir leise auf das nächste Renderintervall (16)
  9. T + 16: Es ist Zeit, erneut zu rendern. previousTime ist 15, currentTime ist 20. Wenn wir also bei T + 16 rendern möchten, sind wir 1 ms durch den 5 ms langen Zeitschritt. Wir sind also 20% des Weges durch den Rahmen (Anteil = 0,2). Beim Rendern zeichnen wir Objekte 20% des Weges zwischen ihrer vorherigen Position und ihrer aktuellen Position.
  10. Kehren Sie zu 3. zurück und fahren Sie auf unbestimmte Zeit fort.

Es gibt noch eine weitere Nuance, zu weit im Voraus zu simulieren, was bedeutet, dass die Eingaben des Benutzers möglicherweise ignoriert werden, obwohl sie vor dem tatsächlichen Rendern des Frames aufgetreten sind. Machen Sie sich darüber jedoch keine Sorgen, bis Sie sicher sind, dass die Schleife reibungslos simuliert.

MrCranky
quelle
NB: Der Pseudocode ist in zweierlei Hinsicht schwach. Erstens wird der Fall der Todesspirale nicht erfasst (das Aktualisieren dauert länger als bei fixedTimeStep, was bedeutet, dass die Simulation immer weiter zurückfällt, praktisch eine Endlosschleife), zweitens wird das renderInterval nie wieder verkürzt. In der Praxis möchten Sie das renderInterval sofort erhöhen, es dann jedoch im Laufe der Zeit schrittweise so gut wie möglich verkürzen, um eine gewisse Toleranz gegenüber der tatsächlichen Frame-Zeit zu erreichen. Andernfalls wird ein schlechtes / langes Update Sie für immer mit einer niedrigen Framerate satteln.
MrCranky
Vielen Dank für diesen @ MrCranky. Ich habe seit Ewigkeiten Probleme damit, das Rendern in meiner Schleife einzuschränken. Ich konnte einfach nicht herausfinden, wie es geht und fragte mich, ob das eines der Probleme sein könnte. Ich werde dies gründlich durchlesen und Ihre Vorschläge ausprobieren, werde darüber berichten!
Nochmals vielen
Danke @MrCranky, OK, ich habe Ihre Antwort gelesen und erneut gelesen, aber ich kann sie nicht verstehen :-( Ich habe versucht, sie zu implementieren, aber es gab mir nur einen leeren Bildschirm. Ich habe wirklich Probleme damit. VorherigerFrame und aktuellerFrame, nehme ich an bezieht sich auf die vorherige und aktuelle Position meiner sich bewegenden Objekte? Und was ist mit der Zeile "currentFrame = Update ();" - Ich verstehe diese Zeile nicht, bedeutet dies, dass Sie update () aufrufen, da ich nicht sehen kann, wo sonst rufe ich update an? Oder bedeutet es nur, currentFrame (position) auf den neuen Wert zu setzen?
Nochmals vielen
Ja, effektiv. Der Grund, warum ich previousFrame und currentFrame als Rückgabewerte von Update und InitialiseWorldState eingegeben habe, liegt darin, dass Sie nicht nur die aktuelle Position jedes einzelnen Renderings haben müssen, damit das Rendern die Welt auf halbem Weg zwischen zwei festen Aktualisierungsschritten zeichnen kann Objekt, das Sie zeichnen möchten, aber auch ihre vorherigen Positionen. Sie können jedes Objekt beide Werte intern speichern lassen, was unhandlich wird.
MrCranky
Es ist aber auch möglich (aber viel schwieriger), Dinge so zu gestalten, dass alle Zustandsinformationen, die zur Darstellung des aktuellen Zustands der Welt zum Zeitpunkt T erforderlich sind, unter einem einzigen Objekt gespeichert werden. Konzeptionell ist dies viel sauberer, wenn Sie erklären, welche Informationen im System vorhanden sind, da Sie den Frame-Status als etwas behandeln können, das durch einen Aktualisierungsschritt erzeugt wurde. Wenn Sie den vorherigen Frame beibehalten, müssen Sie lediglich ein weiteres dieser Frame-Statusobjekte beibehalten. Allerdings könnte ich die Antwort so umschreiben, dass sie ein bisschen mehr so ​​ist, als würden Sie sie wahrscheinlich tatsächlich implementieren.
MrCranky
3

Was Ihnen alle erzählt haben, ist richtig. Aktualisieren Sie niemals die Simulationsposition Ihres Sprites in Ihrer Renderlogik.

Stellen Sie sich das so vor: Ihr Sprite hat zwei Positionen. wo die Simulation sagt, dass er sich seit dem letzten Simulationsupdate befindet und wo das Sprite gerendert wird. Sie sind zwei völlig unterschiedliche Koordinaten.

Das Sprite wird an seiner extrapolierten Position gerendert. Die extrapolierte Position wird für jeden Render-Frame berechnet, zum Rendern des Sprites verwendet und dann weggeworfen. Das ist alles dazu.

Davon abgesehen scheinen Sie ein gutes Verständnis zu haben. Hoffe das hilft.

William Morrison
quelle
Ausgezeichnet @WilliamMorrison - danke für die Bestätigung, ich war mir nie wirklich 100% sicher, dass dies der Fall ist. Ich denke jetzt, ich bin auf dem Weg, dies bis zu einem gewissen Grad zum Laufen zu bringen - Prost!
BungleBonce
Nur neugierig @WilliamMorrison, wie man mit diesen Wegwerfkoordinaten das Problem mildern kann, dass Sprites in andere Objekte "eingebettet" oder "direkt über" gezeichnet werden - das offensichtliche Beispiel sind solide Objekte in einem 2D-Spiel. Müssten Sie Ihren Kollisionscode auch zum Rendern ausführen?
BungleBonce
In meinen Spielen ja, das ist was ich tue. Bitte sei besser als ich, tu das nicht, es ist nicht die beste Lösung. Es verkompliziert den Rendercode mit der Logik, die nicht verwendet werden sollte, und verschwendet CPU für die redundante Kollisionserkennung. Es ist besser, zwischen der vorletzten Position und der aktuellen Position zu interpolieren. Dies löst das Problem, da Sie nicht auf eine schlechte Position extrapolieren, sondern die Dinge komplizieren, wenn Sie einen Schritt hinter der Simulation rendern. Ich würde gerne Ihre Meinung, Ihren Ansatz und Ihre Erfahrungen hören.
William Morrison
Ja, es ist ein schwieriges Problem zu lösen. Ich habe hier eine separate Frage zu gamedev.stackexchange.com/questions/83230/… gestellt, wenn Sie ein Auge darauf haben oder etwas beitragen möchten. Nun, was Sie in Ihrem Kommentar vorgeschlagen haben, mache ich das nicht schon? (Interpolieren zwischen vorherigem und aktuellem Frame)?
BungleBonce
Nicht ganz. Sie extrapolieren gerade. Sie nehmen die aktuellsten Daten aus der Simulation und extrapolieren, wie diese Daten nach gebrochenen Zeitschritten aussehen. Ich schlage vor, dass Sie stattdessen zwischen der letzten Simulationsposition und der aktuellen Simulationsposition durch gebrochene Zeitschritte für das Rendern interpolieren. Das Rendern erfolgt um 1 Zeitschritt hinter der Simulation. Dies stellt sicher, dass Sie niemals ein Objekt in einem Zustand rendern, den die Simulation nicht validiert hat (dh ein Projektil erscheint nicht in einer Wand, es sei denn, die Simulation schlägt fehl.)
William Morrison