J'écris un jeu de tir (comme 1942, des graphismes 2D classiques) et j'aimerais utiliser une approche basée sur les composants. Jusqu'à présent, j'ai pensé à la conception suivante:
Chaque élément de jeu (dirigeable, projectile, power-up, ennemi) est une entité
Chaque entité est un ensemble de composants qui peuvent être ajoutés ou supprimés au moment de l'exécution. Les exemples sont Position, Sprite, Santé, IA, Dommages, BoundingBox etc.
L'idée est que les dirigeables, les projectiles, les ennemis et les bonus ne sont PAS des classes de jeu. Une entité n'est définie que par les composants qu'elle possède (et qui peuvent changer avec le temps). Le joueur Airship commence donc avec les composants Sprite, Position, Santé et Input. Un powerup a le Sprite, Position, BoundingBox. Etc.
La boucle principale gère le jeu "physique", c'est-à-dire comment les composants interagissent entre eux:
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());
...
Les composants sont codés en dur dans l'application C ++ principale. Les entités peuvent être définies dans un fichier XML (la partie IA dans un fichier lua ou python).
La boucle principale ne se soucie pas beaucoup des entités: elle gère uniquement les composants. La conception du logiciel doit permettre:
Étant donné un composant, obtenez l'entité à laquelle il appartient
Étant donné une entité, obtenez le composant de type "type"
Pour toutes les entités, faites quelque chose
Pour tous les composants de l'entité, faites quelque chose (par exemple: sérialiser)
Je pensais à ce qui suit:
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;
Avec cette conception, je peux obtenir # 1, # 2, # 3 (grâce aux algorithmes boost :: fusion :: map) et # 4. De plus, tout est O (1) (ok, pas exactement, mais c'est toujours très rapide).
Il existe également une approche plus "commune":
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
...
};
Une autre approche consiste à se débarrasser de la classe Entity: chaque type Component vit dans sa propre liste. Il y a donc une liste de sprites, une liste de santé, une liste de dégâts, etc. Je sais qu'ils appartiennent à la même entité logique en raison de l'ID d'entité. C'est plus simple, mais plus lent: les composants IA doivent avoir accès essentiellement à tous les composants des autres entités et cela nécessiterait de rechercher la liste des autres composants à chaque étape.
Quelle approche pensez-vous est la meilleure? la carte boost :: fusion est-elle adaptée pour être utilisée de cette manière?
la source
Réponses:
J'ai trouvé que la conception basée sur les composants et la conception orientée données vont de pair. Vous dites qu'avoir des listes homogènes de composants et éliminer l'objet entité de première classe (en optant à la place pour un ID d'entité sur les composants eux-mêmes) sera "plus lent", mais ce n'est ni ici ni là puisque vous n'avez réellement profilé aucun code réel qui met en œuvre les deux approches pour arriver à cette conclusion. En fait, je peux presque vous garantir que l'homogénéisation de vos composants et l'évitement de la virtualisation lourde traditionnelle seront plus rapides en raison des divers avantages de la conception orientée données - parallélisation plus facile, utilisation du cache, modularité, etc.
Je ne dis pas que cette approche est idéale pour tout, mais les systèmes de composants qui sont essentiellement des collections de données qui nécessitent les mêmes transformations effectuées sur chaque trame, crient simplement pour être orientés données. Il y aura des moments où les composants devront communiquer avec d'autres composants de types différents, mais cela va être un mal nécessaire de toute façon. Cependant, cela ne devrait pas conduire la conception, car il existe des moyens de résoudre ce problème, même dans le cas extrême où tous les composants sont traités en parallèle, tels que les files d'attente de messages et les futurs .
Certainement Google autour de la conception orientée données en ce qui concerne les systèmes basés sur des composants, car ce sujet revient souvent et il y a beaucoup de discussions et de données anecdotiques.
la source
si je devais écrire un tel code, je préférerais ne pas utiliser cette approche (et je n'utilise aucun boost si c'est important pour vous), car il peut faire tout ce que vous voulez mais le problème est quand il y a trop d'entités qui ne partagent pas de componnet, trouver ceux qui en ont consommera du temps. à part ça il n'y a pas d'autre problème dont je puisse penser:
dans cette approche, chaque composant est la base d'une entité, étant donné le composant, son pointeur est également une entité! la deuxième chose que vous demandez est d'avoir un accès direct aux composants de certaines entités, par exemple. lorsque j'ai besoin d'accéder à des dommages dans l'une de mes entités que j'utilise
dynamic_cast<damage*>(entity)->value
, donc sientity
a un composant de dommage, il retournera la valeur. si vous ne savez pas si leentity
composant est endommagé ou non, vous pouvez facilement vérifier que laif (dynamic_cast<damage*> (entity))
valeur de retour dedynamic_cast
est toujours NULL si la conversion n'est pas valide et un même pointeur mais avec le type demandé s'il est valide. donc pour faire quelque chose avec tout ceentities
qui en a,component
vous pouvez le faire comme ci-dessouss'il y a d'autres questions, je serai heureux de répondre.
la source
bool isActive
classe de composants de base. il est toujours nécessaire d'introduire des composants utilisables lorsque vous définissez des entités, mais je ne considère pas cela comme un problème, et vous avez toujours des mises à jour de composants séparés (rappelez-vousdynamic_cast<componnet*>(entity)->update()