Conseils sur la liaison entre le système de composants d'entité en C ++

10

Après avoir lu quelques documents sur le système entité-composant, j'ai décidé de mettre en œuvre le mien. Jusqu'à présent, j'ai une classe mondiale qui contient les entités et le gestionnaire de système (systèmes), une classe d'entité qui contient les composants sous forme de std :: map et quelques systèmes. Je tiens des entités en tant que vecteur std :: dans World. Aucun problème jusqu'ici. Ce qui me déroute, c'est l'itération des entités, je ne peux pas avoir un esprit clair à ce sujet, donc je ne peux toujours pas implémenter cette partie. Chaque système devrait-il contenir une liste locale d'entités qui l'intéressent? Ou dois-je simplement parcourir les entités de la classe World et créer une boucle imbriquée pour parcourir les systèmes et vérifier si l'entité a les composants qui intéressent le système? Je veux dire :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

mais je pense qu'un système de masques de bits bloquera un peu la flexibilité en cas d'incorporation d'un langage de script. Ou avoir des listes locales pour chaque système augmentera l'utilisation de la mémoire pour les classes. Je suis terriblement confus.

deniz
la source
Pourquoi espérez-vous que l'approche du masque de bits entrave les liaisons de script? En aparté, utilisez des références (const, si possible) dans les boucles for-each pour éviter de copier des entités et des systèmes.
Benjamin Kloster
en utilisant un bitmask par exemple un int, ne contiendra que 32 composants différents. Je n'implique pas qu'il y aura plus de 32 composants, mais que faire si j'en ai? je devrai créer un autre int ou 64bit int, ce ne sera pas dynamique.
deniz
Vous pouvez utiliser std :: bitset ou std :: vector <bool>, selon que vous souhaitez ou non qu'il soit dynamique au moment de l'exécution.
Benjamin Kloster

Réponses:

7

Le fait d'avoir des listes locales pour chaque système augmentera l'utilisation de la mémoire pour les classes.

C'est un compromis spatio-temporel traditionnel .

Bien que l'itération à travers toutes les entités et la vérification de leurs signatures soit directement du code, cela peut devenir inefficace à mesure que votre nombre de systèmes augmente - imaginez un système spécialisé (laissez-le entrer) qui recherche son entité probablement unique parmi des milliers d'entités non liées .

Cela dit, cette approche peut toujours être suffisante en fonction de vos objectifs.

Bien que, si vous vous inquiétez de la vitesse, il existe bien sûr d'autres solutions à envisager.

Chaque système devrait-il contenir une liste locale d'entités qui l'intéressent?

Exactement. Il s'agit d'une approche standard qui devrait vous donner des performances décentes et est relativement facile à mettre en œuvre. La surcharge de mémoire est négligeable à mon avis - nous parlons de stocker des pointeurs.

Maintenant, comment maintenir ces "listes d'intérêt" peut ne pas être si évident. En ce qui concerne le conteneur de données, std::vector<entity*> targetsla classe système interne est parfaitement suffisante. Maintenant, ce que je fais, c'est ceci:

  • L'entité est vide à la création et n'appartient à aucun système.
  • Chaque fois que j'ajoute un composant à une entité:

    • obtenir sa signature de bit actuelle ,
    • mapper la taille du composant au pool mondial de taille de morceau adéquate (personnellement, j'utilise boost :: pool) et y allouer le composant
    • obtenir la nouvelle signature de bit de l' entité (qui est juste la "signature de bit actuelle" plus le nouveau composant)
    • itérer à travers tous les systèmes du monde et s'il y a un système dont la signature ne correspond signature courante de l'entité et ne correspond à la nouvelle signature, il devient évident que nous devons push_back le pointeur à notre entité là - bas.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

La suppression d'une entité est entièrement analogue, à la seule différence que nous supprimons si un système correspond à notre signature actuelle (ce qui signifie que l'entité était là) et ne correspond pas à la nouvelle signature (ce qui signifie que l'entité ne devrait plus être là ).

Maintenant, vous envisagez peut-être d'utiliser std :: list car la suppression du vecteur est O (n), sans mentionner que vous devrez déplacer un gros morceau de données chaque fois que vous supprimez du milieu. En fait, vous n'êtes pas obligé - puisque nous ne nous soucions pas du traitement de l'ordre à ce niveau, nous pouvons simplement appeler std :: remove et vivre avec le fait qu'à chaque suppression, nous n'avons qu'à effectuer une recherche O (n) pour notre entité à supprimer.

std :: list vous donnerait O (1) supprimer mais de l'autre côté vous avez un peu de surcharge de mémoire supplémentaire. Souvenez-vous également que la plupart du temps, vous allez traiter des entités et non les supprimer - et cela se fait sûrement plus rapidement en utilisant std :: vector.

Si vous êtes très critique en termes de performances, vous pouvez envisager un autre modèle d'accès aux données , mais dans les deux cas, vous conservez une sorte de "listes d'intérêt". N'oubliez pas que si vous conservez votre API Entity System suffisamment abstraite, cela ne devrait pas être un problème pour améliorer les méthodes de traitement des entités des systèmes si votre taux de rafraîchissement baisse à cause d'eux - donc pour l'instant, choisissez la méthode la plus facile à coder pour vous - uniquement puis profilez et améliorez si nécessaire.

Patryk Czachurski
la source
5

Il y a une approche qui mérite d'être considérée où chaque système possède les composants qui lui sont associés et où les entités s'y réfèrent uniquement. Fondamentalement, votre Entityclasse (simplifiée) ressemble à ceci:

class Entity {
  std::map<ComponentType, Component*> components;
};

Lorsque vous avez dit un RigidBodycomposant attaché à un Entity, vous le demandez à votre Physicssystème. Le système crée le composant et laisse l'entité garder un pointeur sur celui-ci. Votre système ressemble alors à:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Cela peut sembler un peu contre-intuitif au début, mais l'avantage réside dans la façon dont les systèmes d'entités composants mettent à jour leur état. Souvent, vous parcourrez vos systèmes et leur demanderez de mettre à jour les composants associés

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

La force d'avoir tous les composants appartenant au système dans la mémoire contiguë est que lorsque votre système parcourt chaque composant et le met à jour, il n'a essentiellement qu'à faire

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

Il n'a pas à parcourir toutes les entités qui n'ont potentiellement pas de composant à mettre à jour et il a également un potentiel de très bonnes performances de cache car les composants seront tous stockés de manière contiguë. C'est l'un, sinon le plus grand avantage de cette méthode. Vous aurez souvent des centaines et des milliers de composants à un moment donné, autant essayer et être aussi performant que possible.

À ce stade, vous Worldne parcourez que les systèmes et les appelez updatesans avoir à réitérer les entités également. C'est (à mon humble avis) une meilleure conception, car les responsabilités des systèmes sont alors beaucoup plus claires.

Bien sûr, il existe une myriade de tels modèles, vous devez donc évaluer soigneusement les besoins de votre jeu et choisir le plus approprié, mais comme nous pouvons le voir ici, ce sont parfois les petits détails de conception qui peuvent faire la différence.

pwny
la source
bonne réponse, merci. mais les composants n'ont pas de fonctions (comme update ()), seulement des données. et le système traite ces données. donc selon votre exemple, je devrais ajouter une mise à jour virtuelle pour la classe de composant et un pointeur d'entité pour chaque composant, ai-je raison?
deniz
@deniz Tout dépend de votre conception. Si vos composants n'ont pas de méthodes mais uniquement des données, le système peut toujours les parcourir et effectuer les actions nécessaires. En ce qui concerne la liaison avec des entités, oui, vous pouvez stocker un pointeur vers l'entité propriétaire dans le composant lui-même ou demander à votre système de maintenir une mappe entre les poignées de composant et les entités. Cependant, vous souhaitez généralement que vos composants soient aussi autonomes que possible. Un composant qui ne connaît pas du tout son entité parent est idéal. Si vous avez besoin de communication dans ce sens, préférez les événements et autres.
pwny
Si vous dites que ce sera meilleur pour l'efficacité, j'utiliserai votre modèle.
deniz
@deniz Assurez-vous de profiler votre code tôt et souvent pour identifier ce qui fonctionne et ne fonctionne pas pour votre
moteur
ok :) je vais faire un peu de stress test
deniz
1

À mon avis, une bonne architecture consiste à créer une couche de composants dans les entités et à séparer la gestion de chaque système dans cette couche de composants. Par exemple, le système logique possède certains composants logiques qui affectent leur entité et stockent les attributs communs qui sont partagés avec tous les composants de l'entité.

Après cela, si vous souhaitez gérer les objets de chaque système en différents points ou dans un ordre particulier, il est préférable de créer une liste de composants actifs dans chaque système. Toutes les listes de pointeurs que vous pouvez créer et gérer dans les systèmes sont moins d'une ressource chargée.

superarce
la source