Partage
  • Partager sur Facebook
  • Partager sur Twitter

ECS : signature des composants via bitset

    12 mai 2018 à 16:24:09

    Hello,

    Je viens vers vous car j'ai un souci concernant la mise en place d'une signature composant via un bitset<64> dans un ECS. Ici le problème ne concerne pas les opérations à effectuer sur les bits (ajouter/supprimer/comparer), mais plutôt de savoir où stocker les signatures ?

    Dans l'idée, voilà ce que je pense faire :

    • Au lieu d'utiliser un simple type pour Entity (actuellement size_t), créer une class/struct  Entity contenant un ID et un bitset correspondant aux composants qui la compose
    • Chaque struct de composant doit contenir son propre bitset constant permettant de l'identifier
    • Les différents systèmes (pas encore codé pour le moment) analyseront le bitset d'une Entity pour savoir si elle doit être prise en compte

    J'ai aussi une classe template Components représentant un simple vector, permettant le maintiens de tous les composants d'un même type ensemble. Cette classe a pour mission de créer des composants en leur associant l'ID de l'Entity correspondante.

    Maintenant, j'avoue que je suis un peu dans le brouillard concernant l'organisation de ces signatures de bits, si jamais quelqu'un a une idée sur la quetion je suis preneur ^^ .

    N.B.: ici un exemple pour illustrer

    Merci.

    -
    Edité par Guit0Xx 12 mai 2018 à 16:26:51

    • Partager sur Facebook
    • Partager sur Twitter

    ...

      12 mai 2018 à 17:17:39

      Salut,

      En fait, l'idée de la clé, c'est que chaque bit représente la présence (ou l'absence) d'un composant particulier qui est rattaché à une entité bien particulière. 

      Tu peux, effectivement, envisager de faire en sorte que chaque entité soit représentée par une paire de donnée composée de son identifiant (un size_t ) et du bitset représentant les composants qui y sont rattachés.

      Il y a cependant un aspect à prendre en compte, si tu crées une structure qui regroupe les deux:

      la... notion d'entité que tu obtiens doit forcément se retrouver avec... une sémantique d'entité, avec tout ce que cela comporte d'interdiction de la copie et de l'assignation, vu que tu ne voudrais en aucun cas te retrouver dans une situation dans laquelle, à un instant de l'exécution T, tu te retrouverais avec... deux données représentant la même entité (et tu voudrais encore moins que la clé associée à chacune de ces deux entités "identiques" soit différente)

      Tu me diras que, de toutes facons, tes clés ne devront jamais être copiées ni assignées, pour, justement, éviter le risque de se retrouver avec deux clés associées à une entité identique mais présentant des valeurs différentes, et que l'on n'a donc pas grand chose à perdre à créer cette structure.

      La seule chose, c'est qu'il faut également éviter autant que possible toute allocation dynamique de la mémoire, au moment de créer l'entité.  Parce que c'est la merde à gérer, et parce que cela demande beaucoup de temps d'y avoir recours.

      Mais, il n'y a rien non plus qui t'empêche d'avoir, d'un coté, un EntityHolder, qui se contente de maintenir la liste des entités qui existent, et de l'autre un KeyHolder qui se contente de maintenir à jour la liste des clés pour chacune de ces entités ;)  Pour autant que les clés soient systématiquement créées et détruites en même temps que les entités (et que les logiques d'ajout / de suppression soient identiques pour les deux listes), il ne devrait pas être *** trop difficile *** de maintenir ces deux listes cohérentes et synchronisées; même si le travail serait sans doute plus facile en n'ayant qu'une seule liste  ;)

      Pour ce qui est de l'association des différents bits de ta clé aux différents types de composant, l'idée est de disposer d'un système qui te permette de faire le "mapping" type de composant <--> index du bit correspondant "assez facilement".

      Une "simple" map proche de

      std::map<std::type_index, size_t> mapper;

      (où la clé correspond au type_index de chaque composant et où le size_t correspond à l'indice du bit équivalent dans la clé) associée à une fonction permettant de récupérer l'indice du bit dans la clé en fonction du type de composant recherché devrait pouvoir faire l'affaire.  En ajoutant un petit helper du genre de

      template <typename T, typename ... Others>
      struct FootprintCreator{
          static std::bitset<64> create(){
              return FootprintCreator<T>::create() | FootprintCreator<Others...>::create();
          }
      };
      template <typename T>
      struct FootprintCreator{
          static std::bitset<64> create(){
              std::bitset<64> key;
              key.set(indexOf(typeid(T)); // si indexOf renvoie l'indice
              return key;                 // du bit correspondant
                                          // au composant
          }
      }

      tu auras tout ce qu'il faut pour pouvoir vérifier si la "clé" d'une entité particulière correspond à "l'emprunte" qu'elle doit présenter pour que l'entité soit utilisable par un système particulier, qui a besoin de la présence de composant bien particulier pour pouvoir fonctionner.

      La seule chose (et sans doute la plus difficile à mettre au point), c'est que chaque composant devra être enregistré auprès de ce "mapper".  Ah, et j'oubliais: chaque composant doit être unique.  Pas de bol, si tu as deux alias de type, par exemple

      using PV = size_t;
      using MP = size_t;

      tu risque d'être mal barre, car, tels quels PV et MP typeid(MP) et typeid(PV) seront tous les deux égaux à... typeid(size_t)...  Ce qui n'est pas l'idéal :P. 

      Tu devras donc trouver un moyen pour lever l'ambiguité, par exemple, avec l'aide d'un tag supplémentaire, sous une forme proche de

      template <typename T, // Le type utilisé comme valeur du composant
                typename TAG, // un tag de levée d'ambiguité
               >
      struct ComponentType{
           Id id; // l'identifiant de l'entité auquel est rattaché
                  // le composant
           T value; // la valeur associée à ce composant
      };
      
      struct mpTag{};
      struct pvTag{};
      using MPComponent = ComponentType<size_t, mpTag>;
      using LiveComponent = ComponentType<size_t, pvTag>;

      De cette manière typeid(MPComponent) et typeid(LiveComponent) seront bel et bien différents ;)

      -
      Edité par koala01 12 mai 2018 à 17:18:04

      • Partager sur Facebook
      • Partager sur Twitter
      Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
        12 mai 2018 à 18:02:32

        koala01 a écrit:

        la... notion d'entité que tu obtiens doit forcément se retrouver avec... une sémantique d'entité

        Effectivement ^^ .

        koala01 a écrit:

        La seule chose, c'est qu'il faut également éviter autant que possible toute allocation dynamique de la mémoire, au moment de créer l'entité.

        Ça marche. On va éviter de se créer des problèmes inutilement alors :p . Bon ça va, en règle générale j'ai tendance à les éviter, sauf obligations ou cas particuliers.

        koala01 a écrit:

        Mais, il n'y a rien non plus qui t'empêche d'avoir, d'un coté, un EntityHolder, qui se contente de maintenir la liste des entités qui existent, et de l'autre un KeyHolder qui se contente de maintenir à jour la liste des clés pour chacune de ces entités ;)

        J'avoue que l'idée est plaisante et pas difficile à mettre en place, je note !

        koala01 a écrit:

        Pour ce qui est de l'association des différents bits de ta clé aux différents types de composant, l'idée est de disposer d'un système qui te permette de faire le "mapping" type de composant <--> index du bit correspondant "assez facilement".

        Cela m'avait déjà traversé l'esprit, mais pas pensé à l'ambiguïté possible ! Je vais me pencher sur cette idée et essayer de mettre ça en place. En attendant, je vais en profiter pour réviser un peu les variadics que j'ai très peu exploré ^^ .

        Bon et bien il y a du pain sur la planche ! Un gros merci pour toutes ces explications ;) .

        -
        Edité par Guit0Xx 12 mai 2018 à 18:06:12

        • Partager sur Facebook
        • Partager sur Twitter

        ...

          13 mai 2018 à 0:55:25

          Un petit détail aussi, la liste des composants qui forment une entité peut varier dans le temps. Prenons par exemple un RPG 3D et considérons le cas d'une épée:

          • L'épée se trouve sur le sol, elle fait partie du décor, elle possède un composant position qui sera utilisé par le moteur de rendu graphique pour la dessiner.
          • L'épée se trouve dans l'inventaire du joueur, elle ne possède pas de composant position.
          • L'épée se trouve dans la main du joueur, elle ne possède toujours pas de composant position, mais un une information pour dire que c'est l'arme que le joueur a en main

          Bien sùr le joueur peut changer d'arme (transaction avec l'inventaire), il peut jeter cette épée ou bien la prendre sur le sol, lors d'un combat, il peut être désarmé. On peut aussi imaginer des trucs plus subtils comme par exemple, l'assassin fourbe qui aura enduit son épée de poison, le magicien qui l'enchante ... Comment traduire tout ça ?

          -
          Edité par int21h 13 mai 2018 à 0:56:13

          • Partager sur Facebook
          • Partager sur Twitter
          Mettre à jour le MinGW Gcc sur Code::Blocks. Du code qui n'existe pas ne contient pas de bug
            13 mai 2018 à 1:50:22

            C'est justement l'avantage de la clé...

            Lorsque tu rajoute un composant (la position, par exemple), tu crées un composant "position" associé à l'identifiant de l'épée, et tu mets à 1 le bit qui correspond au composant "position" dans la clé

            Quand elle est ramassée (et mise dans l'inventaire), tu détruit le composant "position" associé à l'identifiant de l'épée, et tu mets à 0 le bit correspondant à ce composant dans la clé.

            Parallèlement à cela, tu crée un composant "dans l'inventaire" qui est associé à cette épée, et tu met à 1 le bit qui correspond à ce composant dans la clé.

            Quand tu décide d'équiper ton personnage de l'épée, tu détruit le composant "dans l'inventaire" qui est associé à cette épée, et tu remet à 0 le bit correspond  à ce composant.

            Et, parallèlement, tu crées un composant "dans la main"  associé à l'épée mets à 1 le bit qui correspond au composant "dans la main", et ainsi de suite...

            L'idée est vraiment toute simple finalement : chaque fois que tu associe un type de composant à une entité, tu met le bit  correspondant à ce composant (pour la clé correspondant à l'entité concernée) à 1. 

            Chaque fois que tu détruit un type de composant (associé à une entité), tu met le bit correspondant à ce composant (pour la clé correspondant à l'entité concernée) à 0.

            Chaque fois que tu veux savoir si un type de composant est associé à une entité particulière -- pour éviter le temps de recherche du composant, qui peut être coûteux surtout s'il n'existe pas -- tu commences par vérifier la clé associée à cette entité particulière, pour savoir si elle dispose ou on de ce composant.

            Le reste, c'est un principe de masque "classique", car, si tu veux -- par exemple -- les armes qui ont la capacité de provoquer des dégats tranchants et des dégats magique, ce sera comme si tu avais un code proche de

            /* je n'aime pas les define, mais c'est pour bien te montrer */
            #define SLASH_DAMAGE 1
            #define MAGIC_DAMAGE 2
            #define BLUNT_DAMAGE 4
            #define MAGIC_POINT  8

            et tu pourras donc faire une vérification du genre de

            footPrint = SLASH_DAMAGE | MAGIC_DAMAGE;
            if(key & footPrint == footPrint){
               /* il y a des dégats magiques et des dégats coupants */
            }
            footprint2 = MAGIC_POINT | MAGIC_DAMAGE;
            if(key & footPrint2 == footPrint2){
               /* il y a des dégats magiques, et une utilisation de point de magie
                */
            }

            La seule différence, ici, c'est que l'on n'utilise pas les define d'une part et un int de l'autre pour arriver à notre but, mais un système "centralisé" qui définit l'indice des bits pour chaque composant dans un bitset d'une part et... un bitset de l'autre ;)


            -
            Edité par koala01 13 mai 2018 à 1:50:44

            • Partager sur Facebook
            • Partager sur Twitter
            Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
              16 mai 2018 à 1:07:43

              Bon, je reviens suite à un souci de compréhension de récursivité.

              Si je prends l'exemple d'une fonction classique affichant simplement des éléments :

              #include <iostream>
              
              /////////////////////////
              template<typename T>
              std::ostream& printer(std::ostream& os, const T& t)
              {
                  os << "printer(T) : " << t;
                  return os;
              }
              
              template<typename T, typename... Others>
              std::ostream& printer(std::ostream& os, const T& t, const Others&... rest)
              {
                  os << "printer(rest...) : " << t << '\n';
                  return printer(os, rest...);
              }
              
              /////////////////////////
              int main()
              {
                  printer(std::cout, 'a', 1, 3.14f, "hello");
              
                  return 0;
              }

              On comprends bien en voyant l'output que la version variadic est d'abord appelée 3 fois, puis pour le dernier élément, c'est la version 1 argument qui est appelée :

              printer(rest...) : a
              printer(rest...) : 1
              printer(rest...) : 3.14
              printer(T) : hello

              Maintenant, j'ai testé un peu le FootprinterCreator et pas de souci il fait son boulot. Le truc c'est que le fonctionnement m'échappe complètement et en voyant l'output je suis encore plus paumé :

              FootprintCreator<T, Others...>::create()
              FootprintCreator<T, Others...>::create()
              FootprintCreator<T>::create()
              FootprintCreator<T>::create()
              FootprintCreator<T>::create()
              0000000000000000000000000000000000000000000000000000000000000111

              Ce qui me perturbe c'est de voir d'abord 2 appels de la version variadic puis 3 appels de la version normale.

              Si besoin, le code qui va avec :

              #include <iostream>
              #include <bitset>
              #include <typeinfo>
              #include <typeindex>
              #include <map>
              
              ///////////////////////// COMPONENTS
              struct Position
              {
                  float x{};
                  float y{};
              };
              
              struct Velocity
              {
                  float x{};
                  float y{};
              };
              
              struct Color
              {
                  uint8_t R{};
                  uint8_t G{};
                  uint8_t B{};
                  uint8_t A{};
              };
              
              ///////////////////////// MAPPER : [Component type] => bit index
              std::map<std::type_index, const size_t> mapper{
                  {typeid(Position), 0},
                  {typeid(Velocity), 1},
                  {typeid(Color)   , 2}
              };
              
              ///////////////////////// FOOTPRINT CREATOR
              template<typename... Others>
              struct FootprintCreator;
              
              template<typename T, typename... Others>
              struct FootprintCreator<T, Others...>
              {
                  static std::bitset<64> create()
                  {
                      std::cout << "FootprintCreator<T, Others...>::create()" << '\n';
                      return FootprintCreator<T>::create() | FootprintCreator<Others...>::create();
                  }
              };
              
              template<typename T>
              struct FootprintCreator<T>
              {
                  static std::bitset<64> create()
                  {
                      std::cout << "FootprintCreator<T>::create()" << '\n';
                      std::bitset<64> key{};
                      key.set(mapper[typeid(T)]);
                      return key;
                  }
              };
              
              ///////////////////////// MAIN
              int main()
              {
                  std::cout << FootprintCreator<Position, Velocity, Color>::create() << '\n';
              
                  return 0;
              }


              Si jamais quelqu'un à une explication sur le comportement des appels, je suis preneur ^^ .


              -
              Edité par Guit0Xx 16 mai 2018 à 1:21:54

              • Partager sur Facebook
              • Partager sur Twitter

              ...

                16 mai 2018 à 1:47:54

                Si tu affiches avec __PRETTY_FUNCTION__ tu verrais mieux la pile d'appel.

                Si c'est l'ordre d'affichage qui te gènes, sache que l'ordre d'évaluation de la plupart des opérateurs est pas définit. http://en.cppreference.com/w/cpp/language/eval_order

                Avec clang, le résultat est (https://wandbox.org/permlink/vaMYNlkLdVcUq3kG):

                static std::bitset<64> FootprintCreator<Position, Velocity, Color>::create() [Others = <Position, Velocity, Color>]
                static std::bitset<64> FootprintCreator<Position>::create() [Others = <Position>]
                static std::bitset<64> FootprintCreator<Velocity, Color>::create() [Others = <Velocity, Color>]
                static std::bitset<64> FootprintCreator<Velocity>::create() [Others = <Velocity>]
                static std::bitset<64> FootprintCreator<Color>::create() [Others = <Color>]
                0000000000000000000000000000000000000000000000000000000000000111
                
                • Partager sur Facebook
                • Partager sur Twitter
                  16 mai 2018 à 2:42:21

                  D'accord ! Moi qui pensais que tout était toujours évalué dans le même sens (droite vers gauche)... Donc,  sauf dans le cas de certains opérateurs ou indiqué explicitement entre parenthèse, le compilo' fait ce qu'il veut. C'est noté, du coup je vais aussi mettre wandbox de côté, c'est bien sympa pour tester.

                  Ah c'est bien pratique __PRETTY_FUNCTION__ ! C'est plus clair maintenant.

                  Merci.

                  -
                  Edité par Guit0Xx 16 mai 2018 à 2:46:53

                  • Partager sur Facebook
                  • Partager sur Twitter

                  ...

                    16 mai 2018 à 19:47:51

                    Ceci dit, tu te fous pas mal de l'ordre dans lequel les paramètres templates seront définis dans le cas présent, vu que chaque paramètre template sera associé à un bit particulier de ta clé...

                    que le calcul soit effectué sous la forme de

                    00000001
                    00000010
                    00000100
                    --------
                    00000111

                     sous la forme de

                    00000100
                    00000010
                    00000001
                    -------
                    00000111

                    ou sous n'importe quelle autre forme que pourrait autoriser la combinatoire de trois éléments, le résultat restera strictement le même et tu auras toujours un GO/NO GO  sur 00000111 (j'ai un peu écrémé la taille de la clé par facilité ;) )

                    Tu fais ton test sur la clé (00000111), et si la clé de ton entité est validée sur cette emprunte tu travaille sur les composants dans l'ordre qui t'intéresse ;)


                    • Partager sur Facebook
                    • Partager sur Twitter
                    Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                      16 mai 2018 à 20:22:02

                      Yes pour ça pas de souci ^^ , en fait ce qui me dérangeait le plus c'est ça :

                      return FootprintCreator<T>::create() | FootprintCreator<Others...>::create();

                      Je sais pas pouquoi je n'arrivais pas à me faire une représentation mentale du cheminement effectué entre les 2 fonctions.  Mais après coup, j'imagine que ça ressemble à ça :

                      FootprintCreator<T>::create() | FootprintCreator<Others...>::create();
                      FootprintCreator<T>::create() | FootprintCreator<T>::create() | FootprintCreator<Others...>::create();
                      FootprintCreator<T>::create() | FootprintCreator<T>::create() | FootprintCreator<T>::create(); // final return

                      -
                      Edité par Guit0Xx 16 mai 2018 à 20:22:14

                      • Partager sur Facebook
                      • Partager sur Twitter

                      ...

                      ECS : signature des composants via bitset

                      × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                      × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                      • Editeur
                      • Markdown