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.
źródło
Odpowiedzi:
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
velocity
iposition
. Aby znaleźć pozycję, w której obiekt będzie w następnej klatce, po prostu dodajemyvelocity * draw_timestep
do bieżącej pozycji obiektu, aby znaleźć przewidywaną pozycję następnej klatki.draw_timestep
to 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
position
iprevious_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 jakoprevious_state[0]
iprevious_state[1]
. Podobnie potrzebuje dwóch kopiicurrent_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]
doprevious_state[state_index]
i kopiuje nowe dane istotne do rysowania,position
arotation
docurrent_state[state_index]
. Następnie robi tostate_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
elapsed
jest czas, który upłynął w wątku renderowania od ostatniego tiku aktualizacji, iupdate_tick_length
czas, jaki zajmuje ustalona częstotliwość aktualizacji na tik (np. Przy aktualizacjach 20 FPSupdate_tick_length = 0.05
).Jeśli nie wiesz, czym
Lerp
jest 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.źródło
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.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.
źródło