Outils d'utilisateurs

Outils du Site


javaquarium

Javaquarium

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.

main.cpp
#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].

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

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.

main.cpp
#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].

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.

main.cpp
#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.

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.

main.cpp
#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 ======

Partie 3 : Reproductions

Exercice 3.1 : Le désastre du vieillissement

Dans cette partie, on ajoute l'age des êtres vivants.

main.cpp
#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 ======

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.

main.cpp
#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 ======

Exercice 3.3 : Mais… la sexualité des poissons est horriblement compliquée !

Dans cette partie, on ajoute differents types de comportement sexuels.

main.cpp
#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 ======

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).

(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.)

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.

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

javaquarium.txt · Dernière modification: 2016/05/30 20:08 par gbdivers