Czy przechowywanie wszystkich obiektów gry na jednej liście jest akceptowalnym projektem?

24

W każdej grze, którą stworzyłem, po prostu umieszczam wszystkie moje obiekty gry (pociski, samochody, graczy) na jednej liście tablic, którą przeglądam w celu losowania i aktualizacji. Kod aktualizacji dla każdej jednostki jest przechowywany w jej klasie.

Zastanawiam się, czy to właściwy sposób na załatwienie różnych spraw, czy szybszy i bardziej wydajny sposób?

Derek
źródło
4
Zauważyłem, że powiedziałeś „lista tablic”, zakładając, że jest to .Net, zamiast tego powinieneś uaktualnić do Listy <T>. Jest prawie identyczny, z wyjątkiem tego, że Lista <T> jest silna na typ i dlatego nie trzeba wpisywać rzutowania za każdym razem, gdy uzyskujesz dostęp do elementu. Oto dokument: msdn.microsoft.com/en-us/library/6sh2ey19.aspx
John McDonald

Odpowiedzi:

26

Nie ma jednej właściwej drogi. To, co opisujesz, działa i jest prawdopodobnie wystarczająco szybkie, aby nie miało znaczenia, ponieważ wydajesz się pytać, ponieważ martwisz się projektem, a nie dlatego, że spowodowało to problemy z wydajnością. Jest to więc właściwy sposób.

Z pewnością można to zrobić inaczej, na przykład utrzymując jednorodną listę dla każdego typu obiektu lub upewniając się, że wszystko na liście heterogenicznej jest zgrupowane, dzięki czemu poprawiono spójność kodu w pamięci podręcznej lub zezwala się na równoległe aktualizacje. Trudno powiedzieć, czy zauważysz zauważalną poprawę wydajności, nie znając zakresu i skali gier.

Możesz także dodatkowo rozdzielić obowiązki swoich podmiotów - brzmi to tak, jakby podmioty zarówno aktualizowały się, jak i renderowały w tej chwili - aby lepiej postępować zgodnie z zasadą pojedynczej odpowiedzialności. Może to zwiększyć łatwość konserwacji i elastyczność twoich indywidualnych interfejsów poprzez oddzielenie ich od siebie. Ale znowu, czy dostrzegasz korzyść z dodatkowej pracy, która pociąga za sobą, trudno mi powiedzieć, wiedząc, co wiem o twoich grach (co w zasadzie jest niczym).

Radziłbym nie kłaść na to zbyt dużego nacisku i trzymać się z tyłu głowy, że może to być potencjalny obszar do poprawy, jeśli kiedykolwiek zauważysz problemy z wydajnością lub utrzymaniem.

Josh
źródło
16

Jak powiedzieli inni, jeśli jest wystarczająco szybki, to jest wystarczająco szybki. Jeśli zastanawiasz się, czy istnieje lepszy sposób, zadaj sobie pytanie

Czy czuję, że iteruję wszystkie obiekty, ale działam tylko na ich podzbiorze?

Jeśli tak, oznacza to, że możesz chcieć mieć wiele list zawierających tylko odpowiednie odniesienia. Na przykład, jeśli zapętlasz każdy obiekt gry, aby je renderować, ale trzeba tylko renderować podzbiór obiektów, warto zachować osobną listę do renderowania tylko dla tych obiektów, które muszą być renderowane.

Zmniejszyłoby to liczbę obiektów, przez które przechodzi się pętlę i pomija niepotrzebne kontrole, ponieważ wiadomo już, że wszystkie obiekty na liście mają zostać przetworzone.

Musisz go profilować, aby zobaczyć, czy osiągasz znaczny wzrost wydajności.

Michał
źródło
Zgadzam się z tym, ogólnie mam podzbiory mojej listy dla obiektów, które muszą być aktualizowane w każdej ramce, i rozważałem zrobienie tego samego dla obiektów, które są renderowane. Jednak gdy te właściwości mogą się zmieniać na bieżąco, zarządzanie wszystkimi listami może być skomplikowane. Kiedy obiekt zmienia się z renderowalnego na niemożliwy do renderowania lub aktualizowany na niemożliwy do aktualizacji, a teraz dodajesz lub usuwasz je z wielu list jednocześnie.
Nic Foster,
8

Zależy to prawie całkowicie od konkretnych potrzeb gry . Może działać idealnie w prostej grze, ale zawieść w bardziej złożonym systemie. Jeśli działa w twojej grze, nie przejmuj się nią lub staraj się ją przerobić, aż pojawi się taka potrzeba.

Jeśli chodzi o to, dlaczego proste podejście może się nie udać w niektórych sytuacjach, trudno podsumować to w jednym poście, ponieważ możliwości są nieograniczone i zależą wyłącznie od samych gier. Inne odpowiedzi wspominały na przykład, że możesz zgrupować obiekty współdzielące obowiązki razem na osobnej liście.

To jedna z najczęstszych zmian, które możesz wprowadzać w zależności od projektu gry. Zaryzykuję i opiszę kilka innych (bardziej złożonych) przykładów, ale pamiętaj, że wciąż istnieje wiele innych możliwych przyczyn i rozwiązań.

Na początek zwrócę uwagę, że aktualizacja obiektów gry i ich renderowanie może mieć różne wymagania w niektórych grach, dlatego też należy się z nimi obchodzić osobno. Niektóre możliwe problemy z aktualizacją i renderowaniem obiektów gry:

Aktualizowanie obiektów gry

Oto fragment architektury Game Engine Architecture, który polecam przeczytać:

W przypadku zależności między obiektami należy nieco zmodyfikować opisaną powyżej technikę stopniowych aktualizacji. Wynika to z faktu, że zależności między obiektami mogą prowadzić do sprzecznych reguł rządzących kolejnością aktualizacji.

Innymi słowy, w niektórych grach obiekty mogą zależeć od siebie i wymagać określonej kolejności aktualizacji. W tym scenariuszu może być konieczne opracowanie bardziej złożonej struktury niż lista do przechowywania obiektów i ich wzajemnych zależności.

wprowadź opis zdjęcia tutaj

Renderowanie obiektów gry

W niektórych grach sceny i obiekty tworzą hierarchię węzłów nadrzędny-podrzędny i powinny być renderowane we właściwej kolejności oraz z transformacjami względem ich rodziców. W takich przypadkach możesz potrzebować bardziej złożonej struktury, takiej jak wykres sceny (struktura drzewa) zamiast pojedynczej listy:

wprowadź opis zdjęcia tutaj

Innymi przyczynami mogą być na przykład użycie struktury danych partycjonowania przestrzennego w celu uporządkowania obiektów w sposób, który poprawia wydajność wykonywania wygładzania fragmentów widoku.

David Gouveia
źródło
4

Odpowiedź brzmi „tak, w porządku”. Kiedy pracowałem nad dużymi symulacjami żelaza, w których wydajności nie można było negocjować, zdziwiłem się, widząc wszystko w jednym dużym globalnym układzie (BIG i FAT). Później pracowałem nad systemem OO i musiałem porównać oba. Chociaż było to bardzo brzydkie, wersja „jedna tablica do rządzenia nimi wszystkimi” w prostym, pojedynczym wątku starego C skompilowanego podczas debugowania była kilka razy szybsza niż „ładny” wielowątkowy system OO skompilowany -O2 w C ++. Myślę, że trochę miało to związek z doskonałą lokalizacją pamięci podręcznej (zarówno w przestrzeni, jak i czasie) w wersji z dużą ilością tablic.

Jeśli chcesz wydajności, górną granicą będzie jedna wielka globalna tablica C (z doświadczenia).

zaraz
źródło
2

Zasadniczo to, co chcesz zrobić, to tworzyć listy w miarę potrzeb. Jeśli przerysujesz obiekty terenu za jednym razem, przydatna może być ich lista. Ale żeby było to warte czasu, potrzebowałbyś dziesiątków tysięcy z nich i wielu tysięcy rzeczy, które nie są terenem. W przeciwnym razie przeglądanie listy wszystkiego i używanie „if” do wyciągania obiektów terenowych jest wystarczająco szybkie.

Zawsze potrzebowałem jednej głównej listy, bez względu na to, ile innych list miałem. Zwykle robię z tego mapę, tablicę z kluczami lub słownik, aby mieć swobodny dostęp do poszczególnych elementów. Ale prosta lista jest o wiele prostsza; jeśli nie masz dostępu do obiektów gry losowo, nie potrzebujesz mapy. A okazjonalne sekwencyjne przeszukiwanie kilku tysięcy wpisów w celu znalezienia właściwego nie zajmie długo. Powiedziałbym więc, że cokolwiek jeszcze zrobicie, planujcie trzymać się głównej listy. Rozważ użycie mapy, jeśli stanie się duża. (Chociaż jeśli możesz używać indeksów zamiast kluczy, możesz trzymać się tablic i mieć coś znacznie lepszego niż Mapa. ​​Zawsze kończyłem się dużymi przerwami między liczbami indeksów, więc musiałem się poddać).

Słowo o tablicach: są niewiarygodnie szybkie. Ale kiedy wstają z około 100 000 elementów, zaczynają pasować do Śmieciarzy. Jeśli możesz przydzielić przestrzeń raz, a potem jej nie dotykać, dobrze. Ale jeśli stale powiększasz tablicę, stale przydzielasz i zwalniasz duże fragmenty pamięci i możesz zawiesić grę przez kilka sekund, a nawet zawieść z powodu błędu pamięci.

Czyli szybszy i bardziej wydajny? Pewnie. Mapa losowego dostępu. W przypadku naprawdę dużych list lista połączona pobije tablicę, jeśli i kiedy nie potrzebujesz dostępu losowego. Wiele map / list do organizowania obiektów na różne sposoby, w zależności od potrzeb. Możesz zaktualizować aktualizację wielowątkową.

Ale to dużo pracy. Ta pojedyncza tablica jest szybka i prosta. Możesz dodawać inne listy i rzeczy tylko w razie potrzeby i prawdopodobnie nie będziesz ich potrzebować. Pamiętaj tylko, co może pójść nie tak, jeśli Twoja gra stanie się duża. Możesz mieć wersję testową z 10-krotnie większą liczbą obiektów niż w wersji rzeczywistej, aby dać Ci wyobrażenie, kiedy zmierzasz do kłopotów. (Rób to tylko wtedy, gdy gra staje się duża.) (I nie próbuj wielowątkowości bez zastanowienia się. Wszystko inne jest łatwe do rozpoczęcia i możesz zrobić tyle, ile chcesz. i wycofaj się w dowolnym momencie. Dzięki wielowątkowości zapłacisz wysoką cenę za wejście, opłaty za pozostanie w są wysokie i trudno jest się wycofać.)

Innymi słowy, po prostu rób to, co robisz. Twoim największym limitem jest prawdopodobnie twój własny czas, który najlepiej wykorzystujesz.

RalphChapin
źródło
Naprawdę nie sądzę, aby lista połączona przewyższała tablicę w dowolnym rozmiarze, chyba że często dodajesz lub usuwasz elementy ze środka.
Zan Lynx,
@ZanLynx: I nawet wtedy. Myślę, że nowe optymalizacje środowiska wykonawczego bardzo pomagają tablicom - nie dlatego, że były potrzebne. Ale jeśli wielokrotnie i szybko przydzielasz i zwalniasz duże fragmenty pamięci, co może się zdarzyć z dużymi tablicami, możesz związać śmietnik w węzły. (Łączna ilość pamięci jest tutaj również czynnikiem. Problem polega na funkcji wielkości bloków, częstotliwości wolnej i alokacji oraz dostępnej pamięci.) Awaria występuje nagle, z zawieszeniem się lub awarią programu. Zwykle jest dużo wolnej pamięci; GC po prostu nie może z niego korzystać. Połączone listy unikają tego problemu.
RalphChapin
Z drugiej strony listy połączone mają znacznie większe prawdopodobieństwo buforowania pominięcia podczas iteracji ze względu na fakt, że węzły nie są ciągłe w pamięci. To nie jest prawie tak wytrawne i suche. Możesz złagodzić problemy z realokacją, nie robiąc tego (alokacja z góry, jeśli to możliwe, ponieważ często tak jest), a także możesz złagodzić problemy ze spójnością pamięci podręcznej na połączonej liście, przydzielając pule węzłów (chociaż nadal masz następujący koszt odwołania , jest to względnie trywialne).
Josh
Tak, ale nawet w tablicy, czy nie przechowujesz wskaźników do obiektów? (Chyba że jest to krótkotrwała tablica dla układu cząstek lub coś takiego.) Jeśli tak, to nadal będzie wiele braków pamięci podręcznej.
Todd Lehman,
1

Użyłem nieco podobnej metody do prostych gier. Przechowywanie wszystkiego w jednej tablicy jest bardzo przydatne, gdy spełnione są następujące warunki:

  1. Masz małą liczbę lub znane maksimum obiektów gry
  2. Wolisz prostotę niż wydajność (tj. Bardziej zoptymalizowane struktury danych, takie jak drzewa, nie są warte kłopotu)

W każdym razie zaimplementowałem to rozwiązanie jako tablicę o stałej wielkości, która zawiera gameObject_ptrstrukturę. Struktura ta zawierała wskaźnik do samego obiektu gry oraz wartość logiczną wskazującą, czy obiekt był „żywy”.

Wartość logiczna ma na celu przyspieszenie operacji tworzenia i usuwania. Zamiast niszczyć obiekty gry po ich usunięciu z symulacji, po prostu ustawiłbym flagę „żywy” na false.

Podczas wykonywania obliczeń na obiektach kod przechodzi przez pętlę, wykonując obliczenia na obiektach oznaczonych jako „żywe” i renderowane są tylko obiekty oznaczone jako „żywe”.

Inną prostą optymalizacją jest uporządkowanie tablicy według typu obiektu. Na przykład wszystkie pociski mogą znajdować się w ostatnich n komórkach tablicy. Następnie, przechowując liczbę całkowitą z wartością indeksu lub wskaźnika do elementu tablicy, można odwoływać się do określonych typów przy obniżonym koszcie.

Nie trzeba dodawać, że to rozwiązanie nie jest dobre w przypadku gier z dużą ilością danych, ponieważ tablica będzie niedopuszczalnie duża. Nie nadaje się również do gier z ograniczeniami pamięci, które zabraniają pamięci nieużywanym obiektom. Najważniejsze jest to, że jeśli masz znany górny limit liczby obiektów w grze, które będziesz miał w danym momencie, nie ma nic złego w przechowywaniu ich w statycznej tablicy.

To jest mój pierwszy post! Mam nadzieję, że odpowie na twoje pytanie!

blz
źródło
pierwszy post! tak!
Tili
0

Jest to do przyjęcia, choć niezwykłe. Terminy takie jak „szybszy” i „bardziej wydajny” są względne; zazwyczaj organizujesz obiekty gry w osobne kategorie (więc jedna lista dla pocisków, jedna lista dla wrogów itp.), aby programowanie było lepiej zorganizowane (a przez to bardziej wydajne), ale to nie jest tak, że komputer szybciej zapętli wszystkie obiekty .

jhocking
źródło
2
To prawda, jeśli chodzi o większość gier XNA, ale uporządkowanie obiektów może w rzeczywistości przyspieszyć ich iterację: obiekty tego samego typu częściej trafiają do instrukcji i pamięci podręcznej procesora. Brakujące pamięci podręczne są drogie, szczególnie na konsolach. Na przykład na Xbox 360 brak pamięci podręcznej L2 kosztuje ~ 600 cykli. To pozostawia dużą wydajność na stole. Jest to jedno miejsce, w którym kod zarządzany ma przewagę nad kodem rodzimym: obiekty przydzielone blisko siebie w czasie również znajdą się w pamięci, a sytuacja poprawi się po wyrzucaniu elementów bezużytecznych (kompaktowanie).
Chroniczny programista gier
0

Mówisz pojedynczą tablicę i obiekty.

Więc (bez twojego kodu, żeby się przyjrzeć) Domyślam się, że wszystkie są powiązane z „uberGameObject”.

Więc może dwa problemy.

1) Możesz mieć obiekt „boga” - robi tony rzeczy, nie tak naprawdę podzielonych według tego, co robi lub czym jest.

2) Powtarzasz tę listę i przesyłasz tekst? lub rozpakowanie każdego. Ma to duży wpływ na wydajność.

rozważ rozbicie różnych typów (pociski, złoczyńcy itp.) na ich własne, mocno wpisane listy.

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.
Dr9
źródło
Nie ma potrzeby wykonywania rzutowania typu, jeśli istnieje jakaś wspólna klasa podstawowa lub interfejs, który ma wszystkie metody, które zostaną wywołane w pętli aktualizacji (np. update()).
bummzack
0

Mogę zaproponować kompromis między ogólną pojedynczą listą a osobnymi listami dla każdego typu obiektu.

Zachowaj odniesienie do jednego obiektu na wielu listach. Te listy są zdefiniowane przez podstawowe zachowania. Na przykład, MarineSoldierobiekt zostanie wymieniony w PhysicalObjList, GraphicalObjList, UserControlObjList, HumanObjList. Tak więc, kiedy przetwarzasz fizykę, przechodzisz iterację PhysicalObjList, kiedy proces uszkadzający truciznę - HumanObjListjeśli Żołnierz umrze - usuń ją, HumanObjListale trzymaj się PhysicalObjList. Aby ten mechanizm działał, musisz zorganizować automatyczne wstawianie / usuwanie obiektu podczas jego tworzenia / usuwania.

Zalety tego podejścia:

  • wpisana organizacja obiektów (nie AllObjectsListz rzutowaniem przy aktualizacji)
  • listy oparte są na logice, a nie na klasach obiektów
  • brak problemów z obiektami o połączonych zdolnościach ( MegaAirHumanUnderwaterObjectzostałyby wstawione do odpowiednich list zamiast tworzenia nowej listy dla tego typu)

PS: Jeśli chodzi o samodzielną aktualizację, napotkasz problem, gdy wiele obiektów wchodzi w interakcje, a każdy z nich zależy od innych. Aby rozwiązać ten problem, musimy wpływać na obiekt zewnętrznie, a nie w ramach własnej aktualizacji.

brygadir
źródło