Główna pętla gry (fps vs. ups) dlaczego nie ograniczamy fpsów?

Główna pętla gry (fps vs. ups) dlaczego nie ograniczamy fpsów?
ReallyGrid
  • Rejestracja:około 11 lat
  • Ostatnio:24 dni
0

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):

Kopiuj
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.

edytowany 3x, ostatnio: ReallyGrid
Spine
  • Rejestracja:prawie 22 lata
  • Ostatnio:około 2 godziny
  • Postów:6635
2

V-sync chyba zapobiega temu, że "FPSy lecą ile fabryka dała"?
https://scroll.morele.net/technologia/v-sync-informacje-o-synchronizacji-pionowej/


🕹️⌨️🖥️🖱️🎮
ReallyGrid
  • Rejestracja:około 11 lat
  • Ostatnio:24 dni
0

@Spine: Połowicznie się zgodzę ale nie jest to do końca prawdą. Przynajmniej nie w tym przykładzie. Pętla gry jest bytem programowym przetwarzanym przez CPU. CPU w ciągu 1/60 sekundy w w/w. pętli powiedzmy, że jest w stanie przetworzyć (tutaj mam namyśli wysłać żądanie odrysowania ramki obrazu do GPU) 15 żądań repaint(), które są wysyłane do GPU. To czy GPU po wysłaniu pierwszego pakietu danych do monitora zostanie zablokowane przez technologię v-sync na kolejne 14 pakietów czy nie, nie ma znczenia w moim pytaniu.
CPU musi przygotować scenę do wyrenderowania przez GPU.
Dajmy na to taki bardzo uroszczony pseudokod:

Kopiuj
Image image = StworzPustyObiektImage() // tworzy obiekt Image o rozdzielczości ekranu np: 1920 x 1080.
DodajTileOrazSprity(image)             // w pętli nanosi na niego bezpośrednio proste obrazki albo nawet piksel po pikselu
                                       // zapełnia go odpowiednimi kolorami, usuwając te, które znajdują się poza obszarem
                                       // rysowania (poza ekranem)
image.repaint()                        // wyślij żądanie odrysowania do GPU (nie do ekranu ale do GPU!!!)

Więc jak widzisz do GPU i tak trafi w ciągu 1/60 s 15 żądań odrysowania tej samej sceny. GPU przetworzy to sobie z formatu zrozumiałego dla GPU na format zrozumiały dla ekranu. W przypadku OpenGL być może shadery wykonają jeszcze jakąś transormację tej ramki zanim wyślą ją na ekran monitora (w przypadku 3D dodadzą oświetlenie, cienie, mgłę i co tam jeszcze się chce). Ale to jest mało istotne. Podałem tylko ekstremalnie prosty pseudokod.

Pytanie jest zatem aktualne. Dlaczego pętla gry, która wykonuje się na CPU nie ogranicza ilości ich odrysowań? Chociażby do natywnej ilości odświeżania monitora. Albo jeszcze lepiej jako minimum z odświeżania natywnego i ustalonej ilości UPS. Bo jeśli mamy jak powyżej UPS 30 razy na sek. to rendering większej ilości klatek niż 30 / s nie ma chyba sensu. Dla dalszej dyskusji załóżmy, że wyłączyliśmy V-sync albo nasza biblioteka graficzna, w której programujemy grę nie obsługuje jej wyłączenia ;) Pytanie jest cały czas aktualne

edytowany 6x, ostatnio: ReallyGrid
XY
  • Rejestracja:ponad 6 lat
  • Ostatnio:28 dni
  • Postów:257
1

Ale co to znaczy, że nie ogranicza się? Jak najbardziej są gry, które potrafią ograniczać FPS do podanego maksimum niezależnie od v-sync.

edytowany 2x, ostatnio: xy
flowCRANE
Np. Fairtris. :D
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12166
0
ReallyGrid napisał(a):

Możemy mieć zatem sytuację taką:
UPS: 33, FPS: 1089
UPS: 33, FPS: 998
UPS: 33, FPS: 1102
UPS: 33, FPS: 1038

Przecież to głupie jest. Tzn. z jednej strony nie jest, bo mamy dwufunkcyjny mechanizm synchronizujący działanie, co jest przydatnym ficzerem, ale z drugiej strony jednak jest głupie, bo renderuje się 1000 klatek pomimo tego, że faktycznie stan gry (a tym samym obraz na ekranie) zmienił się tylko 33 razy. Summa summarum, zmarnowało się moc obliczeniową renderując 967 klatek-duplikatów, co jest absolutnie bez sensu.

Czy to nie jest jakieś marnotrawstwo zasobów karty graficznej i procesora?

Ano jest — nie trudno to dostrzec. Ogólnie to nie widzę większego sensu w implementacji czegoś takiego, bo nie rozwiązuje to problemu sporadycznego odświeżania stanu świata gry (logiki), a tylko komplikuje sprawę związaną z renderowaniem (nie można przecież jednocześnie aktualizować logiki i renderować, to musi być zsynchronizowane) oraz marnuje moc obliczeniową renderując klatki, które niczym się nie różnią od poprzednich. A skoro gracz nie zobaczy różnicy, to nie ma żadnej różnicy czy się wyrenderuje klatkę czy nie.

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).

V-Sync i podobne technologie mają zapewnić, że klatka zostanie wypełniona danymi do końca i dopiero wtedy zostanie wysłana do monitora, w odpowiednim momencie. To eliminuje problem z rozrywaniem ekranu, a z drugiej strony jest naturalnym odmierzaczem czasu pomiędzy klatkami, dzięki czemu można aktualizować stan gry oraz renderować klatki z taką samą częstotliwością.

Problem pojawia się wtedy, gdy aktualizacja logiki jest długotrwała i nie da się wyciągnąć więcej niż 30-40fps. W takim przypdaku trzeba użyć wątków i logikę gry aktualizować niezależnie od renderowania. Jednak renderowanie klatek w przypadku gdy stan świata się nie zmienił, jest nadal bezcelowe.

V-Sync nadal zapewni brak rozrywania ekranu, jednak aby oszczędzić maksymalnie moc obliczeniową, wątek renderujący powinien najpierw sprawdzić czy stan gry się zmienił i jeśli nie, to odpuścić renderowanie i poczekać aż stan się zmieni. Wtedy nowa klatka będzie zawierać nowe dane (z nowym stanem gry), co będzie widoczne na ekranie.

Moje pytanie jest takie dlaczego w głównej pętli gry nie ogranicza się wyświetlanie FPSów?

To zależy — większość gier jednak ma jakąś kontrolę nad klatkażem, więc to trochę niesprawiedliwie wrzucać wszystko do jednego worka. Można mieć zmienny framerate lub stały, ten stały może być ograniczony przez dewelopera lub przez V-Sync, można mieć częstotliwość aktualizacji logiki oraz renderowania zgodne lub nie. Nieważne co się wybierze, ważne aby gra wykonywała wszystko co potrzebuje, a resztę mocy oddała innym procesom.

To wszystko zależy od wymagań samej gry oraz również od woli deweloperów. Dając możliwość konfiguracji silnika przez graczy, będą mogli dostosować działanie gry do własnego sprzętu. A że współcześnie używamy komputerów o różnej sprawności, od potężnych gamingowych pecetów do budżetowych laptopów, dla mnie (jako gracza) możliwość konfiguracji działania gry to mus. Lepiej jest wyłączyć trochę ficzerów i zmniejszyć rozdzielczość, niż grać w 15fps.

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.

Dokładnie. Choć zawsze możesz się pochwalić, że umiesz w wątki. :D

Skad ta metodyka pisania pętli gry, żeby nie ograniczać FPSów tylko niech lecą ile fabryka dała?

Uniezależnienie pracy modułu aktualizującego logikę od modułu renderującego jest konieczne w przypadku bardzo wymagających gier. Jednak wielowątkowość oraz synchronizacja tych mechanizmów jest rozwiązaniem problemu dużej złożoności obliczeniowej, wymaganej przez grę i taka technika powinna być stosowana tylko w przypadku, gdy niemożliwe jest utrzymanie zgodnej częstotliwości aktualizacji oraz renderowania, albo jeśli chcemy zapewnić działanie gry na starszych sprzętach o niskiej wydajności. Nie robi się tego po to, aby było ”fajnie” czy ”pro”.

W przypadkach gier, które nie są zasobożerne, implementacja czegoś takiego jest absolutnie bezsensowna i będzie działać na szkodę gracza. Zmarnuje zasoby jego komputera i zwiększy pobór mocy, co przełoży się na wyższy rachunek za prąd (który jest coraz droższy) oraz skróci czas pracy baterii (w przypadku laptopów i urządzeń mobilnych).

Brzydko podsumowując, myślę, że wielu wyssało ten mechanizm z tej samej d**y, z której wyssano wciąż popularne przeświadczenie, że po to jest procesor, aby zużywać tyle jego mocy ile tylko oferuje. I przez takie deweloperskie ofermy dana gra bez potrzeby żre tyle mocy, że wentylator nie wyrabia, bateria się szybko rozładowuje, a procesy działające w tle (takie jak przeglądarka, OBS itp.) nie działają wydajnie.

Ma ktoś jakieś doświadczenie czy sensowne przemyślenia na ten temat?

Moja rada dla wszystkich jest taka — nie potrzebujesz zużywać mocy CPU/GPU, to jej do cholery nie zużywaj. Czasy DOS-a/NES-a już dawno minęły, dziś każdy pecet obsługuje setki procesów i przetwarza nawet kilka tysięcy wątków w tle, więc zostaw tyle mocy obliczeniowej dla nich ile tylko możesz.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 7x, ostatnio: flowCRANE
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12166
0

W sumie to nie wspomniałem o istotnej kwestii, czyli o artykule źródłowym, z którego pochodzi pętla z pierwszego posta. Ogólnie traktuje on o tym, żeby aktualizacja stanu gry nie polegała na dowolnie dużej delcie, która to — bez żadnej kontroli — przekazywana jest do metody aktualizującej. Kluczem więc jest obliczenie delty i pojedyncza aktualizacja logiki, a jeśli delta jest za duża, to wykonanie od razu kilku kolejnych kroków.

I to jest jak najbardziej sensowne i logiczne podejście. W samą jakość i poprawność podanego kodu nie wnikam, bo to nieistotne. Obliczenia fizyki mogą bazować na z góry określonej maksymalnej delcie, więc zwiększa się wydajność obliczeń, a żaden lag ich nie da rady popsuć i wykrzaczyć gry.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 3x, ostatnio: flowCRANE
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)