Dobry sposób na zbudowanie pętli gry w OpenGL

31

Obecnie zaczynam uczyć się OpenGL w szkole, a pewnego dnia zacząłem tworzyć prostą grę (na własną rękę, nie dla szkoły). Używam freeglut i buduję go w C, więc w mojej pętli gry naprawdę korzystałem właśnie z funkcji, którą przekazałem, glutIdleFuncaby zaktualizować cały rysunek i fizykę za jednym przejściem. To było w porządku dla prostych animacji, które nie przejmowały się zbytnio liczbą klatek na sekundę, ale ponieważ gra jest oparta głównie na fizyce, naprawdę chcę (muszę) powiązać, jak szybko się aktualizuje.

Tak więc moją pierwszą próbą było przekazanie mojej funkcji do glutIdleFunc( myIdle()), aby śledzić, ile czasu minęło od poprzedniego wywołania, i aktualizować fizykę (i obecnie grafikę) co tyle milisekund. Zwykłem timeGetTime()to robić (za pomocą <windows.h>). I to sprawiło, że pomyślałem, czy korzystanie z funkcji bezczynności jest naprawdę dobrym sposobem na obejście pętli gry?

Moje pytanie brzmi: jaki jest lepszy sposób na implementację pętli gry w OpenGL? Czy powinienem unikać korzystania z funkcji bezczynności?

Jeff
źródło
gamedev.stackexchange.com/questions/651/… Duplikat?
Kaczka komunistyczna
Wydaje mi się, że to pytanie jest bardziej specyficzne dla OpenGL i czy wskazane jest używanie GLUT dla pętli
Jeff
1
Człowieku , nie używaj GLUT do większych projektów . Jednak jest mniejszy w przypadku mniejszych.
bobobobo

Odpowiedzi:

29

Prosta odpowiedź brzmi: nie, nie chcesz używać wywołania zwrotnego glutIdleFunc w grze, która ma jakąś symulację. Powodem tego jest to, że ta funkcja rozdziela animację i rysuje kod z obsługi zdarzeń okna, ale nie asynchronicznie. Innymi słowy, odbieranie i przekazywanie zdarzeń na okno przeciąga kod (lub cokolwiek, co umieścisz w tym wywołaniu zwrotnym), jest to doskonale w przypadku aplikacji interaktywnej (interakcja, a następnie odpowiedź), ale nie w przypadku gry, w której stan fizyki lub gry musi się rozwijać niezależne od interakcji lub czasu renderowania.

Chcesz całkowicie oddzielić obsługę danych wejściowych, stan gry i narysować kod. Istnieje proste i czyste rozwiązanie tego problemu, które nie wymaga bezpośredniej biblioteki graficznej (tzn. Jest przenośne i łatwe do wizualizacji); chcesz, aby cała pętla gry produkowała czas, a symulacja pochłaniała wyprodukowany czas (w kawałkach). Kluczem jest jednak zintegrowanie ilości czasu zużytego przez symulację z animacją.

Najlepsze wyjaśnienie i samouczek, które znalazłem na ten temat, to Fix Your Timestep Glenna Fiedlera

Ten samouczek ma pełne podejście, jednak jeśli nie masz rzeczywistej symulacji fizyki, możesz pominąć prawdziwą integrację, ale podstawowa pętla nadal sprowadza się do (w pełnym pseudo-kodzie):

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

W ten sposób zatrzymanie kodu renderowania, obsługa danych wejściowych lub system operacyjny nie powodują pogorszenia stanu gry. Ta metoda jest również przenośna i niezależna od biblioteki grafiki.

GLUT jest dobrą biblioteką, ale jest ściśle sterowana zdarzeniami. Rejestrujesz połączenia zwrotne i odpalasz główną pętlę. Zawsze przekazujesz kontrolę nad swoją główną pętlą za pomocą GLUT. Można go obejść za pomocą hacków , można także sfałszować zewnętrzną pętlę za pomocą timerów i tym podobne, ale inna biblioteka jest prawdopodobnie lepszym (łatwiejszym) sposobem na przejście. Istnieje wiele alternatyw, oto kilka (z dobrą dokumentacją i szybkimi samouczkami):

  • GLFW, która daje możliwość uzyskania wbudowanych zdarzeń wejściowych (we własnej pętli głównej).
  • SDL , jednak jego nacisk nie jest specjalnie OpenGL.
charstar
źródło
Dobry artykuł. Aby to zrobić w OpenGL, nie użyłbym glutMainLoop, ale zamiast tego własnego? Gdybym to zrobił, czy byłbym w stanie korzystać z glutMouseFunc i innych funkcji? Mam nadzieję, że to ma sens, wciąż jestem całkiem nowy w tym wszystkim
Jeff
Niestety nie. Wszystkie wywołania zwrotne zarejestrowane w GLUT są odpalane z poziomu glutMainLoop, gdy kolejka zdarzeń jest opróżniana i iteruje. Dodałem kilka linków do alternatyw w treści odpowiedzi.
charstar
1
+1 za porzucenie GLUT za cokolwiek innego. O ile mi wiadomo, GLUT nigdy nie był przeznaczony wyłącznie do testowania aplikacji.
Jari Komppa
Nadal jednak musisz obsługiwać zdarzenia systemowe, nie? Zrozumiałem, że używając glutIdleFunc, po prostu obsługuje pętlę GetMessage-TranslateMessage-DispatchMessage (w Windows) i wywołuje twoją funkcję, gdy nie ma żadnej wiadomości do odebrania. Zrobiłbyś to i tak sam, bo inaczej cierpiałbyś z niecierpliwością systemu operacyjnego na oznaczenie aplikacji jako niereagującej, prawda?
Ricket
@Ricket Technicznie, glutMainLoopEvent () obsługuje przychodzące zdarzenia poprzez wywoływanie zarejestrowanych wywołań zwrotnych. glutMainLoop () jest efektywnie {glutMainLoopEvent (); IdleCallback (); } Kluczową kwestią jest tutaj to, że przerysowanie jest również wywołaniem zwrotnym obsługiwanym z poziomu pętli zdarzeń. Państwo może dokonać biblioteki zachowywać zdarzeniami jak jednorazowa napędzane, ale Maslowa Hammer i to wszystko ...
charstar
4

Myślę, że glutIdleFuncjest w porządku; i tak właściwie to byś zrobił ręcznie, gdybyś go nie używał. Bez względu na to, co będziesz mieć ciasną pętlę, która nie jest wywoływana w ustalonym wzorze, musisz dokonać wyboru, czy chcesz ją przekształcić w pętlę o stałym czasie, czy zachować ją w pętli o zmiennym czasie i utworzyć upewnij się, że wszystkie twoje konta matematyczne interpolują czas. A nawet jakoś ich połączenie.

Z pewnością możesz mieć myIdlefunkcję, którą przekazujesz glutIdleFunc, a w tym mierzyć upływający czas, dzielić go przez ustalony czas i wywoływać inną funkcję, która wiele razy.

Oto czego nie robić:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

fixedTimestep nie będzie wywoływany co 10 milisekund. W rzeczywistości działałby wolniej niż w czasie rzeczywistym, i możesz skończyć z fałszowaniem liczb, aby zrekompensować, gdy tak naprawdę przyczyną problemu jest utrata reszty po podzieleniu timeDifferenceprzez 10.

Zamiast tego zrób coś takiego:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

Teraz ta struktura pętli zapewnia, że ​​twoja fixedTimestepfunkcja będzie wywoływana raz na 10 milisekund, bez utraty milisekund jako reszty lub czegoś podobnego.

Ze względu na wielozadaniowość systemów operacyjnych nie można polegać na wywołaniu funkcji dokładnie co 10 milisekund; ale powyższa pętla nastąpiłaby na tyle szybko, że mam nadzieję, że byłaby wystarczająco blisko i można założyć, że każde wywołanie fixedTimestepzwiększy wartość symulacji o 10 milisekund.

Przeczytaj również tę odpowiedź, a zwłaszcza linki podane w odpowiedzi.

Ricket
źródło