„Optymalna” pętla gry dla przewijania bocznego 2D

14

Czy można opisać „optymalny” (pod względem wydajności) układ dla pętli gier side-scroller 2D? W tym kontekście „pętla gry” pobiera dane od użytkowników, aktualizuje stany obiektów gry i rysuje obiekty gry.

Na przykład posiadanie klasy podstawowej GameObject z hierarchią głębokiego dziedziczenia może być przydatne do konserwacji ... możesz zrobić coś takiego:

foreach(GameObject g in gameObjects) g.update();

Myślę jednak, że takie podejście może powodować problemy z wydajnością.

Z drugiej strony dane i funkcje wszystkich obiektów gry mogą mieć charakter globalny. To byłby problem z utrzymaniem głowy, ale może być bliżej optymalnie działającej pętli gry.

jakieś pomysły? Interesują mnie praktyczne zastosowania niemal optymalnej struktury pętli gry ... nawet jeśli dostanę bólu głowy w zamian za doskonałą wydajność.

MrDatabase
źródło

Odpowiedzi:

45

Na przykład posiadanie podstawowej klasy GameObject z hierarchią głębokiego dziedziczenia może być przydatne do konserwacji ...

W rzeczywistości głębokie hierarchie są generalnie gorsze z punktu widzenia łatwości konserwacji niż płytkie, a nowoczesny styl architektoniczny obiektów do gier ma tendencję do płytkich podejść opartych na agregacji .

Myślę jednak, że takie podejście może powodować problemy z wydajnością. Z drugiej strony dane i funkcje wszystkich obiektów gry mogą mieć charakter globalny. To byłby problem z utrzymaniem głowy, ale może być bliżej optymalnie działającej pętli gry.

Pętla, którą pokazałeś, może powodować problemy z wydajnością, ale nie, jak wynika z późniejszej instrukcji, ponieważ masz dane instancji i funkcje składowe w GameObjectklasie. Problem polega raczej na tym, że jeśli traktujesz każdy obiekt w grze dokładnie tak samo, prawdopodobnie nie grupujesz tych obiektów inteligentnie - prawdopodobnie są one losowo rozrzucone po całej liście. Potencjalnie zatem każde wywołanie metody aktualizacji dla tego obiektu (niezależnie od tego, czy ta metoda jest funkcją globalną, czy też nie i czy obiekt ma dane instancji lub „dane globalne” unoszące się w tabeli, do której indeksujesz, czy cokolwiek innego), różni się od wywołania aktualizacji w ostatnich iteracjach pętli.

Może to wywierać większy nacisk na system, ponieważ może być konieczne stronicowanie pamięci odpowiednią funkcją do i z pamięci oraz częstsze napełnianie pamięci podręcznej instrukcji, co powoduje wolniejszą pętlę. To, czy można to zaobserwować gołym okiem (lub nawet w programie profilującym), zależy dokładnie od tego , co jest uważane za „przedmiot gry”, ile z nich istnieje średnio i co jeszcze dzieje się w twojej aplikacji.

Systemy obiektowe zorientowane na komponenty są obecnie popularnym trendem, wykorzystując filozofię, zgodnie z którą agregacja jest lepsza niż dziedziczenie . Takie systemy potencjalnie umożliwiają podzielenie logiki „aktualizacji” komponentów (gdzie „komponent” jest z grubsza zdefiniowany jako pewna jednostka funkcjonalności, na przykład rzecz reprezentująca fizycznie symulowaną część jakiegoś obiektu, która jest przetwarzana przez system fizyki ) na wiele wątków - w zależności od typu komponentu - jeśli to możliwe i pożądane, które mogą mieć wzrost wydajności. Przynajmniej możesz tak zorganizować komponenty, aby wszystkie komponenty danego typu aktualizowały się razem , optymalnie wykorzystując pamięć podręczną procesora. Przykład takiego systemu zorientowanego na komponenty omówiono w tym wątku .

Takie systemy są często silnie odsprzężone, co jest również dobrodziejstwem dla konserwacji.

Projektowanie zorientowane na dane jest podejściem pokrewnym - chodzi przede wszystkim o zorientowanie się wokół danych wymaganych od obiektów, aby dane mogły być efektywnie przetwarzane zbiorczo (na przykład). Zasadniczo oznacza to organizację, która próbuje utrzymać dane wykorzystywane w tym samym klastrze celowym razem i działać jednocześnie. Nie jest to zasadniczo niezgodne z projektem OO i można znaleźć trochę gadania na ten temat tutaj w GDSE w tym pytaniu .

W efekcie bardziej optymalne byłoby podejście do pętli zamiast oryginalnego

foreach(GameObject g in gameObjects) g.update();

coś bardziej jak

ProcessUserInput();
UpdatePhysicsForAllObjects();
UpdateScriptsForAllObjects();
UpdateRenderDataForAllObjects();
RenderEverything();

W takim świecie, każdy GameObjectmoże mieć wskaźnik lub odniesienie do własnej PhysicsDatalub Scriptlub RenderData, w przypadkach, w których mogą być potrzebne do interakcji z obiektami w sposób indywidualny, a rzeczywisty PhysicsData, Scripts, RenderData, et cetera, że wszystko jest w posiadaniu odpowiednich podsystemów (symulator fizyki, środowisko hostowania skryptów, renderer) i przetwarzane zbiorczo, jak wskazano powyżej.

Jest bardzo ważne , aby pamiętać, że takie podejście nie jest cudownym środkiem, a nie zawsze przyniesie poprawę wydajności (chociaż jest ogólnie lepsza konstrukcja niż głębokiego drzewa dziedziczenia). Szczególnie prawdopodobne jest, że zasadniczo nie zauważysz różnicy w wydajności, jeśli masz bardzo mało obiektów lub bardzo wiele obiektów, dla których nie można skutecznie równolegle aktualizować.

Niestety nie ma takiej magicznej pętli, która byłaby najbardziej optymalna - każda gra jest inna i może wymagać dostrajania wydajności na różne sposoby. Dlatego bardzo ważne jest, aby mierzyć (profilować) rzeczy, zanim ślepo skorzystasz z porady jakiegoś przypadkowego faceta w Internecie.

Społeczność
źródło
5
Dobry Boże, to świetna odpowiedź.
Raveline
2

Programowanie w stylu obiekt-gra jest odpowiednie dla mniejszych projektów. Gdy projekt stanie się naprawdę ogromny, skończysz z głęboką hierarchią dziedziczenia, a baza obiektów-gier stanie się naprawdę ciężka!

Jako przykład ... Załóżmy, że zacząłeś tworzyć bazę obiektów gry z minimalnymi atrybutami, np. Pozycja (dla xiy), displayObject (odnosi się do obrazu, jeśli jest to element rysunkowy), szerokość, wysokość itp.

Później, jeśli będziemy musieli dodać podklasę, która będzie miała stan animacji, prawdopodobnie przeniesiemy „kod kontrolny animacji” (np. CurrentAnimationId, liczba klatek dla tej animacji itp.) Do GameObjectBase, sądząc, że większość obiekty będą miały animację.
Później, jeśli chcesz dodać sztywne ciało, które jest statyczne (nie ma żadnej animacji), musisz sprawdzić „kod kontroli animacji” w funkcji aktualizacji GameObject Base (chociaż jest to sprawdzenie, czy animacja jest dostępna, czy nie). ... to ma znaczenie!) W przeciwieństwie do tego, jeśli będziesz postępować zgodnie ze strukturą opartą na komponentach, będziesz mieć dołączony do obiektu element kontrolny animacji. Jeśli będzie to konieczne, dołącz go. W przypadku ciała statycznego nie dołączasz tego komponentu. w ten sposób możemy uniknąć niepotrzebnych kontroli.

Tak więc wybór stylu zależy od wielkości projektu. Struktura GameObject jest łatwa do opanowania tylko dla mniejszych projektów. Mam nadzieję, że to trochę pomaga.

Ayyappa
źródło
@Josh Petrie - Naprawdę świetna odpowiedź!
Ayyappa,
@Josh Petrie - Naprawdę dobra odpowiedź z przydatnymi linkami! (Sry nie wiem, jak umieścić ten komentarz obok swojego postu: P)
Ayyappa
Zwykle możesz kliknąć link „dodaj komentarz” pod moją odpowiedzią, ale wymaga to większej reputacji niż obecnie (patrz gamedev.stackexchange.com/privileges/comment ). I dzięki!