Istnieją dziesiątki artykułów, książek i dyskusji na temat pętli gier. Jednak często spotykam coś takiego:
while(running)
{
processInput();
while(isTimeForUpdate)
{
update();
}
render();
}
To, co w zasadzie przeszkadza mi w tym podejściu, to renderowanie „niezależne od aktualizacji”, np. Renderowanie ramki, gdy nie ma żadnych zmian. Więc moje pytanie brzmi: dlaczego często uczy się tego podejścia?
while (isTimeForUpdate)
, a nieif (isTimeForUpdate)
. Głównym celem nie jest,render()
gdy nie byłoupdate()
, aleupdate()
wielokrotne między nimirender()
. Niezależnie od tego obie sytuacje mają ważne zastosowania. To pierwsze byłoby ważne, jeśli stan może się zmienić poza twojąupdate
funkcją, np. Zmienić to, co jest renderowane na podstawie stanu domyślnego, takiego jak bieżący czas. To ostatnie jest ważne, ponieważ daje Twojemu silnikowi fizycznemu możliwość wykonania wielu małych, precyzyjnych aktualizacji, co np. Zmniejsza ryzyko „wypaczenia” się przez przeszkody.Odpowiedzi:
Historia tego, jak doszliśmy do tej wspólnej konwencji, jest długa, a po drodze mnóstwo fascynujących wyzwań, dlatego postaram się ją motywować etapami:
1. Problem: Urządzenia działają z różnymi prędkościami
Próbowałeś kiedyś zagrać w starą grę DOS na nowoczesnym komputerze i działa ona niemożliwie szybko - tylko rozmycie?
Wiele starych gier miało bardzo naiwną pętlę aktualizacji - zbierali dane wejściowe, aktualizowali stan gry i renderowali tak szybko, jak pozwalał na to sprzęt, bez uwzględnienia czasu, jaki upłynął. Co oznacza, że gdy tylko zmienia się sprzęt, zmienia się rozgrywka.
Zasadniczo chcemy, aby nasi gracze mieli spójne wrażenia i wrażenia z gry na szeregu urządzeń (o ile spełniają pewne minimalne wymagania), niezależnie od tego, czy używają zeszłorocznego telefonu, czy najnowszego modelu, najwyższej klasy pulpitu do gier czy laptop średniej klasy.
W szczególności w przypadku gier konkurencyjnych (dla wielu graczy lub poprzez tabele wyników) nie chcemy, aby gracze korzystający z określonego urządzenia mieli przewagę nad innymi, ponieważ mogą działać szybciej lub mieć więcej czasu na reakcję.
Pewnym rozwiązaniem tutaj jest zablokowanie tempa, z jakim przeprowadzamy aktualizacje stanu gry. W ten sposób możemy zagwarantować, że wyniki będą zawsze takie same.
2. Dlaczego więc nie zablokować klatek na sekundę (np. Przy użyciu VSync) i nadal uruchamiać aktualizacje i renderowanie stanu gry w trybie blokowania?
Może to działać, ale nie zawsze jest smaczne dla publiczności. Minęło dużo czasu, gdy praca przy stałym 30 fps była uważana za złoty standard gier. Teraz gracze rutynowo oczekują 60 fps jako minimalnego paska, szczególnie w grach akcji dla wielu graczy, a niektóre starsze tytuły wyglądają teraz wyraźnie niepewnie, gdy nasze oczekiwania się zmieniły. Istnieje także grupa wokalistów, którzy w szczególności sprzeciwiają się blokowaniu klatek na sekundę. Zapłacili dużo za swój najnowocześniejszy sprzęt i chcą móc korzystać z tego komputera, aby uzyskać jak najbardziej płynny i wierny rendering.
Zwłaszcza w VR framerate jest królem, a standard ciągle rośnie. Na początku niedawnego odrodzenia VR gry często działały z prędkością około 60 klatek na sekundę. Teraz 90 jest bardziej standardowe, a harware, takie jak PSVR, zaczyna obsługiwać 120. To może jeszcze wzrosnąć. Tak więc, jeśli gra VR ogranicza liczbę klatek na sekundę do tego, co jest wykonalne i zaakceptowane dzisiaj, może pozostać w tyle, gdy sprzęt i oczekiwania będą się dalej rozwijać.
(Z reguły należy zachować ostrożność, gdy mówi się, że „gracze nie potrafią dostrzec niczego szybciej niż XXX”, ponieważ zwykle opiera się on na określonym typie „percepcji”, na przykład rozpoznawaniu klatki w sekwencji. Postrzeganie ciągłości ruchu jest zasadniczo o wiele bardziej wrażliwy).
Ostatni problem polega na tym, że gra wykorzystująca zablokowaną szybkość klatek również musi być konserwatywna - jeśli kiedykolwiek trafisz na chwilę w grze, w której aktualizujesz i wyświetlasz niezwykle dużą liczbę obiektów, nie chcesz przegapić swojej klatki termin i spowodować zauważalne jąkanie lub zaczep. Musisz więc ustawić budżety na zawartość na niskim poziomie, aby pozostawić nadmiar, lub zainwestować w bardziej skomplikowane funkcje dynamicznej regulacji jakości, aby uniknąć uzależnienia całej gry od najgorszego działania na sprzęcie o minimalnej specyfikacji.
Może to być szczególnie problematyczne, jeśli problemy z wydajnością pojawiają się na późnym etapie rozwoju, gdy wszystkie istniejące systemy są budowane i dostrajane przy założeniu błyskawicznego renderowania klatek na sekundę, którego teraz nie zawsze możesz trafić. Odsprzężenie aktualizacji i szybkości renderowania daje większą elastyczność w radzeniu sobie ze zmiennością wydajności.
3. Czy aktualizowanie w określonym czasie nie powoduje takich samych problemów jak (2)?
Myślę, że to jest sedno pierwotnego pytania: jeśli odsprzęgniemy nasze aktualizacje i czasami renderujemy dwie ramki bez aktualizacji stanu gry pomiędzy nimi, to czy nie jest to to samo, co renderowanie w trybie blokowania przy niższej prędkości klatek, ponieważ nie ma widocznej zmiany na ekran?
W rzeczywistości istnieje kilka różnych sposobów, w jakie gry wykorzystują odsprzęganie tych aktualizacji, aby uzyskać dobry efekt:
a) Szybkość aktualizacji może być większa niż renderowana liczba klatek na sekundę
Jak zauważa tyjkenn w innej odpowiedzi, w szczególności fizyka jest często przyspieszana z większą częstotliwością niż rendering, co pomaga zminimalizować błędy integracji i dać dokładniejsze kolizje. Tak więc zamiast aktualizować 0 lub 1 między renderowanymi ramkami, możesz mieć 5 lub 10 lub 50.
Teraz odtwarzacz renderujący z prędkością 120 klatek na sekundę może uzyskać 2 aktualizacje na klatkę, podczas gdy gracz przy renderowaniu sprzętowym o niższej specyfikacji przy 30 klatkach na sekundę dostaje 8 aktualizacji na klatkę, a obie gry działają z tą samą szybkością tyknięcia na sekundę w czasie rzeczywistym. Lepszy sprzęt sprawia, że wygląda płynniej, ale nie zmienia radykalnie sposobu działania gry.
Istnieje ryzyko, że jeśli częstotliwość aktualizacji będzie niezgodna z liczbą klatek na sekundę, można uzyskać „częstotliwość uderzeń” między nimi . Na przykład. w większości klatek mamy wystarczająco dużo czasu na 4 aktualizacje stanu gry i trochę resztek, wtedy co jakiś czas mamy dość zapisanych, aby zrobić 5 aktualizacji w ramce, wykonując mały skok lub zacinanie się w ruchu. Można temu zaradzić ...
b) Interpolacja (lub ekstrapolacja) stanu gry między aktualizacjami
Tutaj często pozwalamy, aby stan gry przeszedł jeden ustalony czas w przyszłości i przechowywał wystarczającą ilość informacji z 2 ostatnich stanów, abyśmy mogli wykonać dowolny punkt między nimi. Następnie, gdy jesteśmy gotowi pokazać nową ramkę na ekranie, łączymy się z odpowiednim momentem tylko w celu wyświetlania (tj. Nie modyfikujemy tutaj podstawowego stanu gry)
Po prawidłowym wykonaniu ruch wydaje się gładki, a nawet pomaga ukryć wahania prędkości klatek, o ile nie spadniemy zbyt nisko.
c) Dodanie płynności do zmian stanów nie związanych z rozgrywką
Nawet bez interpolacji stanu gry nadal możemy uzyskać wygrane z wygładzeniem.
Czysto wizualne zmiany, takie jak animacja postaci, układy cząstek lub efekty wizualne, oraz elementy interfejsu użytkownika, takie jak HUD, często aktualizują się osobno od ustalonego czasu trwania stanu gry. Oznacza to, że jeśli tykamy nasz stan gry wiele razy na klatkę, nie płacimy ich kosztem za każdym tyknięciem - tylko przy ostatnim podaniu renderowania. Zamiast tego skalujemy szybkość odtwarzania tych efektów, aby dopasować je do długości ramki, dzięki czemu grają one tak płynnie, jak pozwala na to liczba klatek na sekundę renderowania, bez wpływu na szybkość lub uczciwość gry, jak opisano w (1).
Może to zrobić również ruch kamery - szczególnie w VR, czasami wyświetlamy tę samą klatkę więcej niż raz, ale zmieniamy ją ponownie, aby uwzględnić ruch głowy gracza między nimi , dzięki czemu możemy poprawić postrzegane opóźnienie i komfort, nawet jeśli możemy natywnie renderują wszystko tak szybko. Niektóre systemy przesyłania strumieniowego gier (gdzie gra działa na serwerze, a gracz działa tylko na cienkim kliencie) również używają tej wersji.
4. Dlaczego po prostu nie używać tego (c) stylu do wszystkiego? Jeśli działa w przypadku animacji i interfejsu użytkownika, czy nie możemy po prostu przeskalować naszych aktualizacji stanu gry, aby pasowały do bieżącej liczby klatek na sekundę?
Tak * jest to możliwe, ale nie, nie jest to proste.
Ta odpowiedź jest już trochę długa, więc nie będę wchodził w wszystkie krwawe szczegóły, tylko krótkie podsumowanie:
Mnożenie przez
deltaTime
prace w celu dostosowania do aktualizacji o zmiennej długości w celu zmiany liniowej (np. Ruch ze stałą prędkością, odliczanie timera lub postęp wzdłuż osi czasu animacji)Niestety wiele aspektów gier jest nieliniowych . Nawet coś tak prostego, jak grawitacja, wymaga bardziej wyrafinowanych technik integracji lub etapów o wyższej rozdzielczości, aby uniknąć rozbieżności wyników przy różnych prędkościach klatek. Wejście i kontrola odtwarzacza sama w sobie stanowi ogromne źródło nieliniowości.
W szczególności wyniki wykrywania i rozwiązywania dyskretnych kolizji zależą od częstotliwości aktualizacji, co prowadzi do błędów tunelowania i drgań, jeśli ramki stają się zbyt długie. Tak więc zmienna liczba klatek na sekundę zmusza nas do stosowania bardziej złożonych / kosztownych metod ciągłego wykrywania kolizji w większej ilości treści lub tolerowania zmienności w naszej fizyce. Nawet ciągłe wykrywanie kolizji napotyka trudności, gdy obiekty poruszają się w łukach, co wymaga krótszych czasów ...
Tak więc, w ogólnym przypadku gry o średniej złożoności, utrzymanie spójnego zachowania i uczciwości całkowicie poprzez
deltaTime
skalowanie jest gdzieś pomiędzy bardzo trudnym a intensywnym utrzymaniem do wręcz niemożliwym.Standaryzacja częstotliwości aktualizacji pozwala nam zagwarantować bardziej spójne zachowanie w różnych warunkach , często z prostszym kodem.
Utrzymanie prędkości aktualizacji oddzielonej od renderowania daje nam elastyczność w kontrolowaniu płynności i wydajności wrażeń bez zmiany logiki rozgrywki .
Nawet wtedy nigdy nie uzyskujemy prawdziwie „idealnej” niezależności klatek, ale podobnie jak w przypadku wielu podejść w grach, daje nam to kontrolowaną metodę wybierania „wystarczająco dobrego” na potrzeby danej gry. Dlatego jest powszechnie nauczany jako przydatny punkt wyjścia.
źródło
read-update-render
minimalnym opóźnieniu nasze najgorsze opóźnienie wynosi 17 ms (na razie ignorujemy potok grafiki i opóźnienie wyświetlania). Przy odsprzężonej(read-update)x(n>1)-render
pętli z tą samą szybkością klatek nasze najgorsze opóźnienie może być takie samo lub lepsze, ponieważ sprawdzamy i reagujemy na dane wejściowe tak często lub częściej. :)Inne odpowiedzi są dobre i mówią o tym, dlaczego pętla gry istnieje i powinna być oddzielna od pętli renderowania. Jednak tak jak w przypadku konkretnego przykładu „Po co renderować ramkę, gdy nie wprowadzono żadnych zmian?” To naprawdę sprowadza się do sprzętu i złożoności.
Karty graficzne są automatami państwowymi i są naprawdę dobre w robieniu tego samego w kółko. Jeśli renderujesz tylko rzeczy, które się zmieniły, jest to w rzeczywistości droższe, a nie mniej. W większości scenariuszy nic nie jest statyczne, jeśli nieznacznie przesuniesz się w lewo w grze FPS, zmieniłeś dane pikselowe 98% rzeczy na ekranie, równie dobrze możesz wyrenderować całą klatkę.
Ale przede wszystkim złożoność. Śledzenie wszystkiego, co się zmieniło podczas aktualizacji, jest znacznie droższe, ponieważ musisz albo przerobić wszystko, albo śledzić stary wynik jakiegoś algorytmu, porównać go z nowym wynikiem i renderować ten piksel tylko, jeśli zmiana jest inna. To zależy od systemu.
Konstrukcja sprzętu itp. Jest w dużej mierze zoptymalizowana pod kątem obecnych konwencji, a automat stanowy był dobrym modelem na początek.
źródło
Renderowanie jest zwykle najwolniejszym procesem w pętli gry. Ludzie nie zauważają różnicy w szybkości klatek większej niż 60, więc często mniej ważne jest marnowanie czasu na renderowanie szybciej. Istnieją jednak inne procesy, które mogłyby skorzystać na szybszym tempie. Fizyka jest jedna. Zbyt duża zmiana w jednej pętli może powodować, że przedmioty będą się mrużyć tuż obok ścian. Mogą istnieć sposoby na obejście prostych błędów kolizji przy większych przyrostach, ale w przypadku wielu złożonych interakcji fizycznych po prostu nie osiągniesz takiej samej dokładności. Jeśli pętla fizyki jest uruchamiana częściej, prawdopodobieństwo wystąpienia usterki jest mniejsze, ponieważ obiekty można przesuwać w mniejszych krokach bez renderowania za każdym razem. Więcej zasobów idzie w kierunku wrażliwego silnika fizyki, a mniej marnuje się na rysowanie większej liczby ramek, których użytkownik nie widzi.
Jest to szczególnie ważne w grach intensywniejszych graficznie. Jeśli był jeden render na każdą pętlę gry, a gracz nie miał najpotężniejszej maszyny, w grze mogą występować punkty, w których liczba klatek na sekundę spada do 30 lub 40. Mimo że liczba klatek na sekundę wciąż nie byłaby straszna, gra zaczynałaby się robić dość wolna, gdybyśmy próbowali utrzymać każdą zmianę fizyki na odpowiednio małym poziomie, aby uniknąć usterki. Gracz byłby zirytowany, że jego postać chodzi tylko o połowę normalnej prędkości. Jeśli jednak szybkość renderowania byłaby niezależna od reszty pętli, gracz byłby w stanie utrzymać stałą prędkość chodzenia pomimo spadku częstotliwości klatek.
źródło
Konstrukcja taka jak ta w twoim pytaniu może mieć sens, jeśli podsystem renderowania ma pojęcie „upływu czasu od ostatniego renderowania” .
Rozważmy na przykład podejście, w którym pozycja obiektu w świecie gry jest reprezentowana przez stałe
(x,y,z)
współrzędne z podejściem, które dodatkowo przechowuje bieżący wektor ruchu(dx,dy,dz)
. Teraz możesz napisać swoją pętlę gry, aby zmiana pozycji miała nastąpić wupdate
metodzie, ale możesz również zaprojektować ją tak, aby zmiana ruchu miała nastąpić podczasupdate
. Przy drugim podejściu, nawet jeśli stan gry nie zmieni się do następnegoupdate
, arender
-funkcja, która jest wywoływana z większą częstotliwością, może już narysować obiekt w nieco zaktualizowanej pozycji. Chociaż technicznie prowadzi to do rozbieżności między tym, co widzisz, a tym, co jest reprezentowane wewnętrznie, różnica jest na tyle mała, że nie ma znaczenia dla większości praktycznych aspektów, ale pozwala animacjom wyglądać znacznie płynniej.Przewidywanie „przyszłości” stanu gry (pomimo ryzyka pomyłki) może być dobrym pomysłem, jeśli weźmie się na przykład pod uwagę opóźnienia sieci.
źródło
Oprócz innych odpowiedzi ...
Sprawdzanie zmiany stanu wymaga znacznego przetworzenia. Jeśli sprawdzenie zmian wymaga podobnego (lub więcej!) Czasu, w porównaniu z faktycznym przetwarzaniem, naprawdę nie poprawiłeś sytuacji. W przypadku renderowania obrazu, jak mówi @Waddles, karta wideo jest naprawdę dobra w robieniu tego samego głupiego zadania w kółko, a sprawdzenie każdego fragmentu danych pod kątem zmian jest bardziej kosztowne niż zwykłe przeniesienie go do karty graficznej w celu przetworzenia. Również, jeśli rendering jest rozgrywką, to jest bardzo mało prawdopodobne, aby ekran nie zmienił się w ostatnim tiku.
Zakładasz również, że renderowanie zajmuje dużo czasu procesora. To bardzo zależy od procesora i karty graficznej. Od wielu lat nacisk kładziony jest na odciążanie coraz bardziej wyrafinowanych operacji renderowania na kartę graficzną i zmniejszanie nakładu renderowania wymaganego przez procesor. Idealnie połączenie procesora
render()
powinno po prostu skonfigurować transfer DMA i to wszystko. Pobieranie danych na kartę graficzną jest następnie przekazywane do kontrolera pamięci, a wytwarzanie obrazu jest przekazywane na kartę graficzną. Mogą to robić we własnym czasie, podczas gdy procesor pracuje równoleglekontynuuje fizykę, silnik gry i wszystkie inne rzeczy, które procesor robi lepiej. Oczywiście rzeczywistość jest o wiele bardziej skomplikowana, ale możliwość przeniesienia pracy do innych części systemu jest również znaczącym czynnikiem.źródło