Så här läser du och skriver binära data för dina anpassade filformat

I min tidigare artikel skapar du anpassade binära filformat för ditt spelets data, täckte jag ämnet för använder sig av anpassade binära filformat för att lagra speltillgångar och resurser. I den här korta handledningen tar vi en snabb titt på hur man faktiskt läser och skriver binära data.

Notera: Denna handledning använder pseudokod för att visa hur man läser och skriver binära data, men koden kan enkelt översättas till något programmeringsspråk som stöder grundläggande fil I / O-operationer.


Bitvis Operatörer

Om detta är allt obekant territorium för dig kommer du att märka att några märkliga operatörer används i koden, speciellt &, |, << och >> operatörer. Dessa är standardbitoperatorer, tillgängliga på de flesta programmeringsspråk, som används för att manipulera binära värden.

relaterade inlägg
För mer information om bitvisa operatörer, se:
  • Förstå bitvis operatörer
  • Dokumentationen för ditt programmerade språk

Endianitet och strömmar

Innan vi kan läsa och skriva binär data framgångsrikt finns det två viktiga begrepp som vi behöver förstå: endian och strömmar.

Endianitet dikterar ordningen för multipelbytevärden i en fil eller i en bit av minne. Till exempel, om vi hade ett 16-bitars värde av 0x1020, det värdet kan antingen lagras som 0x10 följd av 0x20 (big-endian) eller 0x20 följd av 0x10 (Little-endian).

Strömmar är arrayliknande objekt som innehåller en sekvens av byte (eller bitar i vissa fall). Binär data läses från och skrivs till dessa strömmar. Den flesta programmeringen kommer att ge en implementering av binära strömmar i en eller annan form; vissa är mer konvolutade än andra, men de alla gör i huvudsak samma sak.


Läser binära data

Låt oss börja med att definiera vissa egenskaper i vår kod. Helst bör dessa alla vara privata egenskaper:

 __stream // Det array-liknande objektet som innehåller byte __endian // Dataens endans i strömmen __length // Antalet byte i strömmen __position // Positionen för nästa byte att läsa från strömmen

Här är ett exempel på hur en grundläggande klasskonstruktör kan se ut:

 klass DataInput (ström, endian) __stream = ström __endian = endian __length = stream.length __position = 0

Följande funktioner läser unsigned heltal från strömmen:

 // Läser en unsigned 8-bitars heltalsfunktion readU8 () // Kasta ett undantag om det inte finns några fler byte att läsa om (__position> = __length) kasta ny Undantag ("...") // Returnera byte värdet och öka __positionsegenskapen returnera __stream [__position ++] // Läser en unsigned 16-bitars heltal funktion readU16 () value = 0 // Endianity måste hanteras för multipelbyte värden om (__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 

Dessa funktioner läser signerade heltal från strömmen:

 // Läser en signerad 8-bitars heltalsfunktion readS8 () // Läs det osignerade värdet värde = readU8 () // Kontrollera om den första (mest signifikanta) biten anger ett negativt värde om (värde >> 7 == 1) // Använd "Två komplement" för att konvertera värdet värde = ~ (värde ^ 0xFF) returvärde // Läser en undertecknad 16-bitars heltal funktion readS16 () value = readU16 () om (värde >> 15 = = 1) värde = ~ (värde ^ 0xFFFF) returvärde // Läser en signerad 24-bitars heltalsfunktion readS24 () value = readU24 () om (värde >> 23 == 1) value = ~ värde ^ 0xFFFFFF) returvärde // Läser en signerad 32-bitars heltalsfunktion readS32 () value = readU32 () om (värde >> 31 == 1) värde = ~ (värde ^ 0xFFFFFFFF) returvärde

Skriva binär data

Låt oss börja med att definiera vissa egenskaper i vår kod. (Dessa är mer eller mindre desamma som de egenskaper som vi definierade för att läsa binär data.) Idealt sett bör dessa alla vara privata egenskaper:

 __stream // Det array-liknande objektet som kommer att innehålla byte __endian // Dataens endans i strömmen __position // Positionen för nästa byte för att skriva till strömmen

Här är ett exempel på hur en grundläggande klasskonstruktör kan se ut:

 klass DataOutput (ström, endian) __stream = ström __endian = endian __position = 0

Följande funktioner kommer att skriva usignerade heltal till strömmen:

 // Skriver en osignerad 8-bitars heltalsfunktion writeU8 (värde) // Se till att värdet är osignerat och inom ett 8-bitars intervallvärde & = 0xFF // Lägg till värdet i strömmen och öka egenskapen __position. __stream [__position ++] = value // Skriver en unsigned 16-bitars heltalsfunktion writeU16 (värde) value & = 0xFFFF // Endianness måste hanteras för multipelbytesvärden om (__endian == BIG_ENDIAN) writeU8 värde >> 8) writeU8 (värde >> 0) annat // LITTLE_ENDIAN writeU8 (värde >> 0) writeU8 (värde >> 8) // Skriv ett osignerat 24-bitars heltal funktion writeU24 (value) value & = 0xFFFFFF om (__endian == BIG_ENDIAN) writeU8 (värde >> 16) writeU8 (värde >> 8) writeU8 (värde >> 0) annars writeU8 (värde >> 0) writeU8 (värde >> 8) writeU8 (värde >> 16) // Skriver en osignerad 32-bitars heltalsfunktion writeU32 (värde) värde & = 0xFFFFFFFF om (__endian == BIG_ENDIAN) writeU8 (värde >> 24) writeU8 (värde >> 16) writeU8 (värde >> 8) writeU8 (värde >> 0) annat writeU8 (värde >> 0) writeU8 (värde >> 8) writeU8 (värde >> 16) writeU8 (värde >> 24)

Och igen kommer dessa funktioner att skriva signerade heltal till strömmen. (Funktionerna är faktiskt alias av writeU * () funktioner, men de ger API-konsistens med läser * () funktioner.)

 // Skriver en signerad 8-bitars värdefunktion writeS8 (value) writeU8 (value) // Skriver en signerad 16-bitars värdefunktion writeS16 (value) writeU16 (value) // Skriver en signerad 24-bitars värdefunktion writeS24 (value) writeU24 (värde) // Skriver en signerad 32-bitars värdefunktion writeS32 (value) writeU32 (value)

Notera: Dessa aliaser fungerar eftersom binär data alltid lagras som osignerade värden; till exempel kommer en enkel byte alltid att ha ett värde i intervallet 0 till 255. Omvandlingen till signerade värden görs när data läses från en ström.


Slutsats

Mitt mål med denna korta handledning var att komplettera min tidigare artikel om att skapa binära filer för spelets data med några exempel på hur man gör själva läsningen och skrivningen. Jag hoppas att det har uppnåtts det; Om det finns mer du vill veta om ämnet, snälla tala upp i kommentarerna!