Crea formati di file binari personalizzati per i dati del tuo gioco

Il tuo gioco ha dati - sprite, effetti sonori, musica, testo - e devi memorizzarlo in qualche modo. A volte puoi incapsulare tutto in un singolo SWF, .Unity3D o EXE file, ma in alcuni casi non sarà adatto. In questo tutorial tecnico, vedremo l'utilizzo di file binari personalizzati per questo scopo.

Nota: Questo tutorial presume che tu abbia una conoscenza di base di bit e byte. Scopri un'introduzione al binario, esadecimale e altro e Capire gli operatori bit a bit su Activetuts + se hai bisogno di rivedere!


Pro e contro dei file binari personalizzati

Ci sono alcuni pro e contro nell'usare formati di file binari personalizzati.

Creare qualcosa come un contenitore di risorse (come farà questo tutorial) ridurrà il thrash del disco e del server e in genere renderà il caricamento delle risorse molto più semplice perché non sarà necessario caricare più file. I formati di file personalizzati possono anche aggiungere un ulteriore livello di sicurezza sotto forma di offuscamento alle risorse del gioco.

D'altra parte, dovrai effettivamente generare i file personalizzati in un modo o nell'altro prima di poterli utilizzare in un gioco, ma non è così difficile come potrebbe sembrare - specialmente se tu o qualcuno che conosci puoi creare qualcosa come un File JAR che può essere rilasciato in un processo di compilazione con relativa facilità.


Comprensione dei tipi di dati primitivi

Prima di poter iniziare a progettare i propri formati di file binari, è necessario conoscere i tipi di dati primitivi (blocchi predefiniti) disponibili. Il numero di tipi di dati primitivi è in realtà illimitato, ma esiste un set comune che la maggior parte dei programmatori conosce e utilizza e questi tipi di dati rappresentano in genere multipli di 8 bit.

Come puoi vedere, questi tipi di dati primitivi forniscono una vasta gamma di valori interi e li troverai al centro della maggior parte delle specifiche di file binari. Esistono alcuni tipi di dati più primitivi, come i numeri in virgola mobile, ma i tipi di dati interi elencati sopra sono più che adeguati per questa introduzione e per la maggior parte dei formati di file binari.


Comprensione dei tipi di dati strutturati

I tipi di dati strutturati (o tipi di dati complessi) rappresentano elementi specifici (chunk) di un file binario e sono costituiti da tipi di dati primitivi o altri tipi di dati strutturati.

Puoi pensare a tipi di dati strutturati come oggetti o istanze di classe in un linguaggio di programmazione, con ogni oggetto che dichiara un insieme di proprietà. I tipi di dati strutturati possono essere visualizzati utilizzando la semplice notazione di oggetti.

Ecco un esempio di intestazione di un file fittizio:

 HEADER firma U24 versione U8 lunghezza U32

Quindi qui il tipo di dati strutturato è chiamato INTESTAZIONE e ha tre proprietà etichettate firma, versione e lunghezza. Ogni proprietà in questo esempio è dichiarata come un tipo di dati primitivo, ma le proprietà possono anche essere dichiarate come tipi di dati strutturati.

Se sei un programmatore, probabilmente stai cominciando a capire quanto sarebbe facile rappresentare un file binario in un linguaggio di programmazione basato su OOP; diamo una rapida occhiata a come questo INTESTAZIONE il tipo di dati potrebbe essere rappresentato in Java:

 intestazione di classe firma int pubblico; // U24 versione pubblica int; // U8 pubblico di lunga durata; // U32

Progettare un file binario personalizzato

A questo punto dovresti avere familiarità con le basi delle strutture di file binari, quindi ora è il momento di dare un'occhiata al processo di progettazione di un formato di file personalizzato funzionante. Questo formato di file sarà progettato per contenere una raccolta di risorse di gioco tra cui immagini e suoni.

L'intestazione

La prima cosa che dovrebbe essere progettata è una struttura dati per l'intestazione del file in modo che il file possa essere identificato prima che il resto del file venga caricato in memoria. Idealmente l'intestazione del file dovrebbe contenere almeno un campo firma e un campo versione:

 HEADER firma U24 versione U8

La firma del file che si sceglie di utilizzare dipende da voi: può essere un numero qualsiasi di byte, ma la maggior parte dei formati di file ha una firma leggibile da umani contenente tre o quattro caratteri ASCII. Per i miei scopi, il firma campo terrà i codici carattere di tre caratteri ASCII (un byte per carattere) e rappresenterà la stringa "RES" (abbreviazione di "RESOURCE"), quindi i valori del byte saranno 0x52, 0x45 e 0x53.

Il versione il campo sarà inizialmente 0x01 perché questa è la versione 1 del formato file.

Il file di risorse stesso è in realtà un tipo di dati strutturato che contiene un'intestazione e in seguito conterrà altri elementi. Attualmente sembra così:

 FILE header HEADER

immagini

La prossima cosa che diamo un'occhiata è la struttura dei dati per le immagini.

Il file di risorse memorizzerà una serie di valori di colore ARGB (uno per pixel) e consentirà la compressione facoltativa dei dati utilizzando l'algoritmo ZLIB. Le dimensioni dell'immagine dovranno anche essere incluse nel file insieme a un identificatore per l'immagine (in modo che l'immagine sia accessibile dopo che è stata caricata in memoria):

 IMMAGINE id STRING larghezza U16 altezza U16 compresso U8 data Lunghezza U32 dati U8 [dataLength]

Ci sono un paio di cose in quella struttura che hanno bisogno della tua attenzione; il primo è il U8 [dataLength] parte della struttura e il secondo è il STRINGA struttura dati utilizzata per id, che non era definito nella tabella dei tipi di dati sopra.

Il primo è la notazione di base dell'array - significa semplicemente che dataLength numero di U8 i valori devono essere letti dal file. Il dati campo contiene i pixel dell'immagine e il compresso campo indica se il dati il campo è compresso. Se la compresso il valore è 0x01 poi il dati il campo è compresso ZLIB, altrimenti il ​​decodificatore di file può assumere il dati il campo non è compresso. Il vantaggio di usare la compressione ZLIB qui è il IMMAGINE la struttura del file avrà dimensioni simili a una versione codificata PNG dell'immagine.

Il STRINGA la struttura dei dati è la seguente:

 STRING dataLength U16 data U8 [dataLength]

Per questo formato di file tutte le stringhe saranno codificate come UTF-8 e i byte della stringa codificata si troveranno nel dati campo del STRINGA struttura dati. Il dataLength campo indica il numero di byte nel dati campo.

La struttura del file di risorse ora appare così:

 FILE header HEADER imageCount U16 imageList IMAGE [imageCount]

Come puoi vedere il file ora contiene un'intestazione, una nuova imageCount campo che indica il numero di immagini nel file e una nuova imageList campo per le immagini. Questo di per sé sarebbe un formato di file utile per archiviare più immagini, ma sarebbe ancora più utile se includesse più tipi di risorse, quindi ora daremo un'occhiata all'aggiunta di suoni al file.

Suoni

I suoni verranno archiviati nel file in un modo simile alle immagini, ma invece di memorizzare valori cromatici di pixel non elaborati, il file memorizzerà campioni audio grezzi in risoluzioni bit differenti:

 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] // Campioni a 32 bit se (dataFormat == 0x02) data U32 [dataLength]

Oh mio Dio, dichiarazioni condizionali! Perché il formato dei dati campo indica la velocità in bit del suono, il formato del dati il campo deve essere variabile, ed è qui che entra in gioco la sintassi dell'istruzione condizionale semplice e programmabile.

Guardando la struttura dei dati si può facilmente vedere quale formato il dati i valori dei campi (campioni sonori) verranno utilizzati, in base a uno specifico formato dei dati valore. Quando aggiungi campi come formato dei dati a una struttura di dati i valori che questi campi possono contenere dipendono interamente da te. I valori 0x01, 0x02 e 0x03 vengono utilizzati in questo esempio semplicemente perché sono i primi valori non utilizzati disponibili nel byte.

La struttura del file di risorse ora appare così:

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

Dati generici

L'ultima cosa che verrà aggiunta a questa struttura di file di risorse è data da dati generici; ciò consentirà di inserire nel file vari dati relativi al gioco (in vari formati).

Come il IMMAGINE struttura dei dati questo nuovo DATI la struttura supporterà la compressione ZLIB opzionale perché i dati basati su testo come JSON e XML in genere beneficiano della compressione e questo offuscherà anche i dati all'interno del file:

 DATA id STRING compressa U8 dataFormat U8 dataLunghezza U32 dati U8 [dataLength]

Il compresso il campo indica se il dati campo è compresso: un valore di 0x01 significa il dati il campo è compresso ZLIB.

Il formato dei dati indica il formato dei dati e i valori che questo campo può contenere dipende da te. Ad esempio potresti usare 0x00 per il testo non elaborato, 0x01 per XML e 0x02 per JSON. Un singolo byte senza segno (U8) può tenere 256 valori diversi e che dovrebbero essere più che sufficienti per tutti i vari formati di dati che potresti voler usare in un gioco.

La struttura finale del file di risorse ha il seguente aspetto:

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

Per quanto riguarda i formati di file, questo è relativamente semplice, ma è funzionale e dimostra come i formati di file possono essere rappresentati e strutturati in modo ragionevole e comprensibile.


Comprensione degli ordini dei byte

C'è un'altra cosa importante che potrebbe essere necessario conoscere sui file binari: i valori multibyte memorizzati nei file binari possono utilizzare uno dei due ordini byte (questo è anche noto come "endian"). L'ordine dei byte può essere LSB (Least Significant Byte first, o "little-endian") o MSB (Most Significant Byte o "big-endian"). La differenza tra i due ordini di byte è semplicemente l'ordine in cui sono memorizzati i byte.

Ad esempio, un valore di colore RGB a 24 bit è composto da tre byte, un byte per ciascun canale di colore. L'ordine dei byte di un file determina se quei byte sono memorizzati nel file come RGB (big-endian) o BGR (little-endian).

Molti linguaggi di programmazione moderni forniscono un'API che consente di commutare l'ordine dei byte mentre si legge un file in memoria, quindi la lettura dei valori multibyte da un file binario non è qualcosa di cui i programmatori di solito devono preoccuparsi. Tuttavia, se stai leggendo un file byte per byte, devi essere a conoscenza dell'ordine dei byte del file.

Il seguente codice Java dimostra come leggere un valore a 24 bit (in questo caso un colore RGB) da un file mentre si considera l'ordine dei byte del file:

 bigEndian boolean = true; int readU24 (input InputStream) genera IOException int value = 0; if (bigEndian) valore | = 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; 

L'effettiva lettura e scrittura di file binari va oltre lo scopo di questo tutorial, ma in questo esempio dovresti essere in grado di vedere come l'ordine dei tre byte di un valore a 24 bit viene capovolto a seconda dell'ordine dei byte (endian) di un file. Ci sono dei vantaggi nell'usare un ordine di byte invece dell'altro? Beh, non proprio: gli ordini di byte sono solo di preoccupazione per l'hardware e non per il software.

Dove vai da qui dipende da te, ma spero che questo tutorial abbia reso i formati di file personalizzati un po 'meno spaventosi da considerare l'utilizzo nei tuoi stessi giochi!