Hoe binaire gegevens voor uw aangepaste bestandsindelingen te lezen en te schrijven

In mijn vorige artikel, Maak aangepaste binaire bestandsindelingen voor uw spelgegevens, besprak ik het onderwerp gebruik makend van aangepaste binaire bestandsindelingen om spelactiva en bronnen op te slaan. In deze korte tutorial zullen we snel bekijken hoe binaire gegevens kunnen worden gelezen en geschreven.

Notitie: Deze tutorial gebruikt pseudo-code om te demonstreren hoe binaire gegevens moeten worden gelezen en geschreven, maar de code kan gemakkelijk worden vertaald naar elke programmeertaal die standaard bestands-I / O-bewerkingen ondersteunt.


Bitwise Operators

Als dit allemaal onbekend terrein voor je is, zul je merken dat een paar vreemde operatoren in de code worden gebruikt, met name de &, |, << en >> operators. Dit zijn standaard bitgewijze operatoren, beschikbaar in de meeste programmeertaal, die worden gebruikt voor het manipuleren van binaire waarden.

gerelateerde berichten
Zie voor meer informatie over bitwise-operators:
  • Bitsgewijze operatoren begrijpen
  • De documentatie voor uw programmeertaal naar keuze

Endianness and Streams

Voordat we binaire gegevens met succes kunnen lezen en schrijven, zijn er twee belangrijke concepten die we moeten begrijpen: endianness en streams.

Endianness dicteert de volgorde van waarden van meerdere bytes binnen een bestand of in een geheugenblok. Als we bijvoorbeeld een 16-bits waarde hadden van 0x1020, die waarde kan worden opgeslagen als 0x10 gevolgd door 0x20 (big-endian) of 0x20 gevolgd door 0x10 (Little-endian).

Streams zijn array-achtige objecten die een reeks bytes bevatten (of bits in sommige gevallen). Binaire gegevens worden gelezen van en geschreven naar deze streams. De meeste programmering zal een implementatie van binaire stromen in een of andere vorm verschaffen; sommige zijn ingewikkelder dan andere, maar ze doen allemaal in wezen hetzelfde.


Binaire gegevens lezen

Laten we beginnen met het definiëren van enkele eigenschappen in onze code. Idealiter zouden dit allemaal privé-eigendommen moeten zijn:

 __stream // Het array-achtige object dat de bytes bevat __endian // De endianness van de gegevens in de stream __length // Het aantal bytes in de stream __position // De positie van de volgende byte die in de stream moet worden gelezen

Hier is een voorbeeld van hoe een bouwer van een basisklasse er uit zou kunnen zien:

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

De volgende functies zullen niet-ondertekende gehele getallen uit de stream lezen:

 // Leest een niet-ondertekende 8-bit integer-functie readU8 () // Genereer een uitzondering als er geen bytes beschikbaar zijn om te lezen if (__position> = __length) throw new Exception ("...") // Geef de byte terug waarde en verhoog het __position eigenschapsterugkeer __stream [__position ++] // Leest een niet-ondertekende 16-bit integer functie readU16 () waarde = 0 // Endianness moet afgehandeld worden voor waarden van meerdere bytes if (__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 

Deze functies lezen ondertekende gehele getallen uit de stream:

 // Leest een ondertekende 8-bits geheel-getalfunctie readS8 () // Lees de niet-ondertekende waarde = readU8 () // Controleer of de eerste (meest significante) bit een negatieve waarde aangeeft als (waarde >> 7 == 1) // Gebruik "Two's complement" om de waarde value te converteren = ~ (waarde ^ 0xFF) retourwaarde // Leest een 16-bits geheel-getal-functie met teken signedS16 () value = readU16 () if (value >> 15 = = 1) value = ~ (waarde ^ 0xFFFF) retourwaarde // Leest een ondertekende 24-bits geheel getalfunctie readS24 () value = readU24 () if (waarde >> 23 == 1) waarde = ~ ( waarde ^ 0xFFFFFF) retourwaarde // Leest een ondertekende 32-bits geheel getalfunctie readS32 () value = readU32 () if (waarde >> 31 == 1) waarde = ~ (waarde ^ 0xFFFFFFFF) retourwaarde

Binaire gegevens schrijven

Laten we beginnen met het definiëren van enkele eigenschappen in onze code. (Deze zijn min of meer hetzelfde als de eigenschappen die we hebben gedefinieerd voor het lezen van binaire gegevens.) Idealiter zouden dit allemaal privé-eigenschappen moeten zijn:

 __stream // Het array-achtige object dat de bytes bevat __endian // De endianness van de gegevens in de stream __position // De positie van de volgende byte die in de stream moet worden geschreven

Hier is een voorbeeld van hoe een bouwer van een basisklasse er uit zou kunnen zien:

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

De volgende functies schrijven niet-ondertekende gehele getallen naar de stream:

 // Schrijft een niet-ondertekende 8-bit integer functie writeU8 (waarde) // Garandeert dat de waarde unsigned is en binnen een 8-bit range waarde & = 0xFF // Voeg de waarde toe aan de stream en verhoog de __position eigenschap. __stream [__position ++] = waarde // Schrijft een 16-bits geheel getal zonder teken writeU16 (waarde) waarde & = 0xFFFF // Endianness moet worden verwerkt voor waarden van meerdere bytes als (__endian == BIG_ENDIAN) writeU8 ( waarde >> 8) writeU8 (waarde >> 0) else // LITTLE_ENDIAN writeU8 (waarde >> 0) writeU8 (waarde >> 8) // Schrijf een niet-ondertekende 24-bit integer functie writeU24 (waarde) waarde & = 0xFFFFFF if (__endian == BIG_ENDIAN) writeU8 (waarde >> 16) writeU8 (waarde >> 8) writeU8 (waarde >> 0) else writeU8 (waarde >> 0) writeU8 (waarde >> 8) writeU8 (waarde >> 16) // Schrijft een niet-ondertekende 32-bit integer functie writeU32 (waarde) waarde & = 0xFFFFFFFF if (__endian == BIG_ENDIAN) writeU8 (waarde >> 24) writeU8 (waarde >> 16) writeU8 (waarde >> 8) writeU8 (waarde >> 0) else writeU8 (waarde >> 0) writeU8 (waarde >> 8) writeU8 (waarde >> 16) writeU8 (waarde >> 24)

En nogmaals, deze functies zullen ondertekende gehele getallen naar de stream schrijven. (De functies zijn eigenlijk aliassen van de writeU * () functies, maar ze bieden API-consistentie met de leest * () functies.)

 // Schrijft een ondertekende 8-bit-waardefunctie writeS8 (waarde) writeU8 (waarde) // Schrijft een 16-bits-tekenwaardefunctie met schrijffunctie writeS16 (waarde) writeU16 (waarde) // Schrijft een 24-bits-tekenwaarde met handtekening writeS24 (waarde) writeU24 (waarde) // Schrijft een 32-bits-tekenwaarde-functie writeS32 (waarde) writeU32 (value)

Notitie: Deze aliassen werken omdat binaire gegevens altijd worden opgeslagen als niet-ondertekende waarden; een enkele byte heeft bijvoorbeeld altijd een waarde tussen 0 en 255. De conversie naar gesigneerde waarden gebeurt wanneer de gegevens uit een stream worden gelezen.


Conclusie

Mijn doel met deze korte tutorial was om mijn vorige artikel over het maken van binaire bestanden voor de gegevens van je spel aan te vullen met enkele voorbeelden van hoe je echt kunt lezen en schrijven. Ik hoop dat het is bereikt; Als je meer wilt weten over het onderwerp, spreek dan in de reacties!