Erstellen Sie benutzerdefinierte Binärdateiformate für die Daten Ihres Spiels

Ihr Spiel enthält Daten - Sprites, Soundeffekte, Musik, Text - und Sie müssen sie irgendwie speichern. Manchmal können Sie alles zu einem einzigen zusammenfassen SWF, .unity3d oder EXE Datei, aber in manchen Fällen ist das nicht geeignet. In diesem technischen Lernprogramm wird für diesen Zweck die Verwendung benutzerdefinierter Binärdateien beschrieben.

Hinweis: In diesem Lernprogramm wird vorausgesetzt, dass Sie grundlegende Kenntnisse über Bits und Bytes besitzen. Sehen Sie sich eine Einführung in Binär-, Hexadezimal- und weitere Funktionen sowie Bitweise Operatoren in Activetuts + an, wenn Sie überarbeiten müssen!


Vor- und Nachteile von benutzerdefinierten Binärdateien

Die Verwendung benutzerdefinierter binärer Dateiformate hat einige Vor- und Nachteile.

Wenn Sie beispielsweise einen Ressourcencontainer erstellen (wie in diesem Lernprogramm beschrieben), reduzieren Sie das Thrashing von Festplatten und Servern und machen das Laden von Ressourcen normalerweise viel einfacher, da nicht mehrere Dateien geladen werden müssen. Benutzerdefinierte Dateiformate können auch eine zusätzliche Sicherheitsebene in Form von Verschleierung der Spielressourcen hinzufügen.

Auf der anderen Seite müssen Sie die benutzerdefinierten Dateien tatsächlich auf die eine oder andere Weise generieren, bevor Sie sie in einem Spiel verwenden können. Dies ist jedoch nicht so schwierig, wie es sich anhört - vor allem, wenn Sie oder jemand, den Sie kennen, so etwas wie JAR-Datei, die relativ einfach in einen Build-Prozess eingefügt werden kann.


Grundlegende Datentypen

Bevor Sie mit dem Entwurf eigener Binärdateiformate beginnen können, müssen Sie sich mit den primitiven Datentypen (Bausteinen) auskennen, die Ihnen zur Verfügung stehen. Die Anzahl der primitiven Datentypen ist eigentlich unbegrenzt, es gibt jedoch eine gemeinsame Menge, mit der die meisten Programmierer vertraut sind und diese verwenden, und diese Datentypen repräsentieren typischerweise Vielfache von 8 Bits.

Wie Sie sehen, bieten diese primitiven Datentypen eine Vielzahl von ganzzahligen Werten, und Sie finden sie im Kern der meisten Binärdateispezifikationen. Es gibt einige primitivere Datentypen, z. B. Fließkommazahlen, aber die oben aufgelisteten Integer-Datentypen sind für diese Einführung und für die meisten binären Dateiformate mehr als ausreichend.


Strukturierte Datentypen verstehen

Strukturierte Datentypen (oder komplexe Datentypen) repräsentieren bestimmte Elemente (Blöcke) einer Binärdatei und bestehen aus primitiven Datentypen oder anderen strukturierten Datentypen.

Sie können sich strukturierte Datentypen als Objekte oder Klasseninstanzen in einer Programmiersprache vorstellen, wobei jedes Objekt einen Satz von Eigenschaften deklariert. Strukturierte Datentypen können mit einfacher Objektnotation visualisiert werden.

Hier ist ein Beispiel für einen fiktiven Dateiheader:

 HEADER Signatur U24 Version U8 Länge U32

Hier heißt also der strukturierte Datentyp HEADER und es hat drei gekennzeichnete Eigenschaften Unterschrift, Ausführung und Länge. Jede Eigenschaft in diesem Beispiel ist als primärer Datentyp deklariert. Eigenschaften können jedoch auch als strukturierte Datentypen deklariert werden.

Wenn Sie Programmierer sind, beginnen Sie wahrscheinlich zu erkennen, wie einfach es ist, eine Binärdatei in einer OOP-basierten Programmiersprache darzustellen. Werfen wir einen kurzen Blick darauf, wie das geht HEADER Datentyp könnte in Java dargestellt werden:

 Klasse Header public int Signatur; // U24 public int version; // U8 public long length; // U32

Entwerfen einer benutzerdefinierten Binärdatei

An diesem Punkt sollten Sie mit den Grundlagen der Binärdateistrukturen vertraut sein. Schauen Sie sich jetzt den Entwurf eines funktionierenden benutzerdefinierten Dateiformats an. Dieses Dateiformat enthält eine Sammlung von Spielressourcen, einschließlich Bilder und Sounds.

Die Kopfzeile

Als erstes sollte eine Datenstruktur für den Dateiheader entworfen werden, damit die Datei identifiziert werden kann, bevor der Rest der Datei in den Speicher geladen wird. Idealerweise sollte der Dateiheader mindestens ein Signaturfeld und ein Versionsfeld enthalten:

 HEADER Signatur U24 Version U8

Die Dateisignatur, die Sie verwenden möchten, liegt bei Ihnen: Sie kann aus einer beliebigen Anzahl von Bytes bestehen, die meisten Dateiformate haben jedoch eine vom Menschen lesbare Signatur mit drei oder vier ASCII-Zeichen. Für meine Zwecke die Unterschrift Das Feld enthält die Zeichencodes von drei ASCII-Zeichen (ein Byte pro Zeichen) und stellt die Zeichenfolge "RES" (kurz für "RESOURCE") dar. Die Byte-Werte sind also 0x52, 0x45 und 0x53.

Das Ausführung Feld wird zunächst sein 0x01 weil dies Version 1 des Dateiformats ist.

Die Ressourcendatei selbst ist tatsächlich ein strukturierter Datentyp, der einen Header enthält und später weitere Elemente enthält. Es sieht aktuell so aus:

 DATEI header HEADER

Bilder

Als Nächstes betrachten wir die Datenstruktur für Bilder.

Die Ressourcendatei speichert ein Array von ARGB-Farbwerten (einen pro Pixel) und ermöglicht die optionale Komprimierung dieser Daten mit dem ZLIB-Algorithmus. Die Bildabmessungen müssen außerdem zusammen mit einer Kennung für das Bild in der Datei enthalten sein (damit auf das Bild zugegriffen werden kann, nachdem es in den Speicher geladen wurde):

 IMAGE id STRING Breite U16 Höhe U16 komprimiert U8 Datenlänge U32 Daten U8 [Datenlänge]

Es gibt ein paar Dinge in dieser Struktur, die Ihre Aufmerksamkeit brauchen; der erste ist der U8 [Datenlänge] Teil der Struktur und der zweite ist der STRING Datenstruktur für die Ich würde, die in der Tabelle der Datentypen oben nicht definiert wurde.

Ersteres ist eine einfache Array-Notation - das bedeutet einfach Datenlänge Anzahl von U8 Werte müssen aus der Datei gelesen werden. Das Daten Feld enthält die Bildpixel und die komprimiert Feld zeigt an, ob die Daten Feld ist komprimiert. Wenn die komprimiert Wert ist 0x01 dann ist die Daten Feld ist ZLIB-komprimiert, andernfalls kann der Datei-Decoder das annehmen Daten Feld ist nicht komprimiert. Der Vorteil der ZLIB-Komprimierung ist hier der BILD Die Dateistruktur hat am Ende eine ähnliche Größe wie eine PNG-codierte Version des Images.

Das STRING Datenstruktur ist wie folgt:

 STRING Datenlänge U16 Daten U8 [Datenlänge]

Bei diesem Dateiformat werden alle Zeichenfolgen als UTF-8 kodiert, und die Bytes der kodierten Zeichenfolge befinden sich im Daten Feld der STRING Datenstruktur. Das Datenlänge Feld zeigt die Anzahl der Bytes in der Daten Feld.

Die Struktur der Ressourcendatei sieht jetzt so aus:

 DATEI header HEADER imageCount U16 imageList IMAGE [imageCount]

Wie Sie sehen, enthält die Datei jetzt einen Header, einen neuen imageCount ein Feld, das die Anzahl der Bilder in der Datei angibt, und ein neues imageList Feld für die Bilder. Dies wäre an sich schon ein nützliches Dateiformat zum Speichern mehrerer Bilder, aber es wäre noch sinnvoller, wenn es mehrere Ressourcentypen enthält. Daher werden jetzt Töne zur Datei hinzugefügt.

Geräusche

Sounds werden in der Datei auf ähnliche Weise wie Bilder gespeichert. Anstatt rohe Pixelfarbwerte zu speichern, speichert die Datei rohe Sound-Samples in verschiedenen Bit-Auflösungen:

 SOUND id STRING dataFormat U8 dataLength U32 // 8-Bit-Samples if (dataFormat == 0x00) data U8 [dataLength] // 16-Bit-Samples if (dataFormat == 0x01) data U16 [dataLength] // 32-Bit-Beispiele if (dataFormat == 0x02) data U32 [dataLength]

Oh mein Gott, bedingte Aussagen! Weil der Datei Format Das Feld zeigt die Bitrate des Sounds und das Format des Tons an Daten field muss variabel sein, und hier kommt die einfache und programmierfreundliche bedingte Anweisungssyntax ins Spiel.

Wenn Sie sich die Datenstruktur ansehen, können Sie leicht erkennen, in welchem ​​Format Daten Feldwerte (Sound-Samples) werden verwendet, wenn ein bestimmter Wert angegeben ist Datei Format Wert. Beim Hinzufügen von Feldern wie Datei Format Die Werte, die diese Felder enthalten können, liegt ganz bei Ihnen. Die Werte 0x01, 0x02 und 0x03 werden in diesem Beispiel verwendet, weil es sich um die ersten nicht verwendeten Werte im Byte handelt.

Die Struktur der Ressourcendatei sieht jetzt so aus:

 FILE header HEADER imageCount U16 imageList IMAGE [imageCount] soundCount U16 soundList SOUND [soundCount]

Generische Daten

Das Letzte, was zu dieser Ressourcendatei-Struktur hinzugefügt wird, sind generische Daten. Dadurch können verschiedene spielbezogene Daten (in verschiedenen Formaten) in die Datei gepackt werden.

Wie BILD Datenstruktur dieses neue DATEN Die Struktur unterstützt die optionale ZLIB-Komprimierung, da textbasierte Daten wie JSON und XML im Allgemeinen von der Komprimierung profitieren. Dadurch werden auch die Daten in der Datei verschleiert:

 DATA id STRING komprimiert U8 dataFormat U8 dataLength U32 data U8 [dataLength]

Das komprimiert Feld zeigt an, ob die Daten Feld ist komprimiert: ein Wert von 0x01 Bedeutet die Daten Feld ist ZLIB-komprimiert.

Das Datei Format zeigt das Format der Daten an und die Werte, die dieses Feld enthalten kann, liegt bei Ihnen. Sie könnten zum Beispiel verwenden 0x00 für rohen Text, 0x01 für XML und 0x02 für JSON. Ein einzelnes vorzeichenloses Byte (U8) kann halten 256 verschiedene Werte und das sollte für alle Datenformate, die Sie in einem Spiel verwenden möchten, mehr als ausreichend sein.

Die endgültige Struktur der Ressourcendatei sieht folgendermaßen aus:

 FILE header HEADER imageCount U16 imageList IMAGE [imageCount] soundCount U16 soundList SOUND [soundCount] dataCount U16 dataList DATEN [dataCount]

In Bezug auf Dateiformate ist dieses Format relativ einfach - aber es ist funktional und zeigt, wie Dateiformate auf sinnvolle und verständliche Weise dargestellt und strukturiert werden können.


Byte-Reihenfolge verstehen

Es gibt noch eine wichtige Sache, die Sie möglicherweise über Binärdateien kennen müssen: In Binärdateien gespeicherte Multibyte-Werte können eine von zwei Byte-Reihenfolgen verwenden (dies wird auch als "Endian" bezeichnet). Die Bytereihenfolge kann entweder LSB (Least Significant Byte First oder "Little Endian") oder MSB (Most Significant Byte First oder Big Endian) sein. Der Unterschied zwischen den zwei Byteaufträgen ist einfach die Reihenfolge, in der die Bytes gespeichert werden.

Ein 24-Bit-RGB-Farbwert besteht beispielsweise aus drei Bytes, ein Byte für jeden Farbkanal. Die Byte-Reihenfolge einer Datei bestimmt, ob diese Bytes in der Datei als RGB (Big-Endian) oder BGR (Little-Endian) gespeichert sind..

Viele moderne Programmiersprachen bieten eine API, mit der Sie die Bytereihenfolge umschalten können, während Sie eine Datei in den Speicher lesen. Das Lesen von Multibyte-Werten aus einer Binärdatei macht Programmierern normalerweise keine Sorgen. Wenn Sie jedoch eine Datei Byte für Byte lesen, müssen Sie die Byte-Reihenfolge der Datei kennen.

Der folgende Java-Code veranschaulicht, wie ein 24-Bit-Wert (in diesem Fall eine RGB-Farbe) aus einer Datei gelesen wird, während die Byte-Reihenfolge der Datei berücksichtigt wird:

 bigEndian boolean = true; int readU24 (Eingabe InputStream) wirft IOException int value = 0; if (bigEndian) value | = input.read () << 16; // red value |= input.read() << 8; // green value |= input.read() << 0; // blue  else // little endian  value |= input.read() << 0; // blue value |= input.read() << 8; // green value |= input.read() << 16; // red  return value; 

Das eigentliche Lesen und Schreiben von Binärdateien ist nicht Gegenstand dieses Tutorials. In diesem Beispiel sollten Sie jedoch in der Lage sein, zu sehen, wie die Reihenfolge der drei Bytes eines 24-Bit-Werts abhängig von der Byte-Reihenfolge (Endian) von eine Datei. Gibt es Vorteile, eine Byte-Reihenfolge anstelle der anderen zu verwenden? Nun, nicht wirklich - Byteaufträge betreffen nur Hardware und nicht Software.

Wo Sie jetzt hingehen, bleibt Ihnen überlassen, aber ich hoffe, dieses Tutorial hat benutzerdefinierte Dateiformate ein wenig weniger beängstigend gemacht, um sie in Ihren eigenen Spielen zu verwenden!