In meinem vorherigen Artikel, Benutzerdefinierte binäre Dateiformate für die Daten Ihres Spiels erstellen, habe ich das Thema behandelt mit benutzerdefinierte binäre Dateiformate zum Speichern von Assets und Ressourcen von Spielen. In diesem kurzen Tutorial werden wir einen kurzen Blick darauf werfen, wie man binäre Daten tatsächlich liest und schreibt.
Hinweis: In diesem Lernprogramm wird mit Pseudocode demonstriert, wie binäre Daten gelesen und geschrieben werden. Der Code kann jedoch problemlos in jede Programmiersprache übersetzt werden, die grundlegende E / A-Vorgänge für Dateien unterstützt.
Wenn dies alles unbekannte Gebiet für Sie ist, werden Sie feststellen, dass im Code einige seltsame Operatoren verwendet werden, insbesondere die &
, |
, <<
und >>
Betreiber. Dies sind standardmäßige bitweise Operatoren, die in den meisten Programmiersprachen verfügbar sind und zur Bearbeitung von Binärwerten verwendet werden.
Bevor wir binäre Daten erfolgreich lesen und schreiben können, müssen wir zwei wichtige Konzepte verstehen: endianness und Ströme.
Endianness gibt die Reihenfolge der Mehrbyte-Werte in einer Datei oder in einem Speicherbereich vor. Zum Beispiel, wenn wir einen 16-Bit-Wert von hatten 0x1020
, Dieser Wert kann entweder als gespeichert werden 0x10
gefolgt von 0x20
(Big-Endian) oder 0x20
gefolgt von 0x10
(Little-Endian).
Streams sind Array-ähnliche Objekte, die eine Folge von Bytes (oder Bits) enthalten. Binäre Daten werden aus diesen Streams gelesen und in diese geschrieben. Die meisten Programme bieten eine Implementierung von binären Strömen in der einen oder anderen Form. Einige sind verworrener als andere, aber sie machen im Wesentlichen dasselbe.
Beginnen wir mit der Definition einiger Eigenschaften in unserem Code. Im Idealfall sollten dies alles private Immobilien sein:
__stream // Das arrayähnliche Objekt, das die Bytes enthält __endian // Die Endianness der Daten innerhalb des Streams __length // Die Anzahl der Bytes im Stream __position // Die Position des nächsten Bytes, das aus dem Stream gelesen werden soll
Hier ein Beispiel, wie ein grundlegender Klassenkonstruktor aussehen könnte:
Klasse DataInput (Stream, Endian) __stream = Stream __endian = Endian __length = Stream.length __position = 0
Die folgenden Funktionen lesen vorzeichenlose Ganzzahlen aus dem Stream:
// Liest eine vorzeichenlose 8-Bit-Integer-Funktion readU8 () // Löst eine Ausnahme aus, wenn keine weiteren Bytes zum Lesen verfügbar sind, wenn (__position> = __length) Neue Exception werfen ("…") // Das Byte zurückgeben value und erhöhen die __position-Eigenschaft return __stream [__position ++] // Liest eine vorzeichenlose 16-Bit-Ganzzahlfunktion readU16 () value = 0 // Endianness muss für Werte mit mehreren Bytes behandelt werden, wenn (__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
Diese Funktionen lesen vorzeichenbehaftete Ganzzahlen aus dem Stream:
// Liest eine vorzeichenbehaftete 8-Bit-Integer-Funktion readS8 () // Liest den vorzeichenlosen Wert value = readU8 () // Prüft, ob das erste (höchstwertige) Bit einen negativen Wert anzeigt, wenn (Wert >> 7 == 1) // Verwenden Sie "Zweierkomplement" zum Konvertieren des Werts value = ~ (value ^ 0xFF) Rückgabewert // Liest eine vorzeichenbehaftete 16-Bit-Integer-Funktion readS16 () value = readU16 () if (value >> 15 = = 1) value = ~ (value ^ 0xFFFF) Rückgabewert // Liest eine 24-Bit-Ganzzahlfunktion mit Vorzeichen readS24 () value = readU24 () if (Wert >> 23 == 1) Wert = ~ ( value ^ 0xFFFFFF) return value // Liest eine vorzeichenbehaftete 32-Bit-Integer-Funktion readS32 () value = readU32 () if (value >> 31 == 1) value = ~ (value ^ 0xFFFFFFFF) return value
Beginnen wir mit der Definition einiger Eigenschaften in unserem Code. (Dies entspricht in etwa den Eigenschaften, die wir für das Lesen von Binärdaten definiert haben.) Idealerweise sollten alle private Eigenschaften sein:
__stream // Das arrayähnliche Objekt, das die Bytes enthalten wird __endian // Die Endianness der Daten innerhalb des Streams __position // Die Position des nächsten Bytes, das in den Stream geschrieben werden soll
Hier ein Beispiel, wie ein grundlegender Klassenkonstruktor aussehen könnte:
Klasse DataOutput (Stream, Endian) __stream = Stream __endian = Endian __position = 0
Die folgenden Funktionen schreiben vorzeichenlose Ganzzahlen in den Stream:
// Schreibt eine vorzeichenlose 8-Bit-Integer-Funktion writeU8 (value) // Stellt sicher, dass der Wert nicht vorzeichenbehaftet ist und innerhalb eines 8-Bit-Bereichs liegt. & = 0xFF // Fügen Sie den Wert zum Stream hinzu und erhöhen Sie die __position -Eigenschaft. __stream [__position ++] = value // Schreibt eine vorzeichenlose 16-Bit-Integer-Funktion writeU16 (value) value & = 0xFFFF // Endianness muss für Werte mit mehreren Bytes behandelt werden, wenn (__endian == BIG_ENDIAN) writeU8 ( Wert >> 8) writeU8 (Wert >> 0) else // LITTLE_ENDIAN writeU8 (Wert >> 0) writeU8 (Wert >> 8) // Schreibe eine vorzeichenlose 24-Bit-Integer-Funktion writeU24 (value) value & = 0xFFFFFF if (__endian == BIG_ENDIAN) writeU8 (Wert >> 16) writeU8 (Wert >> 8) writeU8 (Wert >> 0) else writeU8 (Wert >> 0) writeU8 (Wert >> 8) writeU8 (Wert >> 16) // Schreibt eine vorzeichenlose 32-Bit-Integer-Funktion writeU32 (Wert) Wert & = 0xFFFFFFFF if (__endian == BIG_ENDIAN) writeU8 (Wert >> 24) writeU8 (Wert >> 16) writeU8 (Wert >> 8) writeU8 (Wert >> 0) else writeU8 (Wert >> 0) writeU8 (Wert >> 8) writeU8 (Wert >> 16) writeU8 (Wert >> 24)
Und wiederum schreiben diese Funktionen vorzeichenbehaftete Ganzzahlen in den Stream. (Die Funktionen sind eigentlich Aliase der writeU * ()
Funktionen, aber sie bieten API-Konsistenz mit der readS * ()
Funktionen.)
// Schreibt eine vorzeichenbehaftete 8-Bit-Wertefunktion writeS8 (value) writeU8 (value) // Schreibt eine vorzeichenbehaftete 16-Bit-Wertefunktion writeS16 (value) writeU16 (value) // Schreibt eine vorzeichenbehaftete 24-Bit-Wertefunktion writeS24 (value) writeU24 (value) // Schreibt eine 32-Bit-Funktion mit Vorzeichenfunktion writeS32 (value) writeU32 (value)
Hinweis: Diese Aliasnamen funktionieren, da binäre Daten immer als vorzeichenlose Werte gespeichert werden. Ein einzelnes Byte hat beispielsweise immer einen Wert im Bereich von 0 bis 255. Die Umwandlung in signierte Werte wird ausgeführt, wenn die Daten aus einem Stream gelesen werden.
Mein Ziel in diesem kurzen Tutorial war es, meinen vorherigen Artikel zum Erstellen von Binärdateien für die Spieldaten mit einigen Beispielen zu ergänzen, wie das Lesen und Schreiben tatsächlich ausgeführt wird. Ich hoffe, dass es erreicht wurde. Wenn Sie mehr über das Thema erfahren möchten, sprechen Sie bitte in den Kommentaren nach!