Projektowanie gry opartej na komponentach

16

Piszę strzelankę (jak 1942, klasyczna grafika 2D) i chciałbym użyć aplikacji opartej na komponentach. Do tej pory myślałem o następującym projekcie:

  1. Każdy element gry (sterowiec, pocisk, wzmocnienie, wróg) jest bytem

  2. Każda jednostka to zestaw komponentów, które można dodawać lub usuwać w czasie wykonywania. Przykłady to Pozycja, Sprite, Zdrowie, IA, Obrażenia, BoundingBox itp.

Chodzi o to, że Sterowiec, Pocisk, Wróg, Wzmocnienie NIE są klasami gier. Jednostka jest definiowana tylko przez składniki, które posiada (i które mogą się zmieniać w czasie). Tak więc sterowiec gracza zaczyna się od komponentów Sprite, Position, Health i Input. Powerup ma Sprite, Position, BoundingBox. I tak dalej.

Główna pętla zarządza „fizyką” gry, tj. Wzajemnym oddziaływaniem komponentów:

foreach(entity (let it be entity1) with a Damage component)
    foreach(entity (let it be entity2) with a Health component)
    if(the entity1.BoundingBox collides with entity2.BoundingBox)
    {
        entity2.Health.decrease(entity1.Damage.amount());
    }

foreach(entity with a IA component)
    entity.IA.update(); 

foreach(entity with a Sprite component)
    draw(entity.Sprite.surface()); 

...

Komponenty są zakodowane na stałe w głównej aplikacji C ++. Jednostki można zdefiniować w pliku XML (część IA w pliku lua lub python).

Główna pętla nie dba zbytnio o byty: zarządza tylko komponentami. Projekt oprogramowania powinien umożliwiać:

  1. Biorąc pod uwagę komponent, pobierz encję, do której on należy

  2. Biorąc pod uwagę jednostkę, pobierz komponent typu „typ”

  3. Zrób dla wszystkich bytów

  4. Zrób coś dla wszystkich elementów encji (np .: serializuj)

Myślałem o następujących kwestiach:

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component {...};
class Position : public Component {...};
class IA : public Component {... virtual void update() = 0; };

// I don't remember exactly the boost::fusion map syntax right now, sorry.
class Entity
{
   int id; // entity id
   boost::fusion::map< pair<Sprite, Sprite*>, pair<Position, Position*> > components;
   template <class C> bool has_component() { return components.at<C>() != 0; }
   template <class C> C* get_component() { return components.at<C>(); }
   template <class C> void add_component(C* c) { components.at<C>() = c; }
   template <class C> void remove_component(C* c) { components.at<C>() = 0; }
   void serialize(filestream, op) { /* Serialize all componets*/ }
...
};

std::list<Entity*> entity_list;

Dzięki temu projektowi mogę uzyskać # 1, # 2, # 3 (dzięki algorytmom boost :: fusion :: map) i # 4. Także wszystko jest O (1) (ok, niezupełnie, ale wciąż jest bardzo szybkie).

Istnieje również bardziej „powszechna” metoda:

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component { static const int type_id = 0; };
class Position : public Component { static const int type_id = 1; };

class Entity
{
   int id; // entity id
   std::vector<Component*> components;
   bool has_component() { return components[i] != 0; }
   template <class C> C* get_component() { return dynamic_cast<C> components[C::id](); } // It's actually quite safe
...
};

Innym podejściem jest pozbycie się klasy Entity: każdy typ komponentu żyje na własnej liście. Istnieje więc lista duszków, lista zdrowia, lista obrażeń itp. Wiem, że należą do tej samej jednostki logicznej z powodu identyfikatora jednostki. Jest to prostsze, ale wolniejsze: komponenty IA potrzebują dostępu w zasadzie do wszystkich innych komponentów encji, a to wymagałoby przeszukiwania listy komponentów na każdym etapie.

Który według Ciebie jest lepszy? czy mapa boost :: fusion nadaje się do takiego zastosowania?

Emiliano
źródło
2
dlaczego głosowanie negatywne? Co jest nie tak z tym pytaniem?
Emiliano

Odpowiedzi:

6

Przekonałem się, że projektowanie oparte na komponentach i projektowanie zorientowane na dane idą w parze. Mówisz, że posiadanie jednorodnych list komponentów i eliminowanie obiektu encji pierwszej klasy (zamiast wybierania identyfikatora encji na samych komponentach) będzie „wolniejsze”, ale to nie jest ani tu, ani tam, ponieważ tak naprawdę nie profilowałeś żadnego prawdziwego kodu, który wdraża oba podejścia, aby dojść do tego wniosku. W rzeczywistości mogę prawie zagwarantować, że ujednolicenie komponentów i uniknięcie tradycyjnej silnej wirtualizacji będzie szybsze ze względu na różne zalety projektowania zorientowanego na dane - łatwiejszą równoległość, wykorzystanie pamięci podręcznej, modułowość itp.

Nie twierdzę, że takie podejście jest idealne do wszystkiego, ale systemy składowe, które są w zasadzie kolekcjami danych, które wymagają tych samych przekształceń przeprowadzanych na każdej ramce, po prostu krzyczą, aby były zorientowane na dane. Będą chwile, kiedy komponenty będą musiały komunikować się z innymi komponentami różnych typów, ale i tak będzie to konieczne zło. Nie powinno to jednak napędzać projektu, ponieważ istnieją sposoby rozwiązania tego problemu, nawet w skrajnym przypadku, gdy wszystkie komponenty są przetwarzane równolegle, takie jak kolejki komunikatów i futures .

Zdecydowanie Google do projektowania zorientowanego na dane, ponieważ odnosi się do systemów opartych na komponentach, ponieważ ten temat pojawia się dużo i jest sporo dyskusji i niepotwierdzonych danych.

Skyler York
źródło
co rozumiesz przez „zorientowany na dane”?
Emiliano
W Google jest wiele informacji, ale pojawił się przyzwoity artykuł, który powinien zapewnić ogólny przegląd, a następnie dyskusja na temat systemów składowych: gamesfromwithin.com/data-oriented-design , gamedev. net / topic /…
Skyler York
nie mogę się zgodzić z tym wszystkim, co jest związane z DOD, ponieważ myślę, że sam nie może być kompletny, to znaczy, że tylko DOD może zasugerować bardzo dobrą aprroch do przechowywania danych, ale do wywoływania funkcji i procedur, których musisz użyć albo proceduralne, albo OOP, mam na myśli, że problem polega na tym, jak połączyć te dwie metody, aby uzyskać jak największe korzyści zarówno pod względem wydajności, jak i łatwości kodowania, np. sugeruję, że w strukturze wystąpi problem z wydajnością, gdy wszystkie byty nie będą dzielić niektórych składników, ale można to łatwo rozwiązać za pomocą DOD, wystarczy stworzyć różne tablice dla różnych typów podmiotów.
Ali1S232
To nie odpowiada bezpośrednio na moje pytanie, ale jest bardzo pouczające. Pamiętam coś o przepływach danych w czasach uniwersyteckich. To najlepsza jak dotąd odpowiedź i „wygrywa”.
Emiliano
-1

gdybym miał napisać taki kod, wolałbym raczej używać tej approch (i nie używam żadnego dopalacza, jeśli jest to dla ciebie ważne), ponieważ może zrobić wszystko, co chcesz, ale problem polega na tym, że jest zbyt wiele entetów które nie dzielą jakiegoś komponentu, znalezienie tych, które go mają, zajmie trochę czasu. poza tym nie ma innego problemu, z którym mogę:

// declare components here------------------------------
class component
{
};

class health:public component
{
public:
    int value;
};

class boundingbox:public component
{
public :
    int left,right,top,bottom;
    bool collision(boundingbox& other)
    {
        if (left < other.right || right > other.left)
            if (top < other.bottom || bottom > other.top)
                return true;
        return false;
    }
};

class damage : public component
{
public:
    int value;
};

// declare enteties here------------------------------

class entity
{
    virtual int id() = 0;
    virtual int size() = 0;
};

class aircraft :public entity, public health,public boundingbox
{
    virtual int id(){return 1;}
    virtual int size() {return sizeof(*this);};
};

class bullet :public entity, public damage, public boundingbox
{
    virtual int id(){return 2;}
    virtual int size() {return sizeof(*this);};
};

int main()
{
    entity* gameobjects[3];
    gameobjects[0] = new aircraft;
    gameobjects[1] = new bullet;
    gameobjects[2] = new bullet;
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if (dynamic_cast<boundingbox*>(gameobjects[i]) && dynamic_cast<boundingbox*>(gameobjects[j]) &&
                dynamic_cast<boundingbox*>(gameobjects[i])->collision(*dynamic_cast<boundingbox*>(gameobjects[j])))
                if (dynamic_cast<health*>(gameobjects[i]) && dynamic_cast<damage*>(gameobjects[j]))
                    dynamic_cast<health*>(gameobjects[i])->value -= dynamic_cast<damage*>(gameobjects[j])->value;
}

w tym podejściu każdy komponent jest bazą dla encji, więc biorąc pod uwagę komponent, jego wskaźnik również jest encją! drugą rzeczą, o którą prosisz, jest bezpośredni dostęp do komponentów jakiejś jednostki, np. kiedy muszę uzyskać dostęp do obrażeń w jednym z moich podmiotów, z których korzystam dynamic_cast<damage*>(entity)->value, więc jeśli entityma element powodujący uszkodzenie, zwróci wartość. jeśli nie jesteś pewien, czy entityma uszkodzenie komponentu, czy nie, możesz łatwo sprawdzić, czy if (dynamic_cast<damage*> (entity))wartość zwracana dynamic_castjest zawsze NULL, jeśli rzut nie jest prawidłowy i ten sam wskaźnik, ale z żądanym typem, jeśli jest prawidłowy. więc aby zrobić coś ze wszystkim, entitiesco ma component, możesz to zrobić jak poniżej

for (int i=0;i<enteties.size();i++)
    if (dynamic_cast<component*>(enteties[i]))
        //do somthing here

jeśli będą jakieś inne pytania, chętnie odpowiem.

Ali1S232
źródło
dlaczego dostałem głos negatywny? co było nie tak z moim rozwiązaniem?
Ali1S232
3
Twoje rozwiązanie nie jest tak naprawdę rozwiązaniem opartym na komponentach, ponieważ komponenty nie są oddzielone od klas gier. Wszystkie twoje instancje opierają się na relacji IS A (dziedziczenie) zamiast relacji HAS A (kompozycja). Wykonanie tego w sposób kompozycyjny (byty adresują kilka komponentów) daje wiele korzyści w porównaniu z modelem dziedziczenia (dlatego zwykle używasz komponentów). Twoje rozwiązanie nie daje żadnych korzyści z rozwiązania opartego na komponentach i wprowadza pewne dziwactwa (wielokrotne dziedziczenie itp.). Brak lokalizacji danych, brak oddzielnej aktualizacji składników. Brak modyfikacji komponentów w czasie wykonywania.
nieważne
przede wszystkim pytanie dotyczy struktury, że każda instancja komponentu jest powiązana tylko z jednym bytem, ​​a komponenty można aktywować i dezaktywować, dodając tylko bool isActivepodstawową klasę komponentu. tam jeszcze jest potrzebne do wprowadzenia użytecznych elementów podczas definiowania enteties ale nie uważam, że jako problem, a nadal masz seprate aktualizacje componnent (pamiętaj Somthing jak dynamic_cast<componnet*>(entity)->update().
Ali1S232
i zgadzam się, że nadal będzie problem, gdy będzie chciał mieć komponent, który może udostępniać dane, ale biorąc pod uwagę to, o co prosił, chyba nie będzie z tym problemu, i znowu są pewne sztuczki dla tego problemu, że jeśli chcę, że mogę wyjaśnić.
Ali1S232
Chociaż zgadzam się, że można to wdrożyć w ten sposób, nie sądzę, że to dobry pomysł. Twoi projektanci nie mogą samodzielnie komponować obiektów, chyba że masz jedną klasę über, która dziedziczy wszystkie możliwe komponenty. I chociaż możesz wywołać aktualizację tylko dla jednego komponentu, nie będzie on miał dobrego układu w pamięci, w modelu złożonym wszystkie instancje komponentu tego samego typu mogą być trzymane blisko pamięci i iterowane bez żadnych braków pamięci podręcznej. Opierasz się również na technologii RTTI, która jest zwykle wyłączana w grach ze względu na wydajność. Dobry układ sortowanych obiektów naprawia to głównie.
nieważne