Wie kann ich richtig auf die Komponenten in meinen C ++ Entity-Component-Systemen zugreifen?

18

(Was ich beschreibe, basiert auf diesem Entwurf: Was ist ein Entity-System-Framework? Scrollen Sie nach unten und Sie werden es finden.)

Ich habe einige Probleme beim Erstellen eines Entity-Component-Systems in C ++. Ich habe meine Komponentenklasse:

class Component { /* ... */ };

Welches ist eigentlich eine Schnittstelle für andere Komponenten erstellt werden. Um eine benutzerdefinierte Komponente zu erstellen, implementiere ich einfach die Schnittstelle und füge die Daten hinzu, die im Spiel verwendet werden:

class SampleComponent : public Component { int foo, float bar ... };

Diese Komponenten werden in einer Entity-Klasse gespeichert, die jeder Entity-Instanz eine eindeutige ID gibt:

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

Komponenten werden der Entität durch Hashing des Komponentennamens hinzugefügt (dies ist wahrscheinlich keine so gute Idee). Wenn ich eine benutzerdefinierte Komponente hinzufüge, wird sie als Komponententyp (Basisklasse) gespeichert.

Jetzt habe ich auf der anderen Seite eine Systemschnittstelle, die eine Node-Schnittstelle verwendet. Die Node-Klasse wird zum Speichern einiger Komponenten einer einzelnen Entität verwendet (da das System nicht an der Verwendung aller Komponenten der Entität interessiert ist). Wenn das System dies update()tun muss, muss es nur die von ihm gespeicherten Knoten durchlaufen, die aus verschiedenen Entitäten erstellt wurden. So:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

Jetzt das Problem: Nehmen wir an, ich erstelle die SampleNodes, indem ich eine Entität an das SampleSystem übergebe. Der SampleNode "prüft" dann, ob die Entität über die erforderlichen Komponenten verfügt, die vom SampleSystem verwendet werden sollen. Das Problem tritt auf, wenn ich auf die gewünschte Komponente in der Entität zugreifen muss: Die Komponente ist in einer ComponentAuflistung (Basisklasse) gespeichert , sodass ich nicht auf die Komponente zugreifen und sie auf den neuen Knoten kopieren kann. Ich habe das Problem vorübergehend gelöst, indem ich den ComponentDown auf einen abgeleiteten Typ umwandelte, aber ich wollte wissen, ob es eine bessere Möglichkeit gibt, dies zu tun. Ich verstehe, ob dies bedeuten würde, das, was ich bereits habe, neu zu entwerfen. Vielen Dank.

Federico
quelle

Antworten:

23

Wenn Sie die Components zusammen in einer Sammlung speichern möchten, müssen Sie eine gemeinsame Basisklasse als in der Sammlung gespeicherten Typ verwenden und daher auf den richtigen Typ umwandeln, wenn Sie versuchen, auf die Components in der Sammlung zuzugreifen . Die Probleme des Versuchs, in die falsche abgeleitete Klasse umzuwandeln, können jedoch durch geschickte Verwendung von Vorlagen und der typeidFunktion behoben werden :

Mit einer so deklarierten Karte:

std::unordered_map<const std::type_info* , Component *> components;

eine addComponent Funktion wie:

components[&typeid(*component)] = component;

und eine getComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

Du wirst keine Fehlbesetzung bekommen. Dies liegt daran typeid, dass ein Zeiger auf die Typinformationen des Laufzeittyps (des am häufigsten abgeleiteten Typs) der Komponente zurückgegeben wird. Da die Komponente mit diesen Typinformationen als Schlüssel gespeichert wird, kann die Besetzung möglicherweise keine Probleme verursachen, da die Typen nicht übereinstimmen. Sie erhalten auch eine Überprüfung des Kompilierungszeittyps für den Vorlagentyp, da dieser von Component abgeleitet sein muss, da er static_cast<T*>sonst nicht mit dem übereinstimmt unordered_map.

Sie müssen jedoch nicht die Komponenten verschiedener Typen in einer gemeinsamen Sammlung speichern. Wenn Sie die Idee eines Entityenthaltenden Components aufgeben und stattdessen jedes Componentein Entity(in Wirklichkeit wahrscheinlich nur eine Ganzzahl-ID) speichern lassen, können Sie jeden abgeleiteten Komponententyp in einer eigenen Auflistung des abgeleiteten Typs statt als speichern gemeinsamen Basistyp, und finden Sie die Component"Zugehörigkeit" zu einer Entitydurch diese ID.

Diese zweite Implementierung ist etwas uninteressanter als die erste, könnte jedoch möglicherweise als Implementierungsdetails hinter einer Schnittstelle verborgen sein, sodass sich die Benutzer des Systems nicht darum kümmern müssen. Ich werde nicht kommentieren, was besser ist, da ich das zweite nicht wirklich verwendet habe, aber ich sehe das Verwenden von static_cast nicht als Problem mit einer so starken Garantie für Typen, wie es die erste Implementierung bietet. Beachten Sie, dass RTTI erforderlich ist, was je nach Plattform und / oder philosophischen Überzeugungen ein Problem sein kann oder nicht.

Chewy Gumball
quelle
3
Ich benutze C ++ seit fast 6 Jahren, lerne aber jede Woche einen neuen Trick.
Knight666
Danke für die Antwort. Ich werde zuerst versuchen, die erste Methode zu verwenden, und wenn ich später darüber nachdenke, wie ich die andere Methode verwenden kann. Aber müsste die addComponent()Methode nicht auch eine Template-Methode sein? Wenn ich ein definiere addComponent(Component* c), werden alle von mir hinzugefügten Unterkomponenten in einem ComponentZeiger gespeichert und typeidbeziehen sich immer auf die ComponentBasisklasse.
Federico
2
Typeid gibt den tatsächlichen Typ des Objekts an, auf das verwiesen wird, auch wenn der Zeiger von einer Basisklasse ist
Chewy Gumball
Ich mochte die Antwort von chewy sehr und habe versucht, sie auf mingw32 zu implementieren. Ich bin auf das von fede rico erwähnte Problem gestoßen, bei dem addComponent () alles als Komponente speichert, weil typeid Komponente als Typ für alles zurückgibt. Jemand hier erwähnte, dass typeid den tatsächlichen Typ des Objekts angeben sollte, auf das verwiesen wird, auch wenn der Zeiger auf eine Basisklasse verweist, aber ich denke, dass er je nach Compiler usw. variieren kann. Kann dies jemand anderes bestätigen? Ich habe g ++ std = c ++ 11 mingw32 unter Windows 7 verwendet. Am Ende habe ich getComponent () als Vorlage geändert und dann den Typ
Uhr
Dies ist nicht compilerspezifisch. Sie hatten wahrscheinlich nicht den richtigen Ausdruck als Argument für die typeid-Funktion.
Chewy Gumball
17

Chewy hat es richtig gemacht, aber wenn Sie C ++ 11 verwenden, können Sie einige neue Typen verwenden.

Anstatt const std::type_info*als Schlüssel in Ihrer Map zu verwenden, können Sie auch std::type_index( siehe cppreference.com ) verwenden, einen Wrapper um die std::type_info. Warum würdest du es benutzen? Der std::type_indexspeichert tatsächlich die Beziehung mit dem std::type_infoals Zeiger, aber das ist ein Zeiger weniger, über den Sie sich Sorgen machen müssen.

Wenn Sie tatsächlich C ++ 11 verwenden, würde ich empfehlen, die ComponentReferenzen in intelligenten Zeigern zu speichern . Die Karte könnte also etwa so aussehen:

std::map<std::type_index, std::shared_ptr<Component> > components

So können Sie einen neuen Eintrag hinzufügen:

components[std::type_index(typeid(*component))] = component

Wo componentist der Typ std::shared_ptr<Component>. Das Abrufen eines Verweises auf einen bestimmten Typ Componentkönnte folgendermaßen aussehen:

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

Beachten Sie auch die Verwendung von static_pointer_castanstelle von static_cast.

vijoc
quelle
1
Ich benutze diese Art von Ansatz in meinem eigenen Projekt.
Vijoc
Dies ist eigentlich recht praktisch, da ich C ++ mit dem C ++ 11-Standard als Referenz gelernt habe. Eine Sache, die mir aufgefallen ist, ist, dass alle Entity-Component-Systeme, die ich im Internet gefunden habe, eine Art von verwenden cast. Ich beginne zu denken, dass es unmöglich wäre, dies oder ein ähnliches Systemdesign ohne Abgüsse zu implementieren.
Federico
@Fede Für das Speichern von ComponentZeigern in einem einzelnen Container müssen diese notwendigerweise auf den abgeleiteten Typ reduziert werden . Wie Chewy betonte, stehen Ihnen jedoch andere Optionen zur Verfügung, für die kein Casting erforderlich ist. Ich selbst sehe nichts "Schlechtes" darin, diese Art von Abgüssen im Design zu haben, da sie relativ sicher sind.
Vijoc
@vijoc Sie werden manchmal als schlecht angesehen, da sie möglicherweise Probleme mit der Speicherkohärenz verursachen.
Akaltar