Jakie są wady korzystania z DrawableGameComponent dla każdej instancji obiektu gry?

25

Czytałem w wielu miejscach, że DrawableGameComponents należy zapisać na rzeczy takie jak „poziomy” lub jakiegoś rodzaju menedżerów zamiast ich używać, na przykład dla postaci lub kafelków (tak jak tutaj mówi ten facet ). Ale nie rozumiem, dlaczego tak jest. Przeczytałem ten post i miało to dla mnie wiele sensu, ale to mniejszość.

Zwykle nie zwracałbym zbytniej uwagi na takie rzeczy, ale w tym przypadku chciałbym wiedzieć, dlaczego widoczna większość uważa, że ​​nie jest to dobra droga. Może coś mi umknęło.

Kenji Kina
źródło
Jestem ciekawy, dlaczego dwa lata później usunąłeś znak „zaakceptowany” z mojej odpowiedzi ? Zwykle tak naprawdę mnie to nie obchodzi - ale w tym przypadku powoduje to irytujące wprowadzenie w błąd przez moją VeraShackle mojej odpowiedzi, aby dostać się na szczyt :(
Andrew Russell
@AndrewRussell Niedawno przeczytałem ten wątek (z powodu niektórych odpowiedzi i głosów) i pomyślałem, że nie mam uprawnień do wyrażenia opinii, czy Twoja odpowiedź jest poprawna, czy nie. Jednak, jak mówisz, nie sądzę, aby odpowiedź VeraShackle była jak najbardziej pozytywna, dlatego ponownie zaznaczam twoją odpowiedź.
Kenji Kina,
1
Mogę tutaj skondensować swoją odpowiedź w następujący sposób: „ Użycie DrawableGameComponentblokuje cię w jednym zestawie sygnatur metod i określonego zachowanego modelu danych. Zazwyczaj jest to początkowo zły wybór, a blokowanie znacznie utrudnia ewolucję kodu w przyszłości. „Ale powiem ci, co się tutaj dzieje…
Andrew Russell,
1
DrawableGameComponentjest częścią podstawowego API XNA (ponownie: głównie z powodów historycznych). Początkujący programiści używają go, ponieważ jest „tam” i „działa” i nie wiedzą nic lepszego. Widzą moją odpowiedź, mówiąc im, że są „w błędzie ” i - ze względu na naturę ludzkiej psychologii - odrzucają to i wybierają drugą „stronę”. Który okazuje się być argumentem braku odpowiedzi VeraShackle; który akurat nabrał tempa, ponieważ nie wywołałem go natychmiast (zobacz te komentarze). Czuję, że moje ostateczne obalenie tam jest wystarczające.
Andrew Russell,
1
Jeśli ktoś jest zainteresowany zakwestionowaniem zasadności mojej odpowiedzi na podstawie pozytywnych opinii: chociaż prawdą jest, że (GDSE jest w kontakcie z wyżej wymienionymi nowicjuszami), odpowiedź VeraShackle zdołała uzyskać w ostatnich latach o kilka więcej głosów pozytywnych niż moja. że mam tysiące innych pozytywnych opinii w całej sieci, głównie na XNA. Wdrożyłem interfejs API XNA na wielu platformach. Jestem profesjonalnym twórcą gier korzystających z XNA. Rodowód mojej odpowiedzi jest nieskazitelny.
Andrew Russell,

Odpowiedzi:

22

„Elementy gry” były bardzo wczesną częścią projektu XNA 1.0. Miały działać jak kontrolki dla WinForm. Pomysł polegał na tym, że będziesz mógł przeciągać i upuszczać je do swojej gry za pomocą GUI (coś w rodzaju dodawania elementów niewizualnych, takich jak timery itp.) I ustawiać na nich właściwości.

Więc możesz mieć komponenty takie jak aparat lub ... licznik FPS? ... lub ...?

Zobaczysz? Niestety ten pomysł tak naprawdę nie działa w grach z prawdziwego świata. Komponentów, których potrzebujesz do gry, tak naprawdę nie można ponownie używać. Możesz mieć komponent „gracza”, ale będzie on bardzo różny od komponentu „gracza” każdej innej gry.

Wyobrażam sobie, że z tego powodu i z prostego braku czasu GUI zostało wyciągnięte przed XNA 1.0.

Ale interfejs API jest nadal użyteczny ... Oto dlaczego nie powinieneś go używać:

Zasadniczo sprowadza się do tego, co jest najłatwiejsze do pisania, czytania, debugowania i utrzymywania jako twórca gier. Wykonanie czegoś takiego w kodzie ładowania:

player.DrawOrder = 100;
enemy.DrawOrder = 200;

Jest o wiele mniej jasne niż po prostu robienie tego:

virtual void Draw(GameTime gameTime)
{
    player.Draw();
    enemy.Draw();
}

Zwłaszcza, gdy w prawdziwej grze możesz skończyć z czymś takim:

GraphicsDevice.Viewport = cameraOne.Viewport;
player.Draw(cameraOne, spriteBatch);
enemy.Draw(cameraOne, spriteBatch);

GraphicsDevice.Viewport = cameraTwo.Viewport;
player.Draw(cameraTwo, spriteBatch);
enemy.Draw(cameraTwo, spriteBatch);

(Wprawdzie można udostępnić kamerę i spriteBatch przy użyciu podobnej Servicesarchitektury. Ale to również zły pomysł z tego samego powodu.)

Ta architektura - lub jej brak - jest o wiele bardziej lekka i elastyczna!

Mogę łatwo zmienić kolejność rysowania lub dodać wiele rzutni lub dodać cel renderowania, zmieniając tylko kilka linii. Aby to zrobić w innym systemie, muszę pomyśleć o tym, co robią wszystkie te komponenty - rozmieszczone w wielu plikach - iw jakiej kolejności.

To trochę jak różnica między programowaniem deklaratywnym a imperatywnym. Okazuje się, że przy tworzeniu gier zdecydowanie bardziej pożądane jest programowanie imperatywne.

Andrew Russell
źródło
2
Odniosłem wrażenie, że dotyczyły programowania opartego na komponentach , które najwyraźniej jest szeroko stosowane do programowania gier.
BlueRaja - Danny Pflughoeft
3
Klasy komponentów gry XNA nie mają bezpośredniego zastosowania do tego wzorca. Ten wzór polega na komponowaniu obiektów z wielu komponentów. Klasy XNA są dostosowane do jednego komponentu na obiekt. Jeśli idealnie pasują do twojego projektu, to z pewnością skorzystaj z nich. Ale należy nie budować swój projekt wokół nich! Zobacz także moją odpowiedź tutaj - wypatruj, gdzie wspominam DrawableGameComponent.
Andrew Russell
1
Zgadzam się, że architektura GameComponent / Service nie jest aż tak przydatna i brzmi, jakby koncepcje Office lub aplikacji internetowych zostały wtłoczone w środowisko gry. Ale wolałbym użyć tej player.DrawOrder = 100;metody zamiast rozkazującej kolejności losowania. W tej chwili kończę grę Flixel, która rysuje obiekty w kolejności, w której dodajesz je do sceny. System FlxGroup łagodzi niektóre z nich, ale fajnie byłoby mieć wbudowane sortowanie indeksu Z.
michael.bartnett
@ michael.bartnett Zauważ, że Flixel jest API trybu zatrzymanego, podczas gdy XNA (z wyjątkiem, oczywiście, systemu komponentów gry) jest interfejsem API trybu natychmiastowego. Jeśli używasz interfejsu API w trybie zatrzymanym lub implementujesz własny , możliwość zmiany kolejności losowania w czasie rzeczywistym jest zdecydowanie ważna. W rzeczywistości potrzeba zmiany kolejności losowania w locie jest dobrym powodem do zrobienia czegoś w trybie zatrzymanym (przychodzi na myśl system okienkowy). Ale jeśli można napisać coś tak trybie natychmiastowym, jeśli platforma API (jak XNA) jest zbudowany w ten sposób, to IMO powinieneś.
Andrew Russell,
Więcej informacji na gamedev.stackexchange.com/a/112664/56
Kenji Kina,
26

Hmmm. Obawiam się, że mam wyzwanie dla zachowania równowagi tutaj.

W odpowiedzi Andrew wydaje się, że jest 5 ładunków, więc spróbuję poradzić sobie z nimi kolejno:

1) Komponenty to przestarzały i porzucony projekt.

Idea wzorca projektowania komponentów istniała na długo przed XNA - w gruncie rzeczy jest to po prostu decyzja o stworzeniu obiektów, a nie nadrzędny obiekt gry, dom ich własnych zachowań aktualizacji i losowania. W przypadku obiektów w kolekcji komponentów gra wywoła te metody (i kilka innych) automatycznie w odpowiednim czasie - co najważniejsze, sam obiekt gry nie musi „znać” tego kodu. Jeśli chcesz ponownie wykorzystać swoje klasy gier w różnych grach (a tym samym w różnych obiektach gry), to odsprzęganie obiektów jest nadal fundamentalnie dobrym pomysłem. Szybkie spojrzenie na najnowsze (profesjonalnie wyprodukowane) wersje demonstracyjne XNA4.0 z AppHub potwierdza moje wrażenie, że Microsoft nadal (całkiem słusznie, moim zdaniem) mocno stoi za tym.

2) Andrew nie może wymyślić więcej niż 2 klas gier wielokrotnego użytku.

Mogę. W rzeczywistości mogę wymyślić ładunki! Właśnie sprawdziłem moją aktualną bibliotekę współdzieloną, która zawiera ponad 80 komponentów i usług wielokrotnego użytku . Aby dać ci smak, możesz mieć:

  • Game State / Screen Managers
  • ParticleEmitters
  • Prymitywne rysunki
  • Kontrolery AI
  • Kontrolery wejściowe
  • Kontrolery animacji
  • Billboardy
  • Menedżerowie fizyki i kolizji
  • Aparaty fotograficzne

Każdy konkretny pomysł na grę będzie wymagał co najmniej 5-10 rzeczy z tego zestawu - gwarantowane - i mogą być w pełni funkcjonalne w zupełnie nowej grze z jednym wierszem kodu.

3) Kontrolowanie kolejności losowania według kolejności jawnych wywołań losowania jest lepsze niż użycie właściwości DrawOrder komponentu.

W zasadzie zgadzam się z Andrew w tej sprawie. ALE jeśli pozostawisz wszystkie właściwości DrawOrder komponentu jako domyślne, nadal możesz jawnie kontrolować ich kolejność rysowania według kolejności dodawania ich do kolekcji. Jest to w istocie identyczne pod względem „widoczności” z jawnym zamawianiem ręcznych wywołań losowania.

4) Samodzielne wywoływanie wielu metod rysowania obiektów jest bardziej „lekkie” niż wywoływanie ich za pomocą istniejącej metody szkieletowej.

Czemu? Ponadto we wszystkich projektach, z wyjątkiem najbardziej trywialnych, logika aktualizacji i kod Draw całkowicie przesłaniają metody wywoływania pod względem wydajności w każdym przypadku.

5) Podczas debugowania lepiej jest umieścić kod w jednym pliku niż mieć go w pliku pojedynczego obiektu.

Po pierwsze, kiedy debuguję w Visual Studio, lokalizacja punktu przerwania w zakresie plików na dysku jest obecnie prawie niewidoczna - IDE zajmuje się lokalizacją bez żadnych dramatów. Zastanów się przez chwilę, że masz problem z rysowaniem Skybox. Czy przejście do metody rysowania Skybox jest naprawdę bardziej uciążliwe niż zlokalizowanie kodu rysunkowego Skybox w (prawdopodobnie bardzo długiej) głównej metodzie rysowania w obiekcie Game? Nie, niezupełnie - w rzeczywistości twierdziłbym, że jest wręcz przeciwnie.

Podsumowując - rzućcie okiem na argumenty Andrew tutaj. Nie sądzę, że faktycznie dają merytoryczne podstawy do pisania splątanego kodu gry. Wady rozłączonego wzoru komponentu (używane rozsądnie) są ogromne.

VeraShackle
źródło
2
Właśnie zauważyłem ten post i muszę zdecydowanie odrzucić: nie mam problemu z klasami wielokrotnego użytku! (Które mogą mieć metody losowania i aktualizacji itp.). Mam szczególny problem z używaniem ( Drawable) GameComponentdo zapewnienia architektury gry, z powodu utraty wyraźnej kontroli nad kolejnością losowania / aktualizacji (z którą zgadzasz się w punkcie 3) sztywności ich interfejsów (których nie adresujesz).
Andrew Russell
1
W punkcie nr 4 moje użycie słowa „lekka” oznacza wydajność, gdzie właściwie mam na myśli elastyczność architektoniczną. Twoje pozostałe punkty # 1, # 2, a zwłaszcza # 5 wydają się wznosić absurdalnego słomianego, który moim zdaniem powinien zawierać większość / cały kod w twojej głównej klasie! Przeczytaj moją odpowiedź tutaj, aby zapoznać się z kilkoma bardziej szczegółowymi przemyśleniami na temat architektury.
Andrew Russell
5
Wreszcie: jeśli masz zamiar opublikować odpowiedź na moją odpowiedź, mówiąc, że się mylę, uprzejmie byłoby również dodać odpowiedź do mojej odpowiedzi, aby zobaczyć powiadomienie o tym, niż musiałbym się natknąć to dwa miesiące później.
Andrew Russell
Być może źle zrozumiałem, ale jak to odpowiada na pytanie? Czy powinienem używać GameComponent dla moich duszków?
Superbest
Dalsze obalenie
Kenji Kina,
4

Nie zgadzam się trochę z Andrew, ale myślę też, że na wszystko jest czas i miejsce. Nie użyłbym a Drawable(GameComponent)do czegoś tak prostego jak duszek lub wróg. Jednak użyłbym tego do SpriteManagerlub EnemyManager.

Wracając do tego, co Andrew powiedział o kolejności rysowania i aktualizacji, myślę, że Sprite.DrawOrderbyłoby to bardzo mylące dla wielu duszków. Co więcej, jest stosunkowo łatwy do skonfigurowania DrawOrderi UpdateOrderdla niewielkiej (lub rozsądnej) liczby Menedżerów (Drawable)GameComponents.

Myślę, że Microsoft by ich pozbył, gdyby naprawdę byli tak sztywni i nikt ich nie używał. Ale nie zrobili tego, ponieważ działają idealnie dobrze, jeśli rola jest odpowiednia. Sam używam ich do opracowywania, Game.Servicesktóre są aktualizowane w tle.

Amble Lighthertz
źródło
2

Myślałem również o tym i przyszło mi do głowy: na przykład, aby ukraść przykład w twoim linku, w grze RTS możesz uczynić każdą jednostkę instancją komponentu gry. W porządku.

Ale można również utworzyć komponent UnitManager, który przechowuje listę jednostek, rysuje i aktualizuje je w razie potrzeby. Wydaje mi się, że powodem, dla którego chcesz przekształcić swoje jednostki w komponenty gry, jest podział kodu na części, więc czy to rozwiązanie właściwie osiągnęłoby ten cel?

Superbest
źródło
2

W komentarzach zamieściłem skróconą wersję mojej starej odpowiedzi:

Użycie DrawableGameComponentpowoduje zablokowanie jednego zestawu sygnatur metod i określonego zachowanego modelu danych. Zazwyczaj są to początkowo zły wybór, a blokada znacznie utrudnia ewolucję kodu w przyszłości.

W tym miejscu postaram się zilustrować, dlaczego tak jest, zamieszczając kod z mojego obecnego projektu.


Obiekt główny utrzymujący stan świata gry ma następującą Updatemetodę:

void Update(UpdateContext updateContext, MultiInputState currentInput)

Istnieje wiele punktów wejścia do Update, w zależności od tego, czy gra działa w sieci, czy nie. Możliwe jest na przykład przywrócenie stanu gry do poprzedniego punktu w czasie, a następnie ponowna symulacja.

Istnieją również dodatkowe metody podobne do aktualizacji, takie jak:

void PlayerJoin(int playerIndex, string playerName, UpdateContext updateContext)

W stanie gry znajduje się lista aktorów do aktualizacji. Ale to coś więcej niż zwykłe Updatepołączenie. Istnieją dodatkowe przejścia przed i po zajęciach z fizyki. Jest też kilka fantazyjnych rzeczy do odrodzenia odradzania / niszczenia aktorów, a także przejścia poziomów.

Poza stanem gry istnieje interfejs użytkownika, który musi być zarządzany niezależnie. Ale niektóre ekrany w interfejsie użytkownika muszą także działać w kontekście sieci.


Do rysowania, ze względu na charakter gry, istnieje niestandardowy system sortowania i usuwania, który pobiera dane wejściowe z tych metod:

virtual void RegisterToDraw(SortedDrawList sortedDrawList, Definitions definitions)
virtual void RegisterShadow(ShadowCasterList shadowCasterList, Definitions definitions)

A potem, gdy zorientuje się, co narysować, automatycznie wywołuje:

virtual void Draw(DrawContext drawContext, int tag)

Ten tagparametr jest wymagany, ponieważ niektórzy aktorzy mogą trzymać się innych aktorów - i dlatego muszą mieć możliwość rysowania zarówno powyżej, jak i poniżej trzymanego aktora. System sortowania zapewnia, że ​​dzieje się to we właściwej kolejności.

Do rysowania jest także system menu. I hierarchiczny system do rysowania interfejsu użytkownika, wokół interfejsu, wokół gry.


Teraz porównaj te wymagania z tym, co DrawableGameComponentzapewnia:

public class DrawableGameComponent
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
    public int UpdateOrder { get; set; }
    public int DrawOrder { get; set; }
}

Nie ma rozsądnego sposobu na przejście z systemu DrawableGameComponentdo systemu, który opisałem powyżej. (I nie są to nietypowe wymagania dla gry o nawet niewielkiej złożoności).

Należy również zauważyć, że wymagania te nie pojawiły się po prostu na początku projektu. Ewoluowały one przez wiele miesięcy prac rozwojowych.


To, co zazwyczaj robią ludzie, jeśli zaczynają w dół DrawableGameComponenttrasy, to zaczynają budować na hakach:

  • Zamiast dodawać metody, robią brzydkie rzucanie w dół do własnego typu podstawowego (dlaczego po prostu nie użyć tego bezpośrednio?).

  • Zamiast zastępować kod do sortowania i wywoływania Draw/ Update, robią bardzo powolne rzeczy, aby zmieniać numery porządkowe w locie.

  • A co najgorsze: zamiast dodawać parametry do metod, zaczynają „przekazywać” „parametry” w lokalizacjach pamięci, które nie są stosem ( Servicespopularne jest użycie do tego celu). Jest to skomplikowane, podatne na błędy, powolne, delikatne, rzadko bezpieczne dla wątków i może stać się problemem, jeśli dodasz sieć, stworzysz narzędzia zewnętrzne i tak dalej.

    To również sprawia, ponowne wykorzystanie kodu trudniej , bo teraz twoje Zależności są ukryte wewnątrz „składnik”, zamiast opublikowany na granicy API.

  • Lub prawie widzą, jak szalone to wszystko jest i zaczynają robić „menedżerów”, DrawableGameComponentaby obejść te problemy. Tyle że nadal mają te problemy na granicach „menedżera”.

    Niezmiennie tych „menedżerów” można łatwo zastąpić serią wywołań metod w pożądanej kolejności. Wszystko inne jest zbyt skomplikowane.

Oczywiście, po uruchomieniu zatrudniających te hacki, to wtedy trwa prawdziwa praca, aby cofnąć te hacki raz zdajesz sobie sprawę, trzeba więcej niż to, co DrawableGameComponentoferuje. Stajesz się „ zamknięty ”. Pokusa często polega na tym, aby nadal nakładać się na hacki - co w praktyce cię ugrzęźnie i ograniczy rodzaje gier, które możesz stworzyć do stosunkowo uproszczonych.


Wydaje się, że wiele osób osiąga ten punkt i odczuwa trzewną reakcję - być może dlatego, że używa DrawableGameComponenti nie chce zaakceptować, że jest „zły”. Trzymają się więc faktu, że można przynajmniej „ sprawić” , że będzie to możliwe .

Oczywiście można to zrobić do „pracy”! Jesteśmy programistami - sprawianie, by rzeczy działały pod nietypowymi ograniczeniami, jest praktycznie naszą pracą. Ale to ograniczenie nie jest konieczne, użyteczne ani racjonalne ...


Co jest szczególnie flabbergasting o to, że jest zdumiewająco trywialny bocznym kroku w tym cały problem. Tutaj dam ci nawet kod:

public class Actor
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
}

List<Actor> actors = new List<Actor>();

foreach(var actor in actors)
    actor.Update(gameTime);

foreach(var actor in actors)
    actor.Draw(gameTime);

(Łatwo jest dodać sortowanie według DrawOrderi UpdateOrder. Jednym prostym sposobem jest utrzymanie dwóch list i używanie List<T>.Sort(). Jest także banalne w obsłudze Load/ UnloadContent.)

Nie jest ważne, czym tak naprawdę jest ten kod . Ważne jest to, że mogę go trywialnie modyfikować .

Kiedy zdaję sobie sprawę, że potrzebuję innego systemu pomiaru czasu gameTimelub potrzebuję więcej parametrów (lub całego UpdateContextobiektu zawierającego około 15 rzeczy), albo potrzebuję zupełnie innej kolejności operacji do rysowania, albo potrzebuję dodatkowych metod w przypadku różnych przepustek aktualizacji mogę po prostu to zrobić .

I mogę to zrobić natychmiast. Nie muszę się martwić wybraniem DrawableGameComponentkodu. Lub gorzej: zhakuj go w taki sposób, że będzie to jeszcze trudniejsze, gdy pojawi się taka potrzeba.


Jest naprawdę tylko jeden scenariusz, w którym jest to dobry wybór DrawableGameComponent: i to jest, jeśli dosłownie tworzysz i publikujesz oprogramowanie pośredniczące typu X przeciągnij i upuść . Co było jego pierwotnym celem. Co - dodam - nikt nie robi!

(Podobnie, naprawdę warto używać go tylkoServices przy tworzeniu niestandardowych typów treści ContentManager.)

Andrew Russell
źródło
Chociaż zgadzam się z twoimi punktami, nie widzę też nic szczególnie niewłaściwego w używaniu rzeczy, które dziedziczą DrawableGameComponentpo niektórych komponentach, szczególnie takich jak ekrany menu (menu, wiele przycisków, opcji i innych rzeczy) lub nakładki FPS / debugowania wspomniałeś. W takich przypadkach zwykle nie potrzebujesz drobiazgowej kontroli nad kolejnością rysowania i kończysz się mniejszym kodem, który musisz napisać ręcznie (co jest dobre, chyba że musisz napisać ten kod). Ale myślę, że jest to w zasadzie scenariusz „przeciągnij i upuść”, o którym wspominałeś.
Groo,