Czy pętla gry powinna opierać się na stałych lub zmiennych krokach czasowych? Czy ktoś jest zawsze lepszy, czy właściwy wybór zależy od gry?
Zmienny krok czasowy
Aktualizacje fizyki są przekazywane argumentem „czas, jaki upłynął od ostatniej aktualizacji”, a zatem zależą od liczby klatek na sekundę. Może to oznaczać wykonywanie obliczeń jako position += distancePerSecond * timeElapsed
.
Plusy : płynne, łatwiejsze do kodowania
Wady : niedeterministyczne, nieprzewidywalne przy bardzo małych lub dużych krokach
przykład deWiTERów :
while( game_is_running ) {
prev_frame_tick = curr_frame_tick;
curr_frame_tick = GetTickCount();
update( curr_frame_tick - prev_frame_tick );
render();
}
Naprawiono krok czasowy
Aktualizacje mogą nawet nie zaakceptować „upływu czasu”, ponieważ zakładają, że każda aktualizacja trwa przez określony czas. Obliczenia można wykonać w następujący sposób position += distancePerUpdate
. Przykład obejmuje interpolację podczas renderowania.
Plusy : przewidywalny, deterministyczny (łatwiejszy do synchronizacji z siecią?), Wyraźniejszy kod obliczeniowy
Wady : niezsynchronizowany z monitorem v-sync (powoduje roztrzęsioną grafikę, chyba że interpolujesz), ograniczona maksymalna częstotliwość klatek (chyba, że interpolujesz), ciężko pracować w ramach, które zakładaj zmienne przedziały czasowe (jak Pyglet lub Flixel )
przykład deWiTERów :
while( game_is_running ) {
while( GetTickCount() > next_game_tick ) {
update();
next_game_tick += SKIP_TICKS;
}
interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
/ float( SKIP_TICKS );
render( interpolation );
}
Niektóre zasoby
- Gaffer w grach: napraw swój czas!
- Artykuł pętli gry deWitter
- FPS Quake'a 3 wpływa na fizykę skoków - jest to prawdopodobnie powód, dla którego gra Doom 3 ma 60 klatek na sekundę?
- Flixel wymaga zmiennego czasu (myślę, że jest to określane przez Flash), podczas gdy Flashpunk pozwala na oba typy.
- Podręcznik Box2D § Symulacja świata Box2D sugeruje, że używa on stałych kroków czasowych.
źródło
Odpowiedzi:
Istnieją dwa problemy związane z pytaniem.
W Glen Field's Fix your time step mówi: „Uwolnij fizykę”. Oznacza to, że częstotliwość odświeżania fizyki nie powinna być powiązana z liczbą klatek na sekundę.
W zaleceniach Erin Catto dla Box2D również to popiera.
Czy częstotliwość kroków fizyki powinna być powiązana z liczbą klatek na sekundę? Nie.
Przemyślenia Erin na temat stałego i zmiennego kroku:
Myśli Glena na temat stopniowego i zmiennego stopniowania:
Czy fizyka powinna być stopniowana ze stałymi deltami? Tak.
Sposobem na stopniowanie fizyki ze stałymi deltami i niepowiązanie szybkości aktualizacji fizyki z liczbą klatek wciąż jest użycie akumulatora czasu. W mojej grze posuwam się o krok dalej. Stosuję funkcję wygładzania do czasu przychodzącego. W ten sposób duże skoki FPS nie powodują, że fizyka przeskakuje za daleko, zamiast tego są one symulowane szybciej dla jednej lub dwóch klatek.
Wspominasz, że przy stałej szybkości fizyka nie zsynchronizuje się z wyświetlaczem. Dzieje się tak, jeśli docelowa częstotliwość fizyki jest zbliżona do docelowej częstotliwości klatek. Gorzej, liczba klatek na sekundę jest większa niż częstotliwość fizyki. Zasadniczo lepiej jest celować w aktualizację fizyki dwa razy większą niż docelowy FPS, jeśli możesz sobie na to pozwolić.
Jeśli nie możesz sobie pozwolić na dużą częstotliwość aktualizacji fizyki, zastanów się nad interpolacją pozycji grafiki między ramkami, aby narysowana grafika poruszała się płynniej niż fizyka faktycznie się porusza.
źródło
Myślę, że są naprawdę 3 opcje, ale wymieniasz je tylko 2:
opcja 1
Nic nie robić. Próba aktualizacji i renderowania w określonych odstępach czasu, np. 60 razy na sekundę. Jeśli pozostanie w tyle, pozwól temu i nie martw się. Gry zwolnią, gdy procesor nie nadąży za twoją grą. Ta opcja w ogóle nie działa w przypadku gier dla wielu użytkowników w czasie rzeczywistym, ale jest dobra w przypadku gier dla jednego gracza i została z powodzeniem stosowana w wielu grach.
Opcja 2
Użyj czasu delta między każdą aktualizacją, aby zmienić ruch obiektów. Świetny w teorii, szczególnie jeśli nic w twojej grze nie przyspiesza ani nie zwalnia, a jedynie porusza się ze stałą prędkością. W praktyce wielu programistów źle to wdraża, co może prowadzić do niespójnego wykrywania kolizji i fizyki. Wygląda na to, że niektórzy programiści uważają, że ta metoda jest łatwiejsza niż jest. Jeśli chcesz skorzystać z tej opcji, musisz znacznie przyspieszyć grę i wydobyć matematykę i algorytmy wielkiej broni, na przykład używając integratora fizyki Verleta (zamiast standardowego Eulera, którego używa większość ludzi) i używając promieni do wykrywania kolizji zamiast zwykłych kontroli odległości w Pitagorasie. Jakiś czas temu zadałem pytanie na temat przepełnienia stosu i otrzymałem świetne odpowiedzi:
https://stackoverflow.com/questions/153507/calculate-the-position-of-an-accelerating-body-after-a-certain-time
Opcja 3
Skorzystaj z metody „napraw swój krok” Gaffera. Aktualizuj grę w ustalonych krokach, jak w opcji 1, ale rób to wiele razy na renderowaną klatkę - na podstawie upływu czasu - aby logika gry nadążała za czasem, pozostając w dyskretnych krokach. W ten sposób łatwa do zaimplementowania logika gry, taka jak integratory Eulera i proste wykrywanie kolizji, nadal działa. Masz również opcję interpolacji animacji graficznych w oparciu o czas delta, ale dotyczy to tylko efektów wizualnych i niczego, co nie wpływa na podstawową logikę gry. Możesz potencjalnie wpaść w kłopoty, jeśli twoje aktualizacje są bardzo intensywne - jeśli aktualizacje pozostaną w tyle, będziesz potrzebować ich coraz więcej, aby nadążyć, co może sprawić, że Twoja gra będzie jeszcze mniej responsywna.
Osobiście podoba mi się opcja 1, gdy mogę sobie z tym poradzić, oraz opcja 3, gdy muszę zsynchronizować z czasem rzeczywistym. Szanuję, że Opcja 2 może być dobrym rozwiązaniem, jeśli wiesz, co robisz, ale znam moje ograniczenia na tyle dobrze, aby trzymać się od niej z daleka.
źródło
Bardzo podoba mi się sposób, w jaki XNA Framework implementuje stały krok czasu. Jeśli dane losowanie trwa trochę za długo, będzie wielokrotnie wywoływać aktualizację, aż „dogoni”. Shawn Hargreaves opisuje to tutaj:
http://blogs.msdn.com/b/shawnhar/archive/2007/11/23/game-timing-in-xna-game-studio-2-0.aspx
Moim zdaniem największy profesjonalista to ten, o którym wspomniałeś, że sprawia, że wszystkie obliczenia kodu gry są o wiele prostsze, ponieważ nie musisz uwzględniać tej zmiennej czasowej w dowolnym miejscu.
Uwaga: xna obsługuje również zmienny timepep, to tylko ustawienie.
źródło
Jest jeszcze jedna opcja - oddzielenie aktualizacji gry i aktualizacji fizyki. Próba dostosowania silnika fizyki do czasu gry prowadzi do problemu, jeśli naprawisz swój czas (problem wymknięcia się spod kontroli, ponieważ integracja potrzebuje więcej kroków, które wymagają więcej czasu, co wymaga więcej kroków) lub zmienisz go i uzyskasz dziwną fizykę.
Rozwiązanie, które bardzo często widzę, polega na tym, aby fizyka działała w określonym czasie, w innym wątku (na innym rdzeniu). Gra interpoluje lub ekstrapoluje, biorąc pod uwagę dwie ostatnie prawidłowe klatki, które może pobrać. Interpolacja dodaje pewne opóźnienie, ekstrapolacja dodaje pewnej niepewności, ale twoja fizyka będzie stabilna i nie wywróci kontroli nad czasem.
Nie jest to łatwe do wdrożenia, ale może okazać się sprawdzone w przyszłości.
źródło
Osobiście używam wariacji zmiennej krok czasowy (myślę, że jest to rodzaj hybrydy stałej i zmiennej). Ten system pomiaru czasu przetestowałem na kilka sposobów i używam go do wielu projektów. Czy polecam to do wszystkiego? Prawdopodobnie nie.
Moje pętle gry obliczają liczbę ramek do aktualizacji (nazwijmy to F), a następnie wykonują F dyskretne aktualizacje logiczne. Każda aktualizacja logiki zakłada stałą jednostkę czasu (która w moich grach często wynosi 1/100 sekundy). Każda aktualizacja jest wykonywana po kolei, aż zostaną wykonane wszystkie dyskretne aktualizacje logiki F.
Po co dyskretne aktualizacje w krokach logicznych? Cóż, jeśli spróbujesz zastosować ciągłe kroki i nagle będziesz mieć problemy z fizyką, ponieważ obliczone prędkości i odległości do podróży są mnożone przez ogromną wartość F.
Zła implementacja tego spowoduje po prostu F = aktualny czas - aktualizacje czasu ostatniej klatki. Ale jeśli obliczenia są zbyt daleko w tyle (czasami z powodu okoliczności, na które nie masz wpływu, takich jak inny proces kradnący czas procesora), szybko zobaczysz okropne pominięcie. Szybko ten stabilny FPS, który próbujesz utrzymać, staje się SPF.
W mojej grze pozwalam na „płynne” (spowolnienie) spowolnienie, aby ograniczyć ilość logicznego doładowania, które powinno być możliwe między dwoma losowaniami. Robię to poprzez zaciśnięcie: F = min (F, MAX_FRAME_DELTA), który zwykle ma MAX_FRAME_DELTA = 2/100 * s lub 3/100 * s. Dlatego zamiast pomijać klatki, gdy są zbyt daleko w tyle za logiką gry, odrzuć masową utratę klatek (co spowalnia rzeczy), odzyskaj kilka klatek, narysuj i spróbuj ponownie.
Robiąc to, upewniam się również, że elementy sterujące odtwarzacza są bliżej zsynchronizowane z tym, co faktycznie widać na ekranie.
Pseudokod produktu końcowego jest mniej więcej taki (wspomniano wcześniej o delcie F):
Tego rodzaju aktualizacja nie jest odpowiednia do wszystkiego, ale w przypadku gier zręcznościowych wolałbym raczej spowolnić grę, ponieważ dzieje się wiele rzeczy, niż przegapianie ramek i utrata kontroli nad graczem. Wolę to również od innych podejść ze zmiennym czasem, które kończą się nieodwracalnymi usterkami spowodowanymi utratą ramki.
źródło
To rozwiązanie nie dotyczy wszystkiego, ale istnieje inny poziom zmiennego timepep - zmienny timepep dla każdego obiektu na świecie.
Wydaje się to skomplikowane i może być, ale pomyśl o tym jako o modelowaniu gry jako o dyskretnej symulacji zdarzeń. Każdy ruch gracza może być reprezentowany jako zdarzenie, które rozpoczyna się, gdy zaczyna się ruch, i kończy, gdy ruch się kończy. Jeśli wystąpi jakakolwiek interakcja, która wymaga podzielenia zdarzenia (na przykład kolizji), zdarzenie jest anulowane, a kolejne zdarzenie jest wypychane do kolejki zdarzeń (prawdopodobnie kolejka priorytetowa posortowana według czasu zakończenia zdarzenia).
Renderowanie jest całkowicie odłączone od kolejki zdarzeń. Silnik wyświetlania interpoluje punkty między czasem rozpoczęcia / zakończenia zdarzenia, jeśli to konieczne, i może być tak dokładny lub tak niechlujny w tym oszacowaniu, jak potrzeba.
Aby zobaczyć złożoną implementację tego modelu, zobacz symulator kosmiczny EXOFLIGHT . Używa innego modelu wykonania niż większość symulatorów lotu - modelu opartego na zdarzeniach, a nie tradycyjnego modelu ustalonego przedziału czasu. Podstawowa główna pętla tego typu symulacji wygląda następująco w pseudokodzie:
Głównym powodem użycia jednego w symulatorze kosmicznym jest konieczność zapewnienia dowolnego przyspieszenia czasowego bez utraty dokładności. Niektóre misje w EXOFLIGHT mogą trwać lata gry, a nawet opcja przyspieszenia 32x byłaby niewystarczająca. Potrzebujesz użytecznego przyspieszenia o wartości ponad 1 000 000 razy, co jest trudne w modelu z odcinkami czasu. Dzięki modelowi opartemu na zdarzeniach otrzymujemy dowolne szybkości czasu, od 1 s = 7 ms do 1 s = 1 rok.
Zmiana szybkości czasu nie zmienia zachowania karty SIM, co jest krytyczną cechą. Jeśli nie ma wystarczającej mocy procesora, aby uruchomić symulator z żądaną szybkością, zdarzenia będą się nakładać i możemy ograniczyć odświeżanie interfejsu użytkownika, dopóki kolejka zdarzeń nie zostanie wyczyszczona. Podobnie możemy przesuwać kartę SIM tak szybko, jak chcemy i mieć pewność, że nie marnujemy procesora ani nie poświęcamy dokładności.
Podsumowując: możemy modelować jeden pojazd na długiej, spokojnej orbicie (wykorzystując integrację Runge-Kutta) i inny pojazd jednocześnie odbijając się od podłoża - oba pojazdy będą symulowane z odpowiednią dokładnością, ponieważ nie mamy globalnego pomiaru czasu.
Minusy: złożoność i brak jakichkolwiek gotowych silników fizycznych obsługujących ten model :)
źródło
Ustalony krok czasowy jest użyteczny, biorąc pod uwagę dokładność zmiennoprzecinkową i aby zapewnić spójność aktualizacji.
Jest to prosty fragment kodu, więc warto go wypróbować i sprawdzić, czy działa w twojej grze.
Głównym problemem związanym ze stosowaniem stałego kroku czasowego jest to, że gracze z szybkim komputerem nie będą mogli korzystać z tej prędkości. Renderowanie przy 100 fps, gdy gra jest aktualizowana tylko przy 30 fps, jest tym samym, co renderowanie przy 30 fps.
Biorąc to pod uwagę, może być możliwe zastosowanie więcej niż jednego ustalonego kroku czasowego. 60 klatek na sekundę można wykorzystać do aktualizacji trywialnych obiektów (takich jak interfejs użytkownika lub animowane duszki), a 30 klatek na sekundę do aktualizacji trywialnych systemów (takich jak fizyka i), a nawet wolniejszych timerów do zarządzania za kulisami, takich jak usuwanie nieużywanych obiektów, zasobów itp.
źródło
Oprócz tego, co już powiedziałeś, może sprowadzać się do poczucia, że chcesz, aby gra była. Jeśli nie możesz zagwarantować, że zawsze będziesz mieć stałą liczbę klatek, prawdopodobnie gdzieś zwolnisz, a stałe i zmienne przedziały czasowe będą wyglądać zupełnie inaczej. Naprawiono efekt, przez który twoja gra działała przez chwilę w zwolnionym tempie, co czasem może być zamierzonym efektem (spójrz na strzelankę w starym stylu szkolnym, taką jak Ikaruga, w której ogromne wybuchy powodują spowolnienie po pokonaniu bossa). Zmienne stopnie czasowe utrzymują rzeczy w ruchu z odpowiednią prędkością pod względem czasu, ale możesz zobaczyć nagłe zmiany pozycji itp., Które mogą utrudnić graczowi dokładne wykonywanie akcji.
Naprawdę nie widzę, że ustalony krok czasowy ułatwi rzeczy w sieci, wszystkie byłyby nieco niezsynchronizowane na początku i spowolnienie na jednym komputerze, ale żaden inny nie wypchnąłby rzeczy bardziej zsynchronizowanych.
Zawsze skłaniałem się ku podejściu zmiennemu, ale w tych artykułach jest kilka interesujących rzeczy do przemyślenia. Nadal uważam stałe kroki za dość powszechne, szczególnie na konsolach, gdzie ludzie myślą, że liczba klatek na sekundę wynosi 60 klatek na sekundę w porównaniu z bardzo wysokimi prędkościami osiągalnymi na PC.
źródło
Skorzystaj z metody „napraw swój krok” Gaffera. Aktualizuj grę w ustalonych krokach, jak w opcji 1, ale rób to wiele razy na renderowaną klatkę - na podstawie upływu czasu - aby logika gry nadążała za czasem, pozostając w dyskretnych krokach. W ten sposób łatwa do zaimplementowania logika gry, taka jak integratory Eulera i proste wykrywanie kolizji, nadal działa. Masz również opcję interpolacji animacji graficznych w oparciu o czas delta, ale dotyczy to tylko efektów wizualnych i niczego, co nie wpływa na podstawową logikę gry. Możesz potencjalnie wpaść w kłopoty, jeśli twoje aktualizacje są bardzo intensywne - jeśli aktualizacje pozostaną w tyle, będziesz potrzebować ich coraz więcej, aby nadążyć, co może sprawić, że Twoja gra będzie jeszcze mniej responsywna.
Osobiście podoba mi się opcja 1, gdy mogę sobie z tym poradzić, oraz opcja 3, gdy muszę zsynchronizować z czasem rzeczywistym. Szanuję, że opcja 2 może być dobrym rozwiązaniem, jeśli wiesz, co robisz, ale znam moje ograniczenia na tyle dobrze, aby trzymać się od niej z daleka
źródło
Przekonałem się, że ustalone czasy synchronizacji do 60 klatek na sekundę zapewniają płynną animację lustrzaną. Jest to szczególnie ważne w przypadku aplikacji VR. Wszystko inne jest mdłe fizycznie.
Zmienne kroki czasowe nie są odpowiednie dla VR. Rzuć okiem na kilka przykładów Unity VR, które wykorzystują różne przebiegi czasowe. To jest nieprzyjemne
Zasadą jest, że jeśli gra 3D jest płynna w trybie VR, będzie doskonała w trybie innym niż VR.
Porównaj te dwa (aplikacje Cardboard VR)
(Zmienne czasy)
(Naprawiono timepeps)
Twoja gra musi być wielowątkowa, aby osiągnąć spójny czas / ilość klatek na sekundę. Fizyka, interfejs użytkownika i rendering muszą być podzielone na dedykowane wątki. Synchronizowanie ich przez PITA jest obrzydliwe, ale rezultatem jest lustrzane, płynne renderowanie, które chcesz (szczególnie dla VR).
Gry mobilne są szczególnie. wyzwanie, ponieważ wbudowane procesory i procesory graficzne mają ograniczoną wydajność. Używaj GLSL (slang) oszczędnie, aby odciążyć jak najwięcej pracy z procesora. Należy pamiętać, że przekazywanie parametrów do GPU zużywa zasoby magistrali.
Zawsze miej wyświetlaną liczbę klatek na sekundę podczas programowania. Prawdziwa gra polega na utrzymaniu stałej prędkości 60 klatek na sekundę. Jest to natywna szybkość synchronizacji dla większości ekranów, a także dla większości gałek ocznych.
Środowisko, którego używasz, powinno być w stanie powiadomić Cię o żądaniu synchronizacji lub użyć timera. Nie wprowadzaj opóźnienia uśpienia / oczekiwania, aby to osiągnąć - nawet niewielkie różnice są zauważalne.
źródło
Zmienne przedziały czasowe dotyczą procedur, które powinny być uruchamiane tak często, jak to możliwe: cykle renderowania, obsługa zdarzeń, rzeczy sieciowe itp.
Stałe przedziały czasowe dotyczą sytuacji, gdy potrzebujesz czegoś, co będzie przewidywalne i stabilne. Obejmuje to między innymi fizykę i wykrywanie kolizji.
W praktyce wykrywanie fizyki i kolizji powinno być oddzielone od wszystkiego innego, na jego własnym etapie czasowym. Powodem wykonywania takich procedur na niewielkim stałym etapie jest zachowanie ich dokładności. Wielkość impulsu jest wysoce zależna od czasu, a jeśli interwał staje się zbyt duży, symulacja staje się niestabilna, a szalone rzeczy dzieją się jak odbijająca się piłka przechodząca przez ziemię lub odbijająca się od świata gry, z których żadne nie jest pożądane.
Cała reszta może działać w zmiennym przedziale czasowym (choć profesjonalnie rzecz biorąc, często dobrym pomysłem jest zezwolenie na blokowanie renderowania w określonym przedziale czasu). Aby silnik gry był responsywny, takie rzeczy jak komunikaty sieciowe i dane wejściowe użytkownika powinny być obsługiwane jak najszybciej, co oznacza, że odstęp między odpytywaniem powinien być możliwie jak najkrótszy. Ogólnie oznacza to zmienną.
Cała reszta może być obsługiwana asynchronicznie, dzięki czemu synchronizacja jest kwestią sporną.
źródło