Ostatnio zastanowiłem się nad prawidłowością głównej pętli gry z aktualizajcą stałokrokową. Podstawowy szkielet wygląda następująco (pobrany z tego PDFa):
float dt = 0.0f; // czas od ostatniej aktualizacji
float lastUpdateTime = GetCurrentTime(); // czas ostatniej aktualizacji
// przykladowa funkcja GetCurrentTime() pobiera
// nam od systemu aktualny czas w sekundach
float accumulator = 0.0f;
const float TIME_STEP = 0.03; // krok czasowy, a zarazem czas trwania ramki
// fizyki w sekundach; tutaj 30 milisekund, czyli
// ok. 30 aktualizacji na sekundę
while(true) {
dt = GetCurrentTime() - lastUpdateTime; //obliczenie czasu od ostatniej klatki
lastUpdate += dt; //podmiana
accumulator += dt;
GrabInput(); //<-- zbieranie wejscia z klawiatury, myszki, sieci, itp.
while(accumulator > TIME_STEP) {
UpdateGame(TIME_STEP); //<-- aktualizacja fizyki i logiki gry
accumulator -= TIME_STEP;
}
RenderGame(); //<-- wyswietlenie aktualnego stanu na ekranie
}
Pomijam kilka błędów jak używanie typów float
zamiast long
, zmienna lastUpdate
jest zmieniana ale nigdzie nie odczytywana.
Chodzi mi o to, że aktualizacja stanu gry wykonywana jest tak, jakby była wywoływana co stałe 30 ms nawet jeśli tak nie jest, natomiast rendering sceny za każdym obiegiem pętli głównej.
Można zatem uzyskac efekt, że dla prostej sceny update stanów będzie wykonywał się ok 33 razy na sekundę (Update Per Second), a rendering powiedzmy 1000 razy na sekundę (Frame Per Second).
Możemy mieć zatem sytuację taką:
UPS: 33, FPS: 1089
UPS: 33, FPS: 998
UPS: 33, FPS: 1102
UPS: 33, FPS: 1038
...
Mamy zatem tak, że UpdateGame()
aktualizuje stan gry (przesunięcia obiektów, wykrywanie kolizji, usuwanie, zmienianie, dodanie nowych obiektów do sceny) co 30 ms, po czym następuje rendering sceny, a następnie następuje... znowu rendering tej samej sceny, bo jeszcze nie minęło 30 ms by wykonać następną aktualizację i wypchnięcie tej samej sceny do monitora. I tak jeszcze kilkadziesiąt razy zanim zmieni sie stan gry. Co więcej nawet jeśli minie 30 ms a stan się nie zmieni, to i tak wykona się kolejne kilkadziesiąc renderingów znowu tej samej sceny.
Czy to nie jest jakieś marnotrawstwo zasobów karty graficznej i procesora?
Ponadto załóżmy, że dysponujemy niskobudżetowym monitorem z odświeżaniem natywnym 60 Hz. Mamy teraz ograniczenie z góry na 60 FPS przez monitor, którego bufor graficzny jest nadpisywany kilkadziesiąt razy strumieniem danych z tymi samymi informacjami o kolorach pikseli. Włączenie czy wyłączenie v-sync nic tu nie zmieni, a tylko zapewni, że bufor tylny podmieni bufor ekranu w czasie gdy zapełni ostatni piksel (w celu prewencji poszarpanych klatek).
Moje pytanie jest takie dlaczego w głównej pętli gry nie ogranicza się wyświetlanie FPSów?
Co z tego, że po napisaniu gry, możemy pochwalić się tym, że ma ona 1000 FPSów skoro faktycznie będziemy widzieć tylko 60 klatek na sekundę (większość frameów będzie po prostu zdublowana). Nawet jeśli dysponuje się monitorem 240 Hz to i tak wiele ramek będzie zdublowana docierając od GPU do monitora.
Skad ta metodyka pisania pętli gry, żeby nie ograniczać FPSów tylko niech lecą ile fabryka dała?
Ma ktoś jakieś doświadczenie czy sensowne przemyślenia na ten temat?
PS. Tu nawet nie chodzi o to, że przesyłamy do monitora dokładnie te same klatki ale że przesyłamy ich więcej (tyle ile jest w stanie wyprodukować GPU) niż jest w stanie obsłużyć monitor. Nie jest to kontrolowane przez cokolwiek.