Entwerfen eines komponentenbasierten Spiels

16

Ich schreibe einen Shooter (wie 1942, klassische 2D-Grafik) und möchte einen komponentenbasierten Ansatz verwenden. Bisher habe ich über folgendes Design nachgedacht:

  1. Jedes Spielelement (Luftschiff, Projektil, Powerup, Feind) ist eine Entität

  2. Jede Entität besteht aus einer Reihe von Komponenten, die zur Laufzeit hinzugefügt oder entfernt werden können. Beispiele sind Position, Sprite, Gesundheit, IA, Schaden, BoundingBox usw.

Die Idee ist, dass Luftschiff, Projektil, Feind und Powerup KEINE Spielklassen sind. Eine Entität wird nur durch die Komponenten definiert, deren Eigentümer sie ist (und die sich im Laufe der Zeit ändern können). Das Spieler-Luftschiff beginnt also mit Sprite-, Positions-, Gesundheits- und Eingabekomponenten. Ein Powerup hat das Sprite, Position, BoundingBox. Und so weiter.

Die Hauptschleife verwaltet das Spiel "Physik", dh wie die Komponenten miteinander interagieren:

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()); 

...

Komponenten werden in der Hauptanwendung von C ++ fest codiert. Entitäten können in einer XML-Datei definiert werden (der IA-Teil in einer Lua- oder Python-Datei).

Die Hauptschleife kümmert sich nicht viel um Entitäten: Sie verwaltet nur Komponenten. Das Software-Design sollte Folgendes ermöglichen:

  1. Erhalten Sie für eine gegebene Komponente die Entität, zu der sie gehört

  2. Erhalten Sie bei einer gegebenen Entität die Komponente vom Typ "type"

  3. Tun Sie für alle Entitäten etwas

  4. Tun Sie für alle Entitätskomponenten etwas (z. B. serialisieren)

Ich habe über folgendes nachgedacht:

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;

Mit diesem Design kann ich # 1, # 2, # 3 (dank boost :: fusion :: map Algorithmen) und # 4 erreichen. Auch alles ist O (1) (ok, nicht genau, aber es ist immer noch sehr schnell).

Es gibt auch einen "allgemeineren" Ansatz:

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
...
};

Ein weiterer Ansatz besteht darin, die Entity-Klasse zu entfernen: Jeder Komponententyp lebt in seiner eigenen Liste. Es gibt also eine Sprite-Liste, eine Health-Liste, eine Damage-Liste usw. Ich weiß, dass sie aufgrund der Entity-ID derselben logischen Entität angehören. Dies ist einfacher, aber langsamer: Die IA-Komponenten müssen grundsätzlich auf alle Komponenten der anderen Entität zugreifen können und müssen daher bei jedem Schritt die Liste der anderen Komponenten durchsuchen.

Welcher Ansatz ist Ihrer Meinung nach besser? ist die boost :: fusion map dafür geeignet?

Emiliano
quelle
2
Warum eine Gegenstimme? Was ist los mit dieser Frage?
Emiliano

Antworten:

6

Ich habe festgestellt, dass komponentenbasiertes Design und datenorientiertes Design Hand in Hand gehen. Sie sagen, dass es "langsamer" sein wird, homogene Listen von Komponenten zu haben und das erstklassige Entitätsobjekt zu eliminieren (stattdessen eine Entitäts-ID für die Komponenten selbst zu wählen), aber das ist weder hier noch dort, da Sie tatsächlich keinen echten Code dafür erstellt haben setzt beide Ansätze um, um zu dieser Schlussfolgerung zu gelangen. Wie in der Tat, kann ich garantieren Ihnen fast , dass Ihre Komponenten und vermeidet die traditionelle schwere Virtualisierung Homogenisieren wird schneller durch die verschiedenen Vorteile von datenorientierten Design - einfacher parallelisiert, Cache - Nutzung, Modularität, usw.

Ich sage nicht, dass dieser Ansatz für alles ideal ist, aber Komponentensysteme, bei denen es sich im Grunde um Datensammlungen handelt, für die in jedem Frame die gleichen Transformationen durchgeführt werden müssen, schreien einfach, um datenorientiert zu sein. Es wird Zeiten geben, in denen Komponenten mit anderen Komponenten unterschiedlicher Typen kommunizieren müssen, aber dies wird in beiden Fällen ein notwendiges Übel sein. Das Design sollte jedoch nicht davon abhängen, da es Möglichkeiten gibt, dieses Problem zu lösen, selbst wenn alle Komponenten wie Nachrichtenwarteschlangen und Futures parallel verarbeitet werden .

Auf jeden Fall Google für datenorientiertes Design, da es sich um komponentenbasierte Systeme handelt, da dieses Thema häufig auftaucht und es eine Menge Diskussionen und anekdotische Daten gibt.

Skyler York
quelle
was meinst du mit "datenorientiert"?
Emiliano
Es gibt viele Informationen über Google, aber hier ist ein anständiger Artikel, der auftauchen sollte, der einen Überblick auf hoher Ebene bieten soll, gefolgt von einer Diskussion in Bezug auf Komponentensysteme: gamesfromwithin.com/data-oriented-design , gamedev. net / topic /…
Skyler York
Ich kann nicht mit all dem, was mit DOD zu tun hat, einverstanden sein, da ich denke, dass es selbst nicht vollständig sein kann. Ich meine, nur DOD kann eine sehr gute Anleitung zum Speichern von Daten vorschlagen, aber zum Aufrufen von Funktionen und Prozeduren, die Sie entweder prozedural oder verwenden müssen OOP-Ansatz, ich meine, das Problem ist, wie diese beiden Methoden kombiniert werden können, um den größten Nutzen sowohl für die Leistung als auch für die Einfachheit der Codierung zu erzielen, z. In der Struktur schlage ich vor, dass es Leistungsprobleme geben wird, wenn alle Entitäten einige Komponenten nicht gemeinsam nutzen, dies jedoch mit DOD leicht zu lösen ist. Sie müssen nur verschiedene Arrays für verschiedene Entitätstypen erstellen.
Ali1S232
Dies beantwortet meine Frage nicht direkt, ist aber sehr informativ. Ich erinnerte mich an etwas über Datenflüsse in meiner Zeit an der Universität. Es ist die bisher beste Antwort, und es "gewinnt".
Emiliano
-1

Wenn ich einen solchen Code schreiben würde, würde ich diesen Ansatz lieber nicht verwenden (und ich verwende keinen Boost, wenn es für Sie wichtig ist), da er alles kann, was Sie wollen, aber das Problem ist, wenn es zu viele Entitäten gibt Diejenigen, die einige Komponenten nicht gemeinsam nutzen, werden einige Zeit benötigen, um diejenigen zu finden, die sie haben. Ansonsten gibt es kein anderes Problem, das ich lösen kann:

// 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;
}

In diesem Approach ist jede Komponente eine Basis für eine Entität. Wenn die Komponente also ihren Zeiger hat, ist sie auch eine Entität! Das zweite, wonach Sie fragen, ist der direkte Zugriff auf die Komponenten einer Entität, z. Wenn ich auf Schaden in einer meiner Einheiten zugreifen muss, die ich benutze dynamic_cast<damage*>(entity)->value, entitywird der Wert zurückgegeben , wenn eine Schadenskomponente vorhanden ist. Wenn Sie sich nicht sicher sind, ob entityeine Komponente beschädigt ist oder nicht, können Sie leicht überprüfen , ob der if (dynamic_cast<damage*> (entity))Rückgabewert von dynamic_castimmer NULL ist, wenn die Umwandlung nicht gültig ist und derselbe Zeiger, aber mit dem angeforderten Typ, wenn er gültig ist. Um etwas mit all den Dingen zu machen, die entitieswelche haben component, kannst du es wie folgt machen

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

Bei weiteren Fragen stehe ich gerne zur Verfügung.

Ali1S232
quelle
warum habe ich die down vote bekommen? was war los mit meiner lösung
Ali1S232
3
Ihre Lösung ist nicht wirklich eine komponentenbasierte Lösung, da die Komponenten nicht von Ihren Spielklassen getrennt sind. Ihre Instanzen stützen sich alle auf die IS A-Beziehung (Vererbung) anstelle einer HAS A-Beziehung (Komposition). Die Komposition (Entities adressieren mehrere Komponenten) bietet Ihnen viele Vorteile gegenüber einem Vererbungsmodell (weshalb Sie normalerweise Komponenten verwenden). Ihre Lösung bietet keinen der Vorteile einer komponentenbasierten Lösung und weist einige Besonderheiten auf (Mehrfachvererbung usw.). Keine Datenlokalität, keine separate Komponentenaktualisierung. Keine Laufzeitmodifikation von Komponenten.
ungültig
Zunächst fragt die Frage nach der Struktur, ob jede Komponenteninstanz nur einer Entität zugeordnet ist. Sie können Komponenten aktivieren und deaktivieren, indem Sie nur eine bool isActiveCommponent-Basisklasse hinzufügen. Wenn Sie Entitäten definieren, müssen Sie noch brauchbare Komponenten einführen, aber ich sehe das nicht als Problem an, und Sie haben immer noch separate Komponenten-Updates (denken Sie an etwas Ähnliches) dynamic_cast<componnet*>(entity)->update().
Ali1S232
und ich bin damit einverstanden, dass es immer noch ein Problem geben wird, wenn er eine Komponente haben möchte, die Daten austauschen kann, aber wenn man bedenkt, wonach er gefragt hat, gibt es wahrscheinlich kein Problem dafür, und auch für dieses Problem gibt es einige Tricks, wenn Sie Ich möchte, dass ich es erklären kann.
Ali1S232,
Ich bin damit einverstanden, dass es möglich ist, es auf diese Weise umzusetzen, halte es jedoch nicht für eine gute Idee. Ihre Designer können keine Objekte selbst erstellen, es sei denn, Sie haben eine über-Klasse, die alle möglichen Komponenten erbt. Und während Sie update für nur eine Komponente aufrufen können, ist das Layout im Arbeitsspeicher nicht gut. In einem komponierten Modell können alle Komponenteninstanzen desselben Typs im Arbeitsspeicher gespeichert und ohne Cache-Fehler wiederholt werden. Sie verlassen sich auch auf RTTI, das aus Leistungsgründen in Spielen normalerweise deaktiviert ist. Ein gut sortiertes Objektlayout behebt das meistens.
ungültig