Javaquarium et conception

Venez aider un débutant en conception

a marqué ce sujet comme résolu.

Bonjour à tous,

Je suis en train de faire l'exercice Javaquarium, que je recommande de faire à tous ceux qui débutent en orienté objet, et pour changer un peu, j'ai décidé de faire un Entity Component System. J'ai encore du mal à tout bien comprendre, mais ça vient.

Par contre, j'ai une question sur la communication entre Entities. Si je veux par exemple indiquer que le poisson 42 vient de manger le poisson 7 (donc celui-ci subit 4PV de dégât), comment dois-je m'y prendre ? Je me suis renseigné un peu et j'ai commencé à étudier le pattern Signals / Slots et le pattern Mediator. Je pencherais plus pour le premier, le deuxième me semble être un peu lourd pour ça. Mais tant qu'à faire de la conception, autant bien la faire.

Je suis donc venu vous demander votre avis (et je pense que j'aurais d'autres questions dans le futur). Merci d'avance à tous,

informaticienzero

C'est un pas un system qui est censé gérer ça ? (javais lu des présentations des ECS, mais je n'ai pas encore mis en pratique)

lmghs

J'ai dit Entities pour essayer de me faire comprendre, mais dans la logique d'un ECS, oui ce sont les Systems qui gèrent tous. Mais j'avoue avoir du mal à visualiser comment je peux transmettre ce genre d'informations. Ou bien qu'un poisson s'est reproduit.

Le principe d'un ECS serait bien ici de passer par les systèmes : en fait il y aurait un système qui parcourt tes poissons et regarde quelle action il peuvent faire, et admettons qu'un poisson puisse en manger un autre, alors l'ECS "envoie" le signal (ou message) "le poisson 7 mange le poisson 6" au système qui gère les points de vie via un système de signaux/slots ou simplement de messaging ;) et c'est ce système en charge des points de vie qui modifie la vie du poisson mangé.

Ton code me chiffonne : si je comprends bien ta boucle for serait dans ton système mais d'ou vient ta fonction update ? j'espère qu'elle est dans le système, sinon tu fais tout le contraire de ce que tu devrais faire ! c'est au système de gérer les actions et pas à une quelconque autre entité ! A ce moment, pourquoi as tu séparé ta fonction update de ta boucle ? ^^ Si ton système à pour unique but d'"update" les poissons (soit dit au passage, update est vraiment peu descriptif comme nom, on le ressort à toutes les sauces possibles et imaginables :p ) autant le nommer UpdateSystem et de mettre la boucle et les traitements dans sa méthode principale ;)

Après tu peux faire le choix d'implémenter la base d'un système template qui contient juste la liste des entités a update, la boucle sur les entités, et un appel à un classe de traits/policy fournie à la création du système (via un paramètre template) : ça permet d'avoir des systèmes modulaires et pour créer un nouveau système, il te suffit juste de définir un comportement via la création d'une nouvelle policy et de créer un système avec celle-ci. Personnellement c'est le choix de design que j'ai fait pour avoir un ECS puissant et modulaire, mais pour un plus petit ECS qui suffit à cet ex, avec 2 ou 3 systèmes grand max, c'est peut être pas nécessaire, à toi de voir !

Ton code me chiffonne : si je comprends bien ta boucle for serait dans ton système mais d'ou vient ta fonction update ? j'espère qu'elle est dans le système, sinon tu fais tout le contraire de ce que tu devrais faire !

LeB0ucEtMistere

Oui elle est bien dans un système. Mais du coup, ma boucle est pas terrible. Le code devrait être plus comme ça, étant donnée qu'une Entité est (dans mon cas) un simple entier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
for (auto entity : entities)
{
    if (HealthSystem::get_pv(entity) >= HEALTH_PV_HUNGRY)
    {
        // se reproduire
        Entity e = random_fish();
        if (entity != e && RaceSystem::are_same(entity, e))
        {
            Entity new_fish = world.create_entity();
            RaceSystem::set_race(new_fish, RaceSystem::get_race(entity));
            // etc
        }
    }
    else
    {
        // se nourrir
    }
}

Après tu peux faire le choix d'implémenter la base d'un système template qui contient juste la liste des entités a update, la boucle sur les entités, et un appel à un classe de traits/policy fournie à la création du système (via un paramètre template) : ça permet d'avoir des systèmes modulaires et pour créer un nouveau système, il te suffit juste de définir un comportement via la création d'une nouvelle policy et de créer un système avec celle-ci. Personnellement c'est le choix de design que j'ai fait pour avoir un ECS puissant et modulaire, mais pour un plus petit ECS qui suffit à cet ex, avec 2 ou 3 systèmes grand max, c'est peut être pas nécessaire, à toi de voir !

LeB0ucEtMistere

Ça m'intéresse. Aurais-tu un rapide exemple à nous montrer ?

Ok et donc si je comprends bien ton code, c'est depuis le système que tu récupères les composants de ton entité … Je suis pas fan de ce design (mais là c'est surtout personnel je pense ^^) moi j'ai toujours préféré définir les entités comme plus qu'un id, à savoir un id et un tableau de components : d'une part l'entité possède les composants (et ça ça me semble primordial) et d'autre part si je veux sauvegarder l'état de mon système, j'ai juste à enregistrer mes entités dans un fichier ;) Donc je trouve ça plus "joli" mais ton système est surement tout aussi viable. Je ne peux pas te dire étant donné que je ne l'ai pas en intégralité sous les yeux.

bien sur : Grosso modo j'ai ma classe System de cette forme :

1
2
template <class Policy, class... Components>
class System : public ISystem

Avec Policy la classe de trait et Components un variadic pour décrire les composants à gérer.

après pour créer un système je fais :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct MovementPolicy
{
    static void computeEntity(sgf::System<MovementPolicy, PositionComponent>::EntityWrapper& wrapper)
    {
        auto comp = wrapper->getComponent<PositionComponent>("pos");
        comp.setData(comp.getData().x+1,comp.getData().y+1);
    }
};

typedef sgf::System<MovementPolicy, PositionComponent> MovementSystem;

Et voilà, dès que je créerai un MovementSystem, il saura exactement ce qu'il doit faire et sur quels components agir ! :) Bon après il y a bien plus de code derrière ça (template oblige) mais l'idée est là

Ok et donc si je comprends bien ton code, c'est depuis le système que tu récupères les composants de ton entité … Je suis pas fan de ce design (mais là c'est surtout personnel je pense ^^) moi j'ai toujours préféré définir les entités comme plus qu'un id, à savoir un id et un tableau de components : d'une part l'entité possède les composants (et ça ça me semble primordial) et d'autre part si je veux sauvegarder l'état de mon système, j'ai juste à enregistrer mes entités dans un fichier ;) Donc je trouve ça plus "joli" mais ton système est surement tout aussi viable. Je ne peux pas te dire étant donné que je ne l'ai pas en intégralité sous les yeux.

LeB0ucEtMistere

Pour être tout à fait honnête, j'ai choisi ça parce que on lit beaucoup des deux avis sur Internet. Certains pensent qu'une Entité est un entier point final, d'autres pensent comme toi, Entité + liste de Composants. Je t'avoue que mon choix sur la question n'est pas du tout tranché pour l'instant. Il faudrait faire des tests et comparer s'il y a vraiment une solution plus pratique que l'autre ou bien si le choix ne dépend que des préférences du programmeur.

Pour la partie sur les templates, je réponds dès que je peux.

La théorie voudrait qu'une entité soit effectivement un simple identifiant. Mais du coup ça oblige a créer un wrapper entre une entité et ses composants, dans ce genre la

1
2
3
4
5
struct EntityComponent
{
    Entity entity;
    std::map<ComponentType, Component*> components;
}

Et ça n'apporte pas grand chose, finalement …

Au pire que l'entité soit un simple id ou pas, tant qu'on n'entre pas dans des soucis d'optimisation majeures avec des pools d'entités et de composants, ça pose pas vraiment souci. Personnellement je préfère mon design car il marque très fortement le concept de : un composant appartient à une entité. Après certaines personnes préfèrent le design : un composant appartient à un pool de composants capable de les allouer très vite et l'entité possède des ref vers ces composants qui la caractérise. Mais je trouve que finalement le couplage reste fort et donc que ça n'apporte rien à part de la complexité. Après c'est intéressant quand on veut optimiser mais dans l'idée ça complexifie plus qu'autre chose je trouve.

Salut à tous,

Me revoilà, avançant petit à petit. Aujourd'hui, j'ai travaillé sur le Store faisant la liaisons entre Entités et Composants. En voici la définition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#ifndef STORE_HPP_INCLUDED
#define STORE_HPP_INCLUDED

#include <map>
#include <set>

#include "Component.hpp"
#include "Entity.hpp"

namespace infozero
{
    namespace store
    {
        using Component = infozero::component::Component;
        using Entity = infozero::entity::Entity;

        class Store
        {
            public:
                Store() = default;
                Store(Store const &) = delete;
                Store(Store &&) = delete;
                Store & operator=(Store const &) = delete;

                bool add(Entity, Component *);
                std::size_t count(Entity) const;
                bool has(Entity) const;
                bool has(Entity, Component const &);
                bool remove(Entity);

            private:
                std::map<Entity, std::set<Component *>> m_store;
        };
    }
}

#endif

Mais voilà, pour implémenter la fonction has(Entity, Component cosnt &), j'ai du passer par un const_cast. Est-ce si crade que ça et si oui, quelle solution préconisez-vous ? Merci d'avance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief Check is an entity is present in the Store
* @param entity An Entity
* @return True is entity is indeed in the Store, false otherwise.
*/
bool Store::has(Entity entity) const
{
    return this->m_store.count(entity);
}

bool Store::has(Entity entity, Component const & component)
{
    if (this->has(entity))
    {
        auto it = this->m_store.find(entity)->second;
        if (it.find(&const_cast<Component&>(component)) != it.end())
        {
            return true;
        }
    }
    return false;
}

EDIT : j'ai aussi pensé à cette solution pour la remplacer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
* @brief Return a set of all components associated to an entity. Entity must be in Store.
* @param entity A valid entity (e.g presents in Store).
* @return A set of components linked to the entity.
*/
std::set<Component *> Store::get(Entity entity) const
{
    assert(this->has(entity) && "Store::get : entity is not present in Store");
    return this->m_store.find(entity)->second;
}

@Ricocotam: ton code est strictement équivalent au sien (et de mon avis le sien est plus clair).

Oui c'est assez crade. Mais il faut C++14 pour y remédier, c'est un défaut dans l'interface de std::set. En effet, même si le std::set<Component*> est const, les éléments qu'on manipule sont donc des Component*const, le Component auquel on accède n'est donc pas const du tout.

Par ailleurs on peut le simplifier quelque peu (et rendre cette méthode constante tant qu'à faire).

1
2
3
4
5
6
7
8
9
bool Store::has(Entity entity, Component const& component) const
{
    auto it_map = this->m_store.find(entity);

    if (it_map == end(this->m_store))
        return false;

    return it_map->second.count(&const_cast<Component&>(component));
}

En C++14, ce const_cast n'est plus nécessaire. En l'occurrence il n'est pas dangereux ici car tu ne fais que manipuler des adresses et ne modifies pas l'objet.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte