Miałem pytanie dotyczące architektury gry: Jaki jest najlepszy sposób na komunikowanie się różnych komponentów?
Naprawdę przepraszam, jeśli to pytanie zostało już zadane milion razy, ale nie mogę znaleźć niczego z dokładnie taką informacją, jakiej szukam.
Próbowałem zbudować grę od podstaw (C ++, jeśli ma to znaczenie) i obserwowałem inspirację dla niektórych gier typu open source (Super Maryo Chronicles, OpenTTD i inne). Zauważam, że wiele z tych projektów gier korzysta z globalnych instancji i / lub singletonów w każdym miejscu (np. Do renderowania kolejek, menedżerów encji, menedżerów wideo i tak dalej). Staram się unikać globalnych instancji i singletonów i buduję silnik, który jest tak luźno sprzężony, jak to możliwe, ale napotykam przeszkody, które wynikają z mojego braku doświadczenia w efektywnym projektowaniu. (Częścią motywacji tego projektu jest zajęcie się tym :))
Zbudowałem projekt, w którym mam jeden główny GameCore
obiekt, który ma elementy analogiczne do instancji globalnych, które widzę w innych projektach (tj. Ma menedżera wprowadzania danych, menedżera wideo, GameStage
obiekt, który kontroluje wszystkie jednostki i grę dla dowolnego aktualnie załadowanego etapu itp.). Problem polega na tym, że ponieważ wszystko jest scentralizowane w GameCore
obiekcie, nie mam łatwego sposobu, aby różne komponenty mogły się ze sobą komunikować.
Patrząc na Super Maryo Chronicles, na przykład, ilekroć element gry musi komunikować się z innym składnikiem (tj. Obiekt wroga chce się dodać do kolejki renderowania, która zostanie narysowana na etapie renderowania), po prostu rozmawia z instancja globalna.
Dla mnie muszę sprawić, aby moje obiekty gry przekazywały odpowiednie informacje z powrotem do GameCore
obiektu, aby GameCore
obiekt mógł przekazać te informacje do innego komponentu systemu, który ich potrzebuje (tj .: w powyższej sytuacji każdy obiekt wroga przekażą informacje o renderowaniu z powrotem do GameStage
obiektu, który zebrałby je wszystkie i przekazałby je z powrotem GameCore
, co z kolei przekazałoby je menedżerowi wideo do renderowania). Wydaje mi się, że to naprawdę okropny projekt i starałem się znaleźć rozwiązanie tego problemu. Moje przemyślenia na temat możliwych projektów:
- Instancje globalne (projekt Super Maryo Chronicles, OpenTTD itp.)
- Posiadanie
GameCore
obiektu działa jak pośrednik, przez który komunikują się wszystkie obiekty (obecny projekt opisany powyżej) - Podaj wskaźniki komponentów wszystkim pozostałym komponentom, z którymi będą musiały porozmawiać (tj. W powyższym przykładzie Maryo klasa wroga miałaby wskaźnik do obiektu wideo, z którym musi porozmawiać)
- Podziel grę na podsystemy - na przykład obiekty menedżera w
GameCore
obiekcie obsługujące komunikację między obiektami w ich podsystemie - (Inne opcje? ....)
Wyobrażam sobie, że opcja 4 powyżej jest najlepszym rozwiązaniem, ale mam problem z zaprojektowaniem jej ... być może dlatego, że myślałem o projektach, które widziałem, które wykorzystują globale. Wydaje mi się, że biorę ten sam problem, który istnieje w moim obecnym projekcie i powielam go w każdym podsystemie, tylko na mniejszą skalę. Na przykład GameStage
opisany powyżej obiekt jest w pewnym sensie próbą tego, ale GameCore
obiekt jest nadal zaangażowany w proces.
Czy ktoś może tutaj zaoferować jakieś porady dotyczące projektowania?
Dzięki!
źródło
Odpowiedzi:
Coś, czego używamy w naszych grach do organizowania naszych globalnych danych, to wzorzec projektowy ServiceLocator . Zaletą tego wzorca w porównaniu do wzorca Singleton jest to, że implementacja danych globalnych może ulec zmianie w czasie wykonywania aplikacji. Również obiekty globalne można zmieniać również w czasie wykonywania. Kolejną zaletą jest to, że łatwiej jest zarządzać kolejnością inicjowania obiektów globalnych, co jest bardzo ważne, szczególnie w C ++.
np. (kod C #, który można łatwo przetłumaczyć na C ++ lub Java)
Powiedzmy, że masz interfejs renderowania renderowania, który ma kilka typowych operacji do renderowania rzeczy.
I że masz domyślną implementację renderowania zaplecza
W niektórych projektach wydaje się, że globalny jest dostęp do zaplecza renderowania. We wzorze Singleton oznacza to, że każda implementacja IRenderBackend powinna być implementowana jako unikalna instancja globalna. Ale użycie wzorca ServiceLocator nie wymaga tego.
Oto jak:
Aby uzyskać dostęp do globalnego obiektu, musisz go najpierw zainicjalizować.
Aby pokazać, jak różne implementacje mogą się różnić w czasie wykonywania, powiedzmy, że Twoja gra ma minigrę, w której renderowanie jest izometryczne, a Ty implementujesz IsometricRenderBackend .
Po przejściu z bieżącego stanu do stanu minigry wystarczy zmienić globalny backend renderowania dostarczony przez lokalizator usług.
Kolejną zaletą jest to, że możesz również korzystać z usług zerowych. Na przykład, gdybyśmy mieli usługę ISoundManager , a użytkownik chciałby wyłączyć dźwięk, moglibyśmy po prostu zaimplementować NullSoundManager, który nic nie robi, gdy wywoływane są jego metody, więc ustawiając obiekt usługi ServiceLocator na obiekt NullSoundManager, który możemy osiągnąć ten wynik przy prawie żadnej pracy.
Podsumowując, czasami eliminacja globalnych danych może być niemożliwa, ale nie oznacza to, że nie można ich odpowiednio uporządkować i zorientować obiektowo.
źródło
std::unique_ptr<ISomeService>
.Istnieje wiele sposobów zaprojektowania silnika gry i tak naprawdę wszystko sprowadza się do preferencji.
Aby pozbyć się podstaw, niektórzy programiści wolą zaprojektować go jak piramidę, w której istnieje najwyższa klasa rdzeni, często nazywana klasą jądra, rdzenia lub frameworka, która tworzy, jest właścicielem i inicjuje szereg podsystemów, takich jak jako audio, grafika, sieć, fizyka, sztuczna inteligencja oraz zarządzanie zadaniami, bytem i zasobami. Ogólnie rzecz biorąc, podsystemy te są dla ciebie narażone przez tę klasę frameworka i zwykle przekazujesz tę klasę frameworku swoim klasom jako argument konstruktora, gdzie jest to właściwe.
Uważam, że jesteś na dobrej drodze, myśląc o opcji nr 4.
Pamiętaj, jeśli chodzi o samą komunikację, która nie zawsze musi oznaczać bezpośrednie wywołanie funkcji. Istnieje wiele pośrednich sposobów komunikacji, niezależnie od tego, czy odbywa się to za pomocą jakiejś pośredniej metody przy użyciu,
Signal and Slots
czy przy użyciuMessages
.Czasami w grach ważne jest, aby umożliwić asynchroniczne wykonywanie akcji, aby nasza pętla gry poruszała się tak szybko, jak to możliwe, aby liczba klatek na sekundę była płynna gołym okiem. Gracze nie lubią powolnych i niespokojnych scen, dlatego musimy znaleźć sposoby na utrzymanie płynności dla nich, ale logikę płynącą, ale pod kontrolą i uporządkowaną. Chociaż operacje asynchroniczne mają swoje miejsce, nie są one również odpowiedzią na każdą operację.
Po prostu wiedz, że będziesz mieć połączenie zarówno komunikacji synchronicznej, jak i asynchronicznej. Wybierz to, co jest właściwe, ale wiedz, że będziesz musiał obsługiwać oba style w swoich podsystemach. Projektowanie wsparcia dla obu będzie dobrze służyć w przyszłości.
źródło
Musisz tylko upewnić się, że nie ma żadnych zależności odwrotnych lub cyklicznych. Na przykład, jeśli masz klasę
Core
,Core
która maLevel
, aLevel
lista zawieraEntity
, drzewo zależności powinno wyglądać następująco:Biorąc pod uwagę to początkowe drzewo zależności, nigdy nie powinieneś
Entity
polegać naLevel
lubCore
, iLevel
nigdy nie powinieneś polegaćCore
. Jeżeli jednaLevel
lubEntity
muszą mieć dostęp do danych, które znajduje się wyżej w drzewie zależności, powinien on być przekazany jako parametr przez odniesienie.Rozważ następujący kod (C ++):
Korzystając z tej techniki, możesz zobaczyć, że każdy
Entity
ma dostęp doLevel
iLevel
ma dostęp doCore
. Zauważ, że każdyEntity
przechowuje odniesienie do tej samejLevel
, marnując pamięć. Po zauważeniu tego, powinieneś zapytać, czy każdyEntity
naprawdę potrzebuje dostępu doLevel
.Z mojego doświadczenia wynika, że albo A) Naprawdę oczywiste rozwiązanie pozwala uniknąć odwrotnych zależności, albo B) Nie ma możliwości uniknięcia globalnych instancji i singletonów.
źródło
Więc w zasadzie chcesz uniknąć globalnego stanu zmiennego ? Możesz ustawić go jako lokalny, niezmienny lub wcale nie być stanem. Później jest najbardziej wydajny i elastyczny, imo. Jest to znane jako ukrywanie suplementacji.
źródło
Wydaje się, że w rzeczywistości chodzi o to, jak zredukować sprzężenie bez poświęcania wydajności. Wszystkie globalne obiekty (usługi) zwykle tworzą rodzaj kontekstu, który można modyfikować w czasie wykonywania gry. W tym sensie wzorzec lokalizatora usług rozprasza różne części kontekstu na różne części aplikacji, które mogą być lub nie być tym, czego chcesz. Innym podejściem w świecie rzeczywistym byłoby zadeklarowanie takiej struktury:
I przekażcie go jako nie posiadający surowy wskaźnik
sEnvironment*
. Wskaźniki wskazują tutaj interfejsy, więc sprzężenie jest zredukowane w podobny sposób, jak w przypadku lokalizatora usług. Jednak wszystkie usługi są w jednym miejscu (co może, ale nie musi być dobre). To tylko inne podejście.źródło