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 :

Créer son propre type de sauvegrade


Sujet :

C++

  1. #1
    Membre averti
    Homme Profil pro
    Terminal S
    Inscrit en
    Juin 2013
    Messages
    30
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Val d'Oise (Île de France)

    Informations professionnelles :
    Activité : Terminal S

    Informations forums :
    Inscription : Juin 2013
    Messages : 30
    Par défaut Créer son propre type de sauvegrade
    Bonjours je rencontre un problème sur l'avancement d'un projet personnel, tout est dans le titre, j'aimerais créer un type de sauvegarde pour des maps en 3D et des données quelconques, j'aimerais aussi optimiser ce fichier en créant mes variables bits a bits (du genre ne pas utiliser une variable trop grande comme le char alors que la donnée peut être contenue que de 1 bit et non 1byte = 8bit ).

    Je rencontre une difficulté au faite d'éditer le fichier qui va recueillir tout ça ... j'aimerais vraiment l'éditer à la limite de l'hexadécimal pour que cela soit optimiser.

    Quand a la sauvegarde de la map je ne sais pas du tout comment faire car: c'est une map généré aléatoirement, et je ne veux pas de map du type Tiles ( 0 = sol 1= mur .... ).

    Merci de vos réponses anticipées.

  2. #2
    Expert éminent
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 395
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 395
    Par défaut
    Si tu optimises, attention à bien définir le compromis entre taille occupée et vitesse de sauvegarde/chargement: Plus tu écris en termes plus petits que des entiers système (et pire, en termes plus petits que des bytes), plus ce sera lent (même si tu écris tout en mémoire pour ensuite écrire sur le disque d'un bloc, ce sera plus lent).

    Pour ta map, il faudrait déjà savoir comment tu la stockes en mémoire: C'est le premier pas pour savoir ce qu'il y a à enregistrer dans le fichier.
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  3. #3
    Membre averti
    Homme Profil pro
    Terminal S
    Inscrit en
    Juin 2013
    Messages
    30
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Val d'Oise (Île de France)

    Informations professionnelles :
    Activité : Terminal S

    Informations forums :
    Inscription : Juin 2013
    Messages : 30
    Par défaut
    Le map serra fait avec Perlin noise mais je ne veut pas mettre les données de la map brute dans un fichier ... (pour éviter des modification instantanée) elle serra sauvegardée dans un fichier avec une extension quelconque (je travaille sous linux donc pas besoins de se soucier des extensions). Ma question est n'y a-t-il pas un moyens de faire dans l'entre deux la compresser et la rendre rapidité.
    Je vois montre une structure que j'ai pensé pour la sauvegarde de la map:
    • Coordonée du Chunk (X;Y)[char] <-- pas plus de 255
      • Block ( pour regrouper les données)[utin] <-- pour le designer
        • Id block(0 - ...)[uint]
        • Coordonée du block(X;Y)[int] <--- peut être négatif


    Peut elle être mieux ??

    Pour les données sur le joueur ou autre j'ai compris.

  4. #4
    Expert éminent
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 395
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 395
    Par défaut
    Compresser des données basées sur des valeurs aléatoires? Peu de chance d'y gagner grand-chose.

    Au pire, si tu veux éviter une modif de ta map dans le fichier par un tiers, tu l'écris en brut et tu ajoutes une signature numérique...

    En fait, pour déterminer une compression éventuelle, il faut savoir comment est ta map.
    Un truc tout bète comme ceci:
    Code X : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ######### ##########
    #                  #
    #                  #
    #                  #
    #                  #
    #                  #
    #                   
    #                  #
    #                  #
    #            #     #
    #            #     #
    #                  #
    #                  #
    ####################
    peut facilement se compresser même en RLE, tandis qu'un truc comme ceci c'est plutôt mort de ce côté-là...
    En gros, ça dépend vraiment du format de tes maps.
    En fait quand on y pense, tu peux même ajouter un champ dans ton fichier qui dit le type de compression (ou non) utilisé pour une map donnée...
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  5. #5
    Membre averti
    Homme Profil pro
    Terminal S
    Inscrit en
    Juin 2013
    Messages
    30
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Val d'Oise (Île de France)

    Informations professionnelles :
    Activité : Terminal S

    Informations forums :
    Inscription : Juin 2013
    Messages : 30
    Par défaut
    La map serra basé sur un aléatoire contrôlé par un seed, et pour un temps de chargement moindre je la sauvegarde (effectivement j'ai oublier de signaler que la map est en trois dimensions ^^' ). Mon problème est dans la structure de cette sauvegarde, comment faire en sorte qu'elle soit optimisé, qu'elle prenne le moins de place possible et qu'elle ne soit pas modifiable comme celle que tu m'as présenté.

    La signature est un moyen mais la Map est charger dynamiquement en fonction du personnage et peut s'agrandir et la signature va donc changer. Pour un comparatif mes mapes ressemblerons à celle de minacraft.

  6. #6
    Membre chevronné

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Juin 2007
    Messages
    373
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Royaume-Uni

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Santé

    Informations forums :
    Inscription : Juin 2007
    Messages : 373
    Par défaut
    J'ai aussi codé un clone de minecraft. Le code est un peu vieux maintenant, j'ai commencé une refonte il y a un moment mais je n'ai malheureusement plus le temps de jouer avec ça... En tout cas il y a des binaires à télécharger sur le site, ceux pour linux fonctionnent toujours, pour Windows ça devrait aller aussi. Le code est bien sûr disponible (il y a un dépôt SVN), il n'est pas excessivement documenté mais plutôt propre donc tu peux y jeter un œil si tu veux.

    Pour la sauvegarde des cartes, en gros je m'en était sorti en découpant le monde en régions de 8x8x8 "chunks" (chacun contenant 15x15x15 blocs, donc une région fait 120 blocs de côté). Je sauvegarde ensuite chaque région dans un fichier binaire différent, qui contient un en tête fixe suivi des blocs de la carte compressés en LZMA (avec zlib) pour gagner de la place (chaque bloc est sauvegardé sous la forme {uchar type, uchar sun_light, uchar light}, si possible le même format qu'en RAM pour limiter les conversions).

    Je sauvegarde une région seulement quand elle n'est plus visible, donc soit quand le joueur s'en éloigne trop, soit au moment de quitter le jeu. Et bien sûr je fais tout ça dans un thread séparé, pour ne pas bloquer le jeu.

    Je m'étais bien amusé à coder tout ça, mais autant il suffit de quelques jours pour faire une démo avec une petite carte qui fonctionne, autant avoir un monde infini, modifiable et sauvegardé, avec la propagation de la lumière et les collision, le tout avec des performances acceptables, ça prend nettement plus de temps. Mais on apprend beaucoup.

    Bon courage

    Edit : j'ai oublié de préciser... Inutile de sauvegarder la position des chunks/blocs, leur position en mémoire (ou dans le fichier) donne déjà cette information. En gros si tu n'avais qu'une map 2D de 4 blocs à sauvegarder, tu sais que le bloc en haut à gauche est dans blocs[0], celui en haut à droite dans blocs[1], etc. Ça implique qu'il faut vraiment sauvegarder tous les blocs, y compris les blocs vides. Mais tu y gagnes quand même, grâce à la compression.

  7. #7
    Expert éminent
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 395
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 395
    Par défaut
    La signature est un moyen mais la Map est charger dynamiquement en fonction du personnage et peut s'agrandir et la signature va donc changer.
    Alors tu hard-codes la clé privée de la signature dans ton code, sous forme obfusquée, comme ça tu peux signer à chaque sauvegarde de la map.

    C'est exactement aussi sécuritaire que d'avoir un format bizarre pour sauvegarder la map: Dans les deux cas, ça peut être déjoué par une rétro-ingénierie de ton code d'enregistrement.
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  8. #8
    r0d
    r0d est déconnecté
    Membre expérimenté

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 290
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 4 290
    Billets dans le blog
    2
    Par défaut
    Si je peux me permettre un conseil, d'un point de vue plus architectural que technique, c'est que dans ce genre de sérialisation complexe et évolutive, il faut factoriser les routines. Je m'explique.

    Nous avons affaire ici à un problème classique et récurrent: la sérialisation. Il est tellement récurrent que de nombreux développeurs se sont penchés sur le problème et il existe aujourd'hui de nombreux frameworks et APIs qui proposent des solutions. Différentes approches existent; voir par exemple ceci, ceci ou ceci; qui sont trois approches différentes.

    Pour faire simple, on a des données que l'on veut mémoriser quelque part, puis effectuer l'opération inverse. Ici il s'agit d'un fichier, mais le problème, au niveau architectural, est exactement le même pour des base de données, les flux réseaux, etc.

    La première étape consiste à exporter (sérialisation) les données de façon correcte. Afin de s'assurer qu'elles sont correctes, on arrive tout de suite à la deuxième étape, qui consiste à importer ces données (désérialisation) pour vérifier que la première étape (et au passage la 2ème) s'est bien passée. L'optimisation ne vient qu'à la fin, c'est important. C'est important que ça vienne à la fin je veux dire.

    Lorsque les données à sérialiser sont complexes (c'est le cas ici), il ne faut pas prendre ça à la légère car la moindre modification du code qui gère ces données peut avoir des effets de bords désastreux sur la sérialisation.

    Pour entrer dans les détails, prenons le cas simple d'une classe que l'on veut sérialiser. Il y a deux approches: soit on utilise des fonctions externes qui s'occupent de la sérialisation, soit on implémente les fonctions de sérialisation au sein de la classe. Moi je suis partisan de la seconde méthode, mais ça peut dépendre de l'architecture globale du programme et il existe des cas particuliers. Mais de toutes façons, dans les deux cas, le problème peut se résoudre à un aspect: la présentation des données. Comment la classe (celle qui contient les données) présente les données à sérialiser. L'approche basique consiste à créer des accesseurs/mutateurs sur la classe qui contient les données, et de laisser gérer ça à la classe qui va sérialiser. typiquement:
    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
    class Block
    {
    private:
       int block_type;
    public:
       Block(int block_type):block_type(block_type){}
       inline int get_type() const { return block_type; }
       void set_type (int type) { block_type = type; }
    };
     
    class Serializer
    {
    public:
       void serialize()
       {
          for_each( Block block in block_collection ) { file << block.get_type(); }
       }
     
       void deserialize()
       {
          int block_type;
          while ( read<int>( file, block_type ) ) { block_collection.push_back( new Block( block_type ) ); }
       }
    };
    Cette approche a plusieurs inconvénients. Tout d'abord, la présentation publique des données (accesseurs/mutateurs) casse l'encapsulation. On peut contourner le problème en supprimant ces accesseurs/mutateurs et en déclarant amie la classe Serializer, mais ça pose d'autres problèmes qui découlent sur des débats compliqués. Ensuite, cette méthode commence à montrer ses limites lorsque la classe que l'on veut sérializer est complexe. Parmi plusieurs problème, le plus important pour moi est la maintenance: une classe complexe va forcément évoluer. Et donc, avec cette méthode, à chaque fois qu'on va modifier cette classe, il faudra aussi modifier le code du Serializer. ça a l'air de rien comme ça, mais je peux t'assurer que c'est une source potentielle de sessions d'arrachage de cheveux intensifs; pour peu que tu oublies une fois cette modification, il peut s'avérer extrêmement difficile de trouver l'origine des bugs que ça va engendrer. C'est un peu, et en partie, la même critique que nous faisons à l'égard de l'utilisation de pointeurs nus.

    A mon avis donc, la solution consiste à implémenter, pour chaque classe que l'on souhaite sérialiser, une fonction serialize() et une fonction deserialize() (idéalement avec un héritage qui peut être privé éventuellement). Dans le fond, cela revient à implémenter les opérateurs de flux << et >>, qui est une forme de sérialisation, même si la forme est fort différente.

    Et c'est ce que j'appelle la "factorisation de routines": au lieu de présenter les données sous forme d'accesseurs/mutateurs (ou variables privées avec amitié), tout le code d'accès aux données est factorisé dans une fonction.

    Une fois que tu as ce schéma, tu peux faire ce que tu veux, l'optimisation se fera ensuite et au niveau supérieur, et tout sera plus facile à implémenter et surtout à maintenir. Par exemple:
    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
    class Block : private Serializable
    {
    private:
       int block_type;
       int pos_x, pos_y, pos_z;
       int id_owner;
       string block_name;
       string block_descrption;
       // etc.
     
    public:
       std::stringstream serialize() const
       {
          std::stringstream output;
          output << block_type << pos_x << pos_y << // etc...
          return output;
       }
     
       void deserialize( const std::stringstream & input )
       {
          block_type = input.read<int>();
          pos_x = input.read<int>();
          // etc...
       }
    };
    A noter qu'on peut aussi remplacer la fonction deserialize() par l'appel du constructeur, mais je n'aime personnellement pas cette approche.

    Et ensuite, une fois que tu as ça qui fonctionne, tu peux alors optimiser, au niveau du serializer:
    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
    class Serializer
    {
    public:
       static void serialize_map( std::ofstream & output_file, const BlockCollection & block_collection )
       {
          for_each( Block block in block_collection ) { serialize_block( output_file, block ); }
       }
     
       static void deserialize_map( std::ifstream & input_file, BlockCollection & block_collection )
       {
          // todo
          throw( std::exception ( "not implemented yet" ) );
       }
     
    private:
       static void serialize_block( std::ofstream & output_file, const Block & block )
       {
          std::stringstream sst_data = block.serialize();
          std::bitset<data.size()> bin_data;
          bin_data << sst_data;
          output_file << bin_data;
       }
    };
     
    // note: les fonctions membres statiques c'est bon, mangez-en
    Ce qu'il y a de magique dans cette façon de faire, c'est que tout est bien séparé, et on peut se permettre d'intervenir à différentes étapes du process sans tout casser. Ce n'est pas non plus intrusif (chaque classe gère elle-même sa sérialisation).

    Le principal inconvénient de cette méthode, c'est qu'elle peut poser des problèmes de respect de RAII. En effet, selon l'architecture du code, il peut parfois être difficile de concilier le deserialize() et le constructeur.

    Il existe d'autres façons de faire, et chaque cas est différent, mais celle-ci est suffisamment souple pour fonctionner et être acceptable dans la majorité des cas.

    Hope it helps.

  9. #9
    Expert éminent
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 395
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 395
    Par défaut
    Citation Envoyé par r0d Voir le message
    A noter qu'on peut aussi remplacer la fonction deserialize() par l'appel du constructeur, mais je n'aime personnellement pas cette approche.

    <snip>

    Le principal inconvénient de cette méthode, c'est qu'elle peut poser des problèmes de respect de RAII. En effet, selon l'architecture du code, il peut parfois être difficile de concilier le deserialize() et le constructeur.
    En effet, avoir une méthode de désérialisation séparée du constructeur empêche toute existence d'un membre constant dépendant des données sérialisées.

    On retrouve ces deux approches différentes dans .Net et Windows, où la sérialisation.Net "normale" passe par un constructeur de désérialisation, alors que la sérialisation .Net XML et la sérialisation COM (via IPersistXxxxx) passent par une désérialisation séparée.
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  10. #10
    Membre Expert
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2012
    Messages
    1 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2012
    Messages : 1 711
    Par défaut
    Citation Envoyé par Ninored Voir le message
    La map serra basé sur un aléatoire contrôlé par un seed, et pour un temps de chargement moindre je la sauvegarde (effectivement j'ai oublier de signaler que la map est en trois dimensions ^^' ). Mon problème est dans la structure de cette sauvegarde, comment faire en sorte qu'elle soit optimisé, qu'elle prenne le moins de place possible et qu'elle ne soit pas modifiable comme celle que tu m'as présenté.

    La signature est un moyen mais la Map est charger dynamiquement en fonction du personnage et peut s'agrandir et la signature va donc changer. Pour un comparatif mes mapes ressemblerons à celle de minacraft.
    Hello,

    le temps de chargement est-il vraiment un problème ?
    Je m'explique, tu es dans un monde (la map) à priori infini, ta map est découpée en morceau qui ont tous la même taille.
    Chaque morceau contient un nombre déterminé de blocs.
    Tu garde en mémoire 27 morceaux : celui ou est le joueur et les 26 autours.

    Quant le joueur change de morceau, tu sauvegardes les morceaux qui disparaissent et chargent (ou génère) les morceaux requis pour toujours avoir 27 morceaux aux alentours du joueur.
    -> Tu peux faire cette opération en arrière plan (dans un thread à part) si tu peux être sur que le joueur ne verra pas les morceaux à charger tout de suite (donc si tes morceaux sont assez gros et pas transparent, ou si le seuil de vision est plus faible que la taille d'un morceau).
    Et même si la sauvegarde / chargement prends plusieurs secondes, tant que tu peux être sur que pendant ce temps le joueur ne peux pas voir les blocs à charger, il n'y à pas de problèmes.

    Après plusieurs solutions :
    1 - Sauvegarder seulement les données nécessaire à la génération du morceau (seed initial et paramètres de l'algo de Perlin), ainsi que les modifications apportées au bloc.
    C'est possible seulement si la génération ne prend pas trop longtemps, et ça te générera des petits fichiers tant que le joueur n'a pas fait trop de modifications sur le morceau.
    2 - Dump le morceau, puis y appliquer une compression type zip ou lzw. (Je suppose qu'il y aura très souvent des blocs identiques collés, et donc un fort taux de compression).

  11. #11
    r0d
    r0d est déconnecté
    Membre expérimenté

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 290
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 4 290
    Billets dans le blog
    2
    Par défaut
    <HS>
    Au cas où quelqu'un soit intéressé, il existe un portage en minecraft en c++ qui commence à avoir pas mal de contenu. En plus il est open source, et le code est pas mal: http://minetest.net/
    </HS>

  12. #12
    Membre averti
    Homme Profil pro
    Terminal S
    Inscrit en
    Juin 2013
    Messages
    30
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Val d'Oise (Île de France)

    Informations professionnelles :
    Activité : Terminal S

    Informations forums :
    Inscription : Juin 2013
    Messages : 30
    Par défaut
    J'ai compris la méthode de sérialiser et dé-sérialiser mais voila je trouve cela un peut compliqué vue mon niveau en programmation actuel, je connais certaines choses mais d'autre non, si vous voulez j'ai d'abord regardé des fichier de sauvegarde de map (ex: dans le dossier save de minecraft ou même dans celui de DwarfFortress) la première chose que j'ai remarquer est qu'il n'y a aucun texte brut. j'ai démarrer sur l'idée de structure:

    • Coordonée du Chunk (X;Y)[char] <-- pas plus de 255
      • Block ( pour regrouper les données)[utin] <-- pour le designer son id ou le reconnaitre
        • Id block(0 - ...)[uint] <-- type de block (Terre, eau, sable ...)
        • Coordonée du block(X;Y;Z)[int] <--- peut être négatif

    NB: Je n'impose pas cette structure.
    Je ne sais pas quelle moyens est assez efficace pour gérer cela. J'avais comme idée de garder la map (ou le morceau de la map) dans une variable, soit dans une structure temporaire avec les coordonnées (X;Y) du Chunk [char <-- pas plus de 255] et les Coordonnées de chaque block sous forme de tableau (X;Y;Z) [int], avec éventuellement d'autres choses dans cette structure mais je ne trouve pas de moyens assez simple pour écrire (dans le fichier) cette structure facilement...
    Code C++ : 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
     
    //struct pour les données a sauvegarder. Temporaire.
    // pour peut être éviter d'utiliser trop de mémoire avec les variables il doit y avoir une
    // méthode par la STL (mes connaissances en sont réduites) 
    struct TempMap{
        char ChunkId[x][y];
        int BlockPos[x][y][z];
        unsigned int BlockId;
    } st ;
     
    //sauvegarde de la struct
    /*
    * Il y aura une classe map pour tout ce qui concerne le nom, la génération, et autres.
    * Je compte utiliser une autre classe pour sauvegarder dans n'importe quelle fichier des
    * données qui lui serons envoyées
    */
    bool SaveMap(TempMap* st, std::string MapName)
    {
    /* Ici je ne sais pas du tout comment organiser la chose  */
    }
    // Ou Même utiliser un pointeur vers la classe pour accéder au données directement
    bool SaveMap(TempMap* st, Map* map)
    {
    /* Ici je ne sais pas du tout comment organiser la chose  */
    }

  13. #13
    Expert éminent
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 395
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 395
    Par défaut
    Stocker les coordonnées d'une valeur, ça va pour les matrices "creuses", mais au-dessus d'une certaine densité ça devient plus encombrant que simplement les valeurs (surtout si celles-ci sont facilement compressibles).
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  14. #14
    Membre averti
    Homme Profil pro
    Terminal S
    Inscrit en
    Juin 2013
    Messages
    30
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Val d'Oise (Île de France)

    Informations professionnelles :
    Activité : Terminal S

    Informations forums :
    Inscription : Juin 2013
    Messages : 30
    Par défaut
    Je ne connais pas les matrices creuse (j'ai fait une petite recherche dit moi si c'est faux, c'est bien une matrice qui ne prend en compte que les valeurs non nul).

    Si c'est le cas je pense que je serais obligé de donner les coordonnées de chaque block pour avoir plus de facilité a les charger (au détriment de la vitesse). Mais ceci n'ai pas le problème primaire, j'utiliserais probablement une compression du fichier lui même a l'extinction du programme, et décompression à l'ouverture. (avec Zlib proposer par Kalith ou quelque chose qui s'en rapproche )


    Kalith: Edit : j'ai oublié de préciser... Inutile de sauvegarder la position des chunks/blocs, leur position en mémoire (ou dans le fichier) donne déjà cette information. En gros si tu n'avais qu'une map 2D de 4 blocs à sauvegarder, tu sais que le bloc en haut à gauche est dans blocs[0], celui en haut à droite dans blocs[1], etc. Ça implique qu'il faut vraiment sauvegarder tous les blocs, y compris les blocs vides. Mais tu y gagnes quand même, grâce à la compression.
    En fait je sauvegarderais les positions des chunks car chaque chunk sera sauvegardé dans un fichier et pour éviter des valeurs trop grande pour les blocks je les positionnerais relativement au chunk au-quelle il appartient (l'appartenance n'as pas besoins d'être mentionnée dans une variable car cela seras logique au moment du chargement)

  15. #15
    Membre chevronné

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Juin 2007
    Messages
    373
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Royaume-Uni

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Santé

    Informations forums :
    Inscription : Juin 2007
    Messages : 373
    Par défaut
    Citation Envoyé par r0d Voir le message
    La première étape consiste à exporter (sérialisation) les données de façon correcte. Afin de s'assurer qu'elles sont correctes, on arrive tout de suite à la deuxième étape, qui consiste à importer ces données (désérialisation) pour vérifier que la première étape (et au passage la 2ème) s'est bien passée. L'optimisation ne vient qu'à la fin, c'est important. C'est important que ça vienne à la fin je veux dire.
    On dit ça souvent, et c'est souvent vrai, mais en réalité je pense que ça dépend du projet sur lequel tu travailles. Il y a des optimisations importantes qui ne peuvent être faites que si elles ont été pensées à la base et font partie de la conception du projet. Celui-ci est un bon exemple : mettons que l'on se donne comme objectif d'afficher jusqu'à 256 blocs de distance (ce qui n'est pas immense, sachant qu'un bloc fait 1m de côté dans Minecraft par exemple), on se retrouve avec ~100 millions de blocs à garder en mémoire. Dès lors, il est évident que la taille qu'un bloc occupe en mémoire est une donnée critique, chaque octet ajouté augmentant la RAM occupée de ~100 Mo.

    Sans écrire le moindre code, ces nombres montrent déjà qu'il y a des implémentations qui ne sont pas raisonnables. En particulier :
    1. On sait immédiatement qu'on ne pourra pas créer tous ces blocs sur le tas (avec new), puisque ça implique non seulement de stocker les données de chaque bloc, mais également le pointeur qui permet d'y accéder, qui représente à lui seul 400 Mo de perdus en 32 bits, et 800 Mo en 64 bits.
    2. Il va arriver fréquemment d'avoir à effectuer une opération sur une suite de blocs connectés dans l'espace (exemple : "pour chaque blocs dans ce chunk, générer une liste de points pour le rendu", ou "pour chaque blocs dans ce chunk, mettre à jour la quantité de lumière reçue", etc.). De manière générale, les performances sont toujours meilleures si les données traitées sont proches les unes des autres en mémoire (ce qui permet au CPU de charger les données de la RAM vers le cache moins souvent), donc ça suggère que la position des blocs dans l'espace soit reliée à leur position en mémoire.

    Ces deux points tendent à montrer qu'une structure efficace pour représenter la carte en mémoire est de stocker le plus possible les blocs dans un tableau de manière contiguë (donc std::vector ou, si possible, std::array). Cette implémentation va imposer un certain nombre de choses, par exemple on ne pourra pas utiliser de polymorphisme pour la classe Block, on va devoir garder en mémoire même les blocs qui sont vides, etc.

    En revanche, si l'objectif est d'afficher la carte jusqu'à une distance de 1024 blocs, on parle de 4 à 10 Go de mémoire pour chaque octet de la classe Block... Dans ce cas là, on peut dire adieu au fait de garder tous les blocs en mémoire individuellement, et il faut songer à d'autres représentations de données, en particulier l'octree (qui est une forme de compression de donnée : en bref, il s'agit de grouper ensemble les blocs de type identique pour ne former qu'un seul bloc plus gros). Si je ne m'abuse, c'est ce qu'utilise Minetest.

    Autre point : il est fort probable qu'il y ait besoin à un moment de faire certains calculs en arrière plan car ils ralentiraient trop l'affichage. Ce n'est pas toujours facile, il faut bien veiller à ne pas corrompre les données par des lectures/écritures simultanées, et bien sûr toutes les implémentations ne seront pas aussi performantes les unes que les autres (si on s'y prend mal, ça peut être plus lent encore que de tout faire dans le thread principal...). Pour s'assurer de faire les choses correctement et d'en tirer le plus de bénéfice, il y a des structures de données et des interfaces qui vont être plus faciles à manier.

    Là où je veux en venir, c'est que passer d'une implémentation à une autre n'est pas toujours trivial : certaines interfaces deviennent inadaptées, une partie des algorithmes est à revoir, ou pour un même algorithme générique certains problème de performances qui étaient négligeables avec une implémentation deviennent monstrueux avec l'autre, etc. Le choix de la meilleure implémentation va dépendre des limites que tu veux t'imposer, et dans certains cas ce choix d'implémentation va lui-même avoir un impact sur la conception globale de ton programme. Penser la conception sans prendre en compte les impératifs de performance peut mener dans ces cas là à une application lourdingue (comme on en voit tant de nos jours) qu'on ne peut pas rendre plus rapide sans la remanier depuis la base.

    Dans le fond je suis d'accord avec ce que tu dis, r0d*, mais dans la pratique pour ce genre de projet où les performances sont dès le début un problème, je pense que la bonne approche pour Ninored est la suivante :
    1. Réfléchir à ce tu veux faire avec ton projet. C'est à cette étape que tu vas définir des choses comme la quantité de mémoire maximale que ton programme peut utiliser (si tu veux que ton petit frère puisse y jouer sur son vieux PC qui n'a que 512 Mo de RAM, ou si tu ne vise que les PC de dernière génération avec 8 Go et plus), le genre de choses que peut contenir ton monde (seulement des blocs immobiles, ou aussi des personnages qui se déplacent, des fluides, etc.), quel genre de gameplay (tir à la première personne ou stratégie en vue du dessus), et j'en passe. À cette étape il faut essayer de penser à tout, ou au moins d'avoir une vague idée de ce que tu veux. Et surtout, par la suite il te faudra t'y tenir, et en dévier le moins possible.
    2. Une fois que tu as bien circonscrit tes objectifs, tu peux commencer à coder à l'arrache un prototype qui va te permettre d'identifier les plus gros problèmes de performance (ça consistera en majorité à veiller à l'occupation mémoire, faire travailler le plus efficacement possible la carte graphique, et mettre le plus possible de tâches en arrière plan). Pendant ce processus, tu vas tester différentes structures de données, différentes manières d'afficher le tout à l'écran, et te rendre compte que certains choix sont meilleurs que d'autres. Quand tu arrives à faire tourner ton prototype avec de bonnes performances en situation "réelle" (c'est à dire en particulier avec une carte aussi grande que ce que tu as prévu d'avoir au final), tu as fait le tour des différentes implémentations possibles et tu en a trouvé une qui répond le mieux à tous les critères que tu t'es imposé dans l'étape précédente. Je vois que tu débutes aussi en C++, c'est aussi le meilleur moment pour faire des erreur graves et apprendre à ne plus les faire par la suite
    3. Tu as défini tes exigences, et tu as trouvé une implémentation réaliste qui te permet de faire ce que tu veux. Tu peux maintenant finaliser la conception de ton programme en pensant à tout ce que tu n'a pas fait dans le prototype (majoritairement des trucs plus accessoires du style comment gérer la communication en multijoueur, comment gérer les différents objets dans l'inventaire, etc.), et à comment les différentes parties de ton programme vont s'imbriquer dans un tout cohérent.
    4. Une fois que tu as cette vision d'ensemble, tu peux t'attaquer à coder une version propre de ton projet.


    Ta question initiale, qui est de savoir comment sauvegarder tes données sur le disque pour pouvoir les recharger par la suite, intervient plutôt vers la fin de l'étape 2. C'est un problème "accessoire", mais qui peut potentiellement avoir des conséquences importantes sur les performances (écrire sur le disque est assez lent), et tu devras sans doute faire ça dans un autre thread. Qui plus est, comme le font remarquer les autres, la meilleure manière de faire va dépendre dans une large mesure de comment tu gère ta carte en mémoire dans ton programme, donc tu dois avant tout converger sur ce point.

    Si tu veux un exemple :
    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
    struct Block {
        char type;
        // Mets ce que tu veux là dedans (attention à l'occupation mémoire)
    };
     
    // Choisis la taille que tu veux
    const std::size_t CHUNK_SIZE = 64;
     
    struct Chunk {
        char x, y, z; // NB: a priori tu n'as pas besoin de ça : tu peux le stocker directement dans le nom du fichier (et c'est plus pratique pour le retrouver après !)
        Block blocs[CHUNK_SIZE][CHUNK_SIZE][CHUNK_SIZE];
    };
     
    void save(const std::string& file_name, const Chunk& c) {
        std::ofstream file(file_name, std::ios::binary);
     
        char* compressed;
        std::size_t size_compressed;
        compress(reinterpret_cast<const char*>(&c), sizeof(c), compressed, size_compressed);
     
        file.write(reinterpret_cast<char*>(&size_compressed), sizeof(size_compressed));
        file.write(compressed, size_compressed);
    }
     
    void load(const std::string& file_name, Chunk& c) {
        std::ifstream file(file_name, std::ios::binary);
     
        std::size_t size_compressed;
        file.read(reinterpret_cast<char*>(&size_compressed), sizeof(size_compressed));
        char* compressed;
        file.read(compressed, size_compressed);
     
        decompress(compressed, size_compressed, reinterpret_cast<char*>(&c));
    }
    Ça sera relativement performant si tu as un nombre important de blocs par chunk, de sorte que
    1. la compression soit la plus efficace possible,
    2. tu n'aies pas à charger de fichiers trop souvent.


    @Icadrille: Je ne connais pas les impératifs de l'OP, mais s'il cherche à faire un jeu comme Minecraft, alors il y a des situations où le temps de chargement doit être court, par exemple si tu tombes d'un très haut fossé, tu te déplaces bien plus vite qu'à l'horizontale, et il faut que ton code de chargement tienne le rythme. Dans Minecraft ils n'ont pas de problème avec ça puisqu'ils chargent toute la hauteur de la carte d'un coup (ça a ses avantages et ses inconvénients). Sinon pour le coup de sauvegarder juste la graine de l'algorithme procédural, ça ne fonctionnera pas s'il permet aux blocs qui composent sa carte d'être modifiés manuellement (ce qui est le cas dans Minecraft : l'algo est juste utilisé quand une région doit être générée pour la première fois, mais sinon c'est sauvegardé sur le disque. C'est nettement plus rapide aussi, selon la complexité de l'algorithme de génération de terrain).

    * : quoique, passer par un std::stringstream, c'est certes tout à fait générique, par contre question performances

  16. #16
    Expert éminent
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 395
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 395
    Par défaut
    Ah, je n'avais pas compris que c'était pour les chunks que tu voulais enregistrer les coordonnées, je croyais que tu voulais le faire pour les blocs

    Normalement pour un bloc, tu n'as que deux choses à sauvegarder: Son type (avec potentiellement un sous-type), et son état (qui peut si nécessaire être une union dépendante du type de bloc).

    Pour tes chunks, je me demande si les coordonnées n'ont pas plus leur place dans le nom du fichier que dans le fichier lui-même, car cela te permettra de trouver le chunk sur le disque à partir de ses coordonnées. (par contre, à ce niveau-là rien ne t'empêche d'être redondant pour contrôler l'intégrité des données).
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  17. #17
    Membre averti
    Homme Profil pro
    Terminal S
    Inscrit en
    Juin 2013
    Messages
    30
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Val d'Oise (Île de France)

    Informations professionnelles :
    Activité : Terminal S

    Informations forums :
    Inscription : Juin 2013
    Messages : 30
    Par défaut
    Un Grand merci a vous tous et particulièrement a Kalith. J'ai appris des choses et bien cerner mon problème, je vais appliquer la méthode donnée par Kalith pour réaliser mon projet.

    NB: j'ai compris mon erreur pour les coordonées du chunk, je mettrais cette info dans le nom du fichier comme conseillé.

    Plus tard je mettrais une version sur un GIT, j'éditerais ou le posterais sur ce forum un lien.

    Un Grand Merci a vous tous.

  18. #18
    r0d
    r0d est déconnecté
    Membre expérimenté

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    4 290
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 4 290
    Billets dans le blog
    2
    Par défaut
    Puisque le sujet est résolu, je vais en rajouter une couche (de dirt)
    C'est que c'est passionnant tout ça. Il est d'ailleurs intéressant de noter que le défi, réussi par Notch, a été réussi en java. Je dis ça parce qu'ici même, beaucoup d'intervenants ont tendance à dénigrer java, alors que d'une part java n'est pas un si mauvais langage que l'on croit souvent, même en terme de performances, et d'autres part, les récentes évolutions, et entre autre joGL, ont changé beaucoup de choses.

    Ensuite, pour te répondre Kalith. Tout d'abord, merci pour ton long message, je suis d'accord en grande partie avec toi. Mais mon message s'adressait à Ninored, un débutant. C'est la raison pour laquelle j'insistais sur des principes de base (modularité, séparation des responsabilités, pas d'optimisation précoce, etc.). Ces principes de base sont faits pour améliorer la courbe d'apprentissage. Une fois qu'ils sont compris, et que l'on a pris conscience des pièges qu'ils permettent d'éviter ainsi que de leurs limites et défauts, alors, et seulement alors, on peut se permettre de passer outre. Enfin, c'est ma vision des choses.

    Enfin, il y a tout de même quelque chose qui me gène dans ton message, c'est que tu ne prend pas en compte le fait qu'un programme comme minecraft est séparé en deux parties: client et serveur. Et que:
    1. côté client, on a besoin que du type des blocks. Donc de simples matrices d'entiers. Et en plus, la carte graphique va gérer une grande partie du problème. Il existe même des solutions à ce type de problème qui consistent à utiliser des matrices de textures directement (en fait de sont des pointeurs vers des textures, mais c'est le driver et le gpu qui gèrent tout, avec le z-buffer et tout), et donc tu n'a même pas besoin d'avoir de données en RAM.
    2. côté serveur, il suffit de ne charger que quelques chunks, ceux qui sont près du joueur.

    Sinon oui, effectivement stringstream est affreux en terme de perf, c'était juste un exemple à la rache pour montrer l'idée.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Créer son propre système de fichiers
    Par L'immortel dans le forum Programmation d'OS
    Réponses: 15
    Dernier message: 15/12/2013, 22h16
  2. Créer son propre MessageDlg
    Par snoop94 dans le forum Langage
    Réponses: 4
    Dernier message: 21/11/2005, 18h14
  3. Créer son propre éditeur pour un descendant de tpopupmenu
    Par sfpx dans le forum Composants VCL
    Réponses: 1
    Dernier message: 04/10/2005, 12h21
  4. Créer son propre LayoutManager
    Par tomburn dans le forum Agents de placement/Fenêtres
    Réponses: 9
    Dernier message: 17/03/2005, 16h15
  5. créer son propre protocole
    Par matthew_a_peri dans le forum Développement
    Réponses: 11
    Dernier message: 04/03/2005, 14h16

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