Jak najlepiej ustrukturyzować / zarządzać setkami postaci w grze


10

Stworzyłem prostą grę RTS, która zawiera setki postaci, takich jak Crusader Kings 2, w Unity. Do przechowywania ich najłatwiejszą opcją jest użycie obiektów skryptowych, ale nie jest to dobre rozwiązanie, ponieważ nie można tworzyć nowych w czasie wykonywania.

Stworzyłem więc klasę C # o nazwie „Charakter”, która zawiera wszystkie dane. Wszystko działa dobrze, ale gdy gra symuluje, nieustannie tworzy nowe postacie i zabija niektóre postacie (w miarę wydarzeń w grze). Ponieważ gra nieustannie symuluje, tworzy tysiące postaci. Dodałem prostą kontrolę, aby upewnić się, że znak jest „Żywy” podczas przetwarzania jego funkcji. Pomaga to w wydajności, ale nie mogę usunąć „Postaci”, jeśli on / ona nie żyje, ponieważ potrzebuję jego / jej informacji podczas tworzenia drzewa genealogicznego.

Czy lista jest najlepszym sposobem na zapisanie danych dla mojej gry? A może sprawi to problemy, gdy stworzy się 10000 postaci? Jednym z możliwych rozwiązań jest utworzenie kolejnej listy, gdy lista osiągnie określoną liczbę, i przeniesienie na nią wszystkich martwych postaci.


9
Jedną rzeczą, którą zrobiłem w przeszłości, gdy potrzebuję podzbioru danych postaci po jej śmierci, jest stworzenie obiektu „nagrobka” dla tej postaci. Nagrobek może przenosić informacje, które muszę później wyszukać, ale mogą być mniejsze i powtarzane rzadziej, ponieważ nie wymagają ciągłej symulacji jak żywa postać.
DMGregory


2
Czy gra jest jak CK2, czy może chodzi tylko o to, by mieć dużo postaci? Zrozumiałem to jako całą grę podobną do CK2. W takim przypadku wiele odpowiedzi tutaj nie jest niepoprawnych i zawiera dobre know-how, ale pomijają sens pytania. Nie pomaga ci to, że nazwałeś CK2 strategiczną grą w czasie rzeczywistym, podczas gdy tak naprawdę jest to świetna gra strategiczna . Może się to wydawać dziwaczne, ale ma to związek z problemami, z którymi się borykasz.
Raphael Schmitz

1
Na przykład, kiedy wspomina „1000s znaków”, ludzie myślą o 1000s modeli 3D lub sprites na ekranie w tym samym czasie - tak, w jedności, 1000s GameObjects. W CK2 maksymalna liczba postaci, które widziałem w tym samym czasie, była wtedy, gdy spojrzałem na mój dwór i zobaczyłem tam 10-15 osób (nie grałem jednak zbyt daleko). Równie dobrze armia z 3000 żołnierzami jest tylko jedna GameObject, z liczbą „3000”.
Raphael Schmitz

1
@ R.Schmitz Tak. Powinienem był wyjaśnić, że każda postać nie ma dołączonego do niej obiektu gry. Kiedykolwiek jest to konieczne, np. Przenoszenie postaci z jednego punktu do drugiego. Tworzony jest osobny byt, który zawiera wszystkie informacje o tej Postaci z logiką Ai.
paul p.

Odpowiedzi:


24

Należy wziąć pod uwagę trzy rzeczy:

  1. Czy to faktycznie powoduje problem z wydajnością? Tysiące to właściwie niewiele. Nowoczesne komputery są wyjątkowo szybkie i potrafią obsłużyć wiele rzeczy. Sprawdź, ile czasu zajmuje przetwarzanie znaków i sprawdź, czy to rzeczywiście spowoduje problem, zanim się o to martwisz.

  2. Wierność obecnie minimalnie aktywnych postaci. Częstym błędem początkujących programistów gier jest obsesja na punkcie precyzyjnej aktualizacji postaci poza ekranem w taki sam sposób, jak postaci na ekranie. To błąd, nikogo to nie obchodzi. Zamiast tego musisz stworzyć wrażenie, że postacie z ekranu nadal działają. Zmniejszając liczbę znaków aktualizacji odbieranych poza ekranem, można znacznie skrócić czas przetwarzania.

  3. Rozważ projektowanie zorientowane na dane. Zamiast mieć 1000 obiektów znaków i wywoływać tę samą funkcję dla każdego, miej tablicę danych dla 1000 znaków i jedną pętlę funkcji na 1000 znaków aktualizującą kolejno. Ten rodzaj optymalizacji może znacznie poprawić wydajność.


3
Entity / Component / System działa dobrze w tym celu. Zbuduj każdy „system” według potrzeb, zachowaj tysiące lub dziesiątki tysięcy znaków (ich komponentów) i podaj „identyfikator postaci” do systemu. Umożliwia to oddzielenie i pomniejszenie różnych modeli danych, a także usunięcie martwych postaci z systemów, które ich nie potrzebują. (Możesz także całkowicie rozładować system, jeśli nie używasz go w tej chwili.)
Der Kommissar,

1
Zmniejszając liczbę znaków aktualizacji odbieranych poza ekranem, można znacznie wydłużyć czas przetwarzania. Nie masz na myśli spadku?
Tejas Kale

1
@TejasKale: Tak, poprawione.
Jack Aidley,

2
Tysiąc postaci to niewiele, ale kiedy każda z nich stale sprawdza, czy potrafią kastrować każdą z pozostałych , zaczyna to mieć duży wpływ na ogólną wydajność ...
curiousdannii

1
Zawsze najlepiej to sprawdzić, ale generalnie jest to bezpieczne założenie, że Rzymianie będą chcieli się kastrować;)
ciekawyni

11

W tej sytuacji sugeruję użycie kompozycji :

Zasada, że ​​klasy powinny osiągnąć zachowanie polimorficzne i ponowne użycie kodu według ich składu (poprzez zawieranie instancji innych klas, które implementują pożądaną funkcjonalność)


W tym przypadku wygląda na to, że twoja Characterklasa stała się podobna do boga i zawiera wszystkie szczegóły dotyczące działania postaci na wszystkich etapach jej życia.

Na przykład zauważasz, że martwe postacie są nadal wymagane - ponieważ są używane w drzewach genealogicznych. Jednak jest mało prawdopodobne, aby wszystkie informacje i funkcje twoich żywych postaci były nadal potrzebne tylko do wyświetlenia ich w drzewie genealogicznym. Mogą na przykład potrzebować po prostu imion, daty urodzenia i ikony portretu.


Rozwiązaniem jest podzielenie poszczególnych części Characterna podklasy, których Characterwłaścicielem jest instancja. Na przykład:

  • CharacterInfo może być prostą strukturą danych z imieniem, datą urodzenia, datą śmierci i frakcją,

  • Equipmentmoże zawierać wszystkie przedmioty, które posiada twoja postać, lub ich bieżące zasoby. Może mieć również logikę, która zarządza nimi w funkcjach.

  • CharacterAIlub CharacterControllermoże mieć wszystkie potrzebne informacje o aktualnym celu postaci, jej funkcjach użytkowych itp. Może też mieć rzeczywistą logikę aktualizacji, która koordynuje podejmowanie decyzji / interakcję między jej poszczególnymi częściami.

Po podzieleniu postaci nie musisz już sprawdzać flagi Alive / Dead w pętli aktualizacji.

Zamiast tego, można by po prostu zrób AliveCharacterObjectto ma CharacterController, CharacterEquipmenti CharacterInfoskrypty w załączeniu. Aby „zabić” postać, wystarczy usunąć części, które nie są już istotne (takie jak CharacterController) - nie marnuje pamięci ani czasu przetwarzania.

Zwróć uwagę, że CharacterInfoto prawdopodobnie jedyne dane rzeczywiście potrzebne do drzewa genealogicznego. Rozkładając swoje klasy na mniejsze elementy - możesz łatwiej przechowywać ten mały obiekt danych po śmierci, bez konieczności zachowania całej postaci opartej na sztucznej inteligencji.


Warto wspomnieć, że ten paradygmat jest tym, z którego zbudowano Unity - i dlatego obsługuje wiele osobnych skryptów. Budowanie dużych boskich obiektów rzadko jest najlepszym sposobem na zarządzanie danymi w Unity.


8

Gdy masz dużą ilość danych do obsłużenia, a nie każdy punkt danych jest reprezentowany przez rzeczywisty obiekt gry, zazwyczaj nie jest złym pomysłem rezygnacja z klas specyficznych dla Jedności i po prostu używanie zwykłych starych obiektów C #. W ten sposób minimalizujesz koszty ogólne. Wygląda na to, że jesteś na dobrej drodze.

Przechowywanie wszystkich znaków, żywych lub martwych, na jednej liście ( lub tablicy ) może być przydatne, ponieważ indeks na tej liście może służyć jako kanoniczny identyfikator postaci. Dostęp do pozycji listy według indeksu jest bardzo szybką operacją. Przydałaby się jednak osobna lista identyfikatorów wszystkich żywych postaci, ponieważ prawdopodobnie będziesz musiał powtarzać je znacznie częściej niż martwe postacie.

W miarę postępów w implementacji mechaniki gry możesz również chcieć sprawdzić, jakie inne wyszukiwania przeprowadzasz najczęściej. Jak „wszystkie żywe postacie w określonej lokalizacji” lub „wszyscy żywi lub martwi przodkowie określonej postaci”. Korzystne może być utworzenie dodatkowych struktur danych zoptymalizowanych dla tego rodzaju zapytań. Pamiętaj tylko, że każdy z nich musi być na bieżąco. Wymaga to dodatkowego programowania i będzie źródłem dodatkowych błędów. Więc rób to tylko wtedy, gdy spodziewasz się znacznego wzrostu wydajności.

CKII „ przycina ” znaki ze swojej bazy danych, gdy uzna je za nieistotne w celu oszczędzania zasobów. Jeśli twój stos martwych postaci zużywa zbyt wiele zasobów w długotrwałej grze, możesz zrobić coś podobnego (nie chcę nazywać tego „śmieciem”. Może „pełnym szacunku inkrematorem”?).

Jeśli rzeczywiście masz obiekt gry dla każdej postaci w grze, nowy system Unity ECS i Jobs może ci się przydać. Jest zoptymalizowany do obsługi dużej liczby bardzo podobnych obiektów gry w wydajny sposób. Ale zmusza architekturę oprogramowania do pewnych bardzo sztywnych wzorców.

Nawiasem mówiąc, bardzo podoba mi się CKII i sposób, w jaki symuluje on świat z tysiącami unikalnych postaci kontrolowanych przez AI, więc nie mogę się doczekać, aby zagrać w twoje podejście do tego gatunku.


Witam, dziękuję za odpowiedź. Wszystkie obliczenia są wykonywane przez jednego menedżera GameObject. Przydzielam obiekty gry poszczególnym aktorom tylko wtedy, gdy jest to konieczne (np. Pokazując ruchy armii postaci z jednej pozycji na drugą).
paul p

1
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.Z mojego doświadczenia z modowaniem CK2 jest to zbliżone do tego, jak CK2 obsługuje dane. Wydaje się, że CK2 korzysta z indeksów, które są zasadniczo podstawowymi indeksami baz danych, co przyspiesza wyszukiwanie znaków w konkretnej sytuacji. Zamiast mieć listę znaków, wydaje się, że ma wewnętrzną bazę danych znaków, ze wszystkimi wadami i korzyściami, które się z tym wiążą.
Morfildur,

1

Nie musisz symulować / aktualizować tysięcy postaci, gdy tylko kilka znajduje się w pobliżu gracza. Musisz tylko zaktualizować to, co gracz faktycznie widzi w danym momencie, więc postacie znajdujące się dalej od gracza powinny zostać zawieszone, dopóki gracz nie będzie bliżej nich.

Jeśli to nie zadziała, ponieważ mechanika gry wymaga odleglejszych postaci, aby pokazać upływ czasu, możesz je zaktualizować w jednej „dużej” aktualizacji, gdy gracz się zbliży. Jeśli mechanika gry wymaga, aby każda postać faktycznie reagowała na wydarzenia w grze, niezależnie od tego, gdzie postać jest w stosunku do gracza lub zdarzenia, może to zmniejszyć częstotliwość, z jaką postacie znajdujące się dalej od gracza są zaktualizowane (tj. są nadal aktualizowane w synchronizacji z resztą gry, ale nie tak często, więc wystąpi niewielkie opóźnienie, zanim odległe postacie zareagują na zdarzenie, ale jest mało prawdopodobne, aby spowodowało to problem lub nawet nie zostało zauważone przez gracza ). Alternatywnie możesz zastosować podejście hybrydowe,


to RTS. Powinniśmy założyć, że w danym momencie znaczna liczba jednostek znajduje się na ekranie.
Tom

W RTS świat musi trwać, dopóki gracz nie patrzy. Duża aktualizacja zajęłaby tyle samo czasu, ale byłaby w dużej serii po przesunięciu aparatu.
PStag

1

Wyjaśnienie pytania

Stworzyłem prostą grę RTS, która zawiera setki postaci, takich jak Crusader Kings 2 in Unity.

W tej odpowiedzi zakładam, że cała gra powinna być podobna do CK2, a nie tylko mieć dużo postaci. Wszystko, co widzisz na ekranie w CK2, jest łatwe do zrobienia i nie zagrozi twojej wydajności ani nie będzie skomplikowane do wdrożenia w Unity. Dane za tym się komplikują.

CharacterKlasy braku funkcjonalności

Stworzyłem więc klasę C # o nazwie „Charakter”, która zawiera wszystkie dane.

Dobrze, ponieważ postać w twojej grze to tylko dane. To, co widzisz na ekranie, to tylko przedstawienie tych danych. Te Characterklasy stanowią sedno gry i jako takie mogą stać się „ przedmiotami bożymi ”. Odradzałbym więc ekstremalne środki: Usuń całą funkcjonalność z tych klas. Metoda, GetFullName()która łączy imię i nazwisko, OK, ale nie ma kodu, który faktycznie „coś robi”. Umieść ten kod w dedykowanych klasach, które wykonują jedną akcję; np. klasa Birtherz metodą Character CreateCharacter(Character father, Character mother)okaże się znacznie czystsza niż posiadanie tej funkcji w Characterklasie.

Nie przechowuj danych w kodzie

Do ich przechowywania najłatwiejszą opcją jest użycie obiektów skryptowych

Nie. Przechowuj je w formacie JSON, używając JsonUtility Unity. W przypadku tych Characterklas niefunkcjonalności powinno to być trywialne. Będzie to działać zarówno przy początkowej konfiguracji gry, jak i przy przechowywaniu jej w zapisanych grach. Jednak nadal jest to nudne, więc podałem najłatwiejszą opcję w twojej sytuacji. Możesz także użyć XML, YAML lub dowolnego innego formatu, o ile ludzie mogą go odczytać, gdy jest przechowywany w pliku tekstowym. CK2 robi to samo, właściwie większość gier. Jest to również doskonała konfiguracja umożliwiająca modyfikowanie gry, ale jest to przemyślenie na dużo później.

Myśl abstrakcyjnie

Dodałem prostą kontrolę, aby upewnić się, że postać jest „Żywa” podczas przetwarzania [...], ale nie mogę usunąć „Postaci”, jeśli on nie żyje, ponieważ potrzebuję jego informacji podczas tworzenia drzewa genealogicznego.

Ten łatwiej powiedzieć niż zrobić, ponieważ często koliduje z naturalnym sposobem myślenia. Myślicie w „naturalny” sposób, o „charakterze”. Jednak jeśli chodzi o twoją grę, wydaje się, że istnieją co najmniej 2 różne typy danych, które „są postacią”: nazywam to ActingCharacteri FamilyTreeEntry. Umarła postać FamilyTreeEntrynie musi być aktualizowana i prawdopodobnie potrzebuje o wiele mniej danych niż aktywny ActingCharacter.


0

Będę mówić z trochę doświadczenia, od sztywnego projektu OO do projektu Entity-Component-System (ECS).

Jakiś czas temu byłem taki jak ty , miałem wiele różnych rzeczy o podobnych właściwościach i budowałem różne obiekty i próbowałem użyć dziedziczenia, aby je rozwiązać. Bardzo mądra osoba powiedziała mi , że tego nie rób, a zamiast tego użyj Entity-Component-System.

Teraz ECS to wielka koncepcja i ciężko jest ją dobrze zrozumieć. Jest w to dużo pracy, właściwe budowanie jednostek, komponentów i systemów. Jednak zanim to zrobimy, musimy zdefiniować warunki.

  1. Podmiot : to jest rzecz , gracz, zwierzę, NPC, cokolwiek . To coś, co wymaga dołączonych do niego komponentów.
  2. Składnik : w twoim przypadku jest to atrybut lub właściwość , taka jak „Imię” lub „Wiek” lub „Rodzice”.
  3. System : taka jest logika komponentu lub zachowania . Zazwyczaj budujesz jeden system na komponent, ale nie zawsze jest to możliwe. Ponadto czasami systemy muszą wpływać na inne systemy.

Oto, gdzie bym poszedł z tym:

Przede wszystkim utwórz postać IDdla swoich postaci. int, GuidCokolwiek chcesz. To jest „jednostka”.

Po drugie, zacznij myśleć o różnych zachowaniach. Takie rzeczy jak „Drzewo genealogiczne” - takie zachowanie. Zamiast modelować to jako atrybuty encji, zbuduj system, który przechowuje wszystkie te informacje . System może następnie zdecydować, co z tym zrobić.

Podobnie chcemy zbudować system dla „Czy postać żyje czy nie żyje?” Jest to jeden z najważniejszych systemów w twoim projekcie, ponieważ wpływa na wszystkie pozostałe. Niektóre systemy mogą usuwać „martwe” znaki (takie jak system „sprite”), inne mogą wewnętrznie zmieniać układ, aby lepiej obsługiwać nowy status.

Na przykład zbudujesz system „Sprite”, „Rysowanie” lub „Rendering”. Ten system będzie odpowiedzialny za określenie, z jakiego duszka postać ma być wyświetlana i jak ją wyświetlić. Następnie, gdy postać umiera, usuń ją.

Dodatkowo system „AI”, który może powiedzieć postaci, co ma robić, gdzie iść itp. Powinno to oddziaływać z wieloma innymi systemami i podejmować na ich podstawie decyzje. Znów martwe postacie prawdopodobnie można usunąć z tego systemu, ponieważ tak naprawdę już nic nie robią.

Twój system „Imię” i „Drzewo genealogiczne” prawdopodobnie powinny zachować postać (żywą lub martwą) w pamięci. Ten system musi przywołać te informacje, niezależnie od stanu postaci. (Jim jest nadal Jimem, nawet po tym, jak go pochowaliśmy).

Daje to również korzyść ze zmiany, gdy system reaguje wydajniej: system ma swój własny zegar. Niektóre systemy muszą strzelać szybko, niektóre nie. W tym miejscu zaczynamy wchodzić w to, co sprawia, że ​​gra działa sprawnie. Nie musimy ponownie obliczać pogody co milisekundę, prawdopodobnie możemy to robić co około 5 godzin.

Daje to również bardziej kreatywną dźwignię: możesz zbudować system „Pathfinder”, który może obsłużyć obliczenia ścieżki od A do B, i może aktualizować w razie potrzeby, pozwalając systemowi Ruchu powiedzieć „gdzie muszę idź następny?" Możemy teraz w pełni rozdzielić te obawy i skuteczniej je uzasadniać. Ruch nie musi znaleźć ścieżki, musi cię tylko tam zabrać.

Będziesz chciał odsłonić niektóre części systemu na zewnątrz. W twoim Pathfindersystemie prawdopodobnie będziesz chciał Vector2 NextPosition(int entity). W ten sposób możesz przechowywać te elementy w ściśle kontrolowanych tablicach lub listach. Możesz użyć mniejszych structtypów, które pomogą ci utrzymać komponenty w mniejszych, ciągłych blokach pamięci, co może znacznie przyspieszyć aktualizacje systemu . (Zwłaszcza jeśli zewnętrzne wpływy na system są minimalne, teraz trzeba tylko dbać o jego stan wewnętrzny, np Name.)

Ale i nie mogę tego wystarczająco podkreślić, teraz an Entityjest po prostu IDkafelkami, obiektami itp. Jeśli istota nie należy do systemu, system nie będzie tego śledził. Oznacza to, że możemy tworzyć nasze obiekty „Drzewo”, przechowywać je w systemach Spritei Movement(drzewa się nie poruszają, ale mają one komponent „Pozycja”) i trzymać je z dala od innych systemów. Nie potrzebujemy już specjalnej listy dla drzew, ponieważ renderowanie drzewa nie różni się niczym od postaci oprócz papierowego obijania. (Które Spritesystem może kontrolować, lub Paperdollsystem może kontrolować.) Teraz NextPositionmożemy nieco przepisać: Vector2? NextPosition(int entity)i może zwrócić nullpozycję dla podmiotów, na których mu nie zależy. Stosujemy to również do naszych NameSystem.GetName(int entity), zwraca nullza drzewa i skały.


Zakończę to, ale tutaj chodzi o to, aby dać ci trochę informacji na temat ECS i jak naprawdę możesz go wykorzystać, aby uzyskać lepszy projekt swojej gry. Możesz zwiększyć wydajność, oddzielić niezwiązane elementy i zachować porządek. (To również dobrze łączy się z funkcjonalnymi językami / konfiguracjami, takimi jak F # i LINQ, które gorąco polecam sprawdzenie F #, jeśli jeszcze tego nie zrobiłeś, bardzo dobrze łączy się z C #, gdy używasz ich w połączeniu.)


Witam, dziękuję za tak szczegółową odpowiedź. Używam tylko jednego GameObject Managera, który zawiera odniesienia do wszystkich innych postaci w grze.
paul p

Programowanie w Unity obraca się wokół tak zwanych bytów GameObject, które nie robią wiele, ale mają listę Componentklas wykonujących rzeczywistą pracę. Nie było przesunięcie paradygmatu dotyczącego ECSS rondo dziesięć lat temu , jako wprowadzenie kodu aktorską w odrębnych klas systemowych jest czystsze. Unity również niedawno wdrożyło taki system, ale ich GameObjectsystem jest i zawsze był w dużej mierze ECS. OP korzysta już z ECS.
Raphael Schmitz

-1

Gdy robisz to w Unity, najłatwiejszym podejściem jest:

  • utwórz jeden obiekt gry na postać lub typ jednostki
  • zapisz je jako prefabrykaty
  • w razie potrzeby możesz utworzyć instancję prefabrykatu
  • gdy postać zostanie zabita, zniszcz obiekt gry i nie zajmie on już procesora ani pamięci

W kodzie możesz przechowywać odniesienia do obiektów na czymś w rodzaju Listy, aby oszczędzić Ci korzystania z Find () i jego odmian przez cały czas. Wymieniasz cykle procesora na pamięć, ale lista wskaźników jest dość mała, więc nawet przy kilku tysiącach obiektów nie powinno to stanowić większego problemu.

W miarę postępów w grze zauważysz, że posiadanie pojedynczych obiektów w grze daje mnóstwo korzyści, w tym nawigację i sztuczną inteligencję.

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.