Cómo leer y escribir datos binarios para sus formatos de archivo personalizados

En mi artículo anterior, Crear formatos de archivos binarios personalizados para los datos de su juego, cubrí el tema de utilizando Formatos de archivos binarios personalizados para almacenar recursos y recursos del juego. En este breve tutorial veremos cómo leer y escribir datos binarios..

Nota: Este tutorial usa pseudocódigo para demostrar cómo leer y escribir datos binarios, pero el código puede traducirse fácilmente a cualquier lenguaje de programación que admita operaciones básicas de E / S de archivos..


Operadores de Bitwise

Si todo esto es un territorio desconocido para usted, notará que se están utilizando algunos operadores extraños en el código, específicamente el Y, |, << y >> operadores Estos son operadores estándar bitwise, disponibles en la mayoría de los lenguajes de programación, que se utilizan para manipular valores binarios.

Artículos Relacionados
Para obtener más información sobre los operadores bitwise, consulte:
  • Entendiendo los operadores de Bitwise
  • La documentación para el lenguaje de programación de su elección.

Endianness y arroyos

Antes de que podamos leer y escribir datos binarios con éxito, hay dos conceptos importantes que debemos entender: endianidad y arroyos.

Endianness dicta el orden de los valores de múltiples bytes dentro de un archivo o dentro de una porción de memoria. Por ejemplo, si tuviéramos un valor de 16 bits de 0x1020, ese valor puede ser almacenado como 0x10 seguido por 0x20 (Big Endian) o 0x20 seguido por 0x10 (Little-Endian).

Las secuencias son objetos de tipo matriz que contienen una secuencia de bytes (o bits en algunos casos). Los datos binarios se leen y se escriben en estas secuencias. La mayoría de la programación proporcionará una implementación de flujos binarios de una forma u otra; algunos son más complicados que otros, pero esencialmente todos hacen lo mismo.


Lectura de datos binarios

Empecemos definiendo algunas propiedades en nuestro código. Idealmente, todos estos deben ser propiedades privadas:

 __stream // El objeto similar a una matriz que contiene los bytes __endian // La endiancia de los datos dentro del flujo __length // El número de bytes en el flujo __position // La posición del siguiente byte para leer del flujo

Aquí hay un ejemplo de cómo podría verse un constructor de clase básico:

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

Las siguientes funciones leerán enteros sin signo del flujo:

 // Lee una función de entero de 8 bits sin signo readU8 () // Lanza una excepción si no hay más bytes disponibles para leer si (__position> = __length) lanza una nueva excepción ("...") // Devuelve el byte valore y aumente el retorno de la propiedad __position __stream [__position ++] // Lee una función de entero de 16 bits sin signo readU16 () value = 0 // Endianness debe manejarse para valores de múltiples bytes si (__endian == BIG_ENDIAN) valor | = 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 

Estas funciones leerán enteros con signo del flujo:

 // Lee una función de entero de 8 bits con signo readS8 () // Lee el valor sin signo value = readU8 () // Compruebe si el primer bit (el más significativo) indica un valor negativo si (value >> 7 == 1) // Use "Complemento de dos" para convertir el valor valor = ~ (valor ^ 0xFF) valor devuelto // Lee una función de entero de 16 bits con signo readS16 () value = readU16 () if (value >> 15 = = 1) valor = ~ (valor ^ 0xFFFF) valor de retorno // Lee una función de entero de 24 bits con signo readS24 () valor = readU24 () if (value >> 23 == 1) value = ~ ( valor ^ 0xFFFFFF) valor devuelto // Lee una función de entero de 32 bits con signo readS32 () valor = readU32 () if (valor >> 31 == 1) valor = ~ (valor ^ 0xFFFFFFFF) valor de retorno

Escribiendo datos binarios

Empecemos definiendo algunas propiedades en nuestro código. (Estas son más o menos las mismas que las propiedades que definimos para leer datos binarios). Idealmente, todas deberían ser propiedades privadas:

 __stream // El objeto similar a una matriz que contendrá los bytes __endian // La endianness de los datos dentro de la secuencia __position // La posición del siguiente byte para escribir en la ruta

Aquí hay un ejemplo de cómo podría verse un constructor de clase básico:

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

Las siguientes funciones escribirán enteros sin signo en el flujo:

 // Escribe una función de entero de 8 bits sin signo writeU8 (value) // Asegura que el valor no tenga signo y esté dentro de un valor de rango de 8 bits & = 0xFF // Agregue el valor al flujo y aumente la propiedad __position. __stream [__position ++] = valor // Escribe una función de entero de 16 bits sin signo writeU16 (valor) valor & = 0xFFFF // La endianidad debe manejarse para valores de múltiples bytes si (__endian == BIG_ENDIAN) writeU8 ( valor >> 8) writeU8 (value >> 0) else // LITTLE_ENDIAN writeU8 (value >> 0) writeU8 (value >> 8) // Escribe una función de entero de 24 bits sin signo writeU24 (value) value & = 0xFFFFFF if (__endian == BIG_ENDIAN) writeU8 (valor >> 16) writeU8 (value >> 8) writeU8 (value >> 0) else writeU8 (value >> 0) writeU8 (value >> 8) writeU8 (valor >> 16) // Escribe una función de entero de 32 bits sin signo writeU32 (value) value & = 0xFFFFFFFF if (__endian == BIG_ENDIAN) writeU8 (value >> 24) writeU8 (value >> 16) writeU8 (valor >> 8) writeU8 (value >> 0) else writeU8 (value >> 0) writeU8 (value >> 8) writeU8 (value >> 16) writeU8 (value >> 24)

Y, nuevamente, estas funciones escribirán enteros con signo en el flujo. (Las funciones son en realidad alias del writeU * () funciones, pero proporcionan consistencia API con el lecturas * () funciones.)

 // Escribe una función de valor de 8 bits firmada writeS8 (valor) writeU8 (valor) // Escribe una función de valor de 16 bits firmada writeS16 (valor) writeU16 (valor) // Escribe una función de valor de 24 bits firmada writeS24 (valor) writeU24 (valor) // Escribe una función de valor de 32 bits firmada writeS32 (valor) writeU32 (valor)

Nota: Estos alias funcionan porque los datos binarios siempre se almacenan como valores sin signo; por ejemplo, un solo byte siempre tendrá un valor en el rango de 0 a 255. La conversión a valores firmados se realiza cuando los datos se leen de un flujo.


Conclusión

Mi objetivo con este breve tutorial era complementar mi artículo anterior sobre la creación de archivos binarios para los datos de su juego con algunos ejemplos de cómo hacer la lectura y escritura reales. Espero que se haya logrado eso; Si hay más que le gustaría saber sobre el tema, por favor, hable en los comentarios.!