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.
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 jednaGameObject
, z liczbą „3000”.Odpowiedzi:
Należy wziąć pod uwagę trzy rzeczy:
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.
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.
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ść.
źródło
W tej sytuacji sugeruję użycie kompozycji :
W tym przypadku wygląda na to, że twoja
Character
klasa 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
Character
na podklasy, którychCharacter
właścicielem jest instancja. Na przykład:CharacterInfo
może być prostą strukturą danych z imieniem, datą urodzenia, datą śmierci i frakcją,Equipment
moż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.CharacterAI
lubCharacterController
moż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
AliveCharacterObject
to maCharacterController
,CharacterEquipment
iCharacterInfo
skrypty w załączeniu. Aby „zabić” postać, wystarczy usunąć części, które nie są już istotne (takie jakCharacterController
) - nie marnuje pamięci ani czasu przetwarzania.Zwróć uwagę, że
CharacterInfo
to 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.
źródło
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.
źródło
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ążą.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,
źródło
Wyjaśnienie pytania
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ą.
Character
Klasy braku funkcjonalnościDobrze, ponieważ postać w twojej grze to tylko dane. To, co widzisz na ekranie, to tylko przedstawienie tych danych. Te
Character
klasy 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. klasaBirther
z metodąCharacter CreateCharacter(Character father, Character mother)
okaże się znacznie czystsza niż posiadanie tej funkcji wCharacter
klasie.Nie przechowuj danych w kodzie
Nie. Przechowuj je w formacie JSON, używając JsonUtility Unity. W przypadku tych
Character
klas 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
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
ActingCharacter
iFamilyTreeEntry
. Umarła postaćFamilyTreeEntry
nie musi być aktualizowana i prawdopodobnie potrzebuje o wiele mniej danych niż aktywnyActingCharacter
.źródło
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.
Oto, gdzie bym poszedł z tym:
Przede wszystkim utwórz postać
ID
dla swoich postaci.int
,Guid
Cokolwiek 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
Pathfinder
systemie 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ć mniejszychstruct
typó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, npName
.)Ale i nie mogę tego wystarczająco podkreślić, teraz an
Entity
jest po prostuID
kafelkami, 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 systemachSprite
iMovement
(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óreSprite
system może kontrolować, lubPaperdoll
system może kontrolować.) TerazNextPosition
możemy nieco przepisać:Vector2? NextPosition(int entity)
i może zwrócićnull
pozycję dla podmiotów, na których mu nie zależy. Stosujemy to również do naszychNameSystem.GetName(int entity)
, zwracanull
za 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.)
źródło
GameObject
, które nie robią wiele, ale mają listęComponent
klas 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 ichGameObject
system jest i zawsze był w dużej mierze ECS. OP korzysta już z ECS.Gdy robisz to w Unity, najłatwiejszym podejściem jest:
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ę.
źródło