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.
xna
game-component
Kenji Kina
źródło
źródło
DrawableGameComponent
blokuje 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…DrawableGameComponent
jest 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.Odpowiedzi:
„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:
Jest o wiele mniej jasne niż po prostu robienie tego:
Zwłaszcza, gdy w prawdziwej grze możesz skończyć z czymś takim:
(Wprawdzie można udostępnić kamerę i spriteBatch przy użyciu podobnej
Services
architektury. 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.
źródło
DrawableGameComponent
.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.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ć:
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.
źródło
Drawable
)GameComponent
do 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).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 doSpriteManager
lubEnemyManager
.Wracając do tego, co Andrew powiedział o kolejności rysowania i aktualizacji, myślę, że
Sprite.DrawOrder
byłoby to bardzo mylące dla wielu duszków. Co więcej, jest stosunkowo łatwy do skonfigurowaniaDrawOrder
iUpdateOrder
dla 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.Services
które są aktualizowane w tle.źródło
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?
źródło
W komentarzach zamieściłem skróconą wersję mojej starej odpowiedzi:
Użycie
DrawableGameComponent
powoduje 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ą
Update
metodę: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:
W stanie gry znajduje się lista aktorów do aktualizacji. Ale to coś więcej niż zwykłe
Update
połą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:
A potem, gdy zorientuje się, co narysować, automatycznie wywołuje:
Ten
tag
parametr 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
DrawableGameComponent
zapewnia:Nie ma rozsądnego sposobu na przejście z systemu
DrawableGameComponent
do 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ół
DrawableGameComponent
trasy, 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 (
Services
popularne 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”,
DrawableGameComponent
aby 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
DrawableGameComponent
oferuje. 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
DrawableGameComponent
i 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:
(Łatwo jest dodać sortowanie według
DrawOrder
iUpdateOrder
. Jednym prostym sposobem jest utrzymanie dwóch list i używanieList<T>.Sort()
. Jest także banalne w obsłudzeLoad
/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
gameTime
lub potrzebuję więcej parametrów (lub całegoUpdateContext
obiektu 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
DrawableGameComponent
kodu. 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 tylko
Services
przy tworzeniu niestandardowych typów treściContentManager
.)źródło
DrawableGameComponent
po 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ś.