Die Bildrate beeinflusst die Geschwindigkeit des Objekts

9

Ich experimentiere mit dem Erstellen einer Game Engine von Grund auf in Java und habe ein paar Fragen. Meine Hauptspielschleife sieht folgendermaßen aus:

        int FPS = 60;
        while(isRunning){
            /* Current time, before frame update */
            long time = System.currentTimeMillis();
            update();
            draw();
            /* How long each frame should last - time it took for one frame */
            long delay = (1000 / FPS) - (System.currentTimeMillis() - time);
            if(delay > 0){
                try{
                    Thread.sleep(delay);
                }catch(Exception e){};
            }
        }

Wie Sie sehen können, habe ich die Framerate auf 60 fps eingestellt, die für die delayBerechnung verwendet wird. Die Verzögerung stellt sicher, dass jeder Frame dieselbe Zeit benötigt, bevor der nächste gerendert wird. In meiner update()Funktion, die ich mache x++, erhöht sich der horizontale Wert eines Grafikobjekts, das ich zeichne, mit folgendem:

bbg.drawOval(x,40,20,20);

Was mich verwirrt, ist die Geschwindigkeit. Wenn ich FPSauf 150 setze , geht der gerenderte Kreis sehr schnell über die Geschwindigkeit, während die Einstellung FPSauf 30 mit der halben Geschwindigkeit über den Bildschirm läuft. Beeinflusst die Framerate nicht nur die "Glätte" des Renderings und nicht die Geschwindigkeit der gerenderten Objekte? Ich denke, ich vermisse einen großen Teil, ich würde eine Klarstellung lieben.

Carpetfizz
quelle
4
Hier ist ein guter Artikel über Spielschleife: Korrigieren Sie Ihren Zeitschritt
Kostya Regent
2
Als Randnotiz versuchen wir im Allgemeinen, Dinge, die nicht in jeder Schleife ausgeführt werden müssen, außerhalb von Schleifen zu platzieren. In Ihrem Code 1000 / FPSkönnte Ihre Division durchgeführt und das Ergebnis einer Variablen vor Ihrer while(isRunning)Schleife zugewiesen werden . Dies hilft, ein paar CPU-Anweisungen zu sparen, um etwas mehr als einmal nutzlos zu machen.
Vaillancourt

Antworten:

21

Sie verschieben den Kreis um ein Pixel pro Bild. Es sollte keine große Überraschung sein, dass sich Ihr Kreis bei einer Rendering-Schleife von 30 FPS um 30 Pixel pro Sekunde bewegt.

Grundsätzlich haben Sie drei Möglichkeiten, um mit diesem Problem umzugehen:

  1. Wählen Sie einfach eine Bildrate und bleiben Sie dabei. Das war es, was viele Spiele der alten Schule taten - sie liefen mit einer festen Rate von 50 oder 60 FPS, die normalerweise mit der Bildschirmaktualisierungsrate synchronisiert waren, und entwarfen einfach ihre Spielelogik, um alles Notwendige innerhalb dieses festgelegten Zeitintervalls zu tun. Wenn dies aus irgendeinem Grund nicht geschehen wäre, müsste das Spiel nur einen Frame überspringen (oder möglicherweise abstürzen), was sowohl das Zeichnen als auch die Spielphysik effektiv auf die halbe Geschwindigkeit verlangsamt.

    Insbesondere Spiele, die Funktionen wie die Erkennung von Hardware-Sprite-Kollisionen verwendeten , mussten so funktionieren, da ihre Spielelogik untrennbar mit dem Rendering verbunden war, das in Hardware mit einer festen Rate ausgeführt wurde.

  2. Verwenden Sie einen variablen Zeitschritt für Ihre Spielphysik. Grundsätzlich bedeutet dies, dass Sie Ihre Spieleschleife so umschreiben, dass sie ungefähr so ​​aussieht:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        float timestep = 0.001 * (time - lastTime);  // in seconds
        if (timestep <= 0 || timestep > 1.0) {
            timestep = 0.001;  // avoid absurd time steps
        }
        update(timestep);
        draw();
        // ... sleep until next frame ...
        lastTime = time;
    }

    und im Inneren update()die physikalischen Formeln anpassen, um den variablen Zeitschritt zu berücksichtigen, z. B. wie folgt:

    speed += timestep * acceleration;
    position += timestep * (speed - 0.5 * timestep * acceleration);

    Ein Problem bei dieser Methode ist, dass es schwierig sein kann , die Physik (meistens) unabhängig vom Zeitschritt zu halten . Sie möchten wirklich nicht, dass die Entfernung, über die Spieler springen können, von ihrer Bildrate abhängt. Die Formel, die ich oben gezeigt habe, funktioniert gut für konstante Beschleunigung, z. B. unter Schwerkraft (und die im verknüpften Beitrag ist ziemlich gut, selbst wenn die Beschleunigung im Laufe der Zeit variiert), aber selbst mit den perfektesten physikalischen Formeln ist es wahrscheinlich, dass mit Schwimmern gearbeitet wird erzeugen ein bisschen "numerisches Rauschen", das insbesondere exakte Wiederholungen unmöglich machen kann. Wenn Sie glauben, dass Sie dies möchten, möchten Sie möglicherweise die anderen Methoden bevorzugen.

  3. Entkoppeln Sie das Update und zeichnen Sie die Schritte. Hier besteht die Idee darin, dass Sie Ihren Spielstatus mit einem festen Zeitschritt aktualisieren, aber zwischen den einzelnen Frames eine unterschiedliche Anzahl von Aktualisierungen ausführen. Das heißt, Ihre Spieleschleife könnte ungefähr so ​​aussehen:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        if (time - lastTime > 1000) {
            lastTime = time;  // we're too far behind, catch up
        }
        int updatesNeeded = (time - lastTime) / updateInterval;
        for (int i = 0; i < updatesNeeded; i++) {
            update();
            lastTime += updateInterval;
        }
        draw();
        // ... sleep until next frame ...
    }

    Um die empfundene Bewegung glatte, mögen Sie vielleicht auch Ihre haben draw()Methode interpolieren Dinge wie Objektpositionen glatt zwischen den vorherigen und den nächsten Spiel Staaten. Dies bedeutet, dass Sie den korrekten Interpolationsversatz an die draw()Methode übergeben müssen, z. B.:

        int remainder = (time - lastTime) % updateInterval;
        draw( (float)remainder / updateInterval );  // scale to 0.0 - 1.0

    Außerdem müsste Ihre update()Methode den Spielstatus tatsächlich einen Schritt voraus berechnen (oder möglicherweise mehrere, wenn Sie eine Spline-Interpolation höherer Ordnung durchführen möchten) und vorherige Objektpositionen speichern, bevor Sie sie aktualisieren, damit die draw()Methode interpolieren kann zwischen ihnen. (Es ist auch möglich, vorhergesagte Positionen basierend auf Objektgeschwindigkeiten und -beschleunigungen zu extrapolieren. Dies kann jedoch ruckartig aussehen, insbesondere wenn sich Objekte auf komplizierte Weise bewegen und die Vorhersagen häufig fehlschlagen.)

    Ein Vorteil der Interpolation besteht darin, dass Sie bei einigen Arten von Spielen die Aktualisierungsrate der Spielelogik erheblich reduzieren können, während die Illusion einer reibungslosen Bewegung erhalten bleibt. Beispielsweise können Sie Ihren Spielstatus möglicherweise nur 5 Mal pro Sekunde aktualisieren, während Sie immer noch 30 bis 60 interpolierte Bilder pro Sekunde zeichnen. Wenn Sie dies tun, möchten Sie möglicherweise auch in Betracht ziehen, Ihre Spiellogik mit der Zeichnung zu verschachteln (dh einen Parameter für Ihre update()Methode zu haben, der besagt, dass nur x % eines vollständigen Updates ausgeführt werden sollen, bevor Sie zurückkehren) und / oder die Spielphysik auszuführen. Logik und Rendering-Code in separaten Threads (Vorsicht vor Synchronisationsfehlern!).

Natürlich ist es auch möglich, diese Methoden auf verschiedene Arten zu kombinieren. In einem Client-Server-Multiplayer-Spiel kann es beispielsweise vorkommen, dass der Server (der nichts zeichnen muss) seine Aktualisierungen zu einem festgelegten Zeitpunkt ausführt (für konsistente Physik und genaue Wiederspielbarkeit), während der Client vorausschauende Aktualisierungen durchführt (bis im Falle einer Meinungsverschiedenheit vom Server überschrieben werden) zu einem variablen Zeitschritt für eine bessere Leistung. Es ist auch möglich, Interpolation und Aktualisierungen mit variablen Zeitschritten sinnvoll zu mischen. In dem gerade beschriebenen Client-Server-Szenario macht es beispielsweise nicht viel Sinn, wenn der Client kürzere Aktualisierungszeitschritte als der Server verwendet. Sie können also eine Untergrenze für den Client-Zeitschritt festlegen und in der Zeichenphase interpolieren, um höhere zu ermöglichen FPS.

(Bearbeiten: Code hinzugefügt, um absurde Aktualisierungsintervalle / -zählungen zu vermeiden, falls der Computer beispielsweise für mehr als eine Sekunde vorübergehend angehalten oder auf andere Weise eingefroren wird, während die Spieleschleife läuft. Vielen Dank an Mooing Duck, der mich daran erinnert hat, dass dies erforderlich ist .)

Ilmari Karonen
quelle
1
Vielen Dank, dass Sie sich die Zeit genommen haben, meine Frage zu beantworten. Ich weiß das wirklich zu schätzen. Ich mag den Ansatz von # 3 sehr, er ist für mich am sinnvollsten. Zwei Fragen, durch was wird das updateInterval definiert und warum teilen Sie es?
Carpetfizz
1
@Carpetfizz: updateIntervalist nur die Anzahl der Millisekunden, die Sie zwischen den Aktualisierungen des Spielstatus benötigen . Für beispielsweise 10 Updates pro Sekunde würden Sie festlegen updateInterval = (1000 / 10) = 100.
Ilmari Karonen
1
currentTimeMillisist keine monotone Uhr. Verwenden Sie nanoTimestattdessen, es sei denn, Sie möchten, dass die Synchronisierung der Netzwerkzeit mit der Geschwindigkeit der Dinge in Ihrem Spiel in Konflikt gerät.
user253751
@MooingDuck: Gut entdeckt. Ich habe es jetzt behoben, denke ich. Vielen Dank!
Ilmari Karonen
@IlmariKaronen: Wenn man sich den Code ansieht, ist es vielleicht einfacher, nur zu while(lastTime+=updateInterval <= time). Das ist aber nur ein Gedanke, keine Korrektur.
Mooing Duck
7

Ihr Code wird derzeit jedes Mal ausgeführt, wenn ein Frame gerendert wird. Wenn die Bildrate höher oder niedriger als die angegebene Bildrate ist, ändern sich Ihre Ergebnisse, da die Aktualisierungen nicht das gleiche Timing haben.

Um dies zu lösen, sollten Sie sich auf Delta Timing beziehen .

Der Zweck von Delta Timing besteht darin, die Auswirkungen von Verzögerungen auf Computern zu beseitigen, die versuchen, komplexe Grafiken oder viel Code zu verarbeiten, indem die Geschwindigkeit von Objekten addiert wird, sodass sie sich unabhängig von der Verzögerung schließlich mit derselben Geschwindigkeit bewegen.

Um dies zu tun:

Dazu wird in jedem Frame pro Sekunde ein Timer aufgerufen, der die Zeit zwischen jetzt und dem letzten Aufruf in Millisekunden enthält.

Sie müssten dann die Deltazeit mit dem Wert multiplizieren, den Sie mit der Zeit ändern möchten. Zum Beispiel:

distanceTravelledSinceLastFrame = Speed * DeltaTime
Statisch
quelle
3
Setzen Sie auch Kappen auf die minimale und maximale Deltatime. Wenn der Computer in den Ruhezustand versetzt und dann fortgesetzt wird, möchten Sie nicht, dass Dinge außerhalb des Bildschirms gestartet werden. Wenn ein Wunder erscheint und time()dasselbe zweimal zurückgibt, möchten Sie keine Div / 0-Fehler und keine Verschwendung von Verarbeitung.
Mooing Duck
@MooingDuck: Das ist ein sehr guter Punkt. Ich habe meine eigene Antwort bearbeitet, um sie wiederzugeben. ( In der Regel, Sie sollten nicht alles von dem Zeitschritt in einem typischen Spielzustand Update werden unterteilt, so dass ein Nullzeitschritt sollte sicher sein, aber so dass sie eine zusätzliche Quelle für mögliche Fehler für wenig hinzufügt oder keinen Gewinn, und sollen so sein , vermieden.)
Ilmari Karonen
5

Das liegt daran, dass Sie Ihre Framerate begrenzen, aber nur ein Update pro Frame durchführen. Nehmen wir also an, das Spiel läuft mit dem Ziel von 60 fps. Sie erhalten 60 logische Updates pro Sekunde. Wenn die Bildrate auf 15 fps sinkt, haben Sie nur 15 logische Aktualisierungen pro Sekunde.

Versuchen Sie stattdessen, die bisher verstrichene Frame-Zeit zu akkumulieren und aktualisieren Sie dann Ihre Spiellogik einmal für jede verstrichene Zeitspanne, z. B. um Ihre Logik mit 100 fps auszuführen, führen Sie das Update einmal für alle 10 akkumulierten ms aus (und subtrahieren Sie diese von der Zähler).

Fügen Sie eine Alternative hinzu (besser für die visuelle Darstellung), und aktualisieren Sie Ihre Logik basierend auf der verstrichenen Zeit.

Mario
quelle
1
dh Update (verstrichene Sekunden);
Jon
2
Und innen ist Position + = Geschwindigkeit * verstrichenSekunden;
Jon