Conception d'un jeu basé sur des composants

16

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:

  1. Chaque élément de jeu (dirigeable, projectile, power-up, ennemi) est une entité

  2. 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:

  1. Étant donné un composant, obtenez l'entité à laquelle il appartient

  2. Étant donné une entité, obtenez le composant de type "type"

  3. Pour toutes les entités, faites quelque chose

  4. 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?

Emiliano
la source
2
pourquoi un downvote? Quel est le problème avec cette question?
Emiliano

Réponses:

6

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.

Skyler York
la source
qu'entendez-vous par «orienté données»?
Emiliano
Il y a beaucoup d'informations sur Google, mais voici un article décent qui a surgi qui devrait fournir un aperçu de haut niveau, suivi d'une discussion en ce qui concerne les systèmes de composants: gamesfromwithin.com/data-oriented-design , gamedev. net / topic /…
Skyler York
je ne peux pas être d'accord avec tout ce qui se cache à propos de DOD, car je pense que cela ne peut pas être complet lui-même, je veux dire que seul DOD peut suggérer un très bon aprroch pour stocker des données, mais pour appeler des fonctions et des procédures, vous devez utiliser une procédure ou Approche POO, je veux dire le problème est de savoir comment combiner ces deux méthodes pour tirer le meilleur parti des performances et de la facilité de codage, par exemple. dans la structure, je suggère qu'il y aura un problème de performances lorsque toutes les entités ne partageront pas certains composants, mais cela peut facilement être résolu en utilisant DOD, vous n'avez qu'à créer des tableaux différents pour différents types d'entités.
Ali1S232
Cela ne répond pas directement à ma question mais c'est très instructif. Je me suis souvenu de quelque chose sur Dataflows à l'époque de mes études universitaires. C'est la meilleure réponse à ce jour, et elle "gagne".
Emiliano
-1

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:

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

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 si entitya un composant de dommage, il retournera la valeur. si vous ne savez pas si le entitycomposant est endommagé ou non, vous pouvez facilement vérifier que la if (dynamic_cast<damage*> (entity))valeur de retour de dynamic_castest 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 ce entitiesqui en a, componentvous pouvez le faire comme ci-dessous

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

s'il y a d'autres questions, je serai heureux de répondre.

Ali1S232
la source
pourquoi ai-je obtenu le vote négatif? quel était le problème avec ma solution?
Ali1S232
3
Votre solution n'est pas vraiment une solution basée sur des composants car les composants ne sont pas séparés de vos classes de jeu. Vos instances reposent toutes sur la relation IS A (héritage) au lieu d'une relation HAS A (composition). Le faire de la manière de la composition (les entités adressent plusieurs composants) vous offre de nombreux avantages par rapport à un modèle d'héritage (c'est pourquoi vous utilisez généralement des composants). Votre solution ne donne aucun des avantages d'une solution basée sur les composants et introduit quelques bizarreries (héritage multiple, etc.). Aucune localité de données, aucune mise à jour de composant séparée. Aucune modification d'exécution des composants.
nul
tout d'abord, la question demande une structure selon laquelle chaque instance de composant n'est liée qu'à une entité, et vous pouvez activer et désactiver des composants en ajoutant uniquement une bool isActiveclasse 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-vous dynamic_cast<componnet*>(entity)->update()
quelque chose
et je suis d'accord qu'il y aura toujours un problème quand il veut avoir un composant qui peut partager des données mais compte tenu de ce qu'il a demandé, je suppose qu'il n'y aura pas de problème pour cela, et encore une fois il y a quelques astuces pour ce problème aussi que si vous veux que je puisse expliquer.
Ali1S232
Bien que je convienne qu'il est possible de l'implémenter de cette façon, je ne pense pas que ce soit une bonne idée. Les concepteurs ne peuvent pas composer eux-mêmes d'objets, à moins que vous n'ayez une seule classe über qui hérite de tous les composants possibles. Et bien que vous puissiez appeler la mise à jour sur un seul composant, il n'aura pas une bonne disposition en mémoire, dans un modèle composé, toutes les instances de composants du même type peuvent être gardées proches en mémoire et itérées sans aucun échec de cache. Vous comptez également sur RTTI, qui est généralement désactivé dans les jeux pour des raisons de performances. Une bonne disposition des objets triés corrige cela principalement.
nul