Moje pytanie dotyczy kwestii dosyć precyzyjnej, ale żeby zrozumieć jego sens, wymagane jest trochę kontekstu.
- Silnik napisany jest w C++.
- Renderowanie i odświeżanie logiki gry odbywa się w osobnych wątkach.
- Odświeżanie odbywa się co stały, dowolny interwał (np. 30 razy na sekundę).
- Renderowanie działa niezależnie, z własnym interwałem (np. 60 klatek na sekundę), albo w ogóle bez odgórnego ograniczenia.
- Renderowanie jest opóźnione o jeden cykl względem odświeżania i używa interpolacji do rysowania klatek na bazie dwóch ostatnich stanów świata. Np.:
Pozycja (stan 1) = 50, Pozycja (stan 2) = 70, więc kolejne klatki będą rysowane z wartościami np. 52, 57, 61, 65, 67 itd., zgodnie z upływem czasu rzeczywistego pomiędzy klatkami.
- Interpolacja może zostać wyłączona dla dowolnego przekształcenia, gdy gradacja nie jest potrzebna, np. przy teleportacji obiektu.
Pseudokod wygląda tak:
void Update()
{
//Obecna pozycja postaci gracza to { 100, 100 }.
//Obecna saturacja postaci gracza to 1.0f.
//Poniższe metody tworzą instrukcje dla renderera (dodając je do stosownej listy, indywidualnej dla każdego obiektu do wyrenderowania).
//Ostatni parametr wskazuje, czy zmiana ma być interpolowana. false oznacza jej brak i natychmiastowy przeskok (np. teleportację).
/*1*/ Player.SetPosition(Player.Position.X + 60, Player.Position.Y, true)
/*2*/ Player.SetSaturation(0.25f, false);
/*3*/ Player.SetSaturation(0.80f, true);
/*4*/ Player.SetPosition(21, 56, false)
/*5*/ Player.SetPosition(Player.Position.X + 100, Player.Position.Y, true)
}
Zamysł jest taki, żeby stworzyć instrukcje dla renderera (event queue) odnośnie tego jak ma narysować obiekt na ekranie. Coś na wzór poniżej (pseudo kod):
[INDEX KOMENDY NA LIŚCIE] [KOD OPERACJI] [DANE WEJŚCIOWE] [INTRPOLACJA]
Dla płynnego (interpolowanego) przemieszczenia obiektu na ekranie (komenda nr 1 z kodu powyżej) wyglądałoby tak:
[0][1][{ 100, 100 }, { 160, 100 }][true] //Wykorzystuje starą i nową pozycję.
Dla przemieszczenia obiektu na ekranie poprzez teleportację (komenda nr 4 z kodu powyżej) wyglądałoby z kolei tak:
[0][1][{ 21, 56 }, { 21, 56 }][false] //Przy teleportacji (zmianie nieinterpolowanej) nowa wartość zawsze zastępuje oba parametry
Gdyby natomiast chęć wykonać wszystkie 5 komend kumulatywne, wyglądałoby to tak:
Krok 1 - dodaje do listy komendę przemieszczenia interpolowanego:
[0][1][{ 100, 100 }, { 160, 100 }][true]
Krok 2 - dodaje do listy komendę nieinterpolowanej (skokowej) zmiany saturacji:
[1][11][0.25f, 0.25f][false]
Krok 3 - modyfikuje komendę zmiany saturacji, dodając nową wartość końcową i interpolację:
[1][11][0.25f, 0.80f][true] //interpolacja będzie się więc odbywać w przedziale 0.25 - 0.80, a nie 1.0 - 0.80
Krok 4 - zmienia komendę przemieszczenia na teleportację (czyli de facto Krok 1 zostaje całkowicie zatarty)
[0][1][{ 21, 56 }, { 21, 56 }][false]
Krok 5 - modyfikuje komendę przemieszczenia, dodając nową wartość końcową i interpolację:
[0][1][{ 21, 56 }, { 121, 56 }][true]
Na finalnej liście, przekazywanej rendererowi będzie to więc wyglądać tak:
[0][1][{ 21, 56 }, { 121, 56 }][true]
[1][11][0.25f, 0.80f][true]
No i wreszcie do sedna, czyli czego dotyczy problem. Ano nie do końca mam pomysł na wydajne śledzenie tych komend na liście, czyli rozpoznawanie konieczności dodania nowej lub modyfikacji już istniejącej. Nowe komendy są dodawane dynamiczne, w zależności od zakresu modyfikacji obiektu (a więc nie mają stałych, przewidywalnych indexów), a każdorazowe przelatywanie listy w pętli w poszukiwaniu kodu operacji wydaje się średnim pomysłem. Na ten moment używam std::unordered_map
, ale to też nie jest jakieś super wydaje rozwiązanie, bo mapa do specjalnie szybkich klas nie należy.
A może ktoś ma pomysł na jakieś prostsze albo bardziej wydajne rozwiązanie w zakresie przechowywania i przesyłania rendererowi informacji o stanie świata? Jestem otwarty na wszelkie sugestie.