Nous partons de l' approche de base systèmes-composants-entités .
Créons des assemblages (terme dérivé de cet article) simplement à partir d'informations sur les types de composants . Cela se fait dynamiquement au moment de l'exécution, tout comme nous ajouterions / supprimerions des composants à une entité un par un, mais nommons-le plus précisément car il ne s'agit que d'informations de type.
Ensuite, nous construisons des entités spécifiant l' assemblage pour chacune d'entre elles. Une fois que nous avons créé l'entité, son assemblage est immuable, ce qui signifie que nous ne pouvons pas la modifier directement en place, mais nous pouvons toujours obtenir la signature de l'entité existante sur une copie locale (avec le contenu), y apporter les modifications appropriées et créer une nouvelle entité. de celui-ci.
Maintenant, pour le concept clé: chaque fois qu'une entité est créée, elle est affectée à un objet appelé assemblage bucket , ce qui signifie que toutes les entités de la même signature seront dans le même conteneur (par exemple dans std :: vector).
Désormais, les systèmes se contentent de parcourir tous les domaines de leur intérêt et font leur travail.
Cette approche présente certains avantages:
- les composants sont stockés dans quelques (précisément: nombre de compartiments) morceaux de mémoire contigus - cela améliore la convivialité de la mémoire et il est plus facile de vider l'état du jeu entier
- les systèmes traitent les composants de manière linéaire, ce qui améliore la cohérence du cache - au revoir les dictionnaires et les sauts de mémoire aléatoires
- la création d'une nouvelle entité est aussi simple que de mapper un assemblage à un bucket et de repousser les composants nécessaires à son vecteur
- la suppression d'une entité est aussi simple qu'un appel à std :: move pour échanger le dernier élément avec celui supprimé, car l'ordre n'a pas d'importance en ce moment
Si nous avons beaucoup d'entités avec des signatures complètement différentes, les avantages de la cohérence du cache diminuent, mais je ne pense pas que cela se produirait dans la plupart des applications.
Il y a aussi un problème avec l'invalidation du pointeur une fois les vecteurs réaffectés - cela pourrait être résolu en introduisant une structure comme:
struct assemblage_bucket {
struct entity_watcher {
assemblage_bucket* owner;
entity_id real_index_in_vector;
};
std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;
//...
};
Donc, chaque fois que pour une raison quelconque dans notre logique de jeu, nous voulons garder une trace d'une entité nouvellement créée, à l'intérieur du compartiment , nous enregistrons un entity_watcher , et une fois que l'entité doit être std :: move'd pendant la suppression, nous recherchons ses observateurs et mettons à jour leur real_index_in_vector
à de nouvelles valeurs. La plupart du temps, cela n'impose qu'une seule recherche de dictionnaire pour chaque suppression d'entité.
Y a-t-il d'autres inconvénients à cette approche?
Pourquoi la solution n'est-elle mentionnée nulle part, bien qu'elle soit assez évidente?
EDIT : J'édite la question pour "répondre aux réponses", car les commentaires sont insuffisants.
vous perdez la nature dynamique des composants enfichables, qui a été créé spécifiquement pour s'éloigner de la construction de classes statiques.
Je ne. Je ne l'ai peut-être pas expliqué assez clairement:
auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket
C'est aussi simple que de simplement prendre la signature d'une entité existante, de la modifier et de la télécharger à nouveau en tant que nouvelle entité. Pluggable, nature dynamique ? Bien sûr. Ici, je voudrais souligner qu'il n'y a qu'une seule classe "assemblage" et une seule classe "bucket". Les compartiments sont pilotés par les données et créés au moment de l'exécution en quantité optimale.
vous devez parcourir tous les compartiments pouvant contenir une cible valide. Sans structure de données externe, la détection des collisions pourrait être tout aussi difficile.
Eh bien, c'est pourquoi nous avons les structures de données externes susmentionnées . La solution de contournement est aussi simple que d'introduire un itérateur dans la classe System qui détecte quand passer au compartiment suivant. Le saut serait purement transparent à la logique.