Ostatnio miałem do czynienia z niektórymi problemami z drganiami dotyczącymi liczby klatek na sekundę w mojej grze i wydaje się, że najlepszym rozwiązaniem byłoby to zaproponowane przez Glenna Fiedlera (Gaffer o grach) w klasycznej wersji Napraw swój timestep! artykuł.
Teraz - używam już ustalonego przedziału czasu dla mojej aktualizacji. Problem polega na tym, że nie wykonuję sugerowanej interpolacji do renderowania. Rezultatem jest to, że podwajam lub pomijam klatki, jeśli mój współczynnik renderowania nie odpowiada mojemu współczynnikowi aktualizacji. Mogą być zauważalne wizualnie.
Chciałbym więc dodać interpolację do mojej gry - i interesuje mnie, w jaki sposób inni ustrukturyzowali swoje dane i kod do obsługi tego.
Oczywiście będę musiał przechowywać (gdzie? / Jak?) Dwie kopie informacji o stanie gry odpowiednie dla mojego renderera, aby mogły się między nimi interpolować.
Dodatkowo - wydaje się, że to dobre miejsce na dodawanie wątków. Wyobrażam sobie, że wątek aktualizacji mógłby działać na trzeciej kopii stanu gry, pozostawiając pozostałe dwie kopie jako tylko do odczytu dla wątku renderowania. (Czy to dobry pomysł?)
Wydaje się, że ma dwa lub trzy wersje stanie w grze może wprowadzić wydajność i - znacznie ważniejsze - niezawodność i produktywność programistów problemów, w porównaniu do posiadania tylko jednej wersji. Dlatego szczególnie interesują mnie metody łagodzenia tych problemów.
Myślę, że na szczególną uwagę zasługuje problem z dodawaniem i usuwaniem obiektów ze stanu gry.
Wreszcie wydaje się, że jakiś stan albo nie jest bezpośrednio potrzebny do renderowania, albo byłoby zbyt trudne do śledzenia różnych wersji (np. Silnika fizyki innej firmy, który przechowuje pojedynczy stan) - więc chciałbym wiedzieć, jak to zrobić ludzie przetwarzali tego rodzaju dane w takim systemie.
źródło
Moje rozwiązanie jest znacznie mniej eleganckie / skomplikowane niż większość. Używam Box2D jako mojego silnika fizyki, więc utrzymywanie więcej niż jednej kopii stanu systemu nie jest możliwe do zarządzania (sklonuj system fizyki, a następnie spróbuj je zsynchronizować, może być lepszy sposób, ale nie mogłem wymyślić jeden).
Zamiast tego mam bieżący licznik generacji fizyki . Każda aktualizacja zwiększa generowanie fizyki, gdy podwójne aktualizacje systemu fizyki, a także podwójne aktualizacje licznika generacji.
System renderowania śledzi ostatnie renderowane pokolenie i deltę od tego pokolenia. Podczas renderowania obiektów, które chcą interpolować swoją pozycję, można użyć tych wartości wraz z ich pozycją i prędkością, aby odgadnąć, gdzie obiekt powinien być renderowany.
Nie zastanawiałem się, co zrobić, jeśli silnik fizyki byłby zbyt szybki. Prawie twierdzę, że nie powinieneś interpolować dla szybkiego ruchu. Jeśli zrobiłeś jedno i drugie, musisz uważać, aby nie spowodować, że duszki skaczą, zgadując zbyt wolno, a potem zbyt szybko.
Kiedy pisałem interpolację, grafika działała przy 60 Hz, a fizyka przy 30 Hz. Okazuje się, że Box2D jest znacznie bardziej stabilny, gdy pracuje przy częstotliwości 120 Hz. Z tego powodu mój kod interpolacyjny jest bardzo mało używany. Przez podwojenie docelowej liczby klatek na sekundę fizyka aktualizuje się średnio dwukrotnie na klatkę. Z fluktuacją może być 1 lub 3 razy, ale prawie nigdy 0 lub 4+. Wyższy wskaźnik fizyki sam rozwiązuje problem interpolacji. Podczas uruchamiania zarówno fizyki, jak i klatek na sekundę przy 60 Hz, możesz otrzymać 0-2 aktualizacji na klatkę. Różnica wizualna między 0 a 2 jest ogromna w porównaniu do 1 i 3.
źródło
Słyszałem, że takie podejście do kroków czasowych jest sugerowane dość często, ale przez 10 lat w grach nigdy nie pracowałem nad projektem w świecie rzeczywistym, który opierałby się na ustalonym czasie i interpolacji.
Wydaje się, że generalnie jest to większy wysiłek niż zmienny system pomiaru czasu (przy założeniu rozsądnego zakresu liczby klatek na sekundę w zakresie 25 Hz-100 Hz).
Próbowałem raz zastosować metodę ustalonego timestep + interpolacji dla bardzo małego prototypu - bez wątków, ale aktualizacja logiki o ustalonym timepepie i możliwie najszybsze renderowanie, gdy jej nie aktualizujesz. Moje podejście polegało na tym, aby mieć kilka klas, takich jak CInterpolatedVector i CInterpolatedMatrix - które zapisywały poprzednie / bieżące wartości i korzystały z akcesorium z kodu renderowania w celu pobrania wartości dla bieżącego czasu renderowania (który zawsze byłby między poprzednim a aktualnym aktualne czasy)
Każdy obiekt gry pod koniec aktualizacji ustawiałby swój obecny stan na zbiór tych interpolowalnych wektorów / macierzy. Tego rodzaju rzeczy można rozszerzyć o obsługę wątków, potrzebujesz co najmniej 3 zestawów wartości - jednego, który był aktualizowany, i co najmniej 2 poprzednich wartości, aby interpolować między ...
Zauważ, że niektórych wartości nie można w prosty sposób interpolować (np. „Klatka animacji ikonki”, „aktywny efekt specjalny”). Możesz całkowicie pominąć interpolację lub może to powodować problemy, w zależności od potrzeb gry.
IMHO, najlepiej jest po prostu zmieniać zmienną timepep - chyba że tworzysz RTS lub inną grę, w której masz ogromną liczbę obiektów, i musisz synchronizować 2 niezależne symulacje dla gier sieciowych (wysyłając tylko rozkazy / polecenia przez sieć, a nie pozycje obiektów). W takiej sytuacji jedyną opcją jest ustalenie czasu.
źródło
Tak, na szczęście klucz tutaj jest „odpowiedni dla mojego renderera”. Może to być nic więcej niż dodanie do miksu starej pozycji i znacznika czasu. Biorąc pod uwagę 2 pozycje, możesz interpolować do pozycji między nimi, a jeśli masz system animacji 3D, zazwyczaj możesz po prostu poprosić o pozę w tym samym momencie.
To naprawdę proste - wyobraź sobie, że Twój renderer musi być w stanie renderować obiekt gry. Kiedyś pytał obiekt, jak on wygląda, ale teraz musi zapytać, jak to wyglądało w określonym czasie. Musisz tylko przechowywać wszelkie informacje niezbędne do udzielenia odpowiedzi na to pytanie.
W tym momencie brzmi to jak przepis na dodatkowy ból. Nie zastanowiłem się nad wszystkimi implikacjami, ale domyślam się, że możesz zyskać odrobinę dodatkowej przepustowości kosztem większego opóźnienia. Och, i możesz uzyskać pewne korzyści z używania innego rdzenia, ale nie wiem.
źródło
Zauważ, że tak naprawdę nie szukam interpolacji, więc ta odpowiedź nie rozwiązuje tego problemu; Martwię się tylko o jedną kopię stanu gry dla wątku renderującego, a drugą dla wątku aktualizacji. Nie mogę więc wypowiedzieć się na temat interpolacji, chociaż można zmodyfikować następujące rozwiązanie interpolacji.
Zastanawiałem się nad tym, projektując i myśląc o silniku wielowątkowym. Zadałem więc pytanie dotyczące przepełnienia stosu, dotyczące sposobu implementacji pewnego rodzaju wzorca projektowego „kronikowanie” lub „transakcje” . Otrzymałem kilka dobrych odpowiedzi, a zaakceptowana odpowiedź naprawdę zmusiła mnie do myślenia.
Trudno jest stworzyć niezmienny obiekt, ponieważ wszystkie jego dzieci również muszą być niezmienne, a ty musisz naprawdę uważać, aby wszystko było niezmienne. Ale jeśli jesteś naprawdę ostrożny, możesz stworzyć nadklasę,
GameState
która zawiera wszystkie dane (i subdane itd.) W twojej grze; część „Model” w stylu organizacyjnym Model-View-Controller.Następnie, jak mówi Jeffrey , instancje obiektu GameState są szybkie, wydajne pod względem pamięci i bezpieczne dla wątków. Dużą wadą jest to, że aby zmienić cokolwiek w modelu, trzeba trochę odtworzyć model, dlatego trzeba bardzo uważać, aby kod nie zmienił się w wielki bałagan. Ustawienie zmiennej w obiekcie GameState na nową wartość jest bardziej zaangażowane niż tylko
var = val;
pod względem linii kodu.Strasznie mnie to intryguje. Nie musisz kopiować całej struktury danych w każdej ramce; po prostu kopiujesz wskaźnik do niezmiennej struktury. To samo w sobie jest imponujące, prawda?
źródło
Zacząłem od posiadania trzech kopii stanu gry każdego węzła na wykresie sceny. Jeden jest zapisywany przez wątek wykresu sceny, jeden jest odczytywany przez renderer, a trzeci jest dostępny do odczytu / zapisu, gdy tylko jedna z nich będzie musiała zamienić. To działało dobrze, ale było zbyt skomplikowane.
Wtedy zdałem sobie sprawę, że muszę zachować tylko trzy stany tego, co ma być renderowane. Mój wątek aktualizacyjny zapełnia teraz jeden z trzech znacznie mniejszych buforów „RenderCommands”, a Renderer czyta z najnowszego bufora, do którego obecnie nie jest zapisywane, co uniemożliwia wątkom wzajemne oczekiwanie.
W moim ustawieniu każdy RenderCommand ma geometrię / materiały 3d, macierz transformacji i listę świateł, które mają na to wpływ (nadal renderuje do przodu).
Mój wątek renderujący nie musi już wykonywać żadnych obliczeń związanych z ubijaniem lub niewielką odległością, co znacznie przyspieszyło w dużych scenach.
źródło