Je game bevat data - sprites, geluidseffecten, muziek, tekst - en je moet het op de een of andere manier opslaan. Soms kun je alles inkapselen in een single SWF
, .unity3d
of EXE
bestand, maar in sommige gevallen is dat niet geschikt. In deze technische zelfstudie bekijken we voor dit doel aangepaste binaire bestanden.
Notitie: Deze tutorial gaat ervan uit dat je een basisbegrip hebt van bits en bytes. Bekijk An Introduction to Binary, Hexadecimal, and More and Understanding Bitwise Operators on Activetuts + als je moet herzien!
Er zijn een aantal voor- en nadelen aan het gebruik van aangepaste binaire bestandsindelingen.
Het creëren van zoiets als een broncontainer (zoals deze zelfstudie zal doen) zal het verspillen van schijven / servers verminderen en zal het laden van bronnen veel eenvoudiger maken omdat het niet nodig is om meerdere bestanden te laden. Aangepaste bestandsindelingen kunnen ook een extra beveiligingslaag toevoegen in de vorm van versluiering van gamebronnen.
Aan de andere kant moet je de aangepaste bestanden op de een of andere manier genereren voordat je ze in een game kunt gebruiken, maar dat is niet zo moeilijk als het zou klinken - vooral als jij of iemand die je kent, zoiets als een spel kan maken JAR-bestand dat relatief gemakkelijk in een bouwproces kan worden neergezet.
Voordat u kunt beginnen met het ontwerpen van uw eigen binaire bestandsformaten, hebt u een goed begrip van de primitieve gegevenstypen (bouwstenen) nodig die voor u beschikbaar zijn. Het aantal primitieve gegevenstypen is eigenlijk onbeperkt, maar er is een algemene set die de meeste programmeurs kennen en gebruiken, en deze gegevenstypen vertegenwoordigen meestal veelvouden van 8 bits.
Zoals u kunt zien, bieden deze primitieve gegevenstypen een breed bereik van integerwaarden en zult u ze vinden als de kern van de meeste binaire bestandsspecificaties. Er zijn een paar meer primitieve gegevenstypen, zoals drijvende-kommagetallen, maar de hierboven vermelde integer-gegevenstypen zijn meer dan voldoende voor deze introductie en voor de meeste binaire bestandsindelingen.
Gestructureerde gegevenstypen (of complexe gegevenstypen) vertegenwoordigen specifieke elementen (chunks) van een binair bestand en bestaan uit primitieve gegevenstypen of andere gestructureerde gegevenstypen.
U kunt gestructureerde gegevenstypen beschouwen als objecten of klasseninstanties in een programmeertaal, waarbij elk object een set eigenschappen declareert. Gestructureerde gegevenstypes kunnen worden gevisualiseerd met behulp van eenvoudige objectnotatie.
Hier is een voorbeeld van een fictieve bestandskop:
HEADER handtekening U24-versie U8-lengte U32
Dus hier wordt het gestructureerde gegevenstype genoemd HEADER
en het heeft drie eigenschappen gelabeld handtekening
, versie
en lengte
. Elke eigenschap in dit voorbeeld is gedeclareerd als een primitief gegevenstype, maar eigenschappen kunnen ook worden gedeclareerd als gestructureerde gegevenstypen.
Als je een programmeur bent, begin je je waarschijnlijk te realiseren hoe gemakkelijk het is om een binair bestand te vertegenwoordigen in een op OOP gebaseerde programmeertaal; laten we even kijken hoe dit gaat HEADER
gegevenstype zou in Java kunnen worden weergegeven:
class Header public int signature; // U24 public int-versie; // U8 openbare lange lengte; // U32
Op dit punt moet u bekend zijn met de basisprincipes van binaire bestandsstructuren, dus nu is het tijd om een kijkje te nemen naar het proces van het ontwerpen van een werkend aangepast bestandsformaat. Dit bestandsformaat is ontworpen om een verzameling game-bronnen te bevatten, inclusief afbeeldingen en geluiden.
Het eerste dat moet worden ontworpen, is een gegevensstructuur voor de bestandskop, zodat het bestand kan worden geïdentificeerd voordat de rest van het bestand in het geheugen wordt geladen. In het ideale geval moet de bestandskop ten minste een handtekeningveld en een versieveld bevatten:
HEADER handtekening U24-versie U8
De bestandshandtekening die u kiest, is geheel aan u: het kan een willekeurig aantal bytes zijn, maar de meeste bestandsindelingen hebben een voor mensen leesbare handtekening met drie of vier ASCII-tekens. Voor mijn doeleinden, de handtekening
veld bevat de tekencodes van drie ASCII-tekens (één byte per teken) en vertegenwoordigt de tekenreeks "RES" (een afkorting van "RESOURCE"), dus de bytewaarden worden 0x52
, 0x45
en 0x53
.
De versie
veld zal in eerste instantie zijn 0x01
omdat dit versie 1 van het bestandsformaat is.
Het bronbestand zelf is eigenlijk een gestructureerd gegevenstype dat een koptekst bevat en later andere elementen bevat. Het ziet er momenteel als volgt uit:
BESTAND header HEADER
Het volgende dat we gaan bekijken, is de datastructuur voor afbeeldingen.
Het bronbestand zal een array van ARGB-kleurwaarden (één per pixel) opslaan en toestaan dat die gegevens optioneel worden gecomprimeerd met behulp van het ZLIB-algoritme. De afbeeldingsdimensies moeten ook in het bestand worden opgenomen samen met een ID voor de afbeelding (zodat de afbeelding kan worden geopend nadat deze in het geheugen is geladen):
IMAGE id STRING width U16 height U16 gecomprimeerde U8 dataLength U32 data U8 [dataLength]
Er zijn een paar dingen in die structuur die je aandacht nodig hebben; de eerste is de U8 [datalengte]
een deel van de structuur en de tweede is de DRAAD
datastructuur gebruikt voor de ID kaart
, die niet was gedefinieerd in de bovenstaande tabel met gegevenstypen.
De eerste is de basis array-notatie - het betekent gewoon dat datalengte
aantal U8
waarden moeten uit het bestand worden gelezen. De gegevens
veld bevat de beeldpixels en de gecomprimeerde
veld geeft aan of de gegevens
veld is gecomprimeerd. Als het gecomprimeerde
waarde is 0x01
dan de gegevens
veld is ZLIB gecomprimeerd, anders kan de bestandsdecoder de gegevens
veld is niet gecomprimeerd. Het voordeel van het gebruik van ZLIB-compressie is hier de BEELD
bestandsstructuur zal uiteindelijk een vergelijkbare grootte hebben als een PNG-gecodeerde versie van de afbeelding.
De DRAAD
gegevensstructuur is als volgt:
STRING dataLength U16-gegevens U8 [dataLength]
Voor dit bestandsformaat zullen alle strings worden gecodeerd als UTF-8 en de bytes van de gecodeerde string zullen zich in de gegevens
veld van de DRAAD
data structuur. De datalengte
veld geeft het aantal bytes in de gegevens
veld-.
De structuur van het bronbestand ziet er nu als volgt uit:
BESTAND header HEADER imageCount U16 imageList IMAGE [imageCount]
Zoals je ziet, bevat het bestand nu een header, een nieuw imagecount
veld dat het aantal afbeeldingen in het bestand aangeeft en een nieuw ImageList
veld voor de afbeeldingen. Dit zou op zich een handig bestandsformaat zijn om meerdere afbeeldingen op te slaan, maar het zou nog nuttiger zijn als het meerdere bronsoorten zou bevatten, dus zal men nu kijken naar het toevoegen van geluiden aan het bestand.
Geluiden worden op dezelfde manier opgeslagen in het bestand als afbeeldingen, maar in plaats van ruwe pixelkleurwaarden op te slaan, slaat het bestand onbewerkte geluidssamples op met verschillende bitresoluties:
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 samples if (dataFormat == 0x02) data U32 [dataLength]
Oh mijn god, voorwaardelijke uitspraken! Omdat het data formaat
veld geeft de bitsnelheid van het geluid aan, het formaat van de gegevens
veld moet variabel zijn, en dat is waar de eenvoudige en programmeurvriendelijke syntaxis van de voorwaardelijke verklaring in het spel komt.
Kijkend naar de datastructuur kun je gemakkelijk zien in welk formaat de gegevens
veldwaarden (geluidssteekproeven) zullen worden gebruikt, gegeven een specifiek data formaat
waarde. Bij het toevoegen van velden zoals data formaat
naar een datastructuur zijn de waarden die deze velden kunnen bevatten volledig aan jou. De waarden 0x01
, 0x02
en 0x03
worden in dit voorbeeld gebruikt, simpelweg omdat ze de eerste ongebruikte waarden zijn die beschikbaar zijn in de byte.
De structuur van het bronbestand ziet er nu als volgt uit:
BESTAND header HEADER imageCount U16 imageList IMAGE [imageCount] soundCount U16 soundList SOUND [soundCount]
Het laatste dat aan deze structuur van het bronbestand wordt toegevoegd, is generieke gegevens; hierdoor kunnen verschillende game-gerelateerde gegevens (in verschillende formaten) in het bestand worden ingepakt.
Zoals de BEELD
gegevensstructuur dit nieuwe GEGEVENS
structuur ondersteunt optionele ZLIB-compressie omdat tekstgebaseerde gegevens zoals JSON en XML over het algemeen profiteren van compressie, en dit zal ook de gegevens in het bestand verduisteren:
DATA id STRING gecomprimeerde U8 dataFormat U8 dataLength U32 data U8 [dataLength]
De gecomprimeerde
veld geeft aan of het gegevens
veld is gecomprimeerd: een waarde van 0x01
betekent het gegevens
veld is ZLIB gecomprimeerd.
De data formaat
geeft het formaat van de gegevens aan en de waarden die dit veld kan bevatten, zijn aan u. Je zou bijvoorbeeld kunnen gebruiken 0x00
voor onbewerkte tekst, 0x01
voor XML en 0x02
voor JSON. Een enkele byte zonder teken (U8
) kan houden 256
verschillende waarden en dat zou meer dan genoeg moeten zijn voor alle verschillende gegevensformaten die u in een spel zou kunnen gebruiken.
De uiteindelijke structuur van het resourcebestand ziet er als volgt uit:
BESTAND header HEADER imageCount U16 imageList IMAGE [imageCount] soundCount U16 soundList SOUND [soundCount] dataCount U16 dataList DATA [dataCount]
Wat bestandsindelingen betreft, is deze relatief eenvoudig - maar het is functioneel en het laat zien hoe bestandsindelingen op een verstandige en begrijpelijke manier kunnen worden weergegeven en gestructureerd..
Er is nog een belangrijk ding dat u misschien moet weten over binaire bestanden: multibyte-waarden die in binaire bestanden zijn opgeslagen, kunnen een van de twee byte-orders gebruiken (dit is ook bekend als de "endian"). De bytevolgorde kan LSB (minst belangrijke byte eerst of "little-endian") of MSB (meest significante byte eerst of "big-endian") zijn. Het verschil tussen de twee bytebestellingen is eenvoudig de volgorde waarin de bytes worden opgeslagen.
Een 24-bits RGB-kleurwaarde bestaat bijvoorbeeld uit drie bytes, één byte voor elk kleurkanaal. De bytevolgorde van een bestand bepaalt of die bytes in het bestand worden opgeslagen als RGB (big-endian) of BGR (little-endian).
Veel moderne programmeertalen bieden een API waarmee u de bytevolgorde kunt wijzigen terwijl u een bestand in het geheugen leest, dus het lezen van multibyte-waarden uit een binair bestand is niet iets waar programmeurs zich meestal zorgen over moeten maken. Als u echter een byte by byby-bestand leest, moet u op de hoogte zijn van de bytevolgorde van het bestand.
De volgende Java-code laat zien hoe een 24-bits waarde (in dit geval een RGB-kleur) uit een bestand te lezen terwijl de bytevolgorde van het bestand wordt bekeken:
bigEndian boolean = true; int readU24 (InputStream invoer) gooit 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;
Het feitelijke lezen en schrijven van binaire bestanden valt buiten het bestek van deze zelfstudie, maar in dat voorbeeld zou u moeten kunnen zien hoe de volgorde van de drie bytes van een 24-bits waarde wordt omgedraaid, afhankelijk van de bytevolgorde (endian) van een bestand. Zijn er voordelen van het gebruik van één bytevolgorde in plaats van de andere? Nou, niet echt - bytebestellingen zijn alleen van belang voor hardware en niet voor software.
Waar je heen gaat, is aan jou, maar ik hoop dat deze tutorial aangepaste bestandsindelingen een beetje minder eng heeft gemaakt om te gebruiken in je eigen games!