2010-07-05 8 views
19

Dla tworzonej gry 2D (dla systemu Android) Używam systemu opartego na komponentach, w którym GameObject zawiera kilka obiektów GameComponent. GameComponents mogą być takie, jak komponenty wejściowe, komponenty renderowania, komponenty wypunktowania i tak dalej. Obecnie GameComponents ma odniesienie do obiektu, który je posiada i może go modyfikować, ale GameObject ma po prostu listę komponentów i nie obchodzi go, czym są komponenty, o ile można je aktualizować, gdy obiekt jest aktualizowany.Komunikacja w opartym na komponentach silniku gry

Czasami składnik ma pewne informacje, które GameObject musi znać. Na przykład, do wykrywania kolizji GameObject rejestruje się z podsystemem wykrywania kolizji, który ma być powiadamiany, gdy zderzy się z innym obiektem. Podsystem wykrywania kolizji musi znać ramkę ograniczającą obiektu. Przechowuję X i Y bezpośrednio w obiekcie (ponieważ jest używany przez kilka komponentów), ale szerokość i wysokość są znane tylko komponentowi renderowania, który przechowuje bitmapę obiektu. Chciałbym mieć metodę getBoundingBox lub getWidth w GameObject, która pobiera te informacje. Lub ogólnie, chcę wysłać pewne informacje z komponentu do obiektu. Jednak w moim obecnym projekcie GameObject nie wie, jakie konkretne elementy ma na liście.

mogę myśleć kilka sposobów na rozwiązanie tego problemu:

  1. Zamiast całkowicie rodzajowy listę elementów, mogę pozwolić GameObject mają określonego pola dla niektórych ważnych elementów. Na przykład może mieć zmienną składową o nazwie renderingComponent; ilekroć potrzebuję uzyskać szerokość obiektu, używam tylko renderingComponent.getWidth(). To rozwiązanie wciąż pozwala na generowanie listy komponentów, ale traktuje niektóre z nich w inny sposób i obawiam się, że otrzymam kilka wyjątkowych pól, ponieważ trzeba będzie zapytać o więcej składników. Niektóre obiekty nie mają nawet składników renderujących.

  2. Mieć wymagane informacje jako członkowie GameObject, ale pozwolić komponentom na jego aktualizację. Zatem obiekt ma szerokość i wysokość, które domyślnie są równe 0 lub -1, ale komponent renderujący może ustawić je na prawidłowe wartości w swojej pętli aktualizacji. To wydaje się być hackerem i mogę w końcu pchnąć wiele rzeczy do klasy GameObject dla wygody, nawet jeśli nie wszystkie obiekty ich potrzebują.

  3. Mieć komponenty zaimplementować interfejs, który wskazuje, jakiego rodzaju informacji mogą one dotyczyć. Na przykład komponent renderujący implementowałby interfejs HasSize, który zawiera metody takie jak getWidth i getHeight. Gdy GameObject potrzebuje szerokości, wykonuje pętle nad swoimi komponentami, sprawdzając, czy implementują interfejs HasSize (używając słowa kluczowego instanceof w Javie lub is w języku C#). Wydaje się, że jest to bardziej ogólne rozwiązanie, jedną wadą jest to, że wyszukiwanie komponentu może zająć trochę czasu (ale wtedy większość obiektów ma tylko 3 lub 4 komponenty).

To pytanie nie dotyczy konkretnego problemu. Często pojawia się w moim projekcie i zastanawiałem się, jaki jest najlepszy sposób, aby sobie z tym poradzić. Wydajność jest nieco ważna, ponieważ jest to gra, ale liczba komponentów na obiekt jest z reguły mała (maksymalna to 8).

Wersja krótka

W systemie opartym na składniku do gry, co jest najlepszym sposobem przekazywania informacji z komponentów do obiektu przy zachowaniu konstrukcji generycznych?

Odpowiedz

15

Otrzymujemy wariacje na temat tego pytania trzy lub cztery razy w tygodniu na GameDev.net (gdzie obiekt gry jest zazwyczaj nazywany "podmiotem") i jak dotąd nie ma zgody co do najlepszego podejścia. Udowodniono, że kilka różnych podejść jest wykonalnych, więc nie przejmowałbym się tym zbytnio.

Jednak zazwyczaj problemy dotyczą komunikacji między komponentami. Rzadko ludzie martwią się o pozyskanie informacji z komponentu do jednostki - jeśli jednostka wie, jakich informacji potrzebuje, to prawdopodobnie dokładnie wie, do jakiego typu komponentu musi mieć dostęp i do której właściwości lub metody musi wywołać ten komponent, aby uzyskać dane. jeśli potrzebujesz być reaktywny, a nie aktywny, rejestruj wywołania zwrotne lub skonfiguruj wzorzec obserwatora ze składnikami, aby jednostka wiedziała, kiedy coś w komponencie się zmieniło, i odczytaj wartość w tym punkcie.

Całkowicie generyczne komponenty są w dużej mierze bezużyteczne: muszą zapewniać pewien znany interfejs, w przeciwnym razie istnieje niewielki problem. W przeciwnym razie możesz równie dobrze mieć dużą tablicę asocjacyjną z wartościami bez typów i zrobić z nią. W językach Java, Python, C# i innych językach o nieco wyższym poziomie niż C++ można użyć odbicia, aby uzyskać bardziej ogólny sposób korzystania z określonych podklas bez konieczności kodowania informacji o typie i interfejsie w samych komponentach.

Co do komunikacji:

Niektórzy ludzie robią założenia, że ​​jednostka będzie zawsze zawierać znany zestaw typów komponentów (gdzie każdy przypadek jest jednym z kilku możliwych podklasy), a zatem można po prostu chwycić bezpośrednie odniesienie do inny komponent i odczyt/zapis poprzez swój publiczny interfejs.

Niektóre osoby używają funkcji publikowania/subskrybowania, sygnałów/gniazd itp. Do tworzenia dowolnych połączeń między komponentami. Wydaje się to nieco bardziej elastyczne, ale ostatecznie nadal potrzebujesz czegoś ze znajomością tych niejawnych zależności. (A jeśli jest to znane podczas kompilacji, dlaczego nie skorzystać z poprzedniego podejścia?)

Można też umieścić wszystkie udostępnione dane w samym obiekcie i użyć go jako wspólnego obszaru komunikacyjnego (słabo powiązanego z blackboard system w AI), z którego każdy komponent może odczytać i zapisać. Zwykle wymaga to pewności w obliczu pewnych właściwości, które nie występują, gdy się tego spodziewano. Nie nadaje się też do paralelizmu, choć wątpię, by był to ogromny problem dla małego systemu wbudowanego ...?

Wreszcie niektóre osoby mają systemy, w których podmiot w ogóle nie istnieje. Komponenty działają w ramach ich podsystemów i jedynym pojęciem podmiotu jest wartość identyfikatora w niektórych komponentach - jeśli komponent Rendering (w systemie renderowania) i komponent Gracza (w systemie Gracze) mają ten sam identyfikator, można założyć pierwszy obsługuje rysunek tego ostatniego. Ale nie ma żadnego pojedynczego obiektu, który agregowałby którykolwiek z tych składników.

+0

W pewnym sensie oczekiwałem, że to będzie odpowiedź; że istnieje wiele podejść i wszystkie są wykonalne w różnych kontekstach. Masz rację co do całkowicie ogólnych systemów, byłem zbyt zaawansowany w inżynierii i martwiłem się o problemy, które nie istnieją. –

4

Użyj "event bus". (pamiętaj, że prawdopodobnie nie możesz użyć tego kodu, ale jest to podstawowy pomysł).

Zasadniczo utwórz centralny zasób, w którym każdy obiekt może zarejestrować się jako słuchający i powiedzieć "Jeśli X się wydarzy, chcę to wiedzieć". Kiedy coś dzieje się w grze, odpowiedzialny obiekt może po prostu wysłać wydarzenie X do autobusu zdarzeń, a wszystkie interesujące strony zauważą.

[EDYCJA] Aby uzyskać bardziej szczegółową dyskusję, zobacz message passing (dzięki snk_kid za wskazanie tego).

+1

Brzmi jak architektura przekazywania komunikatów, jak to zwykle się nazywa. –

+0

Jest to prawdopodobnie najbardziej ogólne podejście. Trochę przewyższone o małą grę, jak moja, ale warte rozważenia jako ogólne rozwiązanie. –

+0

Cóż, to sprawia, że ​​projekt jest bardzo prosty, ponieważ jest tak ogólny i łatwy do wdrożenia (tylko <50 linii kodu w Javie, 20 w Pythonie). Gdy gra działa, nadal możesz zoptymalizować niektóre bity, ale szybko działa. –

3

Jednym ze sposobów jest zainicjowanie kontenera komponentów. Każdy komponent może świadczyć usługę i może również wymagać usług z innych komponentów. W zależności od twojego języka programowania i środowiska musisz wymyślić metodę dostarczania tych informacji.

W najprostszej formie masz połączenia jeden-do-jednego między komponentami, ale będziesz także potrzebować połączeń jeden-do-wielu. Na przykład. CollectionDetector będzie zawierał listę komponentów implementujących IBoundingBox.

Podczas inicjalizacji pojemnik połączy połączenia między komponentami, a podczas pracy nie będzie żadnych dodatkowych kosztów.

Jest to rozwiązanie zbliżone do ciebie 3), oczekuj, że połączenia między komponentami będą podłączone tylko raz i nie będą sprawdzane przy każdej iteracji pętli.

Model Managed Extensibility Framework for .NET jest dobrym rozwiązaniem tego problemu. Zdaję sobie sprawę, że zamierzasz rozwijać się na Androida, ale możesz czerpać inspirację z tego systemu.

+0

To jest dobry pomysł. Jest to bardzo ogólne podejście, a połączenia okablowania między komponentami podczas inicjalizacji sprawiają, że jest on wydajny.Wygląda na to, że jest trochę zbyt zaangażowany w małą grę, ale rozważyłbym to, gdyby mój projekt był zbyt skomplikowany lub dla większych projektów. Dzięki. –

10

Jak powiedzieli inni, nie ma tu zawsze właściwej odpowiedzi. Różne gry będą pasować do różnych rozwiązań. Jeśli budujesz dużą złożoną grę z wieloma różnymi rodzajami jednostek, bardziej rozdzielona ogólna architektura z abstrakcyjnym komunikatem między komponentami może być warta wysiłku na rzecz łatwej obsługi. W przypadku prostszej gry z podobnymi elementami może się wydawać, że wystarczy wrzucić cały ten stan do GameObject.

dla konkretnego scenariusza, gdzie trzeba przechowywać obwiednię gdzieś i tylko składnik kolizji dba o to, bym:

  1. przechowywać go w samej składowej kolizji.
  2. Kod wykrywania kolizji działa bezpośrednio z komponentami.

Zamiast tego, aby mechanizm kolizyjny przechodził przez kolekcję GameObjects w celu rozwiązania interakcji, niech iteruje bezpośrednio przez kolekcję CollisionComponents. Gdy dojdzie do kolizji, do składnika będzie należeć dopchnięcie go do nadrzędnego GameObject.

To daje kilka korzyści:

  1. Liście stan kolizji specyficzne z GameObject.
  2. Uwalnia od iteracji nad obiektami GameObject, które nie zawierają elementów kolizji. (Jeśli masz dużo nieinteraktywnych obiektów, takich jak efekty wizualne i dekoracja, możesz zaoszczędzić przyzwoitą liczbę cykli.)
  3. Uwalnia od cykli nagrywania przechodzenia między obiektem a jego składnikiem. Jeśli wykonujesz iteracje po obiektach, to na każdym z nich wykonaj getCollisionComponent(), a poniższe wskazówki mogą spowodować brak pamięci podręcznej. Wykonanie tego dla każdej klatki dla każdego obiektu może wypalić dużo procesora.

Jeśli jesteś zainteresowany, mam więcej na ten wzór here, chociaż wygląda na to, że rozumiesz już większość tego, co jest w tym rozdziale.

+0

Bardzo podoba mi się ten pomysł i zamierzam go użyć do tego konkretnego scenariusza. Znam twoją książkę/stronę i faktycznie moja implementacja jest oparta na tym rozdziale (dlatego nazywam to GameObject, a nie Entity na przykład). Wiem, że mówisz o komunikowaniu się z komponentami, ale zastanawiałem się, jak generyczny powinien być ten podmiot, komunikując się z jego elementami. Dzięki za wspaniały zasób (nadszedł czas, aby dodać kolejny rozdział!). –

+0

Tak, wiem! Książka jest niestety w zawieszeniu na jakiś czas. Niedawno opuściłem branżę gier i przeniosłem się na cały kraj, więc mam inne rzeczy na głowie. Mam nadzieję, że niedługo wrócę do tego. – munificent

+1

Przez kilka lat zastanawiałem się nad strukturą podmiotu/komponentu, od czasu do czasu wracając, aby spróbować ponownie z innym projektem, gdy dostanę wolny czas. Gdybym tylko przeczytał ten rozdział w tamtym czasie, a nie bardziej techniczne opisy, mogłem być znacznie lepiej :). Doskonała lektura. Wygląda na to, że twoja odpowiedź jest poprawna: wypychaj naprawdę popularne rzeczy (np. Pozycję), kilka komponentów, które są już sprzężone behawioralnie (np. Fizyka i kolizja), i odpal niektóre wiadomości dla prostych wyzwalaczy między komponentami. Teraz jestem pompowany, by dać mu kolejny strzał! – orlade

Powiązane problemy