Comment lire et écrire des données binaires pour vos formats de fichiers personnalisés

Dans mon article précédent, Créer des formats de fichier binaires personnalisés pour les données de votre jeu, je couvrais le sujet suivant: en utilisant formats de fichiers binaires personnalisés pour stocker les actifs et les ressources du jeu. Dans ce court tutoriel, nous verrons comment lire et écrire des données binaires..

Remarque: Ce tutoriel utilise un pseudo-code pour montrer comment lire et écrire des données binaires, mais le code peut facilement être traduit en n'importe quel langage de programmation prenant en charge les opérations d'E / S de fichier de base..


Opérateurs binaires

Si tout cela vous est inconnu, vous remarquerez que quelques opérateurs étranges sont utilisés dans le code, en particulier le Et, |, << et >> les opérateurs. Ce sont des opérateurs standard au niveau des bits, disponibles dans la plupart des langages de programmation, utilisés pour manipuler des valeurs binaires..

Articles Similaires
Pour plus d'informations sur les opérateurs au niveau des bits, voir:
  • Comprendre les opérateurs au niveau des bits
  • La documentation de votre langage de programmation de choix

Endianness et Streams

Avant de pouvoir lire et écrire des données binaires avec succès, nous devons comprendre deux concepts importants: endianité et ruisseaux.

L'endianisme dicte l'ordre des valeurs sur plusieurs octets dans un fichier ou dans un bloc de mémoire. Par exemple, si nous avions une valeur de 16 bits de 0x1020, cette valeur peut être stockée sous la forme 0x10 suivi par 0x20 (big-endian) ou 0x20 suivi par 0x10 (petit endian).

Les flux sont des objets de type tableau contenant une séquence d'octets (ou des bits dans certains cas). Les données binaires sont lues et écrites dans ces flux. La plupart des programmes fournissent une implémentation de flux binaires sous une forme ou une autre; certains sont plus compliqués que d'autres, mais ils font tous essentiellement la même chose.


Lecture de données binaires

Commençons par définir certaines propriétés dans notre code. Idéalement, ceux-ci devraient tous être des propriétés privées:

 __stream // Objet de type tableau contenant les octets __endian // Endianité des données dans le flux __length // Nombre d'octets dans le flux __position // Position de l'octet suivant à lire dans le flux

Voici un exemple de ce à quoi un constructeur de classe de base pourrait ressembler:

 class DataInput (stream, endian) __stream = stream __endian = endian __length = stream.length __position = 0

Les fonctions suivantes liront des entiers non signés du flux:

 // Lit un entier non signé de 8 bits, fonction readU8 () // Lance une exception s'il n'y a plus d'octets à lire si (__position> = __length) lance une nouvelle exception ("…") // Renvoie l'octet valeur et augmentation de la propriété __position return __stream [__position ++] // Lit un entier non signé de 16 bits, fonction readU16 () value = 0 // L'endianité doit être gérée pour les valeurs sur plusieurs octets if (__endian == BIG_ENDIAN) valeur | = readU8 () << 8 value |= readU8() << 0  else  // LITTLE_ENDIAN value |= readU8() << 0 value |= readU8() << 8  return value  // Reads an unsigned 24-bit integer function readU24()  value = 0 if( __endian == BIG_ENDIAN )  value |= readU8() << 16 value |= readU8() << 8 value |= readU8() << 0  else  value |= readU8() << 0 value |= readU8() << 8 value |= readU8() << 16  return value  // Reads an unsigned 32-bit integer function readU32()  value = 0 if( __endian == BIG_ENDIAN )  value |= readU8() << 24 value |= readU8() << 16 value |= readU8() << 8 value |= readU8() << 0  else  value |= readU8() << 0 value |= readU8() << 8 value |= readU8() << 16 value |= readU8() << 24  return value 

Ces fonctions liront les entiers signés du flux:

 // Lit un entier signé de 8 bits, fonction readS8 () // Lit la valeur non signée value = readU8 () // Vérifie si le premier bit (le plus significatif) indique une valeur négative if (valeur >> 7 == 1) // Utilise le "complément à deux" pour convertir la valeur valeur = ~ (valeur ^ 0xFF) valeur renvoyée // lit une fonction entière signée de 16 bits readS16 () valeur = readU16 () if (valeur >> 15 = = 1) valeur = ~ (valeur ^ 0xFFFF) valeur de retour // Lit une fonction entière signée de 24 bits readS24 () valeur = readU24 () if (valeur >> 23 == 1) valeur = ~ ( valeur ^ 0xFFFFFF) valeur de retour // Lit une fonction entière signée de 32 bits readS32 () valeur = readU32 () if (valeur >> 31 == 1) valeur = ~ (valeur ^ 0xFFFFFFFF valeur de retour

Écrire des données binaires

Commençons par définir certaines propriétés dans notre code. (Celles-ci sont plus ou moins les mêmes que les propriétés que nous avons définies pour la lecture de données binaires.) Idéalement, elles devraient toutes être des propriétés privées:

 __stream // Objet de type tableau contenant les octets __endian // Endianité des données dans le flux __position // Position de l'octet suivant à écrire dans le flux

Voici un exemple de ce à quoi un constructeur de classe de base pourrait ressembler:

 classe DataOutput (stream, endian) __stream = stream __endian = endian __position = 0

Les fonctions suivantes écriront des entiers non signés dans le flux:

 // Écrit un entier non signé de 8 bits, fonction writeU8 (valeur) // Garantit que la valeur est non signée et comprise dans une plage de valeurs de 8 bits & = 0xFF // Ajoutez la valeur au flux et augmentez la propriété __position. __stream [__position ++] = valeur // Ecrit une fonction d'entier 16 bits non signée writeU16 (valeur) valeur & = 0xFFFF // L'endianité doit être gérée pour les valeurs à plusieurs octets si (__endian == BIG_ENDIAN) writeU8 ( valeur >> 8) writeU8 (valeur >> 0) else // LITTLE_ENDIAN writeU8 (valeur >> 0) writeU8 (valeur >> 8) // Écrire une fonction entière 24 bits non signée writeU24 (valeur) valeur & = 0xFFFFFF if (__endian == BIG_ENDIAN) writeU8 (valeur >> 16) writeU8 (valeur >> 8) writeU8 (valeur >> 0) sinon writeU8 (valeur >> 0) writeU8 (valeur >> 8) writeU8 (valeur >> 16) // Ecrit une fonction d'entier 32 bits non signée writeU32 (valeur) valeur & = 0xFFFFFFFF si (__endian == BIG_ENDIAN) writeU8 (valeur >> 24) writeU8 (valeur >> 16) writeU8 (valeur >> 8) writeU8 (valeur >> 0) else writeU8 (valeur >> 0) writeU8 (valeur >> 8) writeU8 (valeur >> 16) writeU8 (valeur >> 24)

Et, encore une fois, ces fonctions vont écrire des entiers signés dans le flux. (Les fonctions sont en fait des alias du writeU * () fonctions, mais elles assurent la cohérence de l'API avec le readS * () les fonctions.)

 // Ecrit une fonction de valeur 8 bits signée writeS8 (valeur) writeU8 (valeur) // // Ecrit une fonction de valeur 16 bits signée writeS16 (valeur) writeU16 (valeur) // // Ecrit une fonction de valeur 24 bits signée. writeS24 (valeur) writeU24 (valeur) // Écrit une fonction de valeur 32 bits signée writeS32 (valeur) writeU32 (valeur)

Remarque: Ces alias fonctionnent car les données binaires sont toujours stockées sous forme de valeurs non signées. Par exemple, un seul octet aura toujours une valeur comprise entre 0 et 255. La conversion en valeurs signées est effectuée lorsque les données sont lues à partir d'un flux..


Conclusion

Mon objectif avec ce court tutoriel était de compléter mon article précédent sur la création de fichiers binaires pour les données de votre jeu avec quelques exemples sur la manière de lire et d’écrire. J'espère que c'est ce que j'ai réalisé; si vous souhaitez en savoir plus sur le sujet, veuillez prendre la parole dans les commentaires.!