Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

01.09.2022, 13:10

Umsetzung einer korrekten Game Loop

Hi,
da ich hier neu bin, erst ein Mal Hallo an Alle!

Ich habe folgende Frage zu meiner GameLoop und hoffe das Ihr mir hier helfen könnt.

Ich habe für mein kleines Spiel eine Klasse, mit der ich die Anwendung starten kann
und zusätzlich die aktuellen States zuweisen kann. Über die Funktion run() starte ich die Loop:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void App::run()
{
    sf::Clock clock;
    sf::Time  tlu = sf::Time::Zero;   // Zeit, die Vergangen ist, seit dem letzten Update

    while (window.isOpen()) {
        currentState->processEvents(*this, window);
        window.clear(sf::Color(0, 0, 0));
        
                // In die while-Schliefe geht es, sobald tlu größer ist als die Konstant festgelegte Zeit Pro Frame (TPF)
                // Der Wert steht zur Zeit bei 1/60
        while (tlu > TPF) {
            tlu -= TPF;
            currentState->processEvents(*this, window);
            currentState->update(*this, window, TPF);
        }

        currentState->render(window);
        window.display();
        tlu += clock.restart();
    }
}


An die Update-Funktion übergebe ich zusätzlich noch die TPF. Da diese Konstant ist meine Frage:
Ist es überhaupt korrekt diese zu Übergeben und mit zB dem Speed zu multiplizieren?

Hier ein Beispiel aus meiner MainMenu.cpp in der ich schnell ein CircleShape erstellt habe um das ganze zu testen:

C-/C++-Quelltext

1
2
3
4
5
6
7
void MainMenu::update(App& app, sf::RenderWindow& window, sf::Time delta) {
   if (shape.getPosition().x < CONST::DISPLAY_WIDTH && left == false) {
        shape.setPosition(shape.getPosition().x + (speed*delta.asSeconds()), shape.getPosition().y);
    }
   ...
   ...
}


Es scheint zwar so flüssig zu laufen, allerdings kommt mir das ganze falsch vor,
da delta ja Konstant ist und ich delta damit ja eigentlich garnicht als Parameter bräuchte.
Mein eigentliches Ziel mit delta war es aber eigentlich die vergangene Zeit bei Bewegungen auszugleichen bzw. Anzupassen.
Müsste es in der realen Umgebung dann aber nicht so sein, dass delta unterschiedliche größen annimt?

Oder kann es sein das ich hier garnichts mehr mit Speed multiplizieren brauche und das ganze
schon damit ausgeglichen wird, in dem die While-Schleife in App.cpp solange läuft bis der tlu Wert wieder 0 ist und erst dann erneut gerendert wird?

Hoffe Ihr könnt hier etwas Klarheit verschaffen.

Jonathan

Community-Fossil

  • Private Nachricht senden

2

01.09.2022, 16:10

Oder kann es sein das ich hier garnichts mehr mit Speed multiplizieren brauche und das ganze
schon damit ausgeglichen wird, in dem die While-Schleife in App.cpp solange läuft bis der tlu Wert wieder 0 ist und erst dann erneut gerendert wird?

Ja, mehr oder weniger.

Die ganze Schleife ist aber ein wenig komisch. Zunächst: Wenn das Updaten und Rendern beides schnell ist, renderst du das selbe Bild mehrmals, was verschwendete Rechenzeit ist.

Dann: Du musst dir überlegen, ob deine Spiellogik mit einer konstanten oder variablen Framerate laufen soll.

Variabel: Du musst alles mit dem Zeitdelta multiplizieren (bei beschleunigten Bewegungen wird es etwas schwerer, wenn du es richtig machen willst), aber ansonsten nicht viel beachten. Das Ergebnis der Spiellogik kann von der Framewarte abhängen.

Konstant: Du musst nicht mit dem Zeitdelta multiplizieren, dir aber viele zusätzliche Gedanken machen, wenn die Framerate der Logik nicht mehr der des Renderns übereinstimmt. Kritisch wird es, wenn du die Spiellogik beispielsweise 30 mal pro Sekunde aktualisierst, aber du die Szene nur 20 mal pro Sekunde gerendert bekommst. Du kannst das Rendern nicht schneller machen, weil die GPU zu langsam ist und die Spiellogik kannst du nicht langsamer machen, weil sie konstant ist. Ggf. musst du dann mehrere Spiellogik-Schritt pro Rendering berechnen. Aber wenn du jetzt einmal 2 Schritte und einmal 3 Schritte pro gerenderten Frame simulierst, dann sind Bewegungen trotz konstanter Render-Framerate auf einmal ungleichmäßig und ruckelig.
Lieber dumm fragen, als dumm bleiben!

Jonathan

Community-Fossil

  • Private Nachricht senden

3

02.09.2022, 13:00

Was ich gerne mache, weil es einfach und meistens gut genug ist:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while(game_running)
{
  time_delta = last_time - current_time();
  last_time = current_time();

  if(time_delta > 0.1)
    time_delta = 1;

  game.update(time_delta);
  game.render(); // using v sync to limit framerate!
}

void Game::update(time_delta)
{
  ...
  player_position += player_speed * time_delta;
  ...
}


Dank VSync blockiert der Render-Call, d.h. es wird dir nicht passieren, dass das Spiel mit 10000 fps läuft nur weil gerade wenig los ist und die Spiellogik instabil wird, weil time_delta winzig ist. Gleichzeitig wird das Spiel bei niedriger Framerate langsamer laufen, (minimum wäre hier jetzt mal 10 fps) was die Spiellogik auch stabiler macht, weil es zu große Zeitsprünge verhindert. Man kann ohnehin mit unter 10 fps in der Regel nicht vernünftig spielen, daher ist es ok, wenn es dann nicht mehr Echtzeit ist.

Das taugt natürlich nicht unbedingt für Online-Multiplayer und so. Aber in den meisten Fällen braucht man nicht unbedingt mehr, und das hier ist so ziemlich die einfachste Lösung.
Lieber dumm fragen, als dumm bleiben!

4

02.09.2022, 16:00

Hi,
danke erst Mal für deine Ausführlichen Antworten!

In deinem Beispiel bedeutet das, dass sich der Spieler optimalerweise immer um 1 fortbewegt (angenommen Speed == 1), ausser
time_delta ist < 0.1 (zum Beispiel 0,01). Dann bewege ich den Spieler zwar öfters, aber in kleineren Schritten, da
Speed durch die Multiplikation ebenso klein wird. Besser gesagt: Viele Durchläufe - Kleine Schritte (0,01), wenig Durchläufe - große Schritte?

Die 0,1 erhältst du denke ich von 1/10 FPS? Wenn ja hat dies einen bestimmten Grund? Ich habe jetzt während meiner Suche immer was von 1/60 FPS gelesen.

Jonathan

Community-Fossil

  • Private Nachricht senden

5

02.09.2022, 17:24

In deinem Beispiel bedeutet das, dass sich der Spieler optimalerweise immer um 1 fortbewegt (angenommen Speed == 1),

Tatsächlich bedeutet es das nicht, daher ist es gut, dass wir nochmal drüber reden.

Die Idee hier (kann man wie fast alles aber auch anders machen) ist, dass man Geschwindigkeiten in, uhm, Einheiten pro Sekunde definiert. In einem 2D Spiel bewegt sich das Sprite also etwa 20 Pixel pro Sekunde, in einem 3D Spiel meinetwegen einen Meter pro Sekunde, wie man es gerade halt braucht. Die Geschwindigkeit ist in Entfernung pro Sekunde und die hat nichts mit Entfernung pro Frame zu tun.

Damit die Bewegung flüssig aussieht will man wohl mindestens 30 FPS haben, vermutlich aber eher 60, oder auch einfach die jeweilige Obergrenze die der Bildschirm darstellen kann.

Die Abfrage mit dem time_delta > 0.1 sollte normalerweise nie Eintreffen, weil man ja immer mehr als 10 FPS haben will. Die ist nur für den Notfall gedacht, denn es kann aus verschiedenen Gründen ja mal passieren, dass ein Frame länger dauert und dann muss man eben irgendetwas sinnvolles machen. Wenn ein Frame länger als fünf Sekunde dauert kann man z.B. davon ausgehen, dass das Spiel minimiert wurde oder aus sonst irgendeinem Grund richtig hängt, dann will man vielleicht sogar das Spiel ins Menü wechseln lassen anstatt so zu tun als wäre nichts.

Besser gesagt: Viele Durchläufe - Kleine Schritte (0,01), wenig Durchläufe - große Schritte?

Ja, genau.

Die 0,1 erhältst du denke ich von 1/10 FPS? Wenn ja hat dies einen bestimmten Grund? Ich habe jetzt während meiner Suche immer was von 1/60 FPS gelesen.

Sehr niedrige Frameraten will man aus 2 Gründen verhindern: Die Simulation kann kaputt gehen, Objekte bewegen sich z.B. durch Hindernisse hindurch weil sie riesengroße Schritte machen. Und Bewegungen sehen ruckelig aus und Eingaben kommen verzögert an. Die 10 FPS sind hier ziemlich willkürlich, aber die Überlegung ist halt, dass damit die Simulation vermutlich noch stabil genug läuft und 10 FPS eh zu ruckelig sind um beim Spielen Spaß zu machen - daher ist es in diesen Fällen ok wenn das Spiel langsamer läuft, weil man eh seine Grafikdarstellung anpassen will um immer locker über 10 FPS zu landen.

Wichtig ist hier durchaus auch, dass es nicht unbedingt um die mittlere Framerate geht. Vielleicht hat man mal einen Frame der länger dauert, etwa weil man in der Spiellogik etwas kompliziertes Berechnen muss oder so. Und man will halt nicht, dass dann direkt die Simulation im nächsten Schritt explodiert weil time_delta zu groß ist.
Lieber dumm fragen, als dumm bleiben!

6

09.09.2022, 13:24

Wahrscheinlich brauche ich noch ein wenig bis ich das ganze wirklich komplett verstehe :crazy:
Denn tatsächlich habe ich die Framerate der Logik nie wirklich beachtet.
Ich werde jetzt erst ein Mal mit der geposteten Game Loop rumprobieren.

In meinem Fall wird es wahrscheinlich komplett ausreichen die Game Loop so zu gestalten wie du sie gepostet hast,
aber wie läuft sowas dann bei zB Triple A Games ab? Hier dürfe das ja dann auch ein riesen Thema sein und wahrscheinlich wesentlich
komplexer, da man ja bei mordernen Spielen darauf achtet, das alles auf 60 FPS läuft.

David Scherfgen

Administrator

Beiträge: 10 382

Wohnort: Hildesheim

Beruf: Wissenschaftlicher Mitarbeiter

  • Private Nachricht senden

7

09.09.2022, 13:37

komplexer, da man ja bei mordernen Spielen darauf achtet, das alles auf 60 FPS läuft.

Moderne Gaming-Monitore haben eine variable Bildrate, z. B. bis zu 300 Hz. 60 Hz ist da schon die absolute Untergrenze.
Wenn die Logik nur mit 60 Hz läuft, dann kann man trotzdem sinnvoll mehr als 60 Frames pro Sekunde rendern, indem man zwischen den Spielzuständen (z. B. Positionen und Drehungen der Objekte) interpoliert, extrapoliert oder sonst irgendwas Cleveres macht. Wichtig dabei ist jedoch, dass die Eingabe des Spielers möglichst verzögerungsfrei in ein Bild umgesetzt wird.
Such bei Google mal nach fixed framerate logic variable framerate rendering interpolation extrapolation, dann findest du einige Diskussionen zum Thema.

8

09.09.2022, 13:54


Such bei Google mal nach fixed framerate logic variable framerate rendering interpolation extrapolation, dann findest du einige Diskussionen zum Thema.

Danke für den Tipp! Da hab ich ja dann über das WE genug zu lesen :thumbsup:


Was ich gerne mache, weil es einfach und meistens gut genug ist:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
C-/C++-Quelltext

while(game_running)
{
  time_delta = last_time - current_time();
  last_time = current_time();

  if(time_delta > 0.1)
    time_delta = 1;

  game.update(time_delta);
  game.render(); // using v sync to limit framerate!
}

void Game::update(time_delta)
{
  ...
  player_position += player_speed * time_delta;
  ...
}



Hierzu hätte ich noch ein Mal eine Frage, da ich etwas an der Umsetzung scheitere. Der Code geht ja davon aus, dass das ganze in der Game-Class erstellt wird.
Ich hatte ja meine Game Loop in einer Übergeordneten App Klasse. Ist das so dann überhaupt möglich?
Dazu sei gesagt das ich zur Zeit mit der SFML arbeite.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void App::run()
{
    while (window.isOpen()) {
        sf::Clock current_time;   // Hier bin ich mir nicht sicher ob current_time richtig gesetzt ist.
        sf::Time last_time = current_time.getElapsedTime();
        sf::Time time_delta = last_time - current_time.getElapsedTime(); // Könnte ich nicht einfach nur time_delta benutzen und immer die getElapsedTime() nutzen? Eigentlich spare ich mir somit
                                                                                 // alles andere oder?
        

        if (time_delta.asMilliseconds() > 0.1)
            time_delta = 1; // Hier wird mir derzeit noch ein Fehler angezeigt. Leider ist nur das "=" rot unterstrichen und es stehen keine genaueren Fehlerbeschreibungen
                                        // Ich denke das wird irgendwie mit falschen Datentypen etc zusammenhängen?

        currentState->processEvents(*this, window);
        currentState->update(*this, window, time_delta);
        currentState->render(window);
    }
}


EDIT:

So müsste es ja dann eigentlich auch gehen oder?

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (window.isOpen()) {
        sf::Clock clock;
        sf::Time time_delta = clock.getElapsedTime();
        
                 // Hier werden mir allerdings noch Fehler angezeigt, bei denen ich nicht genau weiß woher diese kommen.
        if (time_delta [color=#ff0000][u]>[/u][/color] 0.1)
            time_delta [color=#ff0000][u]=[/u][/color] 1;

        window.clear(sf::Color(0, 0, 0));
        currentState->processEvents(*this, window);
        currentState->update(*this, window, time_delta);
        currentState->render(window);

        clock.restart();
    }


Und wie genau nutze ich V Sync?

Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von »Bytron« (09.09.2022, 14:06)


David Scherfgen

Administrator

Beiträge: 10 382

Wohnort: Hildesheim

Beruf: Wissenschaftlicher Mitarbeiter

  • Private Nachricht senden

9

09.09.2022, 14:27

Und wie genau nutze ich V Sync?

https://www.sfml-dev.org/documentation/2…8f8aff366034f61

V-Sync sorgt dafür, dass ein fertig gerendertes Bild erst dann angezeigt wird, wenn der Monitor mit dem Aufbau eines neuen Bildes beginnt. Es wird dann so lange gewartet, bis dies der Fall ist. Ohne V-Sync wird nicht gewartet, und je nach dem, an welcher Stelle der Monitor gerade mit dem Bildaufbau dran ist, kriegst du vertikal zerteilte Frames (z. B. obere Hälfte des Monitors zeigt ein altes Frame, während die untere das neue zeigt).
Mit V-Sync kann es, wie schon gesagt wurde, nicht passieren, dass du schneller renderst als der Monitor anzeigen kann. Es gibt aber auch Nachteile: Wenn dein Frame nur ein bisschen zu spät fertig ist, dann wird das vorherige Frame zweimal hintereinander angezeigt, und du schaffst effektiv nur 30 fps. Da kommen dann die besagten Monitore mit variabler Bildrate ins Spiel und kombinieren die Vorteile von mit und ohne V-Sync.

Jonathan

Community-Fossil

  • Private Nachricht senden

10

09.09.2022, 16:18

aber wie läuft sowas dann bei zB Triple A Games ab? Hier dürfe das ja dann auch ein riesen Thema sein und wahrscheinlich wesentlich
komplexer

Das wird prinzipiell beliebig kompliziert. Welches Spiel jetzt was genau wie macht, kann ich nicht sagen, aber du hast halt unter Umständen mehrere parallel laufende Systeme, die alle mit unterschiedlicher Framerate laufen. Beispiele:

- Physik läuft mit einer konstanten, und viel höheren Framerate als der Rest des Spiels damit die konstant bleibt. Also Beispielsweise 120 fps, auch wenn das Spiel nur mit 45 fps rendert. Das macht z.B. Bullet so.
- Komplexe Berechnungen werden in separate Threads ausgelagert und dann synchronisiert. Der einfache Fall ist, wenn man auf etwas warten kann (Aufzugstür geht erst dann auf, wenn das nächste Level geladen wurde, während dem Laden kann man aber weiter spielen), aber will man moderne CPUs sinnvoll nutzen läuft Beispielsweise KI, Physik, Spielereingaben und Rendern alles in unterschiedlichen Threads die jeden Frame synchronisiert werden müssen (ggf. auch mehrmals pro Frame?).
- Manchmal wird auch das Level über mehrere Frames hinweg geredert. Das neue Horizon rendert z.B. Wolken über mehrere Frames, weil das sehr teuer ist. Das führt zu unscharfen Bildern, aber das ist ja bei Wolken gerade egal, weshalb es ein guter Trick ist. Man könnte also sagen, dass die Wolken in Wirklichkeit mit 10 fps gerendert werden, während der Rest des Levels mit 60 fps gerendert wird.
- Bei Onlinespielen kann die Framerate die der Server liefert auch geringer sein. Und überhaupt natürlich auch sehr unregelmäßig, man kann sich nicht darauf verlassen, dass das WLAN Signal immer gut ist. Da muss man dann auch entsprechend drauf reagieren.

Es kann dir dann also am Ende passieren, dass dein Spiel mit 5 unterschiedlichen Frameraten gleichzeitig läuft - manche davon konstant, manche variabel. Yay!
Lieber dumm fragen, als dumm bleiben!

Werbeanzeige