Creați formate personalizate de fișiere binare pentru datele jocului

Jocul dvs. are date - sprite, efecte sonore, muzică, text - și trebuie să-l păstrați într-un fel. Uneori puteți încapsula totul într-un singur SWF, .Unity3D sau executabil fișier, dar în unele cazuri nu va fi potrivit. În acest tutorial tehnic, vom examina utilizarea de fișiere binare personalizate în acest scop.

Notă: Acest tutorial presupune că aveți o înțelegere de bază despre biți și octeți. Consultați o introducere în operatori binari, hexazecimali și mai mulți și înțelegând operatorii de biți pe activetuts + dacă aveți nevoie să revizuiți!


Pro și Contra de fișiere binare personalizate

Există câteva argumente pro și contra în utilizarea formatelor personalizate de fișiere binare.

Crearea unui container ca resursă (așa cum va face acest tutorial) va diminua perturbarea discului / serverului și va face ca încărcarea resurselor să devină mult mai ușoară, deoarece nu va fi nevoie să încărcați mai multe fișiere. Formatele personalizate de fișiere pot adăuga, de asemenea, un strat suplimentar de securitate sub formă de obfuscation la resursele de joc.

Pe de altă parte, va trebui să generați fișierele personalizate într-un fel sau altul înainte de a le putea utiliza într-un joc, dar asta nu este atât de dificil cum ar putea suna - mai ales dacă tu sau cineva pe care-l cunoști poate crea ceva de genul Fișier JAR care poate fi abandonat într-un proces de construire cu relativ ușurință.


Înțelegerea tipurilor de date primitive

Înainte de a putea începe să proiectați propriile formate de fișiere binare, veți avea nevoie de o înțelegere a tipurilor de date primitive (blocuri de construcție) care sunt disponibile pentru dvs. Numărul de tipuri primitive de date este de fapt nelimitat, dar există un set comun pe care majoritatea programatorilor îl cunoaște și îl utilizează, iar aceste tipuri de date reprezintă de obicei multipli de 8 biți.

După cum puteți vedea, aceste tipuri de date primitive oferă o gamă largă de valori întregi și le veți găsi în centrul celor mai multe specificații ale fișierelor binare. Există mai multe tipuri de date primitive, cum ar fi numerele cu puncte variabile, dar tipurile de date întregi enumerate mai sus sunt mai mult decât adecvate pentru această introducere și pentru majoritatea formatelor de fișiere binare.


Înțelegerea tipurilor de date structurate

Tipurile structurate de date (sau tipuri complexe de date) reprezintă elemente specifice (fragmente) ale unui fișier binar și constau din tipuri de date primitive sau alte tipuri de date structurate.

Vă puteți gândi la tipurile de date structurate ca obiecte sau instanțe de clasă într-un limbaj de programare, fiecare obiect declarând un set de proprietăți. Tipurile de date structurate pot fi vizualizate folosind o simplă notație de obiect.

Iată un exemplu de antet fictiv:

 HEADER semnătura U24 versiunea U8 lungimea U32

Deci aici este denumit tipul de date structurat ANTET și are trei proprietăți etichetate semnătură, versiune și lungime. Fiecare proprietate din acest exemplu este declarată ca tip de date primitiv, dar proprietățile pot fi declarate și ca tipuri de date structurate.

Dacă sunteți un programator, probabil veți începe să realizați cât de ușor ar fi să reprezentați un fișier binar într-un limbaj de programare bazat pe OOP; Să aruncăm o privire rapidă la modul în care ANTET tipul de date ar putea fi reprezentat în Java:

 clasa Header public public signature; // U24 public int version; // U8 lungime publică publică; // U32

Proiectarea unui fișier binar personalizat

În acest moment ar trebui să fiți familiarizați cu elementele de bază ale structurilor de fișiere binare, deci acum este momentul să examinați procesul de proiectare a unui format de fișier personalizat. Acest format de fișier va fi conceput pentru a deține o colecție de resurse de joc, inclusiv imagini și sunete.

Antetul

Primul lucru care trebuie proiectat este o structură de date pentru antetul fișierului, astfel încât fișierul să poată fi identificat înainte ca restul fișierului să fie încărcat în memorie. În mod ideal, antetul fișierului ar trebui să conțină cel puțin un câmp de semnături și un câmp de versiune:

 HEADER semnătura U24 versiunea U8

Semnătura fișierului pe care o alegeți să o utilizați depinde de dvs.: poate fi orice număr de octeți, dar majoritatea formatelor de fișiere au o semnătură care poate fi citită de oameni, care conține trei sau patru caractere ASCII. În scopurile mele, semnătură câmpul va păstra codurile de caractere ale trei caractere ASCII (un byte pe caracter) și va reprezenta șirul "RES" (scurt pentru "RESOURCE"), astfel încât valorile octetului vor fi 0x52, 0x45 și 0x53.

versiune câmpul va fi inițial 0x01 deoarece aceasta este versiunea 1 a formatului de fișier.

Fișierul de resurse în sine este de fapt un tip de date structurat care conține un antet și va conține mai târziu și alte elemente. În prezent, arată astfel:

 FILE header HEADER

Imagini

Următorul lucru pe care îl vom analiza este structura de date pentru imagini.

Fișierul de resurse va stoca un șir de valori ale culorilor ARGB (unul pe pixel) și va permite ca aceste date să fie comprimate opțional utilizând algoritmul ZLIB. De asemenea, dimensiunile imaginii vor fi incluse în fișier împreună cu un identificator al imaginii (astfel încât imaginea să poată fi accesată după ce a fost încărcată în memorie):

 IMAGE id STRING lățimea U16 înălțime U16 comprimat U8 dateLength U32 date U8 [dataLength]

Există câteva lucruri în această structură care necesită atenția dvs.; primul este U8 [dataLength] o parte a structurii și a doua este structura ŞIR structura de date utilizată pentru id, care nu a fost definit în tabelul cu tipurile de date de mai sus.

Prima este notația de bază a matricei - înseamnă pur și simplu asta dataLength Un numar de U8 valorile trebuie citite din fișier. date câmpul conține pixelii de imagine și comprimat câmpul indică dacă date câmpul este comprimat. În cazul în care comprimat valoarea este 0x01 apoi date câmpul este comprimat ZLIB, altfel decodorul de fișier poate prelua date câmpul nu este comprimat. Beneficiul folosirii compresiei ZLIB aici este IMAGINE structura fișierelor va ajunge la o dimensiune similară unei versiuni PNG codate a imaginii.

ŞIR structura de date este după cum urmează:

 STRING dateLength U16 date U8 [dataLength]

Pentru acest format de fișier, toate șirurile vor fi codificate ca UTF-8, iar octeții șirului codificat vor fi localizați în date câmpul ŞIR structură de date. dataLength câmpul indică numărul de octeți din date camp.

Structura fișierului de resurse arată acum astfel:

 FIȘA antet HEADER imageCount U16 imageList IMAGE [imageCount]

După cum puteți vedea, fișierul conține acum un antet, un nou imageCount câmp care indică numărul de imagini din fișier și un nou Imagelist câmp pentru imagini. Acest lucru ar reprezenta un format de fișier util pentru a stoca mai multe imagini, dar ar fi chiar mai util dacă ar avea mai multe tipuri de resurse, așa că acum vom analiza adăugarea de sunete în fișier.

Sunete

Sunetele vor fi stocate în fișier într-un mod similar cu imaginile, dar în loc să stocheze valorile culorilor de pixeli brute, fișierul va stoca eșantioane brute de sunet în diferite rezoluții de biți:

 (DateFormat == 0x00) date U8 [dataLength] // eșantioane pe 16 biți dacă (dataFormat == 0x01) date U16 [dataLength] // Probe de 32 biți dacă (dataFormat == 0x02) date U32 [dataLength]

Bunătate, afirmații condiționate! Deoarece dataFormat câmpul indică rata de biți a sunetului, formatul date câmpul trebuie să fie variabil, iar în acest caz sintaxa simplă și programabilă prietenoasă a declarației condiționale intră în joc.

Privind structura de date, puteți vedea cu ușurință formatul date valorile câmpului (eșantioanele de sunet) vor fi utilizate, având în vedere anumite date dataFormat valoare. Când adăugați câmpuri ca dataFormat la o structură de date, valorile pe care aceste câmpuri le pot conține sunt în întregime în funcție de dvs. Valorile 0x01, 0x02 și 0x03 sunt utilizate în acest exemplu doar pentru că sunt primele valori neutilizate disponibile în octet.

Structura fișierului de resurse arată acum astfel:

 FILE antet HEADER imagineCount U16 imageList IMAGE [imageCount] sunetCount U16 soundList SOUND [soundCount]

Datele generice

Ultimul lucru care va fi adăugat la această structură a fișierelor de resurse este datele generice; acest lucru va permite ca în fișier să fie incluse diverse date legate de joc (în diverse formate).

Ca IMAGINE structura datelor noi DATE structura va suporta compresia opțională ZLIB deoarece datele bazate pe text, cum ar fi JSON și XML, beneficiază în general de compresie, iar acest lucru va împiedica și datele din fișier:

 DATA id STRING comprimat U8 dataFormat U8 data Lung U32 date U8 [dataLength]

comprimat câmpul indică dacă date câmpul este comprimat: o valoare de 0x01 înseamnă date câmpul este comprimat ZLIB.

dataFormat indică formatul datelor și valorile pe care acest câmp le poate conține este de până la dvs. Ați putea folosi, de exemplu 0x00 pentru textul brut, 0x01 pentru XML și 0x02 pentru JSON. Un octet nesemnat (U8) poate ține 256 diferite valori și care ar trebui să fie mai mult decât suficient pentru toate diferitele formate de date pe care ați putea dori să le utilizați într-un joc.

Structura finală a fișierelor de resurse arată astfel:

 FILE antet HEADER imagineCount U16 imageList IMAGE [imageCount] sunetCount U16 sunetList SOUND [soundCount] dataCount U16 dateList DATA [dataCount]

În ceea ce privește formatele de fișiere, aceasta este relativ simplă - dar este funcțională și demonstrează modul în care formatele de fișiere pot fi reprezentate și structurate într-un mod sensibil și ușor de înțeles.


Înțelegerea comenzilor byte

Există un lucru mai important pe care ar trebui să-l cunoașteți despre fișierele binare: valorile multibyte stocate în fișierele binare pot utiliza una din cele două comenzi de octeți (aceasta este, de asemenea, cunoscută sub numele de "endian"). Ordinul de octeți poate fi LSB (mai întâi cel mai puțin important octet sau "little-endian") sau MSB (cel mai important octet primul sau "big-endian"). Diferența dintre cele două comenzi de octeți este pur și simplu ordinea în care sunt stocate octeții.

De exemplu, o valoare de culoare RGB pe 24 de biți este formată din trei octeți, câte un octet pentru fiecare canal de culoare. Ordinea byte a unui fișier determină dacă acei octeți sunt stocați în fișier ca RGB (big-endian) sau BGR (little-endian).

Multe limbi de programare moderne oferă un API care vă va permite să comutați comanda de octeți în timp ce citiți un fișier în memorie, astfel încât citirea valorilor multibyte dintr-un fișier binar nu este ceva ce programatorii de obicei trebuie să fie preocupați. Cu toate acestea, dacă citiți un fișier byte-by-byte, atunci va trebui să fiți conștienți de ordinul de octet al fișierului.

Următorul cod Java demonstrează modul în care se citește o valoare pe 24 de biți (în acest caz o culoare RGB) dintr-un fișier în timp ce se consideră ordinea byte a fișierului:

 bigEndian boolean = adevărat; int readU24 (intrare InputStream) aruncă IOException valoare int = 0; dacă (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; 

Actualul citire și scriere a fișierelor binare este dincolo de scopul acestui tutorial, dar în acest exemplu ar trebui să puteți vedea cum se comută ordinea celor trei octeți ai unei valori pe 24 biți, în funcție de ordinea byte (endian) a un fișier. Există vreun avantaj al utilizării unei comenzi de byte în locul celuilalt? Ei bine, nu într-adevăr - comenzile byte sunt doar de îngrijorare pentru hardware-ul nu software-ul.

Unde te duci de aici este de până la tine, dar sper că acest tutorial a făcut formate personalizate de fișiere mai puțin înfricoșător să ia în considerare utilizarea în propriile jocuri!