IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

 C++ Discussion :

Observer : renvoi du type d'objet


Sujet :

C++

  1. #21
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Merci pour ce retour !

    Au passage, je tiendrais à préciser que mon objectif initial, pour le moment est de créé un bounty hunter tel que présenté sur la vidéo ICI.
    Bien entendu, si je me limite à ça, une implémentation objet classique type POO est tout à fait suffisante et sans contrainte.
    Cependant, je compte ajouter au fur-et-à-mesure des fonctionnalités différentes, des types différents etc... Bref, une des raisons pour laquelle je m'intéresse maintenant à l'ECS.

    HS :
    Dans ma quatrième intervention, j'ai enfin compris que, si je ne t'aidais pas à sortir du carcan de la programmation objet, tu n'y arriverais pas tout seul. Ce que je trouve d'ailleurs dommage de la part d'un ingénieur, fusse-t-il généraliste
    Je ne m'intéresse pas uniquement à la typologie du problème mais aussi aux différentes manières potentielles, aussi abérentes puissent-elles paraitre, de répondre à cette problématique, sans pour autant que j'y connaisse quoi que ce soit. Comme je le disais, même si je n'ai pas abordé le sujet plus tôt, j'ai commencé à me renseigné sur l'ECS avant que tu n'abordes le sujet. Il peut toujours exister une idée plus ingénieuse cachée quelque part qui sait ?

    Justement!!! Les composants n'ont pas à hériter
    En parlant héritage ici, je n'entends pas forcément la chose sur de l'orienté objet, mais juste sur les points communs pouvant lier les composants entre eux (par exemple, le fait de contenir un ID). Mais oui, le mot est probablement un peu fort, et la solution présentée pas nécessairement adaptée.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct ID
    {
    	uint16_t id;
    };
     
    class Component: ID
    {
    	/* ... */
    };
    Enfin, dernière question, bien que je comprends que cela pourrait violer quelques principes, en quoi une approche mixte ne serait-elle pas une solution viable au problème ?
    Si je demande ça, c'est bien parce qu'à un moment donné, une interface (au sens littéraire du terme) devra être réalisée entre nos données et des objets (au sens POO). Comme je me l'imagine maintenant, mon implémentation de "Entity", il s'agit plus d'une classe pour moi (container des différents Composants que l'on souhaite donner à cette entité même), que d'un simple identifiant que l'on rattacherait à des données qui sont sensées n'être justement que des données (on peut se poser la question selon les implémentations que l'on croise). Ainsi, une entité truc pourrait alors être implémentée de la sorte :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    Entity truc();
     
    truc.addComponent<Position>(400.0f, 300.0f);
    truc.addComponent<Velocity>();
    truc.addComponent<Mass>(25.0f);
    truc.addComponent<UserControl>();
    Me fourvoies-je encore ?

  2. #22
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 635
    Par défaut
    Citation Envoyé par BioKore Voir le message
    HS : Je ne m'intéresse pas uniquement à la typologie du problème mais aussi aux différentes manières potentielles, aussi abérentes puissent-elles paraitre, de répondre à cette problématique, sans pour autant que j'y connaisse quoi que ce soit. Comme je le disais, même si je n'ai pas abordé le sujet plus tôt, j'ai commencé à me renseigné sur l'ECS avant que tu n'abordes le sujet. Il peut toujours exister une idée plus ingénieuse cachée quelque part qui sait ?
    Attention, je n'ai pas trouvé dommage que tu n'aie pas trouvé une solution différente par toi-même...

    Je comprend bien que, n'étant pas développeur, il y a des choses que tu puisse simplement ignorer n'ayant simplement pas la science infuse

    Ce que j'ai trouvé dommage de la part d'un ingénieur, c'est beaucoup plus de "rester figé" sur une position dont j'avais déjà (à mon sens) fait la preuve de l'inefficacité et des dangers.
    En parlant héritage ici, je n'entends pas forcément la chose sur de l'orienté objet, mais juste sur les points communs pouvant lier les composants entre eux (par exemple, le fait de contenir un ID). Mais oui, le mot est probablement un peu fort, et la solution présentée pas nécessairement adaptée.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct ID
    {
    	uint16_t id;
    };
     
    class Component: ID
    {
    	/* ... */
    };
    Je comprend bien, mais non... (même s'il suffit de très peu pour changer complètement la donne )

    Parce qu'il n'y a rien à faire : l'héritage permet forcément de "faire passer" une donnée du type dérivé (Component) "pour une donnée" du type de base (ID), au sens du LSP.

    Or:
    1. l'héritage est la relation la plus forte qui puisse exister entre deux classes, (on parle à tort d'une relation EST-UN car elle est souvent mal comprise, on devrait parler d'une relation "EST-SUBSTITUABLE-A"). Il faut donc en "limiter l'usage" aux cas qui s'y adaptent parfaitement.
    2. Un composant n'est pas une identité : il dispose, éventuellement, d'une entité (comme un chien, un chat et un ours dispose d'un museau, qui n'a absolument rien à voir avec ceux des autres animaux). On devrait donc partir sur une agrégation au lieu d'un héritage.
    3. La relation d'héritage implique **forcément** que l'on peut créer une collection d'élément du type de base (std::vector<ID*>, std::vector<std::wrapper<ID>> ou similaire) dans laquelle nous pourrons mettre n'importe quel type de donnée d'un type dérivé(Component et tous es dérivés, en ne pouvant (sans passer par le double dispatch) les manipuler que "comme s'il était des ID")


    Par contre, si tu t'arrange pour que chaque type de composant reste parfaitement indépendant des autres (tout en présentant une caractéristique commune), il n'y a plus de problème.

    Et c'est, justement, là, l'énorme avantage du C++, qui te permet de combiner plusieurs paradigme.

    En utilisant le paradigme générique sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     
    template <typename C>
    struct ID{
        uint16_t id; /* j'aurais sans doute utilisé size_t, pour que l'ID puisse
                      * correspondre à l'entité à laquelle il est assigné
                      */
    };
    template <typename C>
    struct Component : public ID<C>{
    }
    struct PositionData{
        int x;
        int y;
    };
    /* Un alias de type sur le composant, pour la facilité d'utilisation */
    using PositionComponent = Component<PositionData>;
    [/c] tu obtiens tous les avantages de l'héritage, sans en avoir le principal inconvénient (la substituabilité), car, ce qu'il faut savoir au sujet des classes template, c'est qu'une spécialisation ID<A> représente un type totalement différent de la spécialisation ID<B>Et donc, si, par la suite, je venais à créer la notion de couleur (RGB), je pourrais le faire sous la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    struct ColorData{
        char red;
        char green;
        char blue;
    };
    /* Un alias de type sur le composant, pour la facilité d'utilisation */
    using ColorComponent = Component<ColorData>:
    Si bien que PostitionComponent et ColorComponent seront réellement deux type tout à fait différents, car le premier est un Component<PositionData> , qui hérite de ID<PositionData>) et que le deuxième est un Component<ColorData>, qui hérite de ID<ColorData>.

    Enfin, dernière question, bien que je comprends que cela pourrait violer quelques principes, en quoi une approche mixte ne serait-elle pas une solution viable au problème ?
    Dans ton travail, tu dois respecter certaines règles; entre autre dictées par la résistance des matériaux. Par exemple, le verre est l'une des matières les plus dures que l'homme puisse créer, car seul le diamant est capable de le griffer. Mais c'est aussi une maitère qui a tendance à se briser quand elle rentre en résonance.

    Si tu veux qu'un bloc de verre supporte à lui seul un poids élevé (je parle de plusieurs tonnes), tu vas donc devoir envisager un bloc particulièrement épais car une vitre de 10 mm d'épaisseur cassera à tous les coups.

    Hé bien, le développement informatique suit -- lui aussi -- certaines règles.

    La première est de réfléchir (entre autres, au respect des principes SOLID et de la loi de Déméter) avant d'écrire son code et que, si l'on se rend compte durant cette phase que ce que l'on envisage de faire ne respecte pas les principes de conception, de refuser purement et simplement la solution envisagée.
    Si je demande ça, c'est bien parce qu'à un moment donné, une interface (au sens littéraire du terme) devra être réalisée entre nos données et des objets (au sens POO).
    Justement, tu n'est plus dans un contexte d'utilisation de la POO...

    Comme je me l'imagine maintenant, mon implémentation de "Entity", il s'agit plus d'une classe pour moi (container des différents Composants que l'on souhaite donner à cette entité même), que d'un simple identifiant que l'on rattacherait à des données qui sont sensées n'être justement que des données (on peut se poser la question selon les implémentations que l'on croise).
    Ce ne serait pas judicieux!

    Attention, je ne dis pas que l'on ne peut pas envisager cette solution, je dis juste qu'elle ne serait pas judicieuse, pour deux raisons:

    La première, c'est que tu contreviendrait SRP (Single Responsability Principe : chaque donnée, chaque type de donnée, chaque fonction ne doit s'occuper que d'une seule chose pour pouvoir s'en occuper correctement) dans le sens où tu rendrait -- forcément -- ton entité responsable de la "gestion au jour le jour" de l'ensemble des composants.

    Bah, oui!!! Même si les composants sont tous parfaitement distinct, il faut bien te rendre compte que les données dont une classe dispose (dans l'approche orientée objets, mais dans une approche d'encapsulation, de manière générale) ne sont que des "détails d'implémentation" qui permettent aux différentes fonctions de travailler.

    Or, de manière générale, une donnée quelconque (un composant particulier) n'a de l'intérêt que parce qu'elle est utilisée d'une manière ou d'une autre. Si une donnée n'est pas utilisée, elle n'a aucune raison d'exister.

    Mais, en plus, si on décide d'ajouter une donnée, il faut être en mesure de la manipuler

    Or, pour chaque composant que tu pourrais envisager d'ajouter à ta classe Entity, tu devras prévoir:
    • d'être en mesure de le créer (quand l'entité a besoin d'un composant particulier)
    • d'être en mesure de le détruire (quand l'entité n'en a plus besoin)
    • d'être en mesure de l'interroger (de manière à mettre en place une logique qui fait intervenir plusieurs entités)
    • d'être en mesure (éventuellement) de le modifier (parce qu'un élément qui bouge se trouve, à l'instant T, à la position x1,y2 et à l'instant T+1 à la position x2,y2)

    Au final, tu te retrouverais donc avec une seule classe (la classe Entity) qui se retrouverait avec la très grosse majorité des responsabilités possibles. Cela deviendra rapidement ingérable

    La deuxième raison est qu'il faut aussi prendre en compte certains aspects purement liés au matériel

    Par exemple, il faut comprendre que, bien que certains composants ont de très grandes chances d'être associés à la plupart (mais, a priori, pas à toutes) des entités, la grosse majorité des entités ne disposera pas de tous les composants.

    Le seul moyen pour pouvoir représenter un élément qui "peut ne pas exister" passe par les pointeurs (de préférence intelligents) et par l'allocation dynamique de la mémoire.

    Tu finirais donc par te retrouver avec une classe Enity proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Entity{
    public: 
        /* ... */
    private:
        Type id;
        std::unique_ptr<Position> position;
        std::unique_ptr<Velocity> velocity:
        std::unique_ptr<Mass> mass;
        std::unique_ptr<UserControl> control:
        std::unique_ptr<Color> color;
        /* ainsi que les 120 autres composants dont tu pourrais avoir besoin :-$ */
    };
    Il faut comprendre que les processeurs actuels utilisent (pour la plupart) ce que l'on appelle un "système de cache" (cache L1 / L2 /L3)

    L'idée de ce système est que l'on peut avoir beaucoup de mémoire (peu rapide) ou de la mémoire très rapide (en petite quantité), mais qu'il est impossible -- techniquement parlant -- d'avoir beaucoup de mémoire très rapide.

    La RAM (et les différents systèmes de stockage) ont -- définitivement -- pris la décision de privilégier la quantité au prix de la vitesse

    Du coup, les fondeurs de processeurs ont mis en place un système composé de peu de mémoire (de l'ordre de quelques Mb au maximum) très rapide, en faisant un pari risqué:

    Ils ont en effet "pris le pari" que, lorsque l'on voulait accéder à une donnée qui n'était "pas dans le cache", la donnée (et, même de préférence "un certain nombre de données") à laquelle (auxquelles) on voudrait accéder "juste après" se trouverait "juste à coté" de la première donnée à laquelle on a voulu accéder (et qui n'était pas dans le cache).

    Du coup, au lieu de ne lire que les X bytes qui composent la donnée à laquelle on veut accéder, il vont directement lire "un grand nombre" de byte (plusieurs Kb) et les charger dans une des page de leur cache.

    Evidemment, cela va prendre "un temps bête"! Mais, si les fondeurs gagnent leur pari et que, après avoir essayé d'accéder à la donnée qui se trouve à l'adresse mémoire M, tu essayes effectivement d'accéder à celle qui se trouve à la mémoire M+S (ou S correspond à la taille de la donnée), l'accès "à la donnée suivante" sera beaucoup plus rapide.

    Et si "la chance leur sourit" et que tu essayes "plusieurs fois" d'accéder à la donnée qui se trouve (dans le cache, donc dans une petite quantité de mémoire très rapide) "juste après" celle à laquelle tu accédait "juste avant", le temps de remplir le cache + le temps nécessaire à l'accession au différentes données sera -- proportionnellement -- beaucoup plus intéressant que s'il avait fallu accéder à chaque donnée directement à partir de la RAM.

    Or, si tu envisages l'allocation dynamique de la mémoire pour pouvoir représenter un composant qui "risque de ne pas exister", le problème, c'est que les différents composants assignés à une entité particulière seront alors "disséminés" en mémoire au "gré de l'ordre dans lequel ils sont créés". (et je ne tiens même pas compte du temps infernal nécessaire à l'allocation de la mémoire proprement dite, car, ca aussi, ca prend un temps bête )

    A moins d'avoir énormément de chance (mais la chance n'est jamais un facteur pour un développeur. La malchance arrive bien plus souvent ), chacun des pointeur représenter une adresse beaucoup trop distante de l'adresse représentée par "le pointeur précédant" (exemple: le pointeur position quand on veut accéder à velocity)ou par le "pointeur suivant" (exemple: le pointeur mass quand on veut accéder à velocity), ce qui ne pourra jamais avoir qu'une seule conséquence : obliger les fondeurs de processeurs à perdre leur pari et, de ce fait, provoquer ce que l'on appelle un cache miss.

    Même s'il y a moyen de trouver des causes provoquant un effondrement des performances largement plus importants (dans l'ordre, je dirais : l'accès à un serveur distant, l'accès à un fichier sur le disque et l'allocation dynamique de la mémoire), les cache misses compte très certainement parmi les cause les plus communes d'effondrement des performances les plus importants

    Quand ton but est de maintenir un minimum de 30FPS (mais, idéalement, d'en obtenir 60 voire même 100), éviter autant que possible les caches-misses vient très haut dans tes priorités (juste après avoir essayé de réduire au maximum la complexité de ta logique).
    Ainsi, une entité truc pourrait alors être implémentée de la sorte :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    Entity truc();
     
    truc.addComponent<Position>(400.0f, 300.0f);
    truc.addComponent<Velocity>();
    truc.addComponent<Mass>(25.0f);
    truc.addComponent<UserControl>();
    Parce que tu restes -- désespérément -- accroché à un paradigme inadéquat.

    Ceci dit, je te comprends parfaitement : je sais bien que l'on subit un véritable matraquage quant au fait que la POO est la solution à tous les maux!

    Le fait est que ce n'est absolument pas vrai, et que c'est encore plus faux en C++.

    Mais tu entres dans une logique d'utilisation de l'approche ECS, qui est l'abréviation de Enity Component System (même si je préfère estimer que le S représente le terme Services )

    Si je devais définir rapidement (et, malgré tout, de manière correcte) cette approche, je te dirais que tu dois créer un "système" très proche de la notion que l'on a d'une base de données: C'est un ensemble de tables (qui contiennent, chacune, des composants) utilisant la notion d'entité comme une "clé étrangère" permettant de "relier" chaque composant à une entité particulière.

    Si une entité quelconque n'a pas besoin d'un composant particulier, il n'y a aucune raison pour que l'on trouve, dans la table regroupant tous les composants du type de composant en question, un composant associé à cette entité particulière.

    Si une entité cesse d'exister pour une raison ou une autre (par exemple: si un ennemi est mort, après un certain temps, il disparaît purement et simplement du jeu. Il est -- littéralement -- "oublié" par le jeu), tous les composants qui étaient associés à cette entité doivent aussi cesser d'exister, et disparaître de la table dans laquelle ils se trouvent.

    Les services correspondent, quant à eux, aux différents comportements (aux différentes fonctions) permettant de manipuler les composants existants de manière "cohérente", "structurée" et "efficace".

    Ils correspondraient, dans un système de base de données, à la notion de "requêtes" (et plus précisément, de "procédure stockées") susceptibles d'être effectuées sur la base de données en générale.

    Ce n'est pas un
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    Entity truc;
     
    truc.addComponent<Position>(400.0f, 300.0f);
    /* ... */
    qu'il faut faire, mais plutôt un
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    size_t id = cretaeEntity(); // on crée une entité
    addComponent<Position>(id, 400.0f, 300.0f); // on ajoute un composant de type Position pour l'entité en question à la table ad-hoc
    addComponent<Velocity>(id, 5);              // on ajoute un composant de type Velocity pour l'entité en question à la table ad-hoc
    addComponent<Mass>(id, 25.0f);              // on ajoute un composant de type Mass pour l'entité en question à la table ad-hoc
    Me fourvoies-je encore ?
    Au vu de ma réponse, qu'est ce que tu en penses
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #23
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Bonjour, te merci pour ce retour.

    Parmi tout ce que tu cites, je préciserais que, l'approche de type "base de donnée", est effectivement une approche que j'ai pu voir, ça et là. De même que ta dernière proposition d'implémentation concernant la création et le renseignement d'une entrée de cette "base de donnée". Je ne peut qu'approuver cette méthode, d'autant plus que, la nuit portant conseil, je me suis aperçu que je n'avais pas besoin d'agir ainsi. Il faut que je prenne plus de recul vis à vis des multiples implémentations que l'on peut trouver sur le net...

    Après une très rapide implémentation bien plus orientée telle que tu le suggères, j'ai pu faire un "grand" pas.

    Néanmoins, tout cela étant dit, il me reste, dans tout les cas, à trouver un moyen de "contenir" tous ces composants de type différents.
    Pour cela, tu affirmes dans un précédent post qu'il est possible de répondre à cela sans passer par un std::map (et/ou std::unordered_map) et en s'intéressant de plus près à l'approche générique, et à l'instanciation explicite. Cependant, bien que je commence à entrevoir certaines bases d'une telle implémentation, je n'arrive toujours pas à percevoir comment un tel système peut être implémenté.

    Puisque je parle de "Composants", j'ai besoin de quelques éclaircissements vis à vis de ton code suivant :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template <typename C>
    struct ID{
        uint16_t id; /* j'aurais sans doute utilisé size_t, pour que l'ID puisse
                      * correspondre à l'entité à laquelle il est assigné
                      */
    };
    template <typename C>
    struct Component : public ID<C>{
    }
    struct PositionData{
        int x;
        int y;
    };
    /* Un alias de type sur le composant, pour la facilité d'utilisation */
    using PositionComponent = Component<PositionData>;
    Ici, une instanciation de PositionComponent ne permet que l'accès qu'à son ID. Bien qu'il soit utile de pouvoir accéder à l'ID de cette manière, il reste à trouver comment accéder aux valeurs de x et y...

    Pour ce soir, je conclurais donc sur le fait que j'ai pu avancer un peu dans ma compréhension de la chose, ce qui est un très bon point pour moi. Merci.

    Petit point intermédiaire de la journée : je dois essayer ça, mais avec deux std::map, il me semble voir comment réaliser un tel stockage (en assurant la contiguïté des données en mémoire).
    Voici comme je pourrais procéder :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<typename T>
    static std::unordered_map<entity_t, std::vector<T*> > m_container;
     
    template<typename T>
    bool add(const entity_t &e)
    {
    	/* on s'assure que l'entitée est déjà inssérée dans
    	le container. Le cas échéant, on créé l'entrée   */
    	T* comp(new T());
    	m_container<T>[e].push_back(comp);
    }
    Ce qui me dit que cela pourrait être un premier départ de réflexion c'est que le static std::map m_container ne sera instancié que si un nouveau type est demandé via le template.
    Bien entendu, le code ci-dessus ne représente qu'une suggestion de la forme répondant au sujet initial. De plus, il existe très certainement des points à améliorer sur cette implémentation avant de pouvoir l'utiliser d'une manière ou d'une autre.

    Cependant, j'utilise un map + un vector encore...

  4. #24
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 635
    Par défaut
    Citation Envoyé par BioKore Voir le message
    Bonjour, te merci pour ce retour.

    Parmi tout ce que tu cites, je préciserais que, l'approche de type "base de donnée", est effectivement une approche que j'ai pu voir, ça et là. De même que ta dernière proposition d'implémentation concernant la création et le renseignement d'une entrée de cette "base de donnée". Je ne peut qu'approuver cette méthode, d'autant plus que, la nuit portant conseil, je me suis aperçu que je n'avais pas besoin d'agir ainsi. Il faut que je prenne plus de recul vis à vis des multiples implémentations que l'on peut trouver sur le net...

    Après une très rapide implémentation bien plus orientée telle que tu le suggères, j'ai pu faire un "grand" pas.

    Néanmoins, tout cela étant dit, il me reste, dans tout les cas, à trouver un moyen de "contenir" tous ces composants de type différents.
    Pour cela, tu affirmes dans un précédent post qu'il est possible de répondre à cela sans passer par un std::map (et/ou std::unordered_map)
    Oui, c'est tout à fait possible.

    Quant à savoir s'il est "nécessaire" ou pas de s'en passer, ca, je vais te laisser seul juge.

    Car il faut savoir que, de manière générale, il y a toujours un équilibre à trouver entre l'utilisation de la mémoire (que l'on essaye "autant que faire se peut" de garder la plus faible possible) et les performances de vitesse brutes (que l'on essaye de garder au plus haut niveau possible), et que ce que l'on gagne d'un coté, on le perd "forcément" de l'autre .

    La std::map est basé sur la notion d'arbre binaire. C'est à dire qu'elle manipule une structure proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    struct Node{
        Key const key; // la clé (qui ne peut pas être modifiée facilement)
        Value value; // la valeur (qui peut être modifiée à condition que la clé ne le soit pas)
        Node * lesser; // le noeud vers la paire clé+ valeur "plus petite (idéalement, dont la valeur de la clé est égale à la moitié de celle de l'élément sur lequel on est), s'il existe
        Node * greaer; // le noeud vers la paire clé+ valeur "plus grande (idéalement, dont la valeur de la clé est égale à celle sur lequel on est + la moitié de l'écart restant entre l'élément courant et l'élément le plus grand de la map), s'il existe
    };
    Le tout permet une recherche très efficace appelée "dichotomique", qui présente une complexité en O(log(n)): Chaque fois que l'on décide de s'orienter ver "greater" ou vers "lesser" pour chercher une clé particuilère, on "abandonne" purement et simplement ... l'autre coté, ce qui, dans le meilleur des cas, divise le nombre d'éléments à tester par deux

    Pour information, la meilleure complexité que l'on puisse trouver qui soit encore meilleure que la dichotomie est la complexité "en temps constant"; qui nécessitera un temps d'exécution identique quel que soit l'élément recherché

    Malheureusement, le parcours d'une telle structure est "particulièrement lent" et, comme tous les noeuds subissent une allocation dynamique de la mémoire uniquement pour eux, cela en fait une véritable "catastrophe" pour le système de cache des processeurs (cf mon intervention précédente).

    L'aternative, qui devient tout à fait possible si l'on considère qu'une simple valeur numérique entière (de préférence non signée) suffit à représenter les entités, consiste à utiliser deux tableaux dont le premier va contenir ... bah, l'ensemble des composants d'un type particulier, et dont le deuxième va "simplement" contenir l'index permettant d'accéder à un élément particulier du premier tableau.

    L'idée étant que, si nous représentons une entité sous la forme d'une valeur numérique entière non signée, nous pouvons parfaitement en utiliser la valeur comme ... index dans le deuxième tableau, auquel nous pourrons alors accéder en temps constant de manière à obtenir l'index du composant associé à l'entité en question dans le premier tableau (auquel nous pourrons aussi accéder en temps constant).

    Le gros avantage, outre le fait que l'on effectue tous nos accès en temps constant, c'est que tous les éléments d'un tableau sont -- forcément -- contigus en mémoire, et que cela permet de profiter au mieux de la mise en cache effectuée par le processeur.

    Mais cela ne va pas sans certains inconvénients, parmi lesquels il faut citer:

    1. Le fait qu'il y a sans doute énormément d'entités qui ne disposent pas du composant que l'on recherche. Il faut donc pouvoir représenter, dans le deuxième tableau, un "index (du premier tableau) inexistant" et surtout
    2. le fait qu'il faut forcément être en mesure de représenter l'ensemble des entités existantes dans le deuxième tableau

    Cela signifie que, si tu as 3 000 000 d'entités en utilisation à un moment donné, le deuxième tableau devra forcément être en mesure de fournir ... 3 000 000 d'indexes pour chaque composant que tu pourra créer, et ce, même s'il ne devaient y avoir que deux entités qui disposent effectivement du composant représenté

    Même en considérant qu'un index dans un tableau, c'est un size_t, ce qui correspond -- selon l'architecture -- à 4 ou 8 bytes, cela fait "un sérieux paquet de bytes" (si on part sur 3 000 000 d'entités, cela fait 12 à 24 000 000 de bytes utilisé d'office pour chaque composant) alors que nous n'aurons peut être que deux composants de 16 bytes chacun (8 pour l'identifiant de l'entité auquel il est rattaché et 8 de plus pour représenter la distance, par exemple)

    On obtient donc quelque chose de très efficace, en termes de vitesse, mais "d'autant plus catastrophique" en termes d'utilisation de la mémoire que le nombre d'entité possible est élevé pour un nombre faible d'entité qui disposent effectivement du composant indiqué; même s'il y a tout à fait moyen de s'arranger pour que la taille du deuxième tableau évolue progressivement (mais en veillant à ce qu'elle ne le fasse pas trop souvent malgré tout, car la réallocation d'un tableau pour en augmenter la taille prend -- elle aussi -- "un temps bête")


    et en s'intéressant de plus près à l'approche générique, et à l'instanciation explicite. Cependant, bien que je commence à entrevoir certaines bases d'une telle implémentation, je n'arrive toujours pas à percevoir comment un tel système peut être implémenté.

    Puisque je parle de "Composants", j'ai besoin de quelques éclaircissements vis à vis de ton code suivant :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template <typename C>
    struct ID{
        uint16_t id; /* j'aurais sans doute utilisé size_t, pour que l'ID puisse
                      * correspondre à l'entité à laquelle il est assigné
                      */
    };
    template <typename C>
    struct Component : public ID<C>{
    }
    struct PositionData{
        int x;
        int y;
    };
    /* Un alias de type sur le composant, pour la facilité d'utilisation */
    using PositionComponent = Component<PositionData>;
    Ici, une instanciation de PositionComponent ne permet que l'accès qu'à son ID. Bien qu'il soit utile de pouvoir accéder à l'ID de cette manière, il reste à trouver comment accéder aux valeurs de x et y...
    Au temps pour moi... j'ai oublié un tout petit détail

    Essaye avec
    [CODE]
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    template <typename C>
    struct ID{
        ID(uint16_t id:id{id}{
        }
        uint16_t id; /* j'aurais sans doute utilisé size_t, pour que l'ID puisse
                      * correspondre à l'entité à laquelle il est assigné
                      */
    };
    template <typename C>
    struct Component : public ID<C>,
                                   public C{
    /*  Et une petite amélioration au passage */
        Component():Component(0){
        }
        Component(size_t entity):Component(entity, C{}){
        }
        Compoent(size_t entity, C const & value): Id<C>{entity},
                                                  C{value}{
        }
    }
    struct PositionData{
        int x;
        int y;
    };
    /* Un alias de type sur le composant, pour la facilité d'utilisation */
    using PositionComponent = Component<PositionData>;
    Cela ira tout de suite mieux

    Citation Envoyé par BioKore Voir le message
    Petit point intermédiaire de la journée : je dois essayer ça, mais avec deux std::map, il me semble voir comment réaliser un tel stockage (en assurant la contiguïté des données en mémoire).
    Voici comme je pourrais procéder :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<typename T>
    static std::unordered_map<entity_t, std::vector<T*> > m_container;
     
    template<typename T>
    bool add(const entity_t &e)
    {
    	/* on s'assure que l'entitée est déjà inssérée dans
    	le container. Le cas échéant, on créé l'entrée   */
    	T* comp(new T());
    	m_container<T>[e].push_back(comp);
    }
    Surtout pas, malheureux!!!

    Ce n'est pas tant l'utilisation de std::map que je te reproche ici (comme je l'ai expliqué plus haut, cela reste malgré tout une possibilité "plus que raisonnable"), mais bien:
    1- le fait que tu utilise un tableau de composant, alors que tu n'en as pas besoin (chaque entité ne dispose que "d'un seul exemplaire" d'un composant particulier)
    2- le fait que tu utilise des pointeurs (même pas intelligents en plus) et l'allocation dynamique.

    Quitte à faire de la sorte, un
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <typename C>
    usign ComponentHolder = std::map<size_t, Component<C>>;
    /
    template <typename C
    static ComponentHolder<C> holder;
    template <typename C>
    Component<C> & add(size_t entity, C const & value){
        Component<C> component{entity, value};
        holder.insert(std::make_pair(entity, components);
        return holder.find(entity)->second;
    }
    (quelques corrections à craindre )
    Cependant, j'utilise un map + un vector encore...
    Et, surtout, tu utilise encore l'allocation dynamique de la mémoire

    Mais je vais donc -- quand même -- te présenter le "gestionnaire" (dieux que j'ai horreur de ce terme!!! dis toi bien que je l'utilise à défaut de tout autre terme ) de composants, qui pourrait ressembler à quelque chose comme:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    template <typename C>
    class ComponentHolder{
        /* on doit pouvoir représenter un "index invalide" 
         * en utilisant la valeur maximale représentable par un size_t, on est sur 
         * que l'index représenté ne pourra JAMAIS être atteint
         */
        constexpr size_t invalidIndex{std::numeric_limits<size_t>::max()};
    public:
        /* un alias de type, plus facile pour la représentation des composants */
        using ComponentType = Component<C>;
        /* des alias de type sur les itérateur pour les composants */
        using iterator = typename std::vector<ComponentType>::iterator;
        usign const_iterator = typname std::vector<ComponentType>::const_iterator;
        /* On veut pouvoir
         * 1- ajouter un composant à une entité particulière (pour autant qu'il n'existe pas)
         * 2- supprimer le composant pour une entité particulière (pour autant qu'il existe)
         * 3- parcourir tous les composants qui existent, indépendamment de l'entité à laquelle ils sont assignés (en lecture et en écriture)
         * 4- pouvoir accéder (en lecture et en écriture) au composant spécifiquement assigné à une entité particulière (pour autant qu'il existe)
         * 5- éventuellement, on peux souhaiter savoir s'il existe un composant pour une entité particulière (quand on travaille sur un seul type de composant
         *    cela peut s'avérer "plus facile" que de commencer à travailler avec le "pass-key" dont j'ai parlé plus haut
         */
         ComponentHolder(){
            indexes_.resize(0xFFFF, invalidIndex); // je débute directement avec 65000 et quelques indexes, qui sont tous invalides ;)
            /* NOTA: Je pourrais aussi réserver de l'espace pour les composants.
             *       Mais, dans l'ensemble, la politique d'augmentation de capacité de std::vector
             *       nous permettra de n'avoir "pas trop à nous en faire" sur ce point
             */
        }
        /* "le plus simple" : les fonctions begin() et end() (en version constante et non constante) */
        iterator begin(){
            return components_.begin();
        }
        iterator end(){
            return components_.end();
        }
        const_iterator begin() const{
            return components_.begin();
        }
        const_iterator end() const{
            return components_.end();
        }
        /* ajouter un composant
         *
         * précondition : l'entité indiquée ne doit pas encore disposer du composant
         *
         * mode de fonctionnement : 
         * 1- j'ajoute le composant au premier tableau
         * 2- je met à jour l'index dans le deuxième tableau
         */
        void add(ComponentType const & comp){
            assert(indexes_.size() < comp.id && "id too hight");
            assert(indexes_[comp.id] == invalidIndex  && "entity already has such component");
            components_.push_back(comp);
            indexes_[comp.id]= components_.size()-1; // on est dans un système "premier index à 0"
        }
     
        /* supprimer un composant
         *
         * précondition : l'entité indiquée doit disposer du composant
         *
         * mode de fonctionnement : 
         * 1- je supprime le composant au premier tableau
         * 2- je met à jour l'index dans le deuxième tableau (en lui donnant la valeur invalidIndex)
         */
        void remove(size_t entity){
            assert(indexes_[comp.id] != invalidIndex  && "entity already has such component");
            /* une suppression peut s'effectuer en temps constant dans un tableau si on ne tiens "pas trop"
             * à garder les élément dans un ordre particulier.
             * 
             * Il suffit en effet d'inverser l'élément que l'on veut supprimer avec le dernier élément valide 
             * avant de mettre la taille du tableau à jour
             * 
             * mais, pour cela, on a besoin de l'index (dans le premier tableau) de l'élément à supprimer
             */
           auto index = indexOf(entity);
           auto lastValid = indexes_.size()-1;
           std::swap(components_[index], lastValid());
           components_.resize(lastValid); // le redimentionnement à une taille inférieure ne provoque pas 
                          //la réallocation de la mémoire du tableau
           /* on met l'index à jour */
           indexes[entity]= invalidIndex;
        }
        /* accéder (en lecture et en écriture) au composant spécifiquement assigné à une entité particulière
         *
         * précondition : l'entité indiquée ne doit pas encore disposer du composant
         */
        ComponentType & get(size_t entity ){
            assert(indexes_[entity] != invalidIndex && "Entity doesn't have such component");
            return components_[indexOf(entity)];
        }
        ComponentType const & get(size_t entity) const{
            return const_cast<ComponentHolder &>(this).get(entity);
        }
        /* savoir si un composant est associé à une entité particulière */
        bool exists(size_t entity) const{
            return indexOf(entity)!=invalidIndex;
        }
    private:
        /* En interne, on doit pouvoir récupérer l'index (dans le premier tableau) d'une entité bien précise 
         * 
         * précondition : la taille de l'index doit être plus grande que l'identifiant de l'entité indiquée 
         */
        size_t indexOf(size_t entity) const{
            assert(indexes_.size() < comp.id && "id too hight");
            return indexes_[entity];
        }
        /* le tableau de composants */
        std::vector<ComponentType> components_;
        /* le tableau d'indexes */
        std::vector<size_t> indexes_;
    };
    J'ai ajouté quelques commentaires (normalement, je ne les aurais pas rajoutés dans mon code de production) pour te permettre de comprendre ce que je fais, et quand je le fais...

    N'hésites pas à les (re)lire ni à poser des questions en cas de besoin

    ATTENTION!!!
    1- Cependant il reste un dernier (double) détail que je n'ai pas rajouté ici: la "synchronisation" avec le "gestionnaire d'entités" car, pour bien faire:
    1. si une entité est détruite, il faut s'assurer que tous les composants qui lui sont assignés soient détruit
    2. si on en vient à créer plus d'entités que le nombre arbitraire que j'ai prévu ici (0xFFFF), il faudra élargir le tableau indexes_.

    Cela se fera "assez facilement" en utilisant la classe signal dont j'ai déjà parlé plus tôt
    2- j'ai réécrit ce code, à titre d'exemple, totalement de tête et, la honte est sur moi, absolument sans le tester.

    A priori, il devrait être juste et correcte, et il devrait pouvoir fonctionner tel quel... Mais, comme je suis un humain, que mes lunettes sont parfois sales et que mes doigts s’emmêlent parfois sur mon clavier, je ne peux pas garantir qu'il n'y ait pas l'une ou l'autre erreur qui se soit glissée dedans

    Pour l'utilisattion
    Pour être honnête, je préférerais que cette classe se trouve dans un espace de noms et dans un fichier d'en-tête séparés (ex: Ecs:etails et dans le fichier d'en-tête priv/ComponentHolder.hpp), et qu'il y ait "juste" un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    namespace Ecs{
    namespace Details{
        template <typename C>
        class ComponentHolder; // déclaration anticipée de la classe
    } // namespace Details
    template<typename C>
    extern Details::ComponentHolder<C> holder;
    template <typename C>
    void add(size_t entity, C const & value){
          Component<C> component{entity,value};
          holder.add(component);
    }
    template <typename C>
    void remove(size_t entity){
        holder.remove(entity)
    }
    template <typename C>
    bool existsFor(size_t entity){
        return holder.exists(entity);
    }
    }// namespace Ecs
    et de voir apparaitre, pour chaque composant que tu créera, un fichier *.cpp dont le contenu sera proche de (ici, on va le faire pour la position )
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    #include <priv/ConponentHolder.hpp>
    Details::ComponentHolder<Position> holder;
    Ou mieux encore, nous pourrions envisager de mettre en place un système de "locator" en place
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  5. #25
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Merci pour ce retour une fois encore riche en informations !

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    template <typename C>
    struct ID{
        ID(uint16_t id:id{id}{
        }
        uint16_t id; /* j'aurais sans doute utilisé size_t, pour que l'ID puisse
                      * correspondre à l'entité à laquelle il est assigné
                      */
    };
    template <typename C>
    struct Component : public ID<C>,
                                   public C{
    /*  Et une petite amélioration au passage */
        Component():Component(0){
        }
        Component(size_t entity):Component(entity, C{}){
        }
        Compoent(size_t entity, C const & value): Id<C>{entity},
                                                  C{value}{
        }
    }
    struct PositionData{
        int x;
        int y;
    };
    /* Un alias de type sur le composant, pour la facilité d'utilisation */
    using PositionComponent = Component<PositionData>;
    C'est fou comme un simple public C éclairci le but global de ce code !


    Un beau sujet reste à aborder concernant la suppression des composants, cependant, le code final est bien plus simple et court que ce à quoi on aurait pu s’imaginer de prime abord.
    Je vais me relire tous les posts depuis le début histoire d'assimiler correctement ce nouveau concept pour moi, puis, je tenterais un petit exercice d'essai de tout ça dés demain.

    Au passage, mes bases de C++ datant d'une bonne 15aine d'années, j'en profiterais pour me retaper quelques cours sur le C++ moderne. Un rafraichissement sera d'une grande aide.

    Merci beaucoup pour toutes ces explications très utiles et bien présentées. Je ferais un retour des que j'aurais une implémentation propre et fonctionnelle. Merci !




    Bon, j'ai pu implémenter tout ça, et cela fonctionne.Néanmoins, un détail subsiste dans le code suivant :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    namespace Ecs{
    namespace Details{
        template <typename C>
        class ComponentHolder; // déclaration anticipée de la classe
    } // namespace Details
    template<typename C>
    extern Details::ComponentHolder<C> holder;
    template <typename C>
    void add(size_t entity, C const & value){
          Component<C> component{entity,value};
          holder.add(component);
    }
    [...]
    Au delà du fait que holder.add(component); doive être holder<C>.add(component);, ce *.hpp ne connait pas les nouveaux composants.
    J'ai fait un test avec une structure "A", et mon IDE (je suis sur ma machine windows, avec Code::Blocks) me dit : undefined reference to Ecs::holder<A> au sein de la fonction void add(...)Bien que ce soit aussi la première fois que j'utilise des espaces de noms, je pense avoir piger le truc. Selon moi la solution se trouve ailleurs (et j'ai fait pas mal d'essais avec / sans les espaces de noms, et/ou organisés différemment).

    Si quelqu'un à sa petite idée, je prends. Merci.

  6. #26
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Bon, enfin nous avons trouvés une solution !

    Plutôt que de passer par un template<typename T> extern Details::ComponentHolder<C> holder;, j'utilise alors une fonction permettant de gérer les objets de type :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    template<typename C>
    Details::ComponentHolder<C> & getHolder()
    {
    	static Details::ComponentHolder<C> holder;
    	return holder;
    }
    Après quelques tests sur des composants simples, le tout semble fonctionner sans accroc.
    En addition à ce long projet, il me restera à ajouter la notion de "systèmes" (ou encore "services"), ainsi que les fonctions nécessaires à la suppression d'une entité, ou plutôt, de tous les composants liés à cette entité.
    Compte tenu que je suis en pleine phase d'apprentissage, j'aurais très certainement pas mal de notions à assimiler au cours de mon implémentation.

    D'ici là, je vais devoir m'approprier ce nouveau mode de fonctionnement pour moi (DOP), ou du moins, voir comment l'intégrer dans un projet.

    Je remercie beaucoup Koala1 pour son aide apportée dans ma compréhension de la chose ! Cela m'a été d'un grand secours.

    EDIT : petit esperluette, bien présente dans mon code

  7. #27
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 635
    Par défaut
    N'houblie pas de rajouter une esperluette entre Details::ComponentHolder<C> et getHolder() car c'est elle qui fera toute la différence
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

+ Répondre à la discussion
Cette discussion est résolue.
Page 2 sur 2 PremièrePremière 12

Discussions similaires

  1. Réponses: 4
    Dernier message: 01/04/2019, 18h52
  2. [XL-2013] Code qui renvoie le Type d'Objet dans un UserForm qui a le focus
    Par pickatshou dans le forum Excel
    Réponses: 5
    Dernier message: 16/07/2015, 20h35
  3. Quel type d'objet renvoie Workbooks(nomFichier).Worksheets(1)?
    Par netoale dans le forum Macros et VBA Excel
    Réponses: 2
    Dernier message: 24/03/2011, 11h50
  4. Initialisation d'un type d'objet
    Par fdraven dans le forum Oracle
    Réponses: 3
    Dernier message: 28/10/2005, 11h05
  5. [Appli] Recherche d'un type d'objet précis pour interface
    Par superpatate dans le forum Interfaces Graphiques en Java
    Réponses: 3
    Dernier message: 05/08/2005, 12h02

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo