„Obiekt gry” - i projektowanie oparte na komponentach

25

Pracowałem nad niektórymi projektami hobbystycznymi przez ostatnie 3-4 lata. Po prostu proste gry 2D i 3D. Ale ostatnio zacząłem większy projekt. W ciągu ostatnich kilku miesięcy próbowałem zaprojektować klasę obiektów gry, która może być podstawą wszystkich moich obiektów gry. Po wielu testach typu try & die zwróciłem się do Google'a, który szybko wskazał mi kilka plików PDF i PowerPoint w GDC. A teraz staram się zrozumieć obiekty gier oparte na komponentach.

Rozumiem, że silnik tworzy obiekt gry, a następnie dołącza różne komponenty, które obsługują takie rzeczy jak zdrowie, fizyka, tworzenie sieci i wszystko, co do nich zmuszasz. Ale nie rozumiem, skąd składnik X wie, czy Y zmienił stan obiektu. Na przykład, skąd PhysicsComponent wie, czy gracz żyje, ponieważ zdrowie kontrolowane jest przez HealthComponent ...? I w jaki sposób HealthComponent odgrywa „animację śmierci gracza”?

Miałem wrażenie, że było to coś takiego (w HealthComponent):

if(Health < 0) {
   AnimationComponent.PlayAnimation("played-died-animation")
}

Ale z drugiej strony, skąd HealthComponent wie, że obiekt gry, do którego jest dołączony, ma dołączony AnimationComponent? Jedyne rozwiązanie, które tu widzę, to

  1. Sprawdź, czy AnimationComponent jest dołączony, czy nie (w kodzie komponentu lub po stronie silnika)

  2. Składniki wymagają innych elementów, ale wydaje się, że walczą z całym projektem elementów.

  3. Napisz jak HealthWithAnimationComponent, HealthNoAnimationComponent i tak dalej, co znów wydaje się walczyć z całym pomysłem na komponent.

Hayer
źródło
1
Uwielbiam pytanie. Powinienem był zapytać te same miesiące temu, ale nigdy się do tego nie przyzwyczaiłem. Dodatkowym problemem, z jakim się spotkałem, jest to, że obiekt gry ma wiele wystąpień tego samego komponentu (na przykład wiele animacji). Byłoby wspaniale, gdyby odpowiedzi mogły się z tym wiązać. Skończyło się na używaniu wiadomości do powiadomień, przy czym zmienne są wspólne dla wszystkich składników obiektu gry (więc nie muszą wysyłać wiadomości, aby uzyskać wartość zmiennej).
ADB
1
W zależności od rodzaju gry prawdopodobnie nie będziesz mieć obiektów gry, które mają komponent zdrowia i brak animacji. Wszystkie te obiekty gry prawdopodobnie reprezentują coś w rodzaju Unit. Możesz więc wyrzucić komponent zdrowia i utworzyć UnitComponent, który miałby zdrowie pola i wiedział o wszystkich komponentach, jakie powinna mieć jednostka. Ta szczegółowość komponentów naprawdę nic nie pomaga - bardziej realistyczne jest posiadanie jednego komponentu na domenę (renderowanie, audio, fizyka, logika gry).
Kikaimaru,

Odpowiedzi:

11

We wszystkich twoich przykładach jest okropny problem. Komponent zdrowia musi wiedzieć o każdym typie komponentu, który może wymagać reakcji na śmierć istoty. Dlatego żaden z twoich scenariuszy nie jest odpowiedni. Twoja istota ma składnik zdrowia. Ma komponent animacji. Ani nie polegaj na innych, ani o nich nie wiedz. Komunikują się za pośrednictwem systemu wiadomości.

Kiedy komponent zdrowia wykryje, że jednostka „umarła”, wysyła komunikat „umarłem”. Komponent animacji odpowiada za odpowiedź na ten komunikat, odtwarzając odpowiednią animację.

Składnik kondycji nie wysyła wiadomości bezpośrednio do komponentu animacji. Może transmituje go do każdego elementu w tym bycie, może do całego systemu; być może komponent animacji musi poinformować system przesyłania wiadomości, że jest zainteresowany komunikatami „Umarłem”. Istnieje wiele sposobów wdrożenia systemu przesyłania wiadomości. Bez względu na to, jak go implementujesz, chodzi o to, że komponent kondycji i komponent animacji nigdy nie muszą wiedzieć ani dbać o to, czy ten drugi jest obecny, a dodawanie nowych komponentów nigdy nie będzie wymagało modyfikacji istniejących, aby wysłać im odpowiednie wiadomości.

Blecki
źródło
Okej, to ma sens. Ale kto deklaruje „stany” jak „martwy”, „portal jest zepsuty” itp. Element lub silnik? Ponieważ dodanie stanu „martwego” do rzeczy, która nigdy nie będzie miała dołączonego składnika zdrowia, wydaje mi się trochę marnotrawstwem. Chyba po prostu zanurzę się i zacznę testować kod i zobaczę, co działa.
hayer
Michael i Patrick Hughes mają poprawną odpowiedź powyżej. Komponenty to tylko dane; więc tak naprawdę nie jest to element zdrowia, który wykrywa śmierć istoty i wysyła wiadomość, to element logiki specyficznej dla gry na wyższym poziomie. Jak to abstrakcja zależy od ciebie. Rzeczywisty stan śmierci nigdy nie musi być nigdzie przechowywany. Obiekt jest martwy, jeśli jego kondycja jest mniejsza niż 0, a komponent zdrowia może zawrzeć ten fragment logiki sprawdzania danych bez przerywania „braku zachowania!” ograniczenie, jeśli weźmie się pod uwagę tylko rzeczy, które modyfikują stan komponentu jako zachowanie.
Blecki
Ciekawe, jak poradziłbyś sobie z MovementComponent? Kiedy wykryje wejście, musi zwiększyć prędkość w PositionComponent. Jak wyglądałaby wiadomość?
Porady48
8

Sposób, w jaki Artemis rozwiązuje problem, polega na tym, aby nie przetwarzać elementów w Elementach. Komponenty zawierają tylko potrzebne dane. Systemy odczytują wiele typów komponentów i wykonują wszelkie niezbędne operacje.

Tak więc, w twoim przypadku, możesz mieć RenderSystem, który odczytuje w HealthComponent (i innych) i odtwarza kolejki odpowiednich animacji. Oddzielenie danych od funkcji w ten sposób ułatwia prawidłowe zarządzanie zależnościami.

Michał
źródło
Jest to dobry sposób na rozwiązanie problemu: komponenty reprezentują właściwości, podczas gdy systemy łączą różne właściwości i wykorzystują je do pracy. To ogromne odejście od tradycyjnego myślenia OOP i powoduje u niektórych ból głowy =)
Patrick Hughes
Okej, teraz jestem naprawdę zagubiony. ”Natomiast w ES, jeśli masz 100 jednostek na polu bitwy, każda reprezentowana przez Istotę, to masz zero kopii każdej metody, którą można wywołać na jednostce - ponieważ Jednostki nie zawierają metod. Komponenty nie zawierają metod. Zamiast tego istnieje system zewnętrzny dla każdego aspektu, a system zewnętrzny zawiera wszystkie metody, które można wywołać w dowolnej jednostce, która posiada Komponent, który oznaczy go jako zgodny z tym system." Gdzie są przechowywane dane w GunComponent? Jak rundy itp. Jeśli wszystkie podmioty mają ten sam komponent.
hayer
1
O ile rozumiem, wszystkie jednostki nie współużytkują tego samego komponentu, do każdej encji może być dołączonych N instancji komponentu. System następnie sprawdza grę w poszukiwaniu listy wszystkich podmiotów, do których dołączone są instancje komponentów, o które dbają, a następnie wykonuje na nich wszelkie przetwarzanie
Jake Woods,
To po prostu przesuwa problem. Skąd system wie, jakich komponentów użyć? System może również potrzebować innych systemów (na przykład system StateMachine może chcieć wywołać animację). Jednak rozwiązuje to problem, kto jest właścicielem danych WHO. W rzeczywistości prostszą implementacją byłoby posiadanie słownika w obiekcie gry, a każdy system tworzy tam swoje zmienne.
ADB
Przenosi problem, ale do bardziej znośnego miejsca. Systemy mają podłączone na stałe odpowiednie komponenty. Systemy mogą komunikować się między sobą za pomocą komponentów (StateMachine może ustawić wartość składnika, którą odczytuje animacja, aby wiedzieć, co robić (lub może uruchomić zdarzenie). Podejście słownikowe brzmi jak wzorzec właściwości, który może również działać. Komponenty polegają na tym, że powiązane właściwości są zgrupowane i można je sprawdzić statycznie. Brak dziwnych błędów, ponieważ dodałeś „Obrażenia” w jednym miejscu, ale próbowałeś je odzyskać za pomocą „Obrażeń” w innym.
Michael
6

W twoim kodzie możesz znaleźć sposoby (użyłem ich, być może istnieją inne sposoby), aby wiedzieć, czy obiekt zmienił stan:

  1. Wyślij wiadomość.
  2. Czytaj bezpośrednio dane z komponentu.

1) Sprawdź, czy AnimationComponent jest podłączony, czy nie (wewnątrz kodu komponentu lub po stronie silnika)

W tym celu użyłem: 1. HasComponent funkcji GameObject, lub 2. kiedy dołączasz komponent, możesz sprawdzić zależności w jakiejś funkcji konstruującej, lub 3. Jeśli wiem na pewno, że obiekt ma ten komponent, po prostu go używam.

2) Niech komponenty wymagają innych komponentów, ale wydaje się, że walczy to z całym projektem komponentu.

W niektórych artykułach przeczytałem, że w systemie Ideal komponenty nie zależą od siebie, ale w rzeczywistości tak nie jest.

3) Napisz jak, HealthWithAnimationComponent, HealthNoAnimationComponent i tak dalej, co ponownie wydaje się walczyć z całym pomysłem projektowania komponentów.

Pisanie takich komponentów jest złym pomysłem. W mojej aplikacji stworzyłem komponent Zdrowie najbardziej niezależny. Teraz myślę o pewnym wzorcu obserwatora, który powiadamia subskrybentów o określonym wydarzeniu (np. „Hit”, „heal” itp.). Tak więc AnimationComponent musi sam decydować, kiedy odtwarzać animację.

Ale kiedy przeczytałem artykuł o CBES, zrobiło to na mnie wrażenie, więc jestem bardzo szczęśliwy, kiedy korzystam z CBES i odkrywam nowe możliwości.

Jewhen
źródło
1
Cóż, google.no/... @ slajd 16
Hayer
@ Bobenko, proszę podać link do artykułu o CBES. Też jestem bardzo ciekawy;)
Edward83
1
I lambdor.net/?p=171 @ bottom, taki rodzaj podsumowania mojego pytania Jak można zdefiniować różne funkcje w kategoriach względnie złożonych, niepodzielnych elementów? Jakie są najbardziej podstawowe elementy? W jaki sposób elementy elementarne różnią się od czystych funkcji? W jaki sposób istniejące komponenty mogą automatycznie komunikować się z nowymi wiadomościami z nowych komponentów? Po co ignorować wiadomość, o której składnik nie wie? Co w końcu stało się z modelem wejścia-procesu-wyjścia?
hayer
1
oto dobra odpowiedź na CBES stackoverflow.com/a/3495647/903195 większość artykułów, które badałem, pochodzi z tej odpowiedzi. Zacząłem i zainspirowałem się cowboyprogramming.com/2007/01/05/evolve-your-heirachy, a następnie w Gems 5 (jak pamiętam) był dobry artykuł z przykładami.
Jewhen
Ale co z inną koncepcją programowania reaktywnego funkcjonalnie, dla mnie to pytanie jest wciąż otwarte, ale może być dla ciebie dobrym kierunkiem dla badań.
Jewhen
3

Tak jak mówi Michael, Patrick Hughes i Blecki. Rozwiązaniem pozwalającym uniknąć po prostu przenoszenia problemu jest porzucenie ideologii, która powoduje problem.

To mniej OOD, a bardziej programowanie funkcjonalne. Kiedy zacząłem eksperymentować z projektowaniem opartym na komponentach, zauważyłem ten problem. Poszukałem jeszcze trochę i znalazłem „Functive Reactive Programming” jako rozwiązanie.

Teraz moje komponenty to nic innego jak zbiór zmiennych i pól opisujących jego obecny stan. Następnie mam kilka klas „System”, które aktualizują wszystkie istotne dla nich komponenty. Reaktywną część osiąga się, uruchamiając systemy w ściśle określonej kolejności. Zapewnia to, że niezależnie od tego, jaki System będzie następny w kolejce do przetwarzania i aktualizacji, oraz niezależnie od komponentów i podmiotów, które zamierza czytać i aktualizować, zawsze działa na aktualnych danych.

Jednak w pewnym sensie nadal można twierdzić, że problem znów się przesunął. Bo co, jeśli nie jest łatwo, w której kolejności muszą działać Twoje Systemy? Co się stanie, jeśli istnieją cykliczne relacje i to tylko kwestia czasu, zanim zaczniesz gapić się na bałagan instrukcji if-else i zamiany? To ukryta forma przesyłania wiadomości, nie? Na pierwszy rzut oka myślę, że to niewielkie ryzyko. Zwykle rzeczy są przetwarzane po kolei. Coś w stylu: Wejście gracza -> Pozycje jednostki -> Wykrywanie kolizji -> Logika gry -> Renderowanie -> Zacznij od nowa. W takim przypadku będziesz mieć jeden System dla każdego, zapewnisz każdemu Systemowi metodę update (), a następnie uruchomisz je kolejno w gameloopie.

uhmdown
źródło