BARDZO Mylące z pętlą gry „Constant Speed ​​Speed ​​Maximum FPS”

12

Niedawno przeczytałem ten artykuł na temat Game Loops: http://www.koonsolo.com/news/dewitters-gameloop/

A ostatnia zalecana implementacja głęboko mnie dezorientuje. Nie rozumiem, jak to działa i wygląda na kompletny bałagan.

Rozumiem zasadę: aktualizuj grę ze stałą prędkością, a wszystko, co pozostanie, renderuje grę tyle razy, ile to możliwe.

Rozumiem, że nie możesz użyć:

  • Uzyskaj dane wejściowe dla 25 tyknięć
  • Renderuj grę dla 975 tyknięć

Podejść, ponieważ dostaniesz wkład w pierwszą część drugiej i byłoby to dziwne? Czy to właśnie dzieje się w tym artykule?


Głównie:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

Jak to w ogóle ważne?

Załóżmy jego wartości.

MAX_FRAMESKIP = 5

Załóżmy, że gra next_game_tick została przypisana chwilę po inicjalizacji, zanim główna pętla gry powie… 500.

Wreszcie, ponieważ używam SDL i OpenGL do mojej gry, a OpenGL służy tylko do renderowania, załóżmy, że GetTickCount()zwraca czas od wywołania SDL_Init, co robi.

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

Źródło: http://www.libsdl.org/docs/html/sdlgetticks.html

Autor zakłada również, że:

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

Po rozwinięciu whileinstrukcji otrzymujemy:

while( ( 750 > 500 ) && ( 0 < 5 ) )

750, ponieważ minął czas, odkąd next_game_tickzostał przypisany. loopswynosi zero, jak widać w artykule.

Więc weszliśmy w pętlę while, zróbmy logikę i zaakceptujmy dane wejściowe.

Yadayadayada.

Pod koniec pętli while, o której przypominam, znajduje się w naszej głównej pętli gry:

next_game_tick += SKIP_TICKS;
loops++;

Zaktualizujmy, jak wygląda następna iteracja kodu while

while( ( 1000 > 540 ) && ( 1 < 5 ) )

1000, ponieważ upłynęło czasu na uzyskanie danych wejściowych i zrobienie rzeczy, zanim dotarliśmy do kolejnego ineteracji pętli, w której wywoływana jest funkcja GetTickCount ().

540, ponieważ w kodzie 1000/25 = 40, zatem 500 + 40 = 540

1, ponieważ nasza pętla wykonała iterację raz

5 , wiesz dlaczego.


Skoro więc ta pętla While jest WYŁĄCZNIE zależna, MAX_FRAMESKIPa nie zamierzona, w TICKS_PER_SECOND = 25;jaki sposób gra powinna działać poprawnie?

Nie było dla mnie zaskoczeniem, że kiedy zaimplementowałem to w moim kodzie, mógłbym poprawnie dodać, ponieważ po prostu zmieniłem nazwę funkcji, aby obsłużyć dane wejściowe użytkownika i zwrócić grę do tego, co autor artykułu w swoim przykładowym kodzie, nic nie zrobił .

Umieściłem fprintf( stderr, "Test\n" );wewnętrzną pętlę while, która nie jest drukowana do końca gry.

W jaki sposób ta pętla gry działa 25 razy na sekundę, gwarantowana, a jednocześnie renderuje tak szybko, jak to możliwe?

Dla mnie, chyba że brakuje mi czegoś OGROMNEGO, wygląda na to ... nic.

I czy ta struktura pętli while nie działa podobno 25 razy na sekundę, a następnie aktualizuje grę dokładnie tak, jak wspomniałem wcześniej na początku artykułu?

Jeśli tak jest, dlaczego nie moglibyśmy zrobić czegoś prostego, takiego jak:

while( loops < 25 )
{
    getInput();
    performLogic();

    loops++;
}

drawGame();

I licz na interpolację w inny sposób.

Wybacz moje bardzo długie pytanie, ale ten artykuł wyrządził mi więcej szkody niż pożytku. Jestem teraz poważnie zdezorientowany - i nie mam pojęcia, jak wdrożyć właściwą pętlę gry z powodu wszystkich tych powstałych pytań.

tsujp
źródło
1
Rants są lepiej skierowane do autora artykułu. Która część jest Twoim obiektywnym pytaniem ?
Anko
3
Czy ta pętla jest w ogóle ważna, ktoś wyjaśnia. Z moich testów nie ma poprawnej struktury, aby działała 25 razy na sekundę. Wyjaśnij mi, dlaczego tak jest. To też nie jest rant, to seria pytań. Czy muszę używać emotikonów, czy wydaje mi się zły?
tsujp
2
Ponieważ twoje pytanie sprowadza się do „Czego nie rozumiem na temat tej pętli gry”, a masz wiele odważnych słów, wydaje się to co najmniej zirytowane.
Kirbinator
@ Kirbinator Rozumiem to, ale starałem się uzasadnić wszystko, co uważam za niezwykłe w tym artykule, aby nie było to płytkie i puste pytanie. Nie sądzę, żeby to było kwintesencją tego, ale i tak chciałbym myśleć, że mam pewne ważne punkty - w końcu jest to artykuł próbujący uczyć, ale nie wykonujący tak dobrej roboty.
tsujp
7
Nie zrozum mnie źle, to dobre pytanie, może być o 80% krótsze.
Kirbinator

Odpowiedzi:

8

Myślę, że autor popełnił drobny błąd:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

Powinien być

while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)

To znaczy: dopóki nie jest jeszcze czas na narysowanie naszej następnej klatki i chociaż nie pominęliśmy tyle klatek, co MAX_FRAMESKIP, powinniśmy poczekać.

Nie rozumiem też, dlaczego aktualizuje się next_game_tickw pętli i zakładam, że to kolejny błąd. Ponieważ na początku klatki można określić, kiedy powinna być następna klatka (przy użyciu stałej liczby klatek). next game tickNie zależy od tego, ile czasu nam zostało po uwzględnieniu aktualizacji i renderingu.

Autor popełnia także kolejny powszechny błąd

z resztą renderuj grę tak wiele razy, jak to możliwe.

Oznacza to wielokrotne renderowanie tej samej klatki. Autor jest nawet świadomy tego:

Gra będzie aktualizowana w stałym tempie 50 razy na sekundę, a renderowanie odbywa się tak szybko, jak to możliwe. Zauważ, że gdy renderowanie jest wykonywane więcej niż 50 razy na sekundę, niektóre kolejne klatki będą takie same, więc rzeczywiste klatki wizualne będą wyświetlane z maksymalną szybkością 50 klatek na sekundę.

Powoduje to jedynie, że procesor graficzny wykonuje niepotrzebną pracę, a jeśli renderowanie trwa dłużej niż oczekiwano, może to spowodować rozpoczęcie pracy na następnej ramce później niż planowano, dlatego lepiej poddać się systemowi operacyjnemu i poczekać.

Roy T.
źródło
+ Głosuj. Mmmm To naprawdę mnie zastanawia, co wtedy zrobić. Dziękuję za wgląd. Prawdopodobnie będę się bawić, problemem jest naprawdę wiedza na temat ograniczania FPS lub dynamicznego ustawiania FPS i jak to zrobić. Moim zdaniem wymagana jest stała obsługa danych wejściowych, aby gra działała w tym samym tempie dla wszystkich. To tylko prosta MMO platformówka 2D (w bardzo długim okresie)
tsujp
Podczas gdy pętla wydaje się poprawna, można to zrobić za pomocą funkcji next_game_tick. Ma to na celu utrzymanie symulacji przy stałej prędkości na wolniejszym i szybszym sprzęcie. Uruchamianie kodu renderowania „tak szybko, jak to możliwe” (lub w każdym razie szybsze niż fizyka) ma sens tylko wtedy, gdy interpolacja w renderowaniu kodu jest bardziej płynna dla szybkich obiektów itp. (Koniec artykułu), ale jest to po prostu marnowana energia, jeśli renderuje więcej niż to, co może pokazać urządzenie wyjściowe (ekran).
Dotti
Więc jego kod jest poprawną implementacją. A my po prostu musimy żyć z tymi odpadami? Czy są metody, na które mogę poszukać.
tsujp
Poddanie się systemowi operacyjnemu i czekanie jest dobrym pomysłem tylko wtedy, gdy masz dobry pomysł, kiedy system operacyjny zwróci ci kontrolę - co może nie być tak szybkie, jak chcesz.
Kylotan
Nie mogę głosować nad odpowiedzią. Całkowicie brakuje mu interpolacji, co sprawia, że ​​cała druga połowa odpowiedzi jest nieważna.
AlbeyAmakiir
4

Może najlepiej, jeśli trochę to uproszczę:

while( game_is_running ) {

    current = GetTickCount();
    while(current > next_game_tick) {
        update_game();

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whilePętla wewnątrz pętli głównej służy do uruchamiania kroków symulacji od miejsca, w którym kiedykolwiek była, do miejsca, w którym powinna być teraz. update_game()funkcja powinna zawsze zakładać, że SKIP_TICKSod ostatniego wywołania upłynęło tylko tyle czasu. Dzięki temu fizyka gry będzie działała ze stałą prędkością na powolnym i szybkim sprzęcie.

Zwiększanie next_game_ticko liczbę SKIP_TICKSruchów przybliża go do bieżącego czasu. Kiedy staje się większy niż obecny czas, przerywa ( current > next_game_tick) i mainloop kontynuuje renderowanie bieżącej ramki.

Po renderowaniu następne wywołanie GetTickCount()zwróci nowy bieżący czas. Jeśli ten czas jest dłuższy niż next_game_tickoznacza to, że jesteśmy już za krokami 1-N w symulacji i powinniśmy nadrobić zaległości, wykonując każdy krok w symulacji z tą samą stałą prędkością. W takim przypadku, jeśli jest niższy, po prostu renderowałby ponownie tę samą ramkę (chyba że występuje interpolacja).

Oryginalny kod ograniczył liczbę pętli, jeśli jesteśmy zbyt daleko ( MAX_FRAMESKIP). To tylko sprawia, że ​​faktycznie pokazuje coś i nie wygląda na zablokowane, jeśli na przykład wznawia się od zawieszenia lub GetTickCount()wstrzymuje grę na długi czas (zakładając, że nie zatrzyma się w tym czasie), dopóki nie dogoni z czasem.

Aby pozbyć się bezużytecznego renderowania tej samej ramki, jeśli nie używasz interpolacji w środku display_game(), możesz owinąć ją wewnątrz, jeśli instrukcja taka jak:

while (game_is_running) {
    current = GetTickCount();
    if (current > next_game_tick) {
        while(current > next_game_tick) {
            update_game();

            next_game_tick += SKIP_TICKS;
        }
    display_game();
    }
    else {
    // could even sleep here
    }
}

Jest to również dobry artykuł na ten temat: http://gafferongames.com/game-physics/fix-your-timestep/

fprintfByć może powód, dla którego wyniki po zakończeniu gry mogą być takie, że nie zostały wypłukane.

Przepraszam, za mój angielski.

Dotti
źródło
4

Jego kod wygląda na całkowicie poprawny.

Rozważ whilepętlę ostatniego zestawu:

// JS / pseudocode
var current_time = function () { return Date.now(); }, // in ms
    framerate = 1000/30, // 30fps
    next_frame = current_time(),

    max_updates_per_draw = 5,

    iterations;

while (game_running) {

    iterations = 0;

    while (current_time() > next_frame && iterations < max_updates_per_draw) {
        update_game(); // input, physics, audio, etc

        next_frame += framerate;
        iterations += 1;
    }

    draw();
}

Mam zainstalowany system, który mówi: „gdy gra jest uruchomiona, sprawdź aktualny czas - jeśli jest większy niż nasz licznik klatek na sekundę, i pominęliśmy rysowanie mniej niż 5 klatek, następnie pomiń rysowanie i po prostu zaktualizuj dane wejściowe i fizyka: w przeciwnym razie narysuj scenę i rozpocznij następną iterację aktualizacji ”

Po każdej aktualizacji zwiększa się czas „następnej_ramki” o idealną liczbę klatek na sekundę. Następnie ponownie sprawdź swój czas. Jeśli twój aktualny czas jest teraz krótszy niż czas, kiedy należy zaktualizować następną ramkę, pomiń aktualizację i narysuj to, co masz.

Jeśli Twój aktualny czas jest większy (wyobraź sobie, że ostatni proces losowania zajął naprawdę dużo czasu, ponieważ gdzieś było trochę czkawki lub garść zbierania śmieci w zarządzanym języku lub implementacja pamięci zarządzanej w C ++ lub cokolwiek innego), to losowanie jest pomijane i next_frameaktualizuje się o kolejną dodatkową klatkę czasu, dopóki aktualizacje nie nadążą za tym, gdzie powinniśmy być na zegarze, lub pominiemy rysowanie wystarczającej liczby klatek, które absolutnie MUSIMY narysować, aby gracz mógł zobaczyć co oni robią.

Jeśli twoja maszyna jest superszybka lub gra jest super-prosta, current_timemoże to być rzadsze next_frame, co oznacza, że ​​nie aktualizujesz podczas tych punktów.

Tam właśnie pojawia się interpolacja. Podobnie, możesz mieć oddzielny bool, zadeklarowany poza pętlami, aby zadeklarować „brudną” przestrzeń.

Wewnątrz pętli aktualizacji ustawiłeś dirty = true, co oznacza, że ​​faktycznie wykonałeś aktualizację.

Następnie zamiast dzwonić draw(), powiedziałbyś:

if (is_dirty) {
    draw(); 
    is_dirty = false;
}

Wtedy nie tracisz żadnej interpolacji dla płynnego ruchu, ale upewniasz się, że aktualizujesz tylko wtedy, gdy faktycznie się dzieje (zamiast interpolacji między stanami).

Jeśli jesteś odważny, znajdziesz artykuł zatytułowany „Napraw swój czas!” przez GafferOnGames.
Podchodzi do problemu nieco inaczej, ale uważam, że jest to ładniejsze rozwiązanie, które robi to samo (w zależności od funkcji twojego języka i tego, jak bardzo zależy ci na obliczeniach fizycznych gry).

Norguard
źródło