Slik leser og skriver du binære data for dine egendefinerte filformater

I min forrige artikkel, Lag Custom Binary File Formats for spillets data, dekket jeg emnet for ved hjelp av egendefinerte binære filformater for å lagre spillverdier og ressurser. I denne korte opplæringen tar vi en rask titt på hvordan man faktisk leser og skriver binære data.

Merk: Denne opplæringen bruker pseudokode til å demonstrere hvordan man leser og skriver binære data, men koden kan lett oversettes til hvilket programmeringsspråk som støtter grunnleggende fil I / O-operasjoner.


Bitwise Operators

Hvis dette er alt ukjent territorium for deg, vil du legge merke til at noen merkelige operatører blir brukt i koden, spesielt &, |, << og >> operatører. Disse er standardbitvise operatører, tilgjengelige i de fleste programmeringsspråk, som brukes til å manipulere binære verdier.

Relaterte innlegg
For mer informasjon om bitwise operatører, se:
  • Forstå Bitwise Operators
  • Dokumentasjonen for ditt valg av programmeringsspråk

Endianitet og strømmer

Før vi kan lese og skrive binære data vellykket, er det to viktige begreper som vi trenger å forstå: endianness og strømmer.

Endianitet dikterer rekkefølgen av flere byteverdier i en fil eller i en bit av minne. For eksempel, hvis vi hadde en 16-biters verdi på 0x1020, den verdien kan enten lagres som 0x10 etterfulgt av 0x20 (big-endian) eller 0x20 etterfulgt av 0x10 (Little endian).

Strømmer er array-lignende objekter som inneholder en sekvens av byte (eller biter i noen tilfeller). Binære data er lest fra og skrevet til disse strømmene. De fleste programmering vil gi en implementering av binære strømmer i en eller annen form; Noen er mer innviklede enn andre, men de gjør i det hele tatt det samme.


Lesing av binære data

La oss begynne med å definere noen egenskaper i vår kode. Ideelt sett bør disse være private eiendommer:

 __stream // Den array-lignende gjenstanden som inneholder bytes __endian // Dataendansiteten i strømmen __length // Antallet bytes i strømmen __position // Plasseringen av neste byte for å lese fra strømmen

Her er et eksempel på hva en grunnsklassekonstruktør kan se ut som:

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

Følgende funksjoner vil lese usignerte heltall fra strømmen:

 // Leser en usignert 8-biters heltallfunksjon readU8 () // Kast et unntak hvis det ikke er flere byter tilgjengelig for å lese om (__position> = __length) kaste ny unntak ("...") // Return byte verdi og øk __position eiendomsavkastning __stream [__position ++] // Leser en usignert 16-biters heltallfunksjon readU16 () value = 0 // Endianness må håndteres for flere byteverdier hvis (__endian == BIG_ENDIAN) value | = 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 

Disse funksjonene vil lese signerte heltal fra strømmen:

 // Leser en signert 8-biters heltallfunksjon readS8 () // Les usignert verdiverdi = readU8 () // Sjekk om den første (mest signifikante) bit indikerer en negativ verdi hvis (verdi >> 7 == 1) // Bruk «To komplement» for å konvertere verdien verdi = ~ (verdi ^ 0xFF) returverdi // Leser en signert 16-biters heltallfunksjon readS16 () value = readU16 () hvis (verdi >> 15 = = 1) verdi = ~ (verdi ^ 0xFFFF) returverdi // Leser en signert 24-biters heltallfunksjon readS24 () value = readU24 () hvis (verdi >> 23 == 1) verdi = ~ verdi ^ 0xFFFFFF) returverdi // Leser en signert 32-biters heltallfunksjon readS32 () value = readU32 () hvis (verdi >> 31 == 1) verdi = ~ (verdi ^ 0xFFFFFFFF) returverdi

Skrive binære data

La oss begynne med å definere noen egenskaper i vår kode. (Disse er mer eller mindre de samme som egenskapene vi definerte for å lese binære data.) Ideelt sett bør disse alle være private egenskaper:

 __stream // Den array-lignende objekt som vil inneholde bytes __endian // Endianen av dataene i strømmen __position // Plasseringen av neste byte for å skrive til strømmen

Her er et eksempel på hva en grunnsklassekonstruktør kan se ut som:

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

Følgende funksjoner vil skrive usignerte heltall til strømmen:

 // Skriver en usignert 8-biters heltallfunksjon writeU8 (verdi) // Kontrollerer at verdien er usignert og innenfor en 8-biters rekkevidde & = 0xFF // Legg til verdien til strømmen og øk egenskapen __position. __stream [__position ++] = value // Skriver en usignert 16-biters heltallfunksjon writeU16 (verdi) value & = 0xFFFF // Endianness må håndteres for flere byteverdier hvis (__endian == BIG_ENDIAN) writeU8 verdi >> 8) writeU8 (verdi >> 0) annet // LITTLE_ENDIAN writeU8 (verdi >> 0) writeU8 (verdi >> 8) // Skriv en usignert 24-bit heltallfunksjon writeU24 (verdi) verdi & = 0xFFFFFF hvis (__endian == BIG_ENDIAN) writeU8 (verdi >> 16) writeU8 (verdi >> 8) writeU8 (verdi >> 0) ellers writeU8 (verdi >> 0) writeU8 (verdi >> 8) writeU8 (verdi >> 16) // Skriver en usignert 32-biters heltallfunksjon writeU32 (verdi) verdi & = 0xFFFFFFFF hvis (__endian == BIG_ENDIAN) writeU8 (verdi >> 24) writeU8 (verdi >> 16) writeU8 (verdi >> 8) writeU8 (verdi >> 0) ellers writeU8 (verdi >> 0) writeU8 (verdi >> 8) writeU8 (verdi >> 16) writeU8 (verdi >> 24)

Og igjen vil disse funksjonene skrive signerte heltall til strømmen. (Funksjonene er faktisk aliaser av writeU * () funksjoner, men de gir API-konsistens med Leser * () funksjoner.)

 // Skriver en signert 8-bitsverdiefunksjon writeS8 (value) writeU8 (verdi) // Skriver en signert 16-bitsverdiefunksjon writeS16 (verdi) writeU16 (verdi) // Skriver en signert 24-bitverdiefunksjon writeS24 (value) writeU24 (verdi) // Skriver en signert 32-bitsverdiefunksjon writeS32 (verdi) writeU32 (verdi)

Merk: Disse aliasene fungerer fordi binære data alltid lagres som usignerte verdier; for eksempel vil en enkelt byte alltid ha en verdi i området 0 til 255. Konverteringen til signerte verdier gjøres når dataene leses fra en strøm.


Konklusjon

Målet mitt med denne korte opplæringen var å utfylle min forrige artikkel om å lage binære filer for spillets data med noen eksempler på hvordan du gjør selve lesing og skriving. Jeg håper det er oppnådd det; Hvis det er mer du vil vite om emnet, vennligst snakk i kommentarene!