Struktury danych do interpolacji i wątków?


20

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.

Odpowiedzi:


4

Nie próbuj powielać całego stanu gry. Interpolacja byłaby koszmarem. Po prostu wyodrębnij części, które są zmienne i potrzebne, renderując (nazwijmy to „stanem wizualnym”).

Dla każdej klasy obiektu utwórz klasę towarzyszącą, która będzie w stanie pomieścić stan wizualny obiektu. Ten obiekt zostanie wygenerowany przez symulację i wykorzystany przez rendering. Interpolacja będzie łatwo podłączać się między. Jeśli stan jest niezmienny i przekazany przez wartość, nie będziesz mieć problemów z wątkami.

Renderowanie zwykle nie musi nic wiedzieć o logicznych relacjach między obiektami, dlatego struktura używana do renderowania będzie prostym wektorem lub co najwyżej prostym drzewem.

Przykład

Tradycyjny design

class Actor
{
  Matrix4x3 position;
  float fuel;
  float armor;
  float stamina;
  float age;

  void Simulate(float deltaT)
  {
    age += deltaT;
    armor -= HitByAWeapon();
  }
}

Korzystanie ze stanu wizualnego

class IVisualState
{
  public:
  virtual void Interpolate(const IVisualState &newVS, float f) {}
};
class Actor
{
  struct VisualState: public IVisualState
  {
    Matrix4x3 position;
    float fuel;
    float armor;
    float stamina;
    float age;

    virtual auto_ptr<IVisualState> Interpolate(const IVisualState &newVS, float f)
    {
      const VisualState &newState = static_cast<const VisualState &>(newVS);
      IVisualState *ret = new VisualState;
      ret->age = lerp(this->age,newState.age);
      // ... interpolate other properties as well, using any suitable interpolation method
      // liner, spline, slerp, whatever works best for the given property
      return ret;
    };
  };

  auto_ptr<VisualState> state_;

  void Simulate(float deltaT)
  {
    state_->age += deltaT;
    state_->armor -= HitByAWeapon();
  }
}

1
Twój przykład byłby łatwiejszy do odczytania, gdybyś nie użył słowa „nowy” (słowo zastrzeżone w C ++) jako nazwy parametru.
Steve S,

3

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.


3
Też to znalazłem. Pętla fizyki 120 Hz z aktualizacją ramki prawie 60 Hz czyni interpolację prawie bezwartościową. Niestety działa to tylko w przypadku zestawu gier, które mogą sobie pozwolić na pętlę fizyki 120 Hz.

Właśnie próbowałem przejść do pętli aktualizacji 120 Hz. Wydaje się, że ma to podwójną zaletę, ponieważ moja fizyka jest bardziej stabilna i sprawia, że ​​moja gra wygląda płynnie przy niezbyt częstotliwości 60 Hz. Minusem jest to, że psuje całą moją dokładnie dostrojoną fizykę rozgrywki - więc jest to zdecydowanie opcja, którą należy wybrać na wczesnym etapie projektu.
Andrew Russell,

Ponadto: tak naprawdę nie rozumiem twojego wyjaśnienia twojego systemu interpolacji. Właściwie to brzmi trochę jak ekstrapolacja?
Andrew Russell,

Dobra decyzja. Właściwie opisałem system ekstrapolacji. Biorąc pod uwagę pozycję, prędkość i czas, jaki upłynął od ostatniej aktualizacji fizyki, ekstrapoluję miejsce, w którym znajdowałby się obiekt, gdyby silnik fizyki nie zgasł.
deft_code

2

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.


1
Wydaje się, że przynajmniej Quake 3 stosował to podejście, przy domyślnym „tik” wynoszącym 20 fps (50 ms).
Suma

Ciekawy. Podejrzewam, że ma to zalety w przypadku wysoce konkurencyjnych gier wieloosobowych na PC, aby zapewnić, że szybsze komputery / wyższe liczby klatek na sekundę nie uzyskają zbytniej przewagi (bardziej responsywne sterowanie lub niewielkie, ale możliwe do wykorzystania różnice w zachowaniu fizyki / kolizji) ?
bluescrn

1
Czy od 10 lat nie spotkałeś się z żadną grą, w której fizyka nie jest w kontakcie z symulacją i rendererem? Ponieważ w momencie, gdy to zrobisz, będziesz musiał interpolować lub akceptować postrzegane szarpnięcie w swoich animacjach.
Kaj

2

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ć.

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.

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ł?)

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.


1

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ę, GameStatektó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?


To naprawdę ciekawa struktura. Nie jestem jednak pewien, czy zadziałałoby to dobrze w grze - tak jak w przypadku ogólnego płaskiego drzewa obiektów, które zmieniają się dokładnie raz na klatkę. Również dlatego, że dynamiczna alokacja pamięci jest dużym nie-nie.
Andrew Russell,

Dynamiczna alokacja w takim przypadku jest bardzo łatwa do wykonania skutecznie. Możesz użyć okrągłego bufora, rosnąć z jednej strony, uwolnić z drugiej.
Suma,

... to nie byłby dynamiczny przydział, tylko dynamiczne wykorzystanie wstępnie przydzielonej pamięci;)
Kaj

1

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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.