Ich schreibe einen Shooter (wie 1942, klassische 2D-Grafik) und möchte einen komponentenbasierten Ansatz verwenden. Bisher habe ich über folgendes Design nachgedacht:
Jedes Spielelement (Luftschiff, Projektil, Powerup, Feind) ist eine Entität
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:
Erhalten Sie für eine gegebene Komponente die Entität, zu der sie gehört
Erhalten Sie bei einer gegebenen Entität die Komponente vom Typ "type"
Tun Sie für alle Entitäten etwas
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?
quelle
Antworten:
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.
quelle
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:
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
,entity
wird der Wert zurückgegeben , wenn eine Schadenskomponente vorhanden ist. Wenn Sie sich nicht sicher sind, obentity
eine Komponente beschädigt ist oder nicht, können Sie leicht überprüfen , ob derif (dynamic_cast<damage*> (entity))
Rückgabewert vondynamic_cast
immer 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, dieentities
welche habencomponent
, kannst du es wie folgt machenBei weiteren Fragen stehe ich gerne zur Verfügung.
quelle
bool isActive
Commponent-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()
.