Come leggere e scrivere dati binari per i formati di file personalizzati

Nel mio precedente articolo, Crea formati di file binari personalizzati per i dati del tuo gioco, ho trattato l'argomento di utilizzando formati di file binari personalizzati per archiviare risorse e risorse di gioco. In questo breve tutorial daremo una rapida occhiata a come effettivamente leggere e scrivere dati binari.

Nota: Questo tutorial utilizza pseudo-codice per dimostrare come leggere e scrivere dati binari, ma il codice può essere facilmente tradotto in qualsiasi linguaggio di programmazione che supporti le operazioni di I / O di file di base.


Operatori bit a bit

Se questo è tutto territorio sconosciuto per te, noterai alcuni strani operatori usati nel codice, in particolare il &, |, << e >> operatori. Si tratta di operatori bit a bit standard, disponibili nella maggior parte del linguaggio di programmazione, che vengono utilizzati per manipolare valori binari.

Post correlati
Per ulteriori informazioni sugli operatori bit a bit, vedere:
  • Comprensione degli operatori bit a bit
  • La documentazione per il tuo linguaggio di programmazione di scelta

Endianness e stream

Prima di poter leggere e scrivere dati binari con successo, ci sono due concetti importanti che dobbiamo capire: endianness e i flussi.

Endianness detta l'ordine di valori a più byte all'interno di un file o all'interno di un blocco di memoria. Ad esempio, se avessimo un valore di 16 bit di 0x1020, quel valore può essere memorizzato come 0x10 seguito da 0x20 (big-endian) o 0x20 seguito da 0x10 (Little-endian).

Gli stream sono oggetti di tipo array che contengono una sequenza di byte (o bit in alcuni casi). I dati binari vengono letti e scritti su questi flussi. La maggior parte della programmazione fornirà un'implementazione di flussi binari in una forma o nell'altra; alcuni sono più contorti di altri, ma sostanzialmente fanno la stessa cosa.


Lettura di dati binari

Iniziamo definendo alcune proprietà nel nostro codice. Idealmente questi dovrebbero essere tutti proprietà private:

 __stream // L'oggetto tipo array che contiene i byte __endian // L'endianness dei dati all'interno del flusso __length // Il numero di byte nello stream __position // La posizione del byte successivo da leggere dallo stream

Ecco un esempio di come potrebbe essere un costruttore di classi di base:

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

Le seguenti funzioni leggeranno interi senza segno dallo stream:

 // Legge una funzione integer a 8 bit senza segno readU8 () // Genera un'eccezione se non ci sono più byte disponibili per leggere if (__position> = __length) throw new Exception ("...") // Restituisce il byte valore e aumenta __position proprietà return __stream [__position ++] // Legge unsigned a 16 bit integer function readU16 () value = 0 // Endianness deve essere gestito per valori a byte multipli if (__endian == BIG_ENDIAN) valore | = 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 

Queste funzioni leggeranno interi firmati dallo stream:

 // Legge un intero firmato funzione di 8 bit readS8 () // Leggere il valore valore senza segno = readU8 () // Verifica se il primo (più significativo) bit indica un valore negativo se (valore >> 7 == 1) // Utilizza "complemento a due" per convertire il valore value = ~ (valore ^ 0xFF) ​​// valore di ritorno Legge un segno a 16 bit funzione integer readS16 () value = readU16 () se (valore >> 15 = = 1) valore = ~ (valore ^ 0xFFFF) valore restituito // Legge una funzione intera a 24 bit con segno readS24 () valore = readU24 () if (valore >> 23 == 1) valore = ~ ( value ^ 0xFFFFFF valore restituito // Legge una funzione intera a 32 bit con segno readS32 () value = readU32 () if (valore >> 31 == 1) valore = ~ (valore ^ 0xFFFFFFFF valore restituito

Scrivere dati binari

Iniziamo definendo alcune proprietà nel nostro codice. (Queste sono più o meno le stesse che abbiamo definito per la lettura di dati binari.) Idealmente queste dovrebbero essere tutte proprietà private:

 __stream // L'oggetto matrice simile che conterrà i byte __endian // L'endianness dei dati all'interno del flusso __position // La posizione del prossimo byte scrivere nel flusso

Ecco un esempio di come potrebbe essere un costruttore di classi di base:

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

Le seguenti funzioni scriveranno interi senza segno nello stream:

 // Scrive una funzione intera a 8 bit senza segno writeU8 (valore) // Assicura che il valore sia senza segno e all'interno di un valore di intervallo di 8 bit & = 0xFF // Aggiungi il valore allo stream e aumenta la proprietà __position. __stream [__position ++] = value // Scrive una funzione intero a 16 bit senza segno writeU16 (value) value & = 0xFFFF // Endianness deve essere gestito per valori a byte multipli if (__endian == BIG_ENDIAN) writeU8 ( valore >> 8) writeU8 (valore >> 0) else // LITTLE_ENDIAN writeU8 (valore >> 0) writeU8 (valore >> 8) // Scrivi una funzione intera a 24 bit senza segno writeU24 (valore) valore & = 0xFFFFFF if (__endian == BIG_ENDIAN) writeU8 (valore >> 16) writeU8 (valore >> 8) writeU8 (valore >> 0) else writeU8 (valore >> 0) writeU8 (valore >> 8) writeU8 (valore >> 16) // Scrive una funzione intera a 32 bit senza segno writeU32 (valore) valore & = 0xFFFFFFFF if (__endian == BIG_ENDIAN) writeU8 (valore >> 24) writeU8 (valore >> 16) writeU8 (valore >> 8) writeU8 (valore >> 0) else writeU8 (valore >> 0) writeU8 (valore >> 8) writeU8 (valore >> 16) writeU8 (valore >> 24)

E, ancora, queste funzioni scriveranno interi con segno nello stream. (Le funzioni sono in realtà alias del writeU * () funzioni, ma forniscono coerenza API con il Legge * () funzioni.)

 // Scrive una funzione di valore a 8 bit con segno scrittoS8 ​​(valore) writeU8 (valore) // Scrive una funzione di valore a 16 bit con segno writeS16 (valore) writeU16 (valore) // Scrive una funzione di valore a 24 bit con segno writeS24 (value) writeU24 (value) // Scrive una funzione di valore a 32 bit con segno writeS32 (valore) writeU32 (valore)

Nota: Questi alias funzionano perché i dati binari vengono sempre memorizzati come valori non firmati; ad esempio, un singolo byte avrà sempre un valore compreso tra 0 e 255. La conversione ai valori firmati viene eseguita quando i dati vengono letti da un flusso.


Conclusione

Il mio obiettivo con questo breve tutorial era di completare il mio precedente articolo sulla creazione di file binari per i dati del tuo gioco con alcuni esempi su come eseguire la lettura e la scrittura. Spero che sia stato raggiunto; se c'è altro che vorresti sapere sull'argomento, per favore parla nei commenti!