Jak interpolować między dwoma stanami gry?

24

Jaki jest najlepszy wzorzec do stworzenia systemu, w którym wszystkie obiekty będą interpolowane między dwoma stanami aktualizacji?

Aktualizacja zawsze będzie przebiegać z tą samą częstotliwością, ale chcę mieć możliwość renderowania z dowolnym FPS. Renderowanie będzie więc tak płynne, jak to możliwe, bez względu na liczbę klatek na sekundę, niezależnie od tego, czy będzie niższa, czy wyższa niż częstotliwość aktualizacji.

Chciałbym zaktualizować 1 ramkę do przyszłej interpolacji z bieżącej ramki do przyszłej ramki. Ta odpowiedź zawiera link, który mówi o tym:

Częściowo stały czy w pełni ustalony czas?

Edycja: Jak mogę również użyć ostatniej i aktualnej prędkości w interpolacji? Na przykład przy interpolacji liniowej będzie poruszał się z tą samą prędkością między pozycjami. Potrzebuję sposobu, aby interpolowała pozycję między dwoma punktami, ale weź pod uwagę prędkość w każdym punkcie interpolacji. Byłoby to pomocne w przypadku symulacji o niskiej szybkości, takich jak efekty cząsteczkowe.

AttackingHobo
źródło
2
kleszcze będące logicznymi kleszczami? Więc twoja aktualizacja fps <renderowanie fps?
Kaczka komunistyczna
Zmieniłem termin. Ale tak, logika tyka. I nie, chcę całkowicie zwolnić renderowanie z aktualizacji, więc gra może renderować w 120 Hz lub 22,8 Hz, a aktualizacja będzie nadal działać z tą samą prędkością, pod warunkiem, że użytkownik spełnia wymagania systemowe.
AttackingHobo
może to być bardzo trudne, ponieważ podczas renderowania wszystkie pozycje obiektów powinny pozostać w bezruchu (zmiana ich podczas procesu renderowania może spowodować pewne nieokreślone zachowanie)
Ali1S232
Interpolacja obliczy stan w czasie między 2 już obliczonymi ramkami aktualizacji. Czy to nie pytanie o ekstrapolację, obliczanie stanu na czas po ostatniej ramce aktualizacji? Ponieważ kolejna aktualizacja nie jest jeszcze obliczona.
Maik Semder
Myślę, że jeśli ma tylko jeden wątek aktualizujący / renderujący, nie może się zdarzyć, że ponownie zaktualizuje tylko pozycję renderowania. Po prostu wysyłasz pozycje do GPU, a następnie aktualizujesz ponownie.
zacharmarz

Odpowiedzi:

22

Chcesz oddzielić aktualizacje (tiki logiczne) i rysować (renderować tiki).

Twoje aktualizacje określą pozycję wszystkich obiektów na świecie do narysowania.

Omówię tutaj dwie różne możliwości: tę, o którą prosiłeś, ekstrapolację, a także inną metodę interpolacji.

1.

Ekstrapolacja polega na obliczeniu (przewidywanej) pozycji obiektu w następnej klatce, a następnie interpolacji między bieżącą pozycją obiektów a pozycją, w której obiekt będzie znajdował się w następnej klatce.

Aby to zrobić, każdy obiekt do narysowania musi być powiązany velocityi position. Aby znaleźć pozycję, w której obiekt będzie w następnej klatce, po prostu dodajemy velocity * draw_timestepdo bieżącej pozycji obiektu, aby znaleźć przewidywaną pozycję następnej klatki. draw_timestepto czas, który upłynął od poprzedniego tyknięcia renderowania (czyli poprzedniego wywołania losowania).

Jeśli to zostawisz, zauważysz, że obiekty „migoczą”, gdy ich przewidywana pozycja nie odpowiada rzeczywistej pozycji w następnej klatce. Aby usunąć migotanie, możesz zapisać przewidywaną pozycję i długość między poprzednio przewidywaną pozycją a nową przewidywaną pozycją na każdym etapie losowania, wykorzystując czas, jaki upłynął od poprzedniej aktualizacji, jako czynnik lerp. Nadal będzie to powodować złe zachowanie, gdy szybko poruszające się obiekty nagle zmienią lokalizację, a ty możesz poradzić sobie z tym szczególnym przypadkiem. Wszystko, co zostało powiedziane w tym akapicie, jest powodem, dla którego nie chcesz korzystać z ekstrapolacji.

2)

Interpolacja polega na przechowywaniu stanu dwóch ostatnich aktualizacji i interpolacji między nimi w oparciu o bieżący czas, jaki upłynął od ostatniej aktualizacji. W tym ustawieniu każdy obiekt musi być powiązany positioni previous_position. W takim przypadku nasz rysunek będzie w najgorszym przypadku reprezentował jeden tik aktualizacji za bieżącym stanem gry, aw najlepszym razie dokładnie w tym samym stanie, co bieżący tik aktualizacji.


Moim zdaniem, prawdopodobnie chcesz interpolacji, tak jak to opisałem, ponieważ jest to łatwiejsze do wdrożenia, a rysowanie ułamka sekundy (np. 1/60 sekundy) za twoim aktualnym stanem jest w porządku.


Edytować:

W przypadku, gdy powyższe nie wystarcza, aby wykonać implementację, oto przykład, jak wykonać opisaną przeze mnie metodę interpolacji. Nie będę omawiał ekstrapolacji, ponieważ nie mogę wymyślić żadnego scenariusza z prawdziwego świata, w którym powinieneś go preferować.

Gdy utworzysz obiekt do rysowania, będzie on przechowywał właściwości potrzebne do narysowania (tj. Informacje o stanie potrzebne do narysowania go).

W tym przykładzie przechowamy pozycję i obrót. Możesz także zapisać inne właściwości, takie jak położenie współrzędnych koloru lub tekstury (np. Gdy tekstura przewija się).

Aby zapobiec modyfikowaniu danych podczas rysowania wątku renderowania (tj. Położenie jednego obiektu jest zmieniane podczas rysowania wątku renderowania, ale wszystkie inne nie zostały jeszcze zaktualizowane), musimy wprowadzić pewien rodzaj podwójnego buforowania.

Obiekt przechowuje dwie jego kopie previous_state. Umieszczę je w tablicy i będę odnosił się do nich jako previous_state[0]i previous_state[1]. Podobnie potrzebuje dwóch kopii current_state.

Aby śledzić, która kopia podwójnego bufora jest używana, przechowujemy zmienną state_index, która jest dostępna zarówno dla wątku aktualizacyjnego, jak i rysującego.

Wątek aktualizacji najpierw oblicza wszystkie właściwości obiektu na podstawie własnych danych (dowolnych struktur danych, które chcesz). Następnie kopiuje current_state[state_index]do previous_state[state_index]i kopiuje nowe dane istotne do rysowania, positiona rotationdo current_state[state_index]. Następnie robi to state_index = 1 - state_index, aby przerzucić aktualnie używaną kopię podwójnego bufora.

Wszystko w powyższym akapicie należy wykonać przy zdjętej blokadzie current_state. Wątki aktualizacji i rysowania usuwają tę blokadę. Blokada jest wyjmowana tylko na czas kopiowania informacji o stanie, co jest szybkie.

W nici do renderowania interpolujesz liniowo pozycję i obrót w następujący sposób:

current_position = Lerp(previous_state[state_index].position, current_state[state_index].position, elapsed/update_tick_length)

Gdzie elapsedjest czas, który upłynął w wątku renderowania od ostatniego tiku aktualizacji, i update_tick_lengthczas, jaki zajmuje ustalona częstotliwość aktualizacji na tik (np. Przy aktualizacjach 20 FPS update_tick_length = 0.05).

Jeśli nie wiesz, czym Lerpjest powyższa funkcja, zapoznaj się z artykułem Wikipedii na ten temat: Interpolacja liniowa . Jeśli jednak nie wiesz, co to jest Lerping, prawdopodobnie nie jesteś gotowy do wdrożenia oddzielonej aktualizacji / rysunku z rysunkiem interpolowanym.

Olhovsky
źródło
1
+1 to samo należy zrobić dla orientacji / rotacji i wszystkich innych stanów, które zmieniają się w czasie, tj. Podobnie jak animacje materiałów w układach cząstek itp.
Maik Semder
1
Dobra uwaga Maik, właśnie użyłem pozycji jako przykładu. Musisz zapisać „prędkość” dowolnej właściwości, którą chcesz ekstrapolować (tj. Szybkość zmian w czasie tej właściwości), jeśli chcesz użyć ekstrapolacji. Ostatecznie naprawdę nie mogę wymyślić sytuacji, w której ekstrapolacja jest lepsza niż interpolacja, podałem ją tylko dlatego, że o to pytał pytający. Używam interpolacji. Dzięki interpolacji musimy przechowywać bieżące i poprzednie wyniki aktualizacji dowolnych właściwości do interpolacji, tak jak powiedziałeś.
Olhovsky
Jest to powtórzenie problemu i różnicy między interpolacją a ekstrapolacją; to nie jest odpowiedź.
1
W moim przykładzie zapisałem pozycję i obrót w stanie. Możesz po prostu zapisać prędkość (lub prędkość) również w tym stanie. Następnie przechodzisz między prędkością dokładnie w ten sam sposób ( Lerp(previous_speed, current_speed, elapsed/update_tick_length)). Możesz to zrobić z dowolnym numerem, który chcesz przechowywać w stanie. Lerping po prostu daje wartość między dwiema wartościami, biorąc pod uwagę współczynnik lerp.
Olhovsky
1
Do interpolacji ruchu kątowego zaleca się stosowanie slerp zamiast lerp. Najłatwiej byłoby przechowywać czwartorzędy obu stanów i suwaki między nimi. W przeciwnym razie te same zasady dotyczą prędkości kątowej i przyspieszenia kątowego. Czy masz testowy przykład animacji szkieletu?
Maik Semder
-2

Ten problem wymaga myślenia o definicjach rozpoczęcia i zakończenia nieco inaczej. Początkujący programiści często myślą o zmianie pozycji na klatkę i jest to dobry sposób na początek. Ze względu na moją odpowiedź rozważmy odpowiedź jednowymiarową.

Powiedzmy, że masz małpkę w pozycji x. Teraz masz również „addX”, który dodajesz do pozycji małpy na klatkę w oparciu o klawiaturę lub inny element sterujący. Będzie to działać, dopóki masz gwarantowaną liczbę klatek na sekundę. Powiedzmy, że x wynosi 100, a twój addX wynosi 10. Po 10 klatkach x + = addX powinien się kumulować do 200.

Teraz zamiast addX, kiedy masz zmienną liczbę klatek na sekundę, powinieneś pomyśleć w kategoriach prędkości i przyspieszenia. Przeprowadzę cię przez całą tę arytmetykę, ale jest bardzo prosta. Chcemy wiedzieć, jak daleko chcesz podróżować w ciągu milisekundy (1/1000 sekundy)

Jeśli strzelasz za 30 FPS, twój velX powinien wynosić 1/3 sekundy (10 klatek od ostatniego przykładu przy 30 FPS) i wiesz, że chcesz podróżować 100 'x' w tym czasie, więc ustaw velX na 100 odległości / 10 klatek na sekundę lub 10 odległości na ramkę. W milisekundach osiąga to 1 odległość x na 3,3 milisekundy lub 0,3 'x' na milisekundę.

Teraz, za każdym razem, gdy aktualizujesz, wszystko, co musisz zrobić, to dowiedzieć się, ile czasu minęło. Niezależnie od tego, czy minęło 33 ms (1/30 sekundy), czy cokolwiek innego, wystarczy pomnożyć odległość 0,3 przez liczbę milisekund. Oznacza to, że potrzebujesz timera, który zapewnia dokładność ms (milisekunda), ale większość timerów to zapewnia. Po prostu zrób coś takiego:

var beginTime = getTimeInMillisecond ()

... później ...

var time = getTimeInMillisecond ()

var elapsedTime = time-beginTime

beginTime = czas

... teraz użyj tego elapsedTime, aby obliczyć wszystkie swoje odległości.

Mickey
źródło
1
Nie ma zmiennej częstotliwości aktualizacji. Ma stałą częstotliwość aktualizacji. Szczerze mówiąc, naprawdę nie wiem, o co tu chodzi: /
Olhovsky
1
??? -1. To jest cała kwestia: mam gwarantowaną częstotliwość aktualizacji, ale zmienną szybkość renderowania i chcę, aby była płynna bez zacinania się.
AttackingHobo
Zmienne szybkości aktualizacji nie działają dobrze w przypadku gier sieciowych, gier konkurencyjnych, systemów powtórek lub czegokolwiek innego, co opiera się na deterministyczności rozgrywki.
AttackingHobo
1
Naprawiona aktualizacja pozwala również na łatwą integrację pseudotarcia. Na przykład, jeśli chcesz pomnożyć prędkość przez 0,9 na każdą klatkę, jak obliczyć, przez ile należy pomnożyć, jeśli masz szybką lub wolną klatkę? Naprawiona aktualizacja jest czasem bardzo preferowana - praktycznie wszystkie symulacje fizyczne wykorzystują stałą częstotliwość aktualizacji.
Olhovsky
2
Jeśli użyję zmiennej liczby klatek na sekundę i ustawię złożony stan początkowy z wieloma odbijającymi się obiektami, nie ma gwarancji, że będzie symulować dokładnie to samo. W rzeczywistości najprawdopodobniej za każdym razem będzie symulować nieco inaczej, z małymi różnicami na początku, łącząc się w krótkim czasie w całkowicie różne stany między każdym przebiegiem symulacji.
AttackingHobo