Le sujet de cette série d'exercice est disponible sur Javaquarium - Zeste de Savoir
Voici également quelques réalisations en C++ :
Pour mes implémentations, j'utilise une approche Entity-Component-System, le but étant d'étudier un peu cette approche sur un exemple concret simple.
Les codes sont testés directement sur Coliru.com.
Dans cette première partie, le bu est de simplement créer une liste de poisson et d'algues, et de les afficher.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail = std::pair<name, is_male>; using detail_component = std::pair<entity::id, detail>; enum class type { algue, poisson }; using type_component = std::pair<entity::id, type>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 1 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } void add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::algue)); } void add_poisson(component::name n, component::is_male m) { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::poisson)); internal::_details.push_back(std::make_pair(id, make_pair(n, m))); } void print_algues() { const auto count = std::count_if(begin(internal::_types), end(internal::_types), [](auto p){ return (p.second == component::type::algue); }); std::cout << count << " algues"; } void print_poissons() { if (internal::_details.empty()) { std::cout << "No poissons"; return; } std::cout << internal::_details.size() << " poissons: "; auto print = [](auto p){ return (p.second.first + " [" + (p.second.second ? 'M' : 'F') + ']'); }; std::transform(begin(internal::_details), end(internal::_details) - 1, std::ostream_iterator<std::string>(std::cout, ", "), print); std::cout << print(internal::_details.back()); } void print() { print_algues(); std::cout << ". "; print_poissons(); std::cout << '.' << std::endl; } } int main() { ecs::add_algue(); ecs::add_algue(); ecs::add_poisson("toto", true); ecs::add_algue(); ecs::add_poisson("titi", false); ecs::add_poisson("tata", true); ecs::add_algue(); ecs::print(); return 0; }
Affiche :
4 algues. 3 poissons: toto [M], titi [F], tata [M].
Cette implémentation peut paraître surprenante : je ne crées aucune classe. Surtout que cet exercice est à la base un exercice de programmation orientée objet. Mais le but est justement d'expliquer l'intérêt de la programmation orientée objet.
Quel est le problème avec cette approche ?
Une première critique que l'on peut faire est l’utilisation de variables globales (dans internal
). Cela implique que si on veut avoir plusieurs ECS en même temps, ce n'est pas possible. Et bien sur, le code est moins réutilisable, il est écrit spécifiquement autour de ces variables globales, les fonctions ne sont pas réutilisables sans elles.
Il est facile de corriger ce problème, en passant ces variables en paramètres de fonctions et en déclarant les variables globales dans main
.
entity::id create_entity(entity::entities & entites) { const auto id = entities.empty() ? 1 : entities.back() + 1; entities.push_back(id); return id; }
Le code devient un peu plus lourd dans les fonctions qui utilisent plusieurs variables globales, mais le principe est le même.
Au final, c'est l'approche que l'on utilise en C pour faire de l'objet : on crées des structures contenant les données et des fonctions qui prennent un pointeur sur un objet. (J'utilise std::pair
et std::tuple
, qui sont en pratique des classes dont les variables membres n'ont pas de noms, et des références, mais ça serait la même chose si j'utilisais struct
et des pointeurs).
Quel est l'intérêt de l'approche objet du C++ par rapport à celle utilisée en C ?
Une première réponse serait de dire que l'on est obligé de donner des noms différents à des fonctions qui font la même chose, comme par exemple print
, print_algues
et print_poissons
dans l'exemple. En mettant les fonctions comme membre des classes plutôt que des fonctions libres, on pourrait utiliser le même nom.
algues.print(); poissons.print(); print();
Mais en fait, ce n'est pas un argument valide. En C++, il est possible de surcharger des fonctions, contrairement au C. Il est donc possible de donner le même nom à plusieurs fonctions, le compilateur appellera la fonction correcte en utilisant les paramètres.
print(algues); print(poissons); print();
Une seconde est de dire qu'il n'est pas possible en C de regrouper (“encapsuler”) ensemble les données et les fonctions qui manipulent ces données ensemble, pour avoir un tout cohérent et plus lisible.
class Poissons { public: entity::id add(); void print(); private: std::vector<Poisson> poissons; };
L'argument n'est pas faux, mais est limité. Pourquoi par exemple ne pas remplacer la création d'une classe dans le code précédent par un namespace ? Quelle sera la différence ?
Il y a bien sûr l'accessibilité des membres, publique ou privée. Mais cela sera vite limité aussi : dans un ECS (et dans de nombreux cas), les données sont manipulées par plusieurs classes, ce qui implique de mettre les membres en publique ou d'ajouter des mutateurs (getters et setters)… ce qui est fait en pratique dans de nombreux langages de programmation.
Mais il y a plus à attendre d'une encapsulation. Qu'est-ce que c'est ? Quel problème veut-on corriger avec la programmation orientée objets ?
La réponse est simple : on veut que les données soient utilisées correctement.
Par exemple, dans un ECS, chaque entité doit être unique. Il est nécessaire et indispensable de ne pas avoir de doublons dans la liste des entités. Or, dans l’implémentation proposée, rien n'interdit à l’utilisateur d'écrire le code suivant :
int main() { ecs::internal::_entities.push_back(0); ecs::internal::_entities.push_back(0); }
Ce qui violerait la règle de l'entité unique.
Les règles imposées aux données sont appelées les invariants en programmation par contrat. Les invariants sont importants, puisqu'ils signifient : “si les invariants sont respectés, le programme fera ce qu'on attend de lui. Si les invariants ne sont pas respectés, il n'est pas possible de garantir le comportement du programme”.
L'autre aspect de la programmation par contrat est l'utilisation des pré et post-conditions. Ces conditions s'appliquent sur les fonctions et garantissent que si les pré-conditions et les invariants sont respectés lors de l'appel d'une fonction, alors les post-conditions et les invariants seront aussi respectés à la fin de l'appel de la fonction.
On arrive donc naturellement au principe d'encapsulation. Une encapsulation correcte est donc une encapsulation qui apportera des garanties fortes sur l'utilisation correcte des données. Cela peut être résumé par la phrase suivante :
Facile a utiliser correctement, difficile a utiliser de façon incorrect.
Scott Meyers
Beaucoup pensent que l'encapsulation consiste simplement a mettre des variables en private
et des fonctions en public
dans une classe. Mais en pratique, ce n'est aussi simple : des fonctions libres peuvent respecter l’encapsulation (voir par exemple les fonctions std::begin
et std::end
) et au contraire des fonctions membres peuvent briser l'encapsulation (en premier lieu, les mutateurs : les setters et getters).
Pour concevoir correctement les classes, on conseille souvent de penser les classes en termes de services rendus, pas en termes d'agrégation de données. Dit autrement, il faut se poser la question “quels sont les services que doivent rendre ma classe ?” et pas “quelles sont les données contenues dans ma classe ?”.
Dans cette seconde partie, le but est de créer différentes races de poissons et d'ajouter une fonction pour que les poisson puissent manger.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail = std::pair<name, is_male>; using detail_component = std::pair<entity::id, detail>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using type_component = std::pair<entity::id, type>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; } bool is_algue(component::type t) { return (t == component::type::algue); } bool is_poisson(component::type t) { return !is_algue(t); } bool is_carnivore(component::type t) { return (t == component::type::Mérou || t == component::type::Thon || t == component::type::PoissonClown); } bool is_herbivore(component::type t) { return (t == component::type::Sole || t == component::type::Bar || t == component::type::Carpe); } std::string to_string(component::type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); return types[i]; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 1 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::algue)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, t)); internal::_details.push_back(std::make_pair(id, make_pair(n, m))); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto p){ return (p.first == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto p){ return (p.first == id); }); internal::_types.erase(types_it, internal::_types.end()); } void eat(entity::id eater, entity::id target) { assert(eater != target); const auto eater_it = std::find_if(begin(internal::_types), end(internal::_types), [eater](auto p){ return (p.first == eater); }); const auto target_it = std::find_if(begin(internal::_types), end(internal::_types), [target](auto p){ return (p.first == target); }); assert(is_poisson(eater_it->second)); assert(is_carnivore(eater_it->second) || (is_herbivore(eater_it->second) && is_algue(target_it->second))); remove_entity(target); //eat } void print_algues() { const auto count = std::count_if(begin(internal::_types), end(internal::_types), [](auto p){ return (p.second == component::type::algue); }); std::cout << "algues: " << count; } void print_poissons() { if (internal::_details.empty()) { std::cout << "No poissons"; return; } std::cout << internal::_details.size() << " poissons: "; auto print = [](auto p){ return (p.second.first + " [" + (p.second.second ? 'M' : 'F') + ']'); }; std::transform(begin(internal::_details), end(internal::_details) - 1, std::ostream_iterator<std::string>(std::cout, ", "), print); std::cout << print(internal::_details.back()); } void print() { print_algues(); std::cout << ". "; print_poissons(); std::cout << '.' << std::endl; } } int main() { ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); const auto algue = ecs::add_algue(); const auto carnivore = ecs::add_poisson(ecs::component::type::Mérou, "toto", true); const auto poisson = ecs::add_poisson(ecs::component::type::Mérou, "titi", false); const auto herbivore = ecs::add_poisson(ecs::component::type::Mérou, "tata", true); ecs::add_poisson(ecs::component::type::Mérou, "tuto", false); ecs::add_poisson(ecs::component::type::Mérou, "tyty", true); ecs::print(); ecs::remove_entity(poisson); ecs::print(); ecs::eat(herbivore, algue); ecs::eat(carnivore, herbivore); ecs::print(); return 0; }
affiche :
algues: 8. 5 poissons: toto [M], titi [F], tata [M], tuto [F], tyty [M]. algues: 8. 4 poissons: toto [M], tata [M], tuto [F], tyty [M]. algues: 7. 3 poissons: toto [M], tuto [F], tyty [M].
Dans cette partie, j'ai choisis d'utiliser une énumération pour définir toutes les races (algues, carnivores et herbivores).
enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe };
L'intérêt est que l'on a besoin que d'une seule variable pour manipuler tous les types. Quels sont les défauts de cette approche ? (Uniquement en termes de C++, pas d'ECS. Je reviendrais sur l'impact de l'encodage des données sur l'ECS par la suite).
Une conséquence est que l'on ne sait pas lors de la compilation si le type correspond à un algue ou à un poisson. Or, certaines fonctions sont spécifiques aux algues ou aux poissons. Par exemple, la fonction add
, qui prend des paramètres différents selon le type. Il est donc nécessaire d'avoir deux fonctions, add_algue
et add_poisson
.
void add_algue(); void add_poisson(name, is_male, type);
Je pense que vous voyez tout de suite le problème ici : type
peut être une algue, ce qui n'est pas valide.
Il n'y a pas d'autre solution que de vérifier le type à l'exécution (avec if
ou assert
).
void add_poisson(component::name n, component::is_male m, component::type t) { assert(is_poisson(t)); ... }
Cela pose plusieurs problèmes :
assert
) ;Est-il possible d’améliorer cela ?
A ce niveau des explications, je pense que la solution est évidente : il suffit de créer un type représentant uniquement les poissons, pas les algues. Par exemple :
enum class type { algue, poisson }; enum class race { Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; void add_poisson(component::name n, component::is_male m, component::race r) { // assert(is_poisson(t)); pas nécessaire ... }
Dans cette situation, il n'est plus nécessaire de vérifier a l’exécution que type
n'a pas la valeur algue
, puisque par conception, ce n'est pas possible.
Cette technique est très puissante et est l'une des forces des langages de programmation a typage fort. Au lieu de devoir vérifier les valeurs a l’exécution, on construit un type qui exprime directement les contraintes sur les données, ce qui permet de faire les vérifications a la compilation (améliorer la qualité logicielle) et gagner en performances.
Dans cette partie, on ajoute un système de tours : a chaque tour, tous les poissons mangent quelque chose au hasard.
En termes d’implémentation, cela pose une petite difficulté : il faut parcourir un vector
que l'on va modifier en même temps. Si on parcours le vector
avec des itérateurs, l'itérateur courant sera invalide après avoir supprimé un élément au début. Idem en parcourant avec des indices, si la suppression a lieu a la fin de vector
, il faut décrémenter l'indice de 1.
Dans un ECS, il est préférable de séparer le traitement des entités en deux ou trois phases :
Dans ce cas, je préfère modifier légèrement le gameplay, de façon a avoir quelque chose de plus simple au niveau gestion des éléments. Au lieu de supprimer directement un poisson qui se fait manger, je fais un premier passage pour déterminer qui mange qui, en conservant dans une liste séparée les entités qui doivent être supprimée.
Cette approche n'est pas forcement incohérente. Si A mange B et B mange A, pourquoi l'une des deux entités gagnerait plus que l'autre ? Surtout que le choix de l’entité qui gagne dépend de l'ordre d'apparition dans le vector
, ce qui est un peu arbitraire. En séparant en deux phases, cela est équivalent a avoir une double mort.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> #include <random> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail = std::pair<name, is_male>; using detail_component = std::pair<entity::id, detail>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using type_component = std::pair<entity::id, type>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; std::random_device _random_device; std::default_random_engine _random_engine { _random_device() }; } bool is_algue(component::type t) { return (t == component::type::algue); } bool is_poisson(component::type t) { return !is_algue(t); } bool is_carnivore(component::type t) { return (t == component::type::Mérou || t == component::type::Thon || t == component::type::PoissonClown); } bool is_herbivore(component::type t) { return (t == component::type::Sole || t == component::type::Bar || t == component::type::Carpe); } std::string to_string(component::type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); return types[i]; } template<typename Predicat> std::vector<entity::id> get_entities(Predicat&& predicat) { std::vector<entity::id> algues; std::for_each(begin(internal::_types), end(internal::_types), [&algues, predicat](auto p){ if (predicat(p.second)) { algues.push_back(p.first); } } ); return algues; } template<typename Collection> typename Collection::value_type const& get_component(Collection const& components, entity::id id) { const auto it = find_if(begin(components), end(components), [id](auto p){ return (p.first == id); }); assert(it != end(components)); return (*it); } entity::id create_entity() { const auto id = internal::_entities.empty() ? 0 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::algue)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, t)); internal::_details.push_back(std::make_pair(id, make_pair(n, m))); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto p){ return (p.first == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto p){ return (p.first == id); }); internal::_types.erase(types_it, internal::_types.end()); } void remove_entities(std::vector<ecs::entity::id> const& ids) { for (auto id: ids) remove_entity(id); } void print_algues() { const auto count = std::count_if(begin(internal::_types), end(internal::_types), [](auto p){ return (p.second == component::type::algue); }); std::cout << "algues: " << count; } void print_poissons() { if (internal::_details.empty()) { std::cout << "No poissons"; return; } std::cout << internal::_details.size() << " poissons: "; auto print = [](auto p){ return (p.second.first + " [" + (p.second.second ? 'M' : 'F') + ']'); }; std::transform(begin(internal::_details), end(internal::_details) - 1, std::ostream_iterator<std::string>(std::cout, ", "), print); std::cout << print(internal::_details.back()); } void print() { print_algues(); std::cout << ". "; print_poissons(); std::cout << '.' << std::endl; } } int main() { for (size_t i {}; i < 50; ++i) ecs::add_algue(); for (size_t i {}; i < 10; ++i) ecs::add_poisson(ecs::component::type::Sole, "toto", false); for (size_t i {}; i < 5; ++i) ecs::add_poisson(ecs::component::type::Mérou, "titi", false); ecs::print(); for (size_t tour {}; tour < 10; ++tour) { std::cout << "===== Tour " << tour << " ======" << std::endl; std::vector<ecs::entity::id> entities_to_remove; const auto poissons { ecs::get_entities(ecs::is_poisson) }; const auto algues { ecs::get_entities(ecs::is_algue) }; std::uniform_int_distribution<size_t> entities_distribution(0, ecs::internal::_entities.size() - 1); std::uniform_int_distribution<size_t> algue_distribution(0, algues.size() - 1); for (auto poisson: poissons) { auto const& poisson_type = ecs::get_component(ecs::internal::_types, poisson); if (ecs::is_herbivore(poisson_type.second) && !algues.empty()) { const auto index { algue_distribution(ecs::internal::_random_engine) }; const auto target = algues[index]; entities_to_remove.push_back(target); } else if (ecs::is_carnivore(poisson_type.second) && !ecs::internal::_entities.empty()) { const auto index { entities_distribution(ecs::internal::_random_engine) }; const auto target = ecs::internal::_entities[index]; entities_to_remove.push_back(target); } } ecs::remove_entities(entities_to_remove); ecs::print(); } return 0; }
affiche :
algues: 50. 15 poissons: toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], titi [F], titi [F], titi [F], titi [F], titi [F]. ===== Tour 0 ====== algues: 37. 15 poissons: toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], titi [F], titi [F], titi [F], titi [F], titi [F]. ===== Tour 1 ====== algues: 22. 15 poissons: toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], titi [F], titi [F], titi [F], titi [F], titi [F]. ===== Tour 2 ====== algues: 11. 14 poissons: toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], titi [F], titi [F], titi [F], titi [F]. ===== Tour 3 ====== algues: 4. 13 poissons: toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], titi [F], titi [F], titi [F], titi [F]. ===== Tour 4 ====== algues: 0. 10 poissons: toto [F], toto [F], toto [F], toto [F], toto [F], toto [F], titi [F], titi [F], titi [F], titi [F]. ===== Tour 5 ====== algues: 0. 7 poissons: toto [F], toto [F], toto [F], titi [F], titi [F], titi [F], titi [F]. ===== Tour 6 ====== algues: 0. 4 poissons: toto [F], toto [F], titi [F], titi [F]. ===== Tour 7 ====== algues: 0. 2 poissons: toto [F], titi [F]. ===== Tour 8 ====== algues: 0. 1 poissons: titi [F]. ===== Tour 9 ====== algues: 0. No poissons. ===== Tour 10 ====== algues: 0. No poissons.
Dans cette partie, on ajoute la gestion des points de vie.
Note : comme ce code est volontairement mal encapsule, la fonction main fait beaucoup (trop) de choses. Beaucoup de ces choses devraient être dans des systèmes et correctement organisées en fonctions.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> #include <random> #include <tuple> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail_component = std::tuple<entity::id, name, is_male>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using point_vie = int8_t; using type_component = std::tuple<entity::id, type, point_vie>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; std::random_device _random_device; std::default_random_engine _random_engine { _random_device() }; } bool is_algue(component::type t) { return (t == component::type::algue); } bool is_poisson(component::type t) { return !is_algue(t); } bool is_carnivore(component::type t) { return (t == component::type::Mérou || t == component::type::Thon || t == component::type::PoissonClown); } bool is_herbivore(component::type t) { return (t == component::type::Sole || t == component::type::Bar || t == component::type::Carpe); } bool is_algue_t(component::type_component const& t) { return is_algue(std::get<1>(t)); } bool is_poisson_t(component::type_component const& t) { return is_poisson(std::get<1>(t)); } bool is_carnivore_t(component::type_component const& t) { return is_carnivore(std::get<1>(t)); } bool is_herbivore_t(component::type_component const& t) { return is_herbivore(std::get<1>(t)); } component::point_vie& get_point_vie(component::type_component & t) { return std::get<2>(t); } std::string to_string(component::type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); return types[i]; } template<typename Predicat> std::vector<entity::id> get_entities(Predicat&& predicat) { std::vector<entity::id> entities; std::for_each(begin(internal::_types), end(internal::_types), [&entities, predicat](const auto type){ if (predicat(type)) { entities.push_back(std::get<0>(type)); } } ); return entities; } template<typename Collection> typename Collection::iterator get_component(Collection & components, entity::id id) { auto it = find_if(begin(components), end(components), [id](auto t){ return (std::get<0>(t) == id); }); return it; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 0 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, component::type::algue, 10)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, t, 10)); internal::_details.push_back(std::make_tuple(id, n, m)); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_types.erase(types_it, internal::_types.end()); } void remove_entities(std::vector<ecs::entity::id> const& ids) { for (auto id: ids) remove_entity(id); } void print() { if (internal::_types.empty()) { std::cout << "Aucune entite" << std::endl; return; } std::cout << internal::_types.size() << " entites: " << std::endl; std::cout << "\tid\ttype\tpv" << std::endl; auto print = [](auto t){ return ('\t' + std::to_string(std::get<0>(t)) + '\t' + to_string(std::get<1>(t)) + '\t' + std::to_string(std::get<2>(t))); }; std::transform(begin(internal::_types), end(internal::_types) - 1, std::ostream_iterator<std::string>(std::cout, "\n"), print); std::cout << print(internal::_types.back()) << std::endl; } } int main() { for (size_t i {}; i < 5; ++i) ecs::add_algue(); for (size_t i {}; i < 2; ++i) ecs::add_poisson(ecs::component::type::Sole, "Sole", false); for (size_t i {}; i < 2; ++i) ecs::add_poisson(ecs::component::type::Mérou, "Mérou", false); ecs::print(); for (size_t tour {}; tour < 21; ++tour) { std::cout << "===== Tour " << tour << " ======" << std::endl; // update pv and age for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); // pv if (ecs::is_algue_t(*type)) { ecs::get_point_vie(*type) += 1; } else { ecs::get_point_vie(*type) -= 1; } } // eat const auto herbivores = ecs::get_entities(ecs::is_herbivore_t); const auto algues = ecs::get_entities(ecs::is_algue_t); if (!algues.empty()) { for (const auto herbivore: herbivores) { const auto herbivore_type = ecs::get_component(ecs::internal::_types, herbivore); if (herbivore_type != end(ecs::internal::_types) && ecs::get_point_vie(*herbivore_type) < 5) { std::uniform_int_distribution<size_t> algue_distribution(0, algues.size() - 1); const auto index { algue_distribution(ecs::internal::_random_engine) }; const auto algue = algues[index]; const auto algue_type = ecs::get_component(ecs::internal::_types, algue); ecs::get_point_vie(*herbivore_type) += 3; ecs::get_point_vie(*algue_type) -= 2; } } } const auto carnivores = ecs::get_entities(ecs::is_carnivore_t); for (const auto carnivore_id: carnivores) { const auto carnivore_type = ecs::get_component(ecs::internal::_types, carnivore_id); if (carnivore_type != end(ecs::internal::_types) && ecs::get_point_vie(*carnivore_type) < 5) // a faim ? { const auto carnivore_race = std::get<1>(*carnivore_type); const auto bons_petits_plats = ecs::get_entities( [carnivore_id, carnivore_race](const auto type){ const auto id = std::get<0>(type); const auto race = std::get<1>(type); return ( ecs::is_poisson(race) && (id != carnivore_id) && (race != carnivore_race) ); } ); if (!bons_petits_plats.empty()) { std::uniform_int_distribution<size_t> entities_distribution(0, bons_petits_plats.size() - 1); const auto index { entities_distribution(ecs::internal::_random_engine) }; const auto entity_id = bons_petits_plats[index]; const auto entity_type = ecs::get_component(ecs::internal::_types, entity_id); if (entity_type != end(ecs::internal::_types)) { if (ecs::is_algue_t(*entity_type)) { ecs::get_point_vie(*carnivore_type) += 3; ecs::get_point_vie(*entity_type) -= 2; } else { ecs::get_point_vie(*carnivore_type) += 5; ecs::get_point_vie(*entity_type) -= 4; } } } } } // deads std::vector<ecs::entity::id> entities_to_remove; for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); if (type != end(ecs::internal::_types) && (ecs::get_point_vie(*type) <= 0 )) { entities_to_remove.push_back(entity); } } for (auto entity: entities_to_remove) { ecs::remove_entity(entity); } ecs::print(); } std::cout << "===== Fini ======" << std::endl; return 0; }
affiche :
9 entites: id type pv 0 Algue 10 1 Algue 10 2 Algue 10 3 Algue 10 4 Algue 10 5 Sole 10 6 Sole 10 7 Mérou 10 8 Mérou 10 ===== Tour 0 ====== 9 entites: id type pv 0 Algue 11 1 Algue 11 2 Algue 11 3 Algue 11 4 Algue 11 5 Sole 9 6 Sole 9 7 Mérou 9 8 Mérou 9 ===== Tour 1 ====== 9 entites: id type pv 0 Algue 12 1 Algue 12 2 Algue 12 3 Algue 12 4 Algue 12 5 Sole 8 6 Sole 8 7 Mérou 8 8 Mérou 8 ===== Tour 2 ====== 9 entites: id type pv 0 Algue 13 1 Algue 13 2 Algue 13 3 Algue 13 4 Algue 13 5 Sole 7 6 Sole 7 7 Mérou 7 8 Mérou 7 ===== Tour 3 ====== 9 entites: id type pv 0 Algue 14 1 Algue 14 2 Algue 14 3 Algue 14 4 Algue 14 5 Sole 6 6 Sole 6 7 Mérou 6 8 Mérou 6 ===== Tour 4 ====== 9 entites: id type pv 0 Algue 15 1 Algue 15 2 Algue 15 3 Algue 15 4 Algue 15 5 Sole 5 6 Sole 5 7 Mérou 5 8 Mérou 5 ===== Tour 5 ====== 8 entites: id type pv 0 Algue 14 1 Algue 16 2 Algue 16 3 Algue 14 4 Algue 16 6 Sole 7 7 Mérou 9 8 Mérou 9 ===== Tour 6 ====== 8 entites: id type pv 0 Algue 15 1 Algue 17 2 Algue 17 3 Algue 15 4 Algue 17 6 Sole 6 7 Mérou 8 8 Mérou 8 ===== Tour 7 ====== 8 entites: id type pv 0 Algue 16 1 Algue 18 2 Algue 18 3 Algue 16 4 Algue 18 6 Sole 5 7 Mérou 7 8 Mérou 7 ===== Tour 8 ====== 8 entites: id type pv 0 Algue 17 1 Algue 19 2 Algue 19 3 Algue 15 4 Algue 19 6 Sole 7 7 Mérou 6 8 Mérou 6 ===== Tour 9 ====== 8 entites: id type pv 0 Algue 18 1 Algue 20 2 Algue 20 3 Algue 16 4 Algue 20 6 Sole 6 7 Mérou 5 8 Mérou 5 ===== Tour 10 ====== 7 entites: id type pv 0 Algue 19 1 Algue 21 2 Algue 21 3 Algue 17 4 Algue 21 7 Mérou 9 8 Mérou 9 ===== Tour 11 ====== 7 entites: id type pv 0 Algue 20 1 Algue 22 2 Algue 22 3 Algue 18 4 Algue 22 7 Mérou 8 8 Mérou 8 ===== Tour 12 ====== 7 entites: id type pv 0 Algue 21 1 Algue 23 2 Algue 23 3 Algue 19 4 Algue 23 7 Mérou 7 8 Mérou 7 ===== Tour 13 ====== 7 entites: id type pv 0 Algue 22 1 Algue 24 2 Algue 24 3 Algue 20 4 Algue 24 7 Mérou 6 8 Mérou 6 ===== Tour 14 ====== 7 entites: id type pv 0 Algue 23 1 Algue 25 2 Algue 25 3 Algue 21 4 Algue 25 7 Mérou 5 8 Mérou 5 ===== Tour 15 ====== 7 entites: id type pv 0 Algue 24 1 Algue 26 2 Algue 26 3 Algue 22 4 Algue 26 7 Mérou 4 8 Mérou 4 ===== Tour 16 ====== 7 entites: id type pv 0 Algue 25 1 Algue 27 2 Algue 27 3 Algue 23 4 Algue 27 7 Mérou 3 8 Mérou 3 ===== Tour 17 ====== 7 entites: id type pv 0 Algue 26 1 Algue 28 2 Algue 28 3 Algue 24 4 Algue 28 7 Mérou 2 8 Mérou 2 ===== Tour 18 ====== 7 entites: id type pv 0 Algue 27 1 Algue 29 2 Algue 29 3 Algue 25 4 Algue 29 7 Mérou 1 8 Mérou 1 ===== Tour 19 ====== 5 entites: id type pv 0 Algue 28 1 Algue 30 2 Algue 30 3 Algue 26 4 Algue 30 ===== Tour 20 ====== 5 entites: id type pv 0 Algue 29 1 Algue 31 2 Algue 31 3 Algue 27 4 Algue 31 ===== Fini ======
Dans cette partie, on ajoute l'age des êtres vivants.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> #include <random> #include <tuple> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail_component = std::tuple<entity::id, name, is_male>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using point_vie = int8_t; using age = uint8_t; using type_component = std::tuple<entity::id, type, point_vie, age>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; std::random_device _random_device; std::default_random_engine _random_engine { _random_device() }; } bool is_algue(component::type t) { return (t == component::type::algue); } bool is_poisson(component::type t) { return !is_algue(t); } bool is_carnivore(component::type t) { return (t == component::type::Mérou || t == component::type::Thon || t == component::type::PoissonClown); } bool is_herbivore(component::type t) { return (t == component::type::Sole || t == component::type::Bar || t == component::type::Carpe); } bool is_algue_t(component::type_component const& t) { return is_algue(std::get<1>(t)); } bool is_poisson_t(component::type_component const& t) { return is_poisson(std::get<1>(t)); } bool is_carnivore_t(component::type_component const& t) { return is_carnivore(std::get<1>(t)); } bool is_herbivore_t(component::type_component const& t) { return is_herbivore(std::get<1>(t)); } component::point_vie& get_point_vie(component::type_component & t) { return std::get<2>(t); } component::age& get_age(component::type_component & t) { return std::get<3>(t); } std::string to_string(component::type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); return types[i]; } template<typename Predicat> std::vector<entity::id> get_entities(Predicat&& predicat) { std::vector<entity::id> entities; std::for_each(begin(internal::_types), end(internal::_types), [&entities, predicat](const auto type){ if (predicat(type)) { entities.push_back(std::get<0>(type)); } } ); return entities; } template<typename Collection> typename Collection::iterator get_component(Collection & components, entity::id id) { auto it = find_if(begin(components), end(components), [id](auto t){ return (std::get<0>(t) == id); }); return it; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 0 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, component::type::algue, 10, 0)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, t, 10, 0)); internal::_details.push_back(std::make_tuple(id, n, m)); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_types.erase(types_it, internal::_types.end()); } void remove_entities(std::vector<ecs::entity::id> const& ids) { for (auto id: ids) remove_entity(id); } void print() { if (internal::_types.empty()) { std::cout << "Aucune entite" << std::endl; return; } std::cout << internal::_types.size() << " entites: " << std::endl; std::cout << "\tid\ttype\tpv" << std::endl; auto print = [](auto t){ return ('\t' + std::to_string(std::get<0>(t)) + '\t' + to_string(std::get<1>(t)) + '\t' + std::to_string(std::get<2>(t))); }; std::transform(begin(internal::_types), end(internal::_types) - 1, std::ostream_iterator<std::string>(std::cout, "\n"), print); std::cout << print(internal::_types.back()) << std::endl; } } int main() { for (size_t i {}; i < 5; ++i) ecs::add_algue(); for (size_t i {}; i < 2; ++i) ecs::add_poisson(ecs::component::type::Sole, "Sole", false); for (size_t i {}; i < 2; ++i) ecs::add_poisson(ecs::component::type::Mérou, "Mérou", false); ecs::print(); for (size_t tour {}; tour < 21; ++tour) { std::cout << "===== Tour " << tour << " ======" << std::endl; // update pv and age for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); // pv if (ecs::is_algue_t(*type)) { ecs::get_point_vie(*type) += 1; } else { ecs::get_point_vie(*type) -= 1; } // age ecs::get_age(*type) += 1; } // eat const auto herbivores = ecs::get_entities(ecs::is_herbivore_t); const auto algues = ecs::get_entities(ecs::is_algue_t); if (!algues.empty()) { for (const auto herbivore: herbivores) { const auto herbivore_type = ecs::get_component(ecs::internal::_types, herbivore); if (herbivore_type != end(ecs::internal::_types) && ecs::get_point_vie(*herbivore_type) < 5) { std::uniform_int_distribution<size_t> algue_distribution(0, algues.size() - 1); const auto index { algue_distribution(ecs::internal::_random_engine) }; const auto algue = algues[index]; const auto algue_type = ecs::get_component(ecs::internal::_types, algue); ecs::get_point_vie(*herbivore_type) += 3; ecs::get_point_vie(*algue_type) -= 2; } } } const auto carnivores = ecs::get_entities(ecs::is_carnivore_t); for (const auto carnivore_id: carnivores) { const auto carnivore_type = ecs::get_component(ecs::internal::_types, carnivore_id); if (carnivore_type != end(ecs::internal::_types) && ecs::get_point_vie(*carnivore_type) < 5) // a faim ? { const auto carnivore_race = std::get<1>(*carnivore_type); const auto bons_petits_plats = ecs::get_entities( [carnivore_id, carnivore_race](const auto type){ const auto id = std::get<0>(type); const auto race = std::get<1>(type); return ( ecs::is_poisson(race) && (id != carnivore_id) && (race != carnivore_race) ); } ); if (!bons_petits_plats.empty()) { std::uniform_int_distribution<size_t> entities_distribution(0, bons_petits_plats.size() - 1); const auto index { entities_distribution(ecs::internal::_random_engine) }; const auto entity_id = bons_petits_plats[index]; const auto entity_type = ecs::get_component(ecs::internal::_types, entity_id); if (entity_type != end(ecs::internal::_types)) { if (ecs::is_algue_t(*entity_type)) { ecs::get_point_vie(*carnivore_type) += 3; ecs::get_point_vie(*entity_type) -= 2; } else { ecs::get_point_vie(*carnivore_type) += 5; ecs::get_point_vie(*entity_type) -= 4; } } } } } // deads std::vector<ecs::entity::id> entities_to_remove; for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); if (type != end(ecs::internal::_types) && (ecs::get_point_vie(*type) <= 0 || ecs::get_age(*type) > 20)) { entities_to_remove.push_back(entity); } } for (auto entity: entities_to_remove) { ecs::remove_entity(entity); } ecs::print(); } std::cout << "===== Fini ======" << std::endl; return 0; }
affiche :
9 entites: id type pv 0 Algue 10 1 Algue 10 2 Algue 10 3 Algue 10 4 Algue 10 5 Sole 10 6 Sole 10 7 Mérou 10 8 Mérou 10 ===== Tour 0 ====== 9 entites: id type pv 0 Algue 11 1 Algue 11 2 Algue 11 3 Algue 11 4 Algue 11 5 Sole 9 6 Sole 9 7 Mérou 9 8 Mérou 9 ===== Tour 1 ====== 9 entites: id type pv 0 Algue 12 1 Algue 12 2 Algue 12 3 Algue 12 4 Algue 12 5 Sole 8 6 Sole 8 7 Mérou 8 8 Mérou 8 ===== Tour 2 ====== 9 entites: id type pv 0 Algue 13 1 Algue 13 2 Algue 13 3 Algue 13 4 Algue 13 5 Sole 7 6 Sole 7 7 Mérou 7 8 Mérou 7 ===== Tour 3 ====== 9 entites: id type pv 0 Algue 14 1 Algue 14 2 Algue 14 3 Algue 14 4 Algue 14 5 Sole 6 6 Sole 6 7 Mérou 6 8 Mérou 6 ===== Tour 4 ====== 9 entites: id type pv 0 Algue 15 1 Algue 15 2 Algue 15 3 Algue 15 4 Algue 15 5 Sole 5 6 Sole 5 7 Mérou 5 8 Mérou 5 ===== Tour 5 ====== 8 entites: id type pv 0 Algue 16 1 Algue 14 2 Algue 14 3 Algue 16 4 Algue 16 6 Sole 7 7 Mérou 9 8 Mérou 9 ===== Tour 6 ====== 8 entites: id type pv 0 Algue 17 1 Algue 15 2 Algue 15 3 Algue 17 4 Algue 17 6 Sole 6 7 Mérou 8 8 Mérou 8 ===== Tour 7 ====== 8 entites: id type pv 0 Algue 18 1 Algue 16 2 Algue 16 3 Algue 18 4 Algue 18 6 Sole 5 7 Mérou 7 8 Mérou 7 ===== Tour 8 ====== 8 entites: id type pv 0 Algue 19 1 Algue 17 2 Algue 17 3 Algue 19 4 Algue 17 6 Sole 7 7 Mérou 6 8 Mérou 6 ===== Tour 9 ====== 8 entites: id type pv 0 Algue 20 1 Algue 18 2 Algue 18 3 Algue 20 4 Algue 18 6 Sole 6 7 Mérou 5 8 Mérou 5 ===== Tour 10 ====== 7 entites: id type pv 0 Algue 21 1 Algue 19 2 Algue 19 3 Algue 21 4 Algue 19 7 Mérou 9 8 Mérou 9 ===== Tour 11 ====== 7 entites: id type pv 0 Algue 22 1 Algue 20 2 Algue 20 3 Algue 22 4 Algue 20 7 Mérou 8 8 Mérou 8 ===== Tour 12 ====== 7 entites: id type pv 0 Algue 23 1 Algue 21 2 Algue 21 3 Algue 23 4 Algue 21 7 Mérou 7 8 Mérou 7 ===== Tour 13 ====== 7 entites: id type pv 0 Algue 24 1 Algue 22 2 Algue 22 3 Algue 24 4 Algue 22 7 Mérou 6 8 Mérou 6 ===== Tour 14 ====== 7 entites: id type pv 0 Algue 25 1 Algue 23 2 Algue 23 3 Algue 25 4 Algue 23 7 Mérou 5 8 Mérou 5 ===== Tour 15 ====== 7 entites: id type pv 0 Algue 26 1 Algue 24 2 Algue 24 3 Algue 26 4 Algue 24 7 Mérou 4 8 Mérou 4 ===== Tour 16 ====== 7 entites: id type pv 0 Algue 27 1 Algue 25 2 Algue 25 3 Algue 27 4 Algue 25 7 Mérou 3 8 Mérou 3 ===== Tour 17 ====== 7 entites: id type pv 0 Algue 28 1 Algue 26 2 Algue 26 3 Algue 28 4 Algue 26 7 Mérou 2 8 Mérou 2 ===== Tour 18 ====== 7 entites: id type pv 0 Algue 29 1 Algue 27 2 Algue 27 3 Algue 29 4 Algue 27 7 Mérou 1 8 Mérou 1 ===== Tour 19 ====== 5 entites: id type pv 0 Algue 30 1 Algue 28 2 Algue 28 3 Algue 30 4 Algue 28 ===== Tour 20 ====== Aucune entite ===== Fini ======
La gestion des points de vie et de la reproduction sont un peu lourde, ce qui m'a obligée de modifier en partie le code correspondant.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> #include <random> #include <tuple> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail_component = std::tuple<entity::id, name, is_male>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using point_vie = int8_t; using age = uint8_t; using type_component = std::tuple<entity::id, type, point_vie, age>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; std::random_device _random_device; std::default_random_engine _random_engine { _random_device() }; } bool is_algue(component::type t) { return (t == component::type::algue); } bool is_poisson(component::type t) { return !is_algue(t); } bool is_carnivore(component::type t) { return (t == component::type::Mérou || t == component::type::Thon || t == component::type::PoissonClown); } bool is_herbivore(component::type t) { return (t == component::type::Sole || t == component::type::Bar || t == component::type::Carpe); } bool is_algue_t(component::type_component const& t) { return is_algue(std::get<1>(t)); } bool is_poisson_t(component::type_component const& t) { return is_poisson(std::get<1>(t)); } bool is_carnivore_t(component::type_component const& t) { return is_carnivore(std::get<1>(t)); } bool is_herbivore_t(component::type_component const& t) { return is_herbivore(std::get<1>(t)); } component::point_vie& get_point_vie(component::type_component & t) { return std::get<2>(t); } component::age& get_age(component::type_component & t) { return std::get<3>(t); } bool random_sexe() { static std::uniform_int_distribution<size_t> algue_distribution(0,1); return (algue_distribution(ecs::internal::_random_engine) == 0); } component::type random_race() { static std::uniform_int_distribution<size_t> algue_distribution(0,6); return static_cast<component::type>(algue_distribution(ecs::internal::_random_engine)); } std::string to_string(component::type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); return types[i]; } template<typename Predicat> std::vector<entity::id> get_entities(Predicat&& predicat) { std::vector<entity::id> entities; std::for_each(begin(internal::_types), end(internal::_types), [&entities, predicat](const auto type){ if (predicat(type)) { entities.push_back(std::get<0>(type)); } } ); return entities; } template<typename Collection> typename Collection::iterator get_component(Collection & components, entity::id id) { auto it = find_if(begin(components), end(components), [id](auto t){ return (std::get<0>(t) == id); }); return it; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 0 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue(ecs::component::point_vie pv = 10) { const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, component::type::algue, pv, 0)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m, ecs::component::point_vie pv = 10) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, t, pv, 0)); internal::_details.push_back(std::make_tuple(id, n, m)); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_types.erase(types_it, internal::_types.end()); } void remove_entities(std::vector<ecs::entity::id> const& ids) { for (auto id: ids) remove_entity(id); } void print(size_t tour, size_t entites, size_t algues, size_t herbivores, size_t carnivores) { std::cout << tour << "\t\t" << entites << "\t\t" << algues << "\t\t" << herbivores << "\t\t" << carnivores << std::endl; } } int main() { std::cout << "Tour\t\tEntities\tAlgues\t\tHerbivores\tCarnivores" << std::endl; for (size_t i {}; i < 20; ++i) { const auto race = ecs::random_race(); if (ecs::is_algue(race)) ecs::add_algue(); else ecs::add_poisson(race, ecs::to_string(race), ecs::random_sexe()); } for (size_t tour {}; tour < 100; ++tour) { // update pv and age for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); // pv if (ecs::is_algue_t(*type)) { if (ecs::get_point_vie(*type) > 10) { ecs::get_point_vie(*type) /= 2; ecs::add_algue(ecs::get_point_vie(*type)); } else { ecs::get_point_vie(*type) += 1; } } else { ecs::get_point_vie(*type) -= 1; } // age ecs::get_age(*type) += 1; } const auto herbivores = ecs::get_entities(ecs::is_herbivore_t); const auto algues = ecs::get_entities(ecs::is_algue_t); ecs::print(tour, ecs::internal::_entities.size(), algues.size(), herbivores.size(), ecs::internal::_entities.size() - algues.size() - herbivores.size()); const auto poissons = ecs::get_entities(ecs::is_poisson_t); for (const auto poisson_id: poissons) { const auto type = ecs::get_component(ecs::internal::_types, poisson_id); if (type != end(ecs::internal::_types)) { const auto race = std::get<1>(*type); if (ecs::get_point_vie(*type) < 5) { // manger if (ecs::is_herbivore_t(*type) && !algues.empty()) { std::uniform_int_distribution<size_t> algue_distribution(0, algues.size() - 1); const auto index { algue_distribution(ecs::internal::_random_engine) }; const auto algue_id = algues[index]; const auto herbivore_type = ecs::get_component(ecs::internal::_types, poisson_id); const auto algue_type = ecs::get_component(ecs::internal::_types, algue_id); if ((herbivore_type != end(ecs::internal::_types)) && (algue_type != end(ecs::internal::_types))) { ecs::get_point_vie(*herbivore_type) += 3; ecs::get_point_vie(*algue_type) -= 2; } } else { const auto bons_petits_plats = ecs::get_entities( [poisson_id, race](const auto _type){ const auto _id = std::get<0>(_type); const auto _race = std::get<1>(_type); return ( ecs::is_poisson(_race) && (_id != poisson_id) && (_race != race) ); } ); if (!bons_petits_plats.empty()) { std::uniform_int_distribution<size_t> entities_distribution(0, bons_petits_plats.size() - 1); const auto index { entities_distribution(ecs::internal::_random_engine) }; const auto entity_id = bons_petits_plats[index]; const auto entity_type = ecs::get_component(ecs::internal::_types, entity_id); if (entity_type != end(ecs::internal::_types)) { if (ecs::is_algue_t(*entity_type)) { ecs::get_point_vie(*type) += 3; ecs::get_point_vie(*entity_type) -= 2; } else { ecs::get_point_vie(*type) += 5; ecs::get_point_vie(*entity_type) -= 4; } } } } } else { // bais... se reproduire std::uniform_int_distribution<size_t> poissons_distribution(0, poissons.size() - 1); const auto index { poissons_distribution(ecs::internal::_random_engine) }; const auto poisson_2_id = poissons[index]; const auto poisson_sex = ecs::get_component(ecs::internal::_details, poisson_id); const auto poisson_2_sex = ecs::get_component(ecs::internal::_details, poisson_2_id); const auto poisson_2_race = ecs::get_component(ecs::internal::_types, poisson_2_id); if ((poisson_sex != end(ecs::internal::_details)) && (poisson_2_sex != end(ecs::internal::_details)) && (poisson_2_race != end(ecs::internal::_types)) && (std::get<2>(*poisson_sex) != std::get<2>(*poisson_2_sex)) && (race != std::get<1>(*poisson_2_race))) { ecs::add_poisson(race, "Bebe", ecs::random_sexe(), 5); } } } } // deads std::vector<ecs::entity::id> entities_to_remove; for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); if (type != end(ecs::internal::_types) && (ecs::get_point_vie(*type) <= 0 || ecs::get_age(*type) > 20)) { entities_to_remove.push_back(entity); } } for (auto entity: entities_to_remove) { ecs::remove_entity(entity); } } std::cout << "===== Fini ======" << std::endl; return 0; }
affiche :
Tour Entities Algues Herbivores Carnivores 0 20 1 8 11 1 24 2 10 12 2 33 2 13 18 3 45 1 19 25 4 57 0 25 32 5 63 0 28 35 6 62 0 28 34 7 69 0 31 38 8 72 0 31 41 9 81 0 34 47 10 84 0 33 51 11 97 0 40 57 12 106 0 44 62 13 110 0 44 66 14 120 0 45 75 15 130 0 52 78 16 137 0 58 79 17 159 0 61 98 18 179 0 69 110 19 190 0 72 118 20 206 0 77 129 21 228 0 82 146 22 242 0 85 157 23 273 0 92 181 24 288 0 97 191 25 310 0 97 213 26 332 0 100 232 27 365 0 107 258 28 390 0 114 276 29 427 0 119 308 30 449 0 119 330 31 499 0 132 367 32 533 0 140 393 33 580 0 144 436 34 641 0 150 491 35 668 0 156 512 36 713 0 159 554 37 741 0 156 585 38 806 0 176 630 39 875 0 189 686 40 931 0 187 744 41 982 0 183 799 42 1019 0 177 842 43 1053 0 179 874 44 1093 0 175 918 45 1138 0 175 963 46 1189 0 163 1026 47 1239 0 158 1081 48 1282 0 143 1139 49 1329 0 132 1197 50 1319 0 109 1210 51 1341 0 92 1249 52 1321 0 69 1252 53 1302 0 47 1255 54 1244 0 24 1220 55 1163 0 3 1160 56 1137 0 1 1136 57 1113 0 1 1112 58 1090 0 1 1089 59 1045 0 0 1045 60 997 0 0 997 61 948 0 0 948 62 896 0 0 896 63 843 0 0 843 64 626 0 0 626 65 442 0 0 442 66 258 0 0 258 67 128 0 0 128 68 0 0 0 0 ===== Fini ======
Dans cette partie, on ajoute differents types de comportement sexuels.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> #include <random> #include <tuple> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail_component = std::tuple<entity::id, name, is_male>; enum class race { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using point_vie = int8_t; using age = uint8_t; using type_component = std::tuple<entity::id, race, point_vie, age>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; std::random_device _random_device; std::default_random_engine _random_engine { _random_device() }; } bool is_algue(component::race r) { return (r == component::race::algue); } bool is_poisson(component::race r) { return !is_algue(r); } bool is_carnivore(component::race r) { return (r == component::race::Mérou || r == component::race::Thon || r == component::race::PoissonClown); } bool is_herbivore(component::race r) { return (r == component::race::Sole || r == component::race::Bar || r == component::race::Carpe); } component::race& get_race(component::type_component & t) { return std::get<1>(t); } component::race get_race(component::type_component const& t) { return std::get<1>(t); } component::point_vie& get_point_vie(component::type_component & t) { return std::get<2>(t); } component::age& get_age(component::type_component & t) { return std::get<3>(t); } component::is_male is_male(component::detail_component const& d) { return std::get<2>(d); } component::is_male & is_male(component::detail_component & d) { return std::get<2>(d); } bool is_algue_t(component::type_component const& t) { return is_algue(get_race(t)); } bool is_poisson_t(component::type_component const& t) { return is_poisson(get_race(t)); } bool is_carnivore_t(component::type_component const& t) { return is_carnivore(get_race(t)); } bool is_herbivore_t(component::type_component const& t) { return is_herbivore(get_race(t)); } bool can_reproduce(component::type_component const& type_papa, component::type_component const& type_maman, component::detail_component const& detail_papa, component::detail_component const& detail_maman) { if (get_race(type_papa) != get_race(type_maman)) return false; return (is_male(detail_papa) != is_male(detail_maman)); } bool random_sexe(component::race r) { if (r == component::race::Carpe || r == component::race::Thon) { static std::uniform_int_distribution<size_t> algue_distribution(0,1); return (algue_distribution(ecs::internal::_random_engine) == 0); } else { return true; } } void update_sexe(component::type_component type_poisson, component::is_male & is_male_poisson) { if ((get_race(type_poisson) == component::race::Bar || get_race(type_poisson) == component::race::Mérou) && (get_age(type_poisson) >= 10)) { is_male_poisson = false; } } component::race random_race() { static std::uniform_int_distribution<size_t> algue_distribution(0,6); return static_cast<component::race>(algue_distribution(ecs::internal::_random_engine)); } std::string to_string(component::race r) { static const std::string races[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(r); assert(i < (sizeof(races) / sizeof(*races))); return races[i]; } template<typename Predicat> std::vector<entity::id> get_entities(Predicat&& predicat) { std::vector<entity::id> entities; std::for_each(begin(internal::_types), end(internal::_types), [&entities, predicat](const auto type){ if (predicat(type)) { entities.push_back(std::get<0>(type)); } } ); return entities; } template<typename Collection> typename Collection::iterator get_component(Collection & components, entity::id id) { auto it = find_if(begin(components), end(components), [id](auto t){ return (std::get<0>(t) == id); }); return it; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 0 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue(ecs::component::point_vie pv = 10) { const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, component::race::algue, pv, 0)); return id; } entity::id add_poisson(component::race t, component::name n, component::is_male m, ecs::component::point_vie pv = 10) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, t, pv, 0)); internal::_details.push_back(std::make_tuple(id, n, m)); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto t){ return (std::get<0>(t) == id); }); internal::_types.erase(types_it, internal::_types.end()); } void remove_entities(std::vector<ecs::entity::id> const& ids) { for (auto id: ids) remove_entity(id); } void print(size_t tour, size_t entites, size_t algues, size_t herbivores, size_t carnivores) { std::cout << tour << "\t\t" << entites << "\t\t" << algues << "\t\t" << herbivores << "\t\t" << carnivores << std::endl; } } int main() { std::cout << "Tour\t\tEntities\tAlgues\t\tHerbivores\tCarnivores" << std::endl; for (size_t i {}; i < 20; ++i) { const auto race = ecs::random_race(); if (ecs::is_algue(race)) ecs::add_algue(); else ecs::add_poisson(race, ecs::to_string(race), ecs::random_sexe(race)); } for (size_t tour {}; tour < 100; ++tour) { // update pv and age for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); // pv if (ecs::is_algue_t(*type)) { if (ecs::get_point_vie(*type) > 10) { ecs::get_point_vie(*type) /= 2; ecs::add_algue(ecs::get_point_vie(*type)); } else { ecs::get_point_vie(*type) += 1; } } else { ecs::get_point_vie(*type) -= 1; } // age ecs::get_age(*type) += 1; } const auto herbivores = ecs::get_entities(ecs::is_herbivore_t); const auto algues = ecs::get_entities(ecs::is_algue_t); ecs::print(tour, ecs::internal::_entities.size(), algues.size(), herbivores.size(), ecs::internal::_entities.size() - algues.size() - herbivores.size()); const auto poissons = ecs::get_entities(ecs::is_poisson_t); for (const auto poisson_id: poissons) { const auto type = ecs::get_component(ecs::internal::_types, poisson_id); if (type != end(ecs::internal::_types)) { const auto race = std::get<1>(*type); if (ecs::get_point_vie(*type) < 5) { // manger if (ecs::is_herbivore_t(*type) && !algues.empty()) { std::uniform_int_distribution<size_t> algue_distribution(0, algues.size() - 1); const auto index { algue_distribution(ecs::internal::_random_engine) }; const auto algue_id = algues[index]; const auto herbivore_type = ecs::get_component(ecs::internal::_types, poisson_id); const auto algue_type = ecs::get_component(ecs::internal::_types, algue_id); if ((herbivore_type != end(ecs::internal::_types)) && (algue_type != end(ecs::internal::_types))) { ecs::get_point_vie(*herbivore_type) += 3; ecs::get_point_vie(*algue_type) -= 2; } } else { const auto bons_petits_plats = ecs::get_entities( [poisson_id, race](const auto _type){ const auto _id = std::get<0>(_type); const auto _race = std::get<1>(_type); return ( ecs::is_poisson(_race) && (_id != poisson_id) && (_race != race) ); } ); if (!bons_petits_plats.empty()) { std::uniform_int_distribution<size_t> entities_distribution(0, bons_petits_plats.size() - 1); const auto index { entities_distribution(ecs::internal::_random_engine) }; const auto entity_id = bons_petits_plats[index]; const auto entity_type = ecs::get_component(ecs::internal::_types, entity_id); if (entity_type != end(ecs::internal::_types)) { if (ecs::is_algue_t(*entity_type)) { ecs::get_point_vie(*type) += 3; ecs::get_point_vie(*entity_type) -= 2; } else { ecs::get_point_vie(*type) += 5; ecs::get_point_vie(*entity_type) -= 4; } } } } } else { // bais... se reproduire std::uniform_int_distribution<size_t> poissons_distribution(0, poissons.size() - 1); const auto index { poissons_distribution(ecs::internal::_random_engine) }; const auto poisson_2_id = poissons[index]; const auto poisson_sex = ecs::get_component(ecs::internal::_details, poisson_id); const auto poisson_2_sex = ecs::get_component(ecs::internal::_details, poisson_2_id); const auto poisson_2_race = ecs::get_component(ecs::internal::_types, poisson_2_id); if ((poisson_sex != end(ecs::internal::_details)) && (poisson_2_sex != end(ecs::internal::_details)) && (poisson_2_race != end(ecs::internal::_types)) && (std::get<2>(*poisson_sex) != std::get<2>(*poisson_2_sex)) && (race != std::get<1>(*poisson_2_race))) { ecs::add_poisson(race, "Bebe", ecs::random_sexe(race), 5); } } // update sex const auto details = ecs::get_component(ecs::internal::_details, poisson_id); if (details != end(ecs::internal::_details)) { ecs::update_sexe(*type, ecs::is_male(*details)); } } } // deads std::vector<ecs::entity::id> entities_to_remove; for (auto entity: ecs::internal::_entities) { const auto type = ecs::get_component(ecs::internal::_types, entity); if (type != end(ecs::internal::_types) && (ecs::get_point_vie(*type) <= 0 || ecs::get_age(*type) > 20)) { entities_to_remove.push_back(entity); } } for (auto entity: entities_to_remove) { ecs::remove_entity(entity); } } std::cout << "===== Fini ======" << std::endl; return 0; }
Affiche :
Tour Entities Algues Herbivores Carnivores 0 20 2 10 8 1 27 4 13 10 2 33 4 15 14 3 37 3 17 17 4 45 3 23 19 5 48 0 27 21 6 44 0 25 19 7 54 0 31 23 8 54 0 30 24 9 58 0 31 27 10 57 0 30 27 11 67 0 33 34 12 72 0 32 40 13 74 0 35 39 14 85 0 37 48 15 88 0 36 52 16 95 0 40 55 17 102 0 39 63 18 111 0 43 68 19 125 0 48 77 20 135 0 48 87 21 142 0 47 95 22 138 0 46 92 23 142 0 46 96 24 145 0 44 101 25 149 0 43 106 26 146 0 36 110 27 153 0 34 119 28 160 0 35 125 29 157 0 28 129 30 163 0 25 138 31 156 0 16 140 32 148 0 8 140 33 142 0 3 139 34 136 0 0 136 35 130 0 0 130 36 125 0 0 125 37 120 0 0 120 38 115 0 0 115 39 88 0 0 88 40 58 0 0 58 41 41 0 0 41 42 23 0 0 23 43 0 0 0 0 ===== Fini ======
Dans cette partie, le but n'est pas simplement de discuter d’implémentation, mais plus le choix des composants du point de vue du jeu (game design).
Dans un ECS (tout au moins, dans la façon dont je comprends les ECS), les composants sont les données du jeu. La question est donc quelles données doivent (peuvent) être ensemble dans un composant et celle qui doivent (peuvent) être séparées dans des composants différents.
(Note : ces approches sont également valides si on considere que la première donnée est la liste des identifiants des entités et la seconde un composant ou une donnée quelconque. C'est également valide avec un composant et une liste de ressources dans un système.)
Pour bien comprendre, il est possible de représenter les composants dans un tableau a deux dimensions (similaire a une matrice creuse).
(Image provenant de http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/)
Le premier critère pour savoir si deux données peuvent appartenir au même composant est de savoir si une entité peut ou non avoir que l'une des deux données.
Par exemple, dans le javaquarium, toutes les entités “poisson” possèdent une données “nom” et “age”. Il est donc possible de les mettre dans un même composant. Au contraire, la donnée “points de vie” concerne toutes les entités “être vivant”, pas uniquement les entités “poisson”. Il sera donc logique de la placer dans un composant différent.
Il faut cependant modérer un peu ce critère. Supposons que 99 % des entités sont des poissons. Dans ce cas, cela signifie que si on regroupe les trois données (nom, age et points de vie), il y aura 1 % des composants qui auront deux données vides (nom et age, pour les entités “algue”). Or, séparer ces données dans deux composants aura un coût (mémoire et/ou performances). Il peut être intéressant d'avoir un seul composant qui regroupe toutes les données dans ce cas.
using Component = std::vector<A, B>;
Un autre point a prendre en compte et qui peut modifier ce premier critère est l'utilisation des données par les algorithmes. Dans le javaquarium, la donnée “age” sera utilisée a chaque tour (pour la mettre a jour, pour mettre a jour le sexe des poissons hermaphrodites, etc.). Au contraire, la donnée “nom” ne sera pas utilisée aussi souvent, elle sera par exemple utilisée dans un éditeur graphique. Dans cette situation, il peut être intéressant d'optimiser le cache pour les données utilisées a chaque tour et donc a séparer les données “nom” et “age” dans deux composants différents.
using ComponentA = std::vector<A>; using ComponentB = std::vector<B>;
Se pose alors le probleme de comment faire le lien entre les données dans des composants ? Plusieurs solutions sont possibles.
Dans la première approche, les données sont dans le même composant. Supprimer un composant supprime donc directement les deux données. L’accès au composant permet d'obtenir directement les deux données.
Dans le seconde approche, les données sont séparées dans deux composants et chaque composant est manipulés dans deux collections de même taille.
struct Data { using ComponentA = std::vector<A>; using ComponentB = std::vector<B>; void insert(); // insert dans A et B void remove(); // remove dans A et B ... };
Dans cette approche, il est nécessaire de conserver la cohérence des données, c'est a dire que les données correspondant au même indice dans les deux collections appartiennent a la même entité. Avec cette structure de données, il est assez facile de maintenir la cohérence, il suffit d'effectuer les mêmes opérations sur les deux collections en même temps (insertion, suppression, tri, etc.).
Lorsque l'on accède a un élément d'une de deux collections, il est possible d’accéder directement a la seconde collection, en utilisant l'indice ou avec deux boucles synchronisées (une autre approche est d’utiliser boost.zip_iterator). L'indice peut être déterminé via une variable (par exemple dans une boucle) ou avec std::distance
(complexité algorithmique constant).
// boucles synchronisées auto it_a = begin(v_a); auto it_b = begin(v_b); for (; it_a != end(v_a) && it_b != end(v_b); ++it_a, ++it_b) { ... } // une boucle avec indice for (auto it = begin(v_a); it != end(v_a); ++it) { const auto i = std::distance(begin(v_a), it); assert(i < v_b.size()); b = v_b[i]; }
Dans la troisièmement approche, les éléments de la seconde collection ne sont plus identifiés par rapport a leur position dans la collection, mais par l'ajout d'une donnée supplémentaire permettant d'identifier chaque élément (par exemple l'identifiant des entités).
using ComponentA = std::vector<A>; using ComponentB = std::vector<std::pair<entity_id, B>>;
Dans cette approche, il n'est pas nécessaire de synchroniser les données, il est possible d’ajouter ou de supprimer un élément dans la collection A sans modifier la collection B (et réciproquement). Par contre, les accès aux composants A et B nécessiteront de faire une recherche dans les deux collections (par exemple pour supprimer les composants lors de la suppression d'une entité).
Dans ce cas, plus une collection sera grande, plus le temps d’accès sera longue.
const auto it = std::find(begin(v_b), end(v_b), [entity_id](auto p){ return (p.second == entity_id); );
La dernière approche consiste a conserver une indirection dans la donnée A vers la donnée B, ce qui permet un accès direct, sans devoir faire une recherche (coûteuse) dans la collection B. Cette indirection peut être un pointeur (nu a priori), un itérateur ou un indice. (Ou encore un pointeur vers une classe de base d'une hiérarchie d'objets, mais cela répondrait a d'autres problématiques.)
using ComponentA = std::vector<std::pair<A, ComponentB::iterator>>; using ComponentB = std::vector<B>;
L’intérêt de cette approche est bien sur la rapidité d’accès vers les composants B depuis les composants A. Par contre, cela implique qu'il faut mettre a jour les indirections a chaque fois qu'un élément est ajouté ou supprimé dans la collection B.
Un critère intéressant a prendre en compte est l'efficace d'utilisation de la mémoire pour chaque approche. Le calcul de la taille totale des collections selon les différentes approches est simple. (Sans prendre en compte le surcoût lié a std::vector
).
// Approche 1 size = v.size() * (sizeof(A) + sizeof(B)); // Approche 2 size = v_a.size() * (sizeof(A) + sizeof(B)); // Approche 3 size = v_a.size() * sizeof(A) + v_b.size() * (sizeof(id) + sizeof(B)); // Approche 4 size = v_a.size() * (sizeof(A) + sizeof(void*)) + v_b.size() * sizeof(B);
Avec ces formules, il est intéressant de simuler quelques valeurs. Pour cela, je prend sizeof(id) == sizeof(void*) == 1
, un rapport v_b.size() / v_a.size()
(en ligne) compris entre 0,1 et 1 et sizeof(B) / sizeof(A)
(en colonne) qui vaut 1, 2, 4 et 8. Et comme c'est la différence entre les approches qui nous intéresse, je représente uniquement les approches 2, 3 et 4, en pourcentage de taille par rapport a l'approche 1. (Donc par exemple “50” signifie que l'approche prend deux fois moins de mémoire que l'approche 1).
Sans surprise, l'approche 2 consomme la même quantité de mémoire que l'approche 1. L'approche 3 sera plus intéressante lorsque les données seront volumineuses et qu'il y aura beaucoup plus d’entités que de composants. L’approche 4 sera plus intéressant quand les données seront volumineuses et qu'il y aura beaucoup d’entités avec ces composants.
Bien sur, ces analyses ne prennent pas en compte l’efficacité du cache ou la corrélation avec l'algorithme. Dans tous les cas, une analyse plus approfondie (profiling) sera nécessaire pour bien choisir l'approche a utiliser.
Voila les principales approches qui me semblent intéressantes pour un ECS. Mais je ne prétends pas être exhaustif. Par exemple, si la majorité des entités ont une majorité de composants ou si le nombre d’entités et de composants n'est pas important, il est possible de stocker toutes les données directement dans un tableau 2D, avec les entités en ligne et les systèmes en colonne.
Je me suis également limite aux collections triées, pour permettre les accès les plus rapides. Mais il est par exemple possible d'utiliser l'approche 4 sans trier les objets dans la collection B. Lors de la suppression d'un composant, on laisse simplement un “trou” dans la collection et on l'utilise lorsque l'on ajoute un nouveau composant. Dans ce cas, il y a un surcoût pour trouver les “trous” et un cache qui peut être moins efficace, mais on gagne par rapport au l’étape de “compactage” de la mémoire. Voire il est possible de conserver les composants B sans collection. Dans ce cas, on gagne sur les étapes de “compactage” et de gestion des “trous”, mais on perd au niveau du cache mémoire et des allocations et désallocations.
De plus, l'approche ECS sera intéressante surtout si le tableau contient plus de ligne que de colonne. Si par exemple, on a 1000 composants possible et 10 entités utilisant 2 ou 3 composants différents, alors chaque collection de composants pourra contenir 0 ou 1 éléments. Et la complexité d'un ECS sera au final pénalisant. Dans ce cas, une approche orientée objet classique, basée sur une hiérarchie de classes, pourra être une approche intéressante.
La conclusion est que le choix de l'approche ne peut pas être définie uniquement sur des considérations techniques d’implémentation. Le game design influencera beaucoup l'approche (ou plus probablement “les approches”) selon les données a manipuler et les traitements appliquées. Par exemple, on pourra choisir l'approche 4 pour le système de rendu (la majorité des entités sera visibles) et l'approche 3 pour les armes (si les ennemis humains portant une arme sont minoritaires).
Un bon moteur d'ECS, a mon sens, ne proposera donc pas une approche fixe dans son design, mais proposera aux développeurs de choisir le design qu'ils préfèrent utiliser, selon les besoin du game design. (Le C++ permet de proposer plusieurs approches sans surcoût a l’exécution. Ici, les classes de traits et de politique est une approche intéressante pour implémenter cela.)
Dans cette partie, je vais réécrire le code, mais en séparant a l’extrême les composants, c'est a dire en considérant que une information correspond a un composant.