Komponenty gry, menedżery gier i właściwości obiektów

15

Staram się omijać projektowanie encji oparte na komponentach.

Moim pierwszym krokiem było stworzenie różnych komponentów, które można by dodać do obiektu. Dla każdego typu komponentu miałem menedżera, który wywoływałby funkcję aktualizacji każdego komponentu, przekazując rzeczy takie jak stan klawiatury itp. Zgodnie z wymaganiami.

Następną rzeczą, którą zrobiłem, było usunięcie obiektu i po prostu posiadanie każdego komponentu o identyfikatorze. Zatem obiekt jest definiowany przez komponenty mające te same identyfikatory.

Teraz myślę, że nie potrzebuję menedżera dla wszystkich moich komponentów, na przykład mam SizeComponent, który ma tylko Sizewłaściwość). W rezultacie SizeComponentnie ma metody aktualizacji, a metoda aktualizacji menedżera nic nie robi.

Moją pierwszą myślą było stworzenie ObjectPropertyklasy, którą komponenty mogłyby sprawdzać, zamiast mieć je jako właściwości komponentów. Obiekt miałby więc liczbę ObjectPropertyi ObjectComponent. Komponenty miałyby logikę aktualizacji, która pyta obiekt o właściwości. Menedżer zarządzałby wywoływaniem metody aktualizacji komponentu.

Wydaje mi się to nadmierną inżynierią, ale nie sądzę, żebym mógł pozbyć się komponentów, ponieważ potrzebuję sposobu, aby menedżerowie wiedzieli, jakie obiekty potrzebują do uruchomienia logiki komponentów (w przeciwnym razie po prostu usunę komponent całkowicie i wrzuć logikę aktualizacji do menedżera).

  1. Jest to (o ObjectProperty, ObjectComponenti ComponentManagerklasach) over-inżynierii?
  2. Jaka byłaby dobra alternatywa?
George Duckett
źródło
1
Masz dobry pomysł, próbując nauczyć się modelu składowego, ale musisz lepiej zrozumieć, co musi zrobić - a jedynym sposobem na to jest [przeważnie] ukończenie gry bez jego użycia. Myślę, że tworzenie a SizeComponentjest przesadą - możesz założyć, że większość obiektów ma rozmiar - to takie rzeczy, jak rendering, sztuczna inteligencja i fizyka, gdzie używany jest model komponentu; Rozmiar zawsze zachowuje się tak samo - dzięki czemu możesz udostępniać ten kod.
Jonathan Dickinson
@JonathanDickinson, @Den: Myślę, że mój problem polega na tym, gdzie mam przechowywać wspólne właściwości. Np. Obiekt jako pozycja, która jest używana przez a RenderingComponenti a PhysicsComponent. Czy zastanawiam się nad tym, gdzie umieścić nieruchomość? Czy powinienem po prostu go wsadzić, a następnie poprosić drugą kwerendę o obiekt dla komponentu, który ma wymaganą właściwość?
George Duckett
Mój poprzedni komentarz i przemyślany za nim proces sprawiły, że mam osobną klasę dla właściwości (lub grupy powiązanych właściwości), które komponenty mogą wyszukiwać.
George Duckett
1
Naprawdę podoba mi się ten pomysł - warto spróbować; ale obiekt opisujący każdą indywidualną właściwość jest naprawdę drogi. Możesz spróbować PhysicalStateInstance(jeden na obiekt) obok GravityPhysicsShared(jeden na grę); kusi mnie jednak, by powiedzieć, że zapuszcza się w sferę euforii architektów, nie buduj się w dziurze (dokładnie tak, jak to zrobiłem z moim pierwszym systemem komponentów). POCAŁUNEK.
Jonathan Dickinson

Odpowiedzi:

6

Prosta odpowiedź na twoje pierwsze pytanie brzmi: tak, jesteś nad inżynierią projektu. „Jak daleko się rozbijam?” pytanie jest bardzo częste, gdy podejmowany jest następny krok i obiekt centralny (zwykle nazywany bytem) jest usuwany.

Kiedy rozkładasz obiekty na tak szczegółowy poziom, że same mają rozmiar, projekt poszedł za daleko. Sama wartość danych nie jest składnikiem. Jest to wbudowany typ danych i często można go nazwać dokładnie tak, jak je nazwałeś, właściwością. Właściwość nie jest składnikiem, ale składnik zawiera właściwości.

Oto kilka przewodnich wskazówek, które staram się stosować podczas opracowywania w systemie komponentów:

  • Nie ma łyżki.
    • Jest to krok, który już zrobiłeś, aby pozbyć się centralnego obiektu. To usuwa całą debatę na temat tego, co wchodzi do obiektu Entity, a co do komponentu, ponieważ teraz wszystko, co masz, to komponenty.
  • Komponenty nie są strukturami
    • Jeśli podzielisz coś na to, co zawiera tylko dane, to nie będzie to już element, tylko struktura danych.
    • Komponent powinien zawierać całą funkcjonalność potrzebną do wykonania bardzo określonego zadania w określony sposób.
    • Interfejs IRenderable zapewnia ogólne rozwiązanie do wizualnego wyświetlania czegokolwiek w grze. CRenderableSprite i CRenderableModel to implementacja komponentu tego interfejsu, która zapewnia specyfikę renderowania odpowiednio w 2D i 3D.
    • IUseable to interfejs do czegoś, z czym gracz może wchodzić w interakcje. CUseableItem byłby komponentem, który albo strzela aktywnym pistoletem, albo pije wybraną miksturę, podczas gdy CUseableTrigger może być miejscem, w którym gracz idzie wskoczyć do wieży lub rzucić dźwignię, aby upuścić most zwodzony.

Ponieważ wytyczne dotyczące komponentów, które nie są strukturami, SizeComponent został zepsuty zbyt daleko. Zawiera tylko dane, a to, co określa rozmiar czegoś, może się różnić. Na przykład w komponencie renderującym może to być skalar 1d lub wektor 2 / 3d. W komponencie fizyki może to być objętość graniczna obiektu. W elemencie ekwipunku może być to, ile miejsca zajmuje na siatce 2D.

Spróbuj narysować dobrą linię między teorią a praktycznością.

Mam nadzieję że to pomoże.

James
źródło
Nie zapominajmy, że na niektórych platformach wywoływanie funkcji z interfejsu jest dłuższe niż wywoływanie funkcji z klasy nadrzędnej (ponieważ twoja odpowiedź zawierała wzmianki o interfejsach i klasach)
ADB
Warto pamiętać, ale starałem się pozostać niezależnym od języka i po prostu używać ich w ogólnych terminach projektowych.
James
„Jeśli podzielisz coś na to, co zawiera tylko dane, nie będzie to już składnikiem, tylko struktura danych”. -- Dlaczego? „Składnik” jest tak ogólnym słowem, że może również oznaczać strukturę danych.
Paul Manta
@PaulManta Tak, to ogólny termin, ale sednem tego pytania i odpowiedzi jest to, gdzie wytyczyć granicę. Moja odpowiedź, jak zacytowałeś, to tylko moja propozycja praktycznej reguły, aby to zrobić. Jak zawsze opowiadam się za tym, aby nie kierować się teorią lub projektami, a to miało na celu pomóc.
James
1
@James To była interesująca dyskusja. :) chat.stackexchange.com/rooms/2175 Moją największą przeszkodą jest to, że twoja implementacja polega na tym, że komponenty wiedzą za dużo o tym, co są zainteresowane innymi komponentami. Chciałbym kontynuować dyskusję w przyszłości.
Paul Manta,
14

Już zaakceptowałeś odpowiedź, ale oto mój cios w CBS. Odkryłem, że Componentklasa ogólna ma pewne ograniczenia, więc wybrałem projekt opisany przez Radical Entertainment na GDC 2009, który zasugerował rozdzielenie komponentów na Attributesi Behaviors. („ Teoria i praktyka architektury komponentu obiektu gry ”, Marcin Chady)

Moje decyzje projektowe wyjaśniam w dwustronicowym dokumencie. Po prostu opublikuję link, ponieważ jest on zbyt długi, aby wkleić go tutaj. Obecnie obejmuje tylko komponenty logiczne (nie dotyczy to również renderowania i fizyki), ale powinien dać ci wyobrażenie o tym, co próbowałem zrobić:

http://www.pdf-archive.com/2012/01/08/entity-component-system/preview/page/1

Oto fragment dokumentu:

Atrybuty i zachowania w skrócie

Attributeszarządzać jedną kategorią danych, a ich logika ma ograniczony zakres. Na przykład Healthmoże upewnić się, że jego bieżąca wartość nigdy nie przekracza wartości maksymalnej, a nawet może powiadomić inne komponenty, gdy wartość bieżąca spadnie poniżej pewnego poziomu krytycznego, ale nie zawiera bardziej złożonej logiki. Attributesnie są zależne od innych Attributeslub Behaviors.

Behaviorskontrolować, jak jednostka reaguje na zdarzenia w grze, podejmować decyzje i zmieniać wartości w Attributesrazie potrzeby. Behaviorszależą niektóre z Attributes, ale nie może bezpośrednio kontaktować się ze sobą - reagują tylko w jaki sposób Attributes’wartości są zmieniane przez innych Behaviorsi do wydarzeń są one wysyłane.


Edycja: A oto schemat relacji, który pokazuje, jak komponenty komunikują się ze sobą:

Schemat komunikacji między atrybutami a zachowaniami

Szczegół implementacji: poziom jednostki EventManagerjest tworzony tylko wtedy, gdy jest używany. EntityKlasa tylko przechowuje wskaźnik na EventManagerktóry jest inicjowany, jeśli tylko niektóre wnioski składowe go.


Edycja: Na inne pytanie udzieliłem podobnej odpowiedzi na to pytanie. Można go znaleźć tutaj, być może dla lepszego wyjaśnienia systemu:
/gamedev//a/23759/6188

Paul Manta
źródło
2

To naprawdę zależy od właściwości, których potrzebujesz i gdzie ich potrzebujesz. Ilość pamięci, którą będziesz miał, oraz moc / typ przetwarzania, którego będziesz używać. Widziałem i próbuję wykonać następujące czynności:

  • Właściwości używane przez wiele komponentów, ale modyfikowane tylko przez jeden są przechowywane w tym komponencie. Kształt jest dobrym przykładem w grze, w której system AI, system fizyki, a także system renderowania potrzebują dostępu do podstawowego kształtu, jest to ciężka właściwość i jeśli to możliwe, powinien pozostać tylko w jednym miejscu.
  • Właściwości takie jak pozycja muszą czasem zostać zduplikowane. Na przykład, jeśli uruchamiasz wiele systemów równolegle, chcesz uniknąć podglądania między systemami i raczej zsynchronizujesz pozycję (skopiuj z komponentu głównego lub zsynchronizuj delty lub w razie potrzeby z przepustką kolizyjną).
  • Właściwości pochodzące z kontroli lub „intencji” AI mogą być przechowywane w dedykowanym systemie, ponieważ można je zastosować do innych systemów bez widoczności z zewnątrz.
  • Proste właściwości mogą się komplikować. Czasami twoja pozycja będzie wymagała dedykowanego systemu, jeśli chcesz udostępnić wiele danych (pozycja, orientacja, delta klatki, całkowity ruch delta prądu, ruch delty dla bieżącej klatki i dla poprzedniej klatki, obrót ...). W takim przypadku będziesz musiał przejść z systemem i uzyskać dostęp do najnowszych danych z dedykowanego komponentu i być może będziesz musiał to zmienić za pomocą akumulatorów (delt).
  • Czasami twoje właściwości mogą być przechowywane w surowej tablicy (podwójne *), a twoje komponenty będą miały po prostu wskaźniki do tablic zawierających różne właściwości. Najbardziej oczywistym przykładem jest potrzeba masywnych obliczeń równoległych (CUDA, OpenCL). Tak więc posiadanie jednego systemu do prawidłowego zarządzania wskaźnikami może się przydać.

Zasady te mają swoje ograniczenia. Oczywiście będziesz musiał przesunąć geometrię do renderera, ale prawdopodobnie nie będziesz chciał jej stamtąd odzyskać. Główna geometria zostanie zapisana w silniku fizyki w przypadku wystąpienia deformacji i zsynchronizowana z rendererem (od czasu do czasu w zależności od odległości obiektów). W pewnym sensie i tak go skopiujesz.

Nie ma doskonałych systemów. A niektórym grom będzie lepiej z prostszym systemem, podczas gdy inne będą wymagały bardziej złożonych synchronizacji między systemami.

Najpierw upewnij się, że wszystkie właściwości są dostępne w prosty sposób z twoich komponentów, abyś mógł zmienić sposób przechowywania właściwości w przejrzysty sposób po rozpoczęciu dostrajania systemów.

Kopiowanie niektórych właściwości nie jest wstydem. Jeśli kilka komponentów musi przechowywać kopię lokalną, czasami bardziej wydajne jest kopiowanie i synchronizowanie niż uzyskiwanie dostępu do „zewnętrznej” wartości ”

Również synchronizacja nie musi się zdarzać w każdej klatce. Niektóre komponenty mogą być synchronizowane rzadziej niż inne. Komponent renderujący jest często dobrym przykładem. Te, które nie wchodzą w interakcje z graczami, mogą być synchronizowane rzadziej, tak jak te, które są daleko. Te odległe i poza polem kamery mogą być synchronizowane jeszcze rzadziej.


Jeśli chodzi o komponent rozmiaru, prawdopodobnie można go powiązać z komponentem pozycji:

  • nie wszystkie byty o rozmiarze mają komponent fizyki, na przykład obszary, więc powiązanie go z fizyką nie leży w twoim najlepszym interesie.
  • rozmiar prawdopodobnie nie będzie miał znaczenia bez pozycji
  • wszystkie obiekty z pozycją będą prawdopodobnie miały rozmiar (który może być używany do skryptów, fizyki, sztucznej inteligencji, renderowania ...).
  • rozmiar prawdopodobnie nie jest aktualizowany w każdym cyklu, ale pozycja może być.

Zamiast rozmiaru można nawet użyć modyfikatora rozmiaru, może się przydać.

Jeśli chodzi o przechowywanie wszystkich właściwości w ogólnym systemie przechowywania nieruchomości ... Nie jestem pewien, czy zmierzasz we właściwym kierunku ... Skoncentruj się na właściwościach, które są kluczowe dla twojej gry, i twórz komponenty, które zawierają jak najwięcej powiązanych właściwości. Dopóki odpowiednio wyodrębnisz dostęp do tych właściwości (na przykład za pomocą modułów pobierających w potrzebnych składnikach), powinieneś być w stanie przenieść je, skopiować i zsynchronizować później, bez naruszania zbyt dużej logiki.

Kojot
źródło
2
BTW +1 lub -1 mnie, ponieważ mój obecny przedstawiciel ma 666 od 9 listopada ... To przerażające.
Coyote
1

Jeśli komponenty można dowolnie dodawać do encji, potrzebujesz sposobu, aby zapytać, czy dany komponent istnieje w encji i uzyskać odniesienie do niego. Możesz iterować listę obiektów pochodzących z ObjectComponent, aż znajdziesz ten, który chcesz, i zwrócisz go. Ale zwróciłbyś obiekt poprawnego typu.

W C ++ lub C # zwykle oznacza to, że masz metodę szablonu na obiekcie, takim jak T GetComponent<T>(). A kiedy już znajdziesz to odniesienie, wiesz dokładnie, jakie dane członka posiada, więc po prostu uzyskaj do nich bezpośredni dostęp.

W czymś takim jak Lua lub Python niekoniecznie masz wyraźny typ tego obiektu i prawdopodobnie nie obchodzi Cię to. Ale znowu, możesz po prostu uzyskać dostęp do zmiennej elementu i obsłużyć każdy wyjątek powstały w wyniku próby uzyskania dostępu do czegoś, czego nie ma.

Zapytanie o właściwości obiektu wyraźnie brzmi jak powielanie pracy, którą język może dla Ciebie wykonać, albo w czasie kompilacji dla języków o typie statycznym, albo w czasie wykonywania w przypadku języków o typie dynamicznym.

Kylotan
źródło
Rozumiem, jak uzyskiwać silnie typowane komponenty od encji (używając generycznych lub podobnych), moje pytanie dotyczy bardziej tego, gdzie powinny iść te właściwości, szczególnie tam, gdzie właściwość jest używana przez wiele komponentów i nie można powiedzieć, że jest to żaden pojedynczy komponent. Zobacz mój trzeci i czwarty komentarz do pytania.
George Duckett,
Po prostu wybierz istniejący, jeśli pasuje, lub dodaj właściwość do nowego komponentu, jeśli nie pasuje. Na przykład Unity ma komponent „Transformacja”, który jest tylko pozycją, a jeśli cokolwiek innego musi zmienić pozycję obiektu, robią to za pośrednictwem tego komponentu.
Kylotan 16.11.11
1

„Myślę, że mój problem polega na tym, gdzie przechowuję wspólne właściwości. Np. Obiekt jako pozycja, która jest używana przez RenderingComponent i PhysicsComponent. Czy zastanawiam się nad tym, gdzie umieścić właściwość? w którymś z nich, a następnie czy inne zapytanie jest obiektem dla komponentu, który ma wymaganą właściwość? "

Chodzi o to, że RenderingComponent używa pozycji, ale zapewnia ją fizyka . Potrzebujesz tylko sposobu, aby powiedzieć każdemu komponentowi użytkownika, którego dostawcy użyć. Najlepiej w sposób agnostyczny, w przeciwnym razie wystąpi zależność.

„... moje pytanie dotyczy bardziej tego, gdzie powinny iść te właściwości, szczególnie tam, gdzie właściwość jest używana przez wiele składników i nie można powiedzieć, że jest to jeden składnik. Zobacz mój trzeci i czwarty komentarz do pytania”.

Nie ma wspólnej reguły. Zależy od konkretnej właściwości (patrz wyżej).

Stwórz grę z brzydką, ale opartą na komponentach architekturą, a następnie zmodyfikuj ją.

Legowisko
źródło
Nie sądzę, że rozumiem, co PhysicsComponentpowinienem wtedy zrobić. Widzę to jako zarządzanie symulowaniem obiektu w środowisku fizycznym, co prowadzi mnie do tego zamieszania: nie wszystkie rzeczy, które trzeba wyrenderować, będą musiały zostać zasymulowane, więc dodawanie, PhysicsComponentgdy dodam , wydaje się błędem, RenderingComponentponieważ zawiera pozycję który RenderingComponentużywa. Z łatwością mogłem zobaczyć, jak kończy się sieć połączonych ze sobą komponentów, co oznacza, że ​​wszystkie / większość trzeba dodać do każdej jednostki.
George Duckett
Właściwie miałem podobną sytuację :). Mam PhysicsBodyComponent i SimpleSpatialComponent. Oba zapewniają pozycję, kąt i rozmiar. Ale pierwszy uczestniczy w symulacji fizyki i ma dodatkowe istotne właściwości, a drugi przechowuje tylko te dane przestrzenne. Jeśli masz własny silnik fizyczny, możesz nawet odziedziczyć pierwszy z drugiego.
Den
„Z łatwością mogłem zobaczyć, jak kończy się sieć połączonych ze sobą komponentów, co oznacza, że ​​wszystkie / większość trzeba dodać do każdej jednostki”. To dlatego, że nie masz rzeczywistego prototypu gry. Mówimy tutaj o kilku podstawowych elementach. Nic dziwnego, że będą wszędzie używane.
Den
1

Jelit jest informacją, że posiadanie ThingProperty, ThingComponentoraz ThingManagerdla każdego Thingrodzaju składnika a trochę przesadą. Myślę, że to prawda.

Ale potrzebujesz sposobu, aby śledzić powiązane komponenty pod względem tego, z których systemów korzystają, do jakiej jednostki należą, itp.

TransformPropertybędzie dość powszechny. Ale kto za to odpowiada, system renderowania? System fizyki? System dźwiękowy? Dlaczego Transformskładnik miałby nawet się aktualizować?

Rozwiązaniem jest usunięcie dowolnego kodu z właściwości poza obiektami pobierającymi, ustawiającymi i inicjującymi. Komponenty to dane, które są używane przez systemy w grze do wykonywania różnych zadań, takich jak renderowanie, AI, odtwarzanie dźwięku, ruch itp.

Przeczytaj o Artemis: http://piemaster.net/2011/07/entity-component-artemis/

Spójrz na jego kod, a zobaczysz, że jest oparty na systemach, które deklarują swoje zależności jako listy ComponentTypes. Piszesz każdą ze swoich Systemklas, a w metodzie konstruktora / init deklarujesz, od jakiego rodzaju systemu zależy.

Podczas konfiguracji poziomów lub czegokolwiek, tworzysz swoje byty i dodajesz do nich komponenty. Następnie każesz temu podmiotowi zgłosić się z powrotem do Artemidy, a Artemis następnie ustala na podstawie składu tego bytu, które systemy byłyby zainteresowane wiedzą o tym bycie.

Następnie, w fazie aktualizacji pętli, masz Systemteraz listę jednostek do aktualizacji. Teraz możesz mieć granulację składników, dzięki czemu można opracować systemy szalone, podmioty zbudować tematyce ModelComponent, TransformComponent, FliesLikeSupermanComponent, i SocketInfoComponent, i zrobić coś dziwnego jak dokonać latający spodek, że muchy między klientami podłączony do gry wieloosobowej. Dobra, może nie to, ale pomysł polega na tym, że utrzymuje to w oddzieleniu i elastycznie.

Artemida nie jest doskonała, a przykłady na stronie są trochę podstawowe, ale separacja kodu i danych jest potężna. Jest to również dobre dla twojej pamięci podręcznej, jeśli zrobisz to dobrze. Artemida prawdopodobnie nie robi tego dobrze na tym froncie, ale dobrze jest uczyć się od.

michael.bartnett
źródło