====== Javaquarium ======
Le sujet de cette série d'exercice est disponible sur [[https://zestedesavoir.com/forums/sujet/447/javaquarium/|Javaquarium - Zeste de Savoir]]
Voici également quelques réalisations en C++ :
* https://zestedesavoir.com/forums/sujet/2241/appliquer-lecs-a-javaquarium/
* https://zestedesavoir.com/forums/sujet/2420/question-sur-le-javaquarium-en-c/
* https://openclassrooms.com/forum/sujet/javaquarium-c
* https://openclassrooms.com/forum/sujet/javaquarium-2-1
* https://openclassrooms.com/forum/sujet/javaquarium-3-p
* https://openclassrooms.com/forum/sujet/javaquarium-4
* https://openclassrooms.com/forum/sujet/le-javaquarium-de-mrnecrosi
* https://github.com/Richard-Degenne/Javaquarium
Pour mes implémentations, j'utilise une approche [[ecs|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.
===== Partie 1 : Peuplons notre Javaquarium =====
==== Exercice 1.1 : Remplissage de l'aquarium ====
Dans cette première partie, le bu est de simplement créer une liste de poisson et d'algues, et de les afficher.
#include
#include
#include
#include
#include
#include
namespace ecs {
namespace entity {
using id = size_t;
using entities = std::vector;
}
namespace component {
using name = std::string;
using is_male = bool;
using detail = std::pair;
using detail_component = std::pair;
enum class type { algue, poisson };
using type_component = std::pair;
}
namespace system {
using details = std::vector;
using types = std::vector;
}
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::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].
=== Contrat et encapsulation ===
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 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 ?".
==== Exercice 1.2 : un peu de diversité ====
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
#include
#include
#include
#include
#include
#include
namespace ecs {
namespace entity {
using id = size_t;
using entities = std::vector;
}
namespace component {
using name = std::string;
using is_male = bool;
using detail = std::pair;
using detail_component = std::pair;
enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe };
using type_component = std::pair;
}
namespace system {
using details = std::vector;
using types = std::vector;
}
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(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::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].
=== Typage fort ===
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 :
* surcoût à l'exécution (uniquement en debug pour ''assert'') ;
* on est prévenu du problème tardivement (à l'exécution, et sous condition que les tests unitaires couvrent ce problème) ;
* on peut faire une erreur (souvenez vous de la citation de Scott Meyers : "Facile a utiliser correctement, difficile a utiliser de façon incorrect").
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.
===== Partie 2 : Mange, tu ne sais pas qui te mangera =====
==== Exercice 2.1 : Miam miam miam ! ====
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 :
* l'utilisation des composants par les systèmes ;
* la création et la suppression des entités ;
* la création et la suppression des composants.
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
#include
#include
#include
#include
#include
#include
#include
namespace ecs {
namespace entity {
using id = size_t;
using entities = std::vector;
}
namespace component {
using name = std::string;
using is_male = bool;
using detail = std::pair;
using detail_component = std::pair;
enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe };
using type_component = std::pair;
}
namespace system {
using details = std::vector;
using types = std::vector;
}
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(t);
return types[i];
}
template
std::vector get_entities(Predicat&& predicat) {
std::vector 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::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 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::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 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 entities_distribution(0, ecs::internal::_entities.size() - 1);
std::uniform_int_distribution 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.
==== Exercice 2.2 : un peu de douceur dans ce monde de brutes ====
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
#include
#include
#include
#include
#include
#include
#include
#include
namespace ecs {
namespace entity {
using id = size_t;
using entities = std::vector;
}
namespace component {
using name = std::string;
using is_male = bool;
using detail_component = std::tuple;
enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe };
using point_vie = int8_t;
using type_component = std::tuple;
}
namespace system {
using details = std::vector;
using types = std::vector;
}
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(t);
return types[i];
}
template
std::vector get_entities(Predicat&& predicat) {
std::vector 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::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 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::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 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 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 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 ======
===== Partie 3 : Reproductions =====
==== Exercice 3.1 : Le désastre du vieillissement ====
Dans cette partie, on ajoute l'age des êtres vivants.
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace ecs {
namespace entity {
using id = size_t;
using entities = std::vector;
}
namespace component {
using name = std::string;
using is_male = bool;
using detail_component = std::tuple;
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;
}
namespace system {
using details = std::vector;
using types = std::vector;
}
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(t);
return types[i];
}
template
std::vector get_entities(Predicat&& predicat) {
std::vector 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::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 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::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 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 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 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 ======
==== Exercice 3.2 : Le miracle de la jeunesse ====
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
#include
#include
#include
#include
#include
#include
#include
#include
namespace ecs {
namespace entity {
using id = size_t;
using entities = std::vector;
}
namespace component {
using name = std::string;
using is_male = bool;
using detail_component = std::tuple;
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;
}
namespace system {
using details = std::vector;
using types = std::vector;
}
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 algue_distribution(0,1);
return (algue_distribution(ecs::internal::_random_engine) == 0);
}
component::type random_race() {
static std::uniform_int_distribution algue_distribution(0,6);
return static_cast(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(t);
return types[i];
}
template
std::vector get_entities(Predicat&& predicat) {
std::vector 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::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 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 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 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 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 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 ======
==== Exercice 3.3 : Mais… la sexualité des poissons est horriblement compliquée ! ====
Dans cette partie, on ajoute differents types de comportement sexuels.
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace ecs {
namespace entity {
using id = size_t;
using entities = std::vector;
}
namespace component {
using name = std::string;
using is_male = bool;
using detail_component = std::tuple;
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;
}
namespace system {
using details = std::vector;
using types = std::vector;
}
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 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 algue_distribution(0,6);
return static_cast(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(r);
assert(i < (sizeof(races) / sizeof(*races)));
return races[i];
}
template
std::vector get_entities(Predicat&& predicat) {
std::vector 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::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 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 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 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 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 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 ======
=== Comment choisir les composants ? ===
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).
{{http://cowboyprogramming.com/images/eyh/Fig-2.gif}}
(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.
{{ :ecs1.png |}}
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;
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.
{{ :ecs2.png |}}
using ComponentA = std::vector;
using ComponentB = std::vector;
Se pose alors le probleme de comment faire le lien entre les données dans des composants ? Plusieurs solutions sont possibles.
{{ :ecs3.png |}}
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;
using ComponentB = std::vector;
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;
using ComponentB = std::vector>;
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>;
using ComponentB = std::vector;
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).
{{ :ecs_mem.png |}}
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.
{{ :ecs4.png |}}
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.)
=== Réécriture de la partie 3.3 ===
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.
{{ :ecs5.png |}}
===== Partie 4 : Rendons notre simulateur d'aquarium pratique =====
==== Exercice 4.1 : Sauvez Willy ! ====
==== Exercice 4.2 : Un fichier pour les enregistrer tous… ====
==== Exercice 4.3 : … et dans un fichier les charger ====
==== Exercice 4.4 : Tourne, la roue tourne… ====
==== Exercice 4.5 : Le petit neveu ====