Synchronizacja między wątkiem logiki gry a wątkiem renderującym


16

W jaki sposób oddzielna logika gry i rendering? Wiem, że wydaje się, że są tutaj pytania, które dokładnie o to pytają, ale odpowiedzi nie są dla mnie zadowalające.

Z tego, co do tej pory rozumiem, rozdzielenie ich na różne wątki polega na tym, aby logika gry mogła natychmiast uruchomić kolejny tik zamiast czekać na następny vsync, w którym renderowanie w końcu powraca z wywołania swapbuffera, które blokuje.

Ale konkretnie, jakie struktury danych są używane, aby zapobiec warunkom wyścigu między wątkiem logiki gry a wątkiem renderowania. Prawdopodobnie wątek renderujący potrzebuje dostępu do różnych zmiennych, aby dowiedzieć się, co narysować, ale logika gry może aktualizować te same zmienne.

Czy istnieje de facto standardowa technika rozwiązania tego problemu? Może jak kopiowanie danych potrzebnych do renderowania po każdym wykonaniu logiki gry. Jakim rozwiązaniem będzie narzut związany z synchronizacją, czy czymś mniejszym niż tylko uruchamianie wszystkiego w jednym wątku?


1
Nienawidzę po prostu spamować linku, ale uważam, że jest to bardzo dobra lektura i powinien on odpowiedzieć na wszystkie pytania: altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


1
Te linki dają typowy wynik końcowy, jaki by się chciał, ale nie wyjaśniają, jak to zrobić. Czy skopiowałbyś cały wykres sceny dla każdej klatki lub czegoś innego? Dyskusje są zbyt wysokie i niejasne.
user782220,

Myślałem, że linki były dość wyraźne na temat tego, ile stanów jest kopiowane w każdym przypadku. na przykład. (z pierwszego linku) „Partia zawiera wszystkie informacje niezbędne do narysowania ramki, ale nie zawiera żadnego innego stanu gry”. lub (z 2. linku) „Dane muszą jednak zostać udostępnione, ale teraz zamiast każdego systemu uzyskującego dostęp do wspólnej lokalizacji danych, aby powiedzieć, uzyskać dane pozycji lub orientacji, każdy system ma swoją kopię” (patrz zwłaszcza 3.2.2 - Stan Manager)
DMGregory

Ktokolwiek napisał ten artykuł Intela, nie wie, że wątki najwyższego poziomu to bardzo zły pomysł. Nikt nie robi czegoś tak głupiego. Nagle cała aplikacja musi się komunikować za pośrednictwem wyspecjalizowanych kanałów, a wszędzie są blokady i / lub ogromne skoordynowane wymiany stanów. Nie wspominając już o tym, że nie wiadomo, kiedy wysłane dane zostaną przetworzone, więc niezwykle trudno jest zrozumieć, co robi kod. Znacznie łatwiej jest skopiować odpowiednie dane sceny (niezmienne jako wskaźniki przeliczane, zmienne - według wartości) w jednym punkcie i pozwolić podsystemowi uporządkować je, jak chce.
snake5

Odpowiedzi:


1

Pracowałem nad tym samym. Dodatkowym problemem jest to, że OpenGL (i, o ile mi wiadomo, OpenAL) oraz szereg innych interfejsów sprzętowych, to skutecznie maszyny stanu, które nie radzą sobie dobrze z byciem wywoływanym przez wiele wątków. Nie sądzę, że ich zachowanie jest nawet zdefiniowane, a dla LWJGL (być może również JOGL) często rzuca wyjątek.

Skończyło się na tym, że stworzyłem sekwencję wątków implementujących określony interfejs i ładowałem je na stos obiektu kontrolnego. Gdy ten obiekt otrzyma sygnał do zamknięcia gry, będzie przebiegał przez każdy wątek, wywoływał zaimplementowaną metodę ceaseOperations () i czekał na ich zamknięcie przed zamknięciem. Uniwersalne dane, które mogą być istotne przy renderowaniu dźwięku, grafiki lub innych danych, są przechowywane w sekwencji obiektów, które są ulotne lub powszechnie dostępne dla wszystkich wątków, ale nigdy nie są przechowywane w pamięci wątków. Jest tam niewielka kara za wydajność, ale właściwie zastosowana, pozwoliła mi elastycznie przypisać dźwięk do jednego wątku, grafikę do drugiego, fizykę do innego i tak dalej, bez wiązania ich z tradycyjną (i przerażającą) „pętlą gry”.

Tak więc z reguły wszystkie wywołania OpenGL przechodzą przez wątek Graphics, wszystkie OpenAL przez wątek Audio, wszystkie dane wejściowe przez wątek Input i wszystko, o co powinien martwić się organizacyjny wątek kontrolny, to zarządzanie wątkiem. Stan gry odbywa się w klasie GameState, na którą wszyscy mogą przyjrzeć się, gdy tego potrzebują. Jeśli kiedykolwiek zdecyduję, że powiedzmy, że JOAL ma datę i chcę zamiast tego użyć nowej edycji JavaSound, po prostu zaimplementuję inny wątek dla Audio.

Mam nadzieję, że rozumiecie, co mówię, mam już kilka tysięcy wierszy na temat tego projektu. Jeśli chcesz, żebym spróbował zeskrobać próbkę, zobaczę, co mogę zrobić.


Problem, z którym w końcu się spotkasz, polega na tym, że ta konfiguracja nie skaluje się szczególnie dobrze na maszynie wielordzeniowej. Tak, istnieją aspekty gry, które zazwyczaj najlepiej są obsługiwane we własnym wątku, takie jak audio, ale znaczna część pozostałej części pętli gry może być zarządzana szeregowo w połączeniu z zadaniami puli wątków. Jeśli pula wątków obsługuje maski koligacji, możesz łatwo ustawić w kolejce powiedz zadania renderowania do wykonania w tym samym wątku, a program do planowania wątków zarządza kolejkami roboczymi wątków i wykonuje operacje kradzieży w razie potrzeby, zapewniając obsługę wielowątkowości i wielordzeniowy.
Naros

1

Zwykle logika zajmująca się renderowaniem grafiki przebiega osobno (i ich harmonogram, a kiedy mają się uruchomić itp.) Obsługiwana jest przez osobny wątek. Jednak ten wątek jest już zaimplementowany (uruchomiony i uruchomiony) przez platformę, której używasz do rozwijania pętli gry (i gry).

Aby więc uzyskać pętlę gry, w której logika gry aktualizuje się niezależnie od harmonogramu odświeżania grafiki, nie trzeba tworzyć dodatkowych wątków, wystarczy skorzystać z już istniejącego wątku dla wspomnianych aktualizacji grafiki.

To zależy od używanej platformy. Na przykład:

  • jeśli robisz to na większości platform związanych z Open GL ( GLUT dla C / C ++ , JOLG dla Java , akcja związana z OpenGL ES dla Androida ), zwykle dają ci metodę / funkcję, która jest okresowo wywoływana przez wątek renderujący i którą może zintegrować się z twoją pętlą gry (bez uzależniania iteracji gameloopa od tego, kiedy wywoływana jest ta metoda). Dla GLUT za pomocą C, robisz coś takiego:

    glutDisplayFunc (myFunctionForGraphicsDrawing);

    glutIdleFunc (myFunctionForUpdatingState);

  • w JavaScript możesz używać Web Workers, ponieważ nie ma wielowątkowości (które możesz osiągnąć programowo) , możesz także użyć mechanizmu "requestAnimationFrame", aby otrzymywać powiadomienia o planowanym renderowaniu grafiki i odpowiednio aktualizować stan gry .

Zasadniczo to, czego chcesz, to mieszana pętla gry: masz kod, który aktualizuje stan gry i który jest wywoływany w głównym wątku twojej gry, a także chcesz okresowo korzystać z (lub zostać oddzwonionym) już istniejący wątek renderowania grafiki dla heads up, kiedy nadszedł czas na odświeżenie grafiki.


0

W Javie jest słowo kluczowe „synchronizowane”, które blokuje przekazywane do niego zmienne, aby były bezpieczne dla wątków. W C ++ możesz to samo osiągnąć przy pomocy Mutex. Na przykład:

Jawa:

synchronized(a){
    //code using a
}

C ++:

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

Blokowanie zmiennych zapewnia, że ​​nie zmieniają się podczas uruchamiania następującego po nim kodu, więc zmienne nie są zmieniane przez wątek aktualizujący podczas ich renderowania (w rzeczywistości zmieniają się, ale z punktu widzenia wątku renderującego nie zmieniają się) t). Musisz jednak uważać na zsynchronizowane słowo kluczowe w Javie, ponieważ tylko upewnia się, że wskaźnik do zmiennej / Object się nie zmieni. Atrybuty mogą się nadal zmieniać bez zmiany wskaźnika. Aby to rozważyć, możesz samodzielnie skopiować obiekt lub zsynchronizować wszystkie atrybuty obiektu, którego nie chcesz zmieniać.


1
Muteksy niekoniecznie są tutaj odpowiedzią, ponieważ OP nie tylko musiałby oddzielić logikę gry i renderowanie, ale także chciałby uniknąć utknięcia w martwym punkcie możliwości przejścia jednego wątku do przodu w przetwarzaniu, niezależnie od tego, gdzie drugi wątek aktualnie przetwarza pętla.
Naros,

0

To, co ogólnie widziałem w obsłudze komunikacji wątków logicznych / renderujących, to potrójne buforowanie danych. W ten sposób wątek renderujący mówi, że wiadro 0 odczytuje. Wątek logiczny używa segmentu 1 jako źródła danych wejściowych dla następnej ramki i zapisuje dane ramki w segmencie 2.

W punktach synchronizacji indeksy każdego z trzech segmentów są zamieniane, dzięki czemu dane następnej ramki są przekazywane do wątku renderowania i wątek logiczny może kontynuować.

Ale niekoniecznie jest powód, aby podzielić renderowanie i logikę na odpowiednie wątki. W rzeczywistości można utrzymać szeregową pętlę gry i oddzielić liczbę klatek renderowania od kroku logicznego za pomocą interpolacji. Aby skorzystać z procesorów wielordzeniowych korzystających z tego rodzaju konfiguracji, potrzebna jest pula wątków, która działa na grupach zadań. Zadania te mogą polegać na tym, że zamiast iterować listę obiektów od 0 do 100, iterujesz listę w 5 segmentach po 20 w 5 wątkach, skutecznie zwiększając wydajność, ale nie komplikując głównej pętli.


0

To jest stary post, ale wciąż się pojawia, więc chciałem tu dodać moje 2 centy.

Pierwsza lista danych, które powinny być przechowywane w interfejsie użytkownika / wątku wyświetlania vs wątku logicznym. W wątku interfejsu użytkownika możesz dołączyć siatkę 3D, tekstury, informacje o świetle oraz kopię danych pozycji / obrotu / kierunku.

W wątku logiki gry może być potrzebny rozmiar obiektu gry w 3D, obwiednia prymitywów (kula, sześcian), uproszczone dane siatki 3d (na przykład szczegółowe kolizje), wszystkie atrybuty wpływające na ruch / zachowanie, takie jak prędkość obiektu, współczynnik obrotu itp., a także dane pozycji / obrotu / kierunku.

Jeśli porównasz dwie listy, zobaczysz, że tylko kopia danych pozycji / obrotu / kierunku musi zostać przekazana z logiki do wątku interfejsu użytkownika. Możesz także potrzebować pewnego rodzaju identyfikatora korelacji, aby ustalić, do którego obiektu gry należą te dane.

To, jak to zrobisz, zależy od języka, z którym pracujesz. W Scali możesz używać Software Transactional Memory, w Javie / C ++ pewien rodzaj blokowania / synchronizacji. Lubię niezmienne dane, więc zwracam nowy niezmienny obiekt dla każdej aktualizacji. Jest to trochę marnowania pamięci, ale w przypadku nowoczesnych komputerów nie jest to taka wielka sprawa. Jednak jeśli chcesz zablokować udostępnione struktury danych, możesz to zrobić. Sprawdź klasę Exchanger w Javie, użycie dwóch lub więcej buforów może przyspieszyć proces.

Zanim zaczniesz udostępniać dane między wątkami, sprawdź, ile danych faktycznie musisz przekazać. Jeśli masz oktodę dzielącą przestrzeń 3d i możesz zobaczyć 5 obiektów z 10 obiektów, nawet jeśli twoja logika wymaga aktualizacji wszystkich 10, musisz przerysować tylko 5, które widzisz. Więcej informacji można znaleźć na tym blogu: http://gameprogrammingpatterns.com/game-loop.html Nie chodzi tu o synchronizację, ale pokazuje ona, w jaki sposób logika gry jest oddzielona od wyświetlania i jakie wyzwania należy pokonać (FPS). Mam nadzieję że to pomoże,

znak

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.