Het maken van een visueel aantrekkelijke en gevarieerde tileset is een tijdrovend proces, maar de resultaten zijn vaak de moeite waard. Echter, zelfs na het maken van de kunst, moet je het nog steeds allemaal in je level uitdelen!
Je kunt elke tegel één voor één handmatig plaatsen, of je kunt het proces automatiseren door te gebruiken bitmasking, je hoeft dus alleen de vorm van het terrein te tekenen.
Tegelbitmasking is een methode om automatisch de juiste sprite uit een gedefinieerde tileset te selecteren. Hiermee kunt u een generieke placeholder-tegel plaatsen overal waar u een bepaald type terrein wilt laten verschijnen in plaats van met de hand een potentieel enorme selectie van verschillende tegels te plaatsen.
Zie deze video voor een demonstratie:
(U kunt de demo's en bronbestanden downloaden van de GitHub-repo.)
Bij meerdere soorten terreinen kan het aantal verschillende variaties 300 of meer tegels overschrijden. Het tekenen van dit veel verschillende sprites is zeker een tijdrovend proces, maar tegel bitmasking zorgt ervoor dat het plaatsen van deze tegels snel en efficiënt is.
Met een statische implementatie van bitmasking, worden kaarten gegenereerd tijdens runtime. Met een paar kleine aanpassingen kun je bitmasking uitbreiden om dynamische tegels toe te staan die tijdens het spelen veranderen. In deze zelfstudie bespreken we de basisprincipes van bitmaskeren van tegels terwijl we werken aan meer gecompliceerde implementaties waarbij hoektegels en meerdere terreintypen worden gebruikt.
Tegel-bitmasking heeft alles te maken met het berekenen van een numerieke waarde en het toewijzen van een specifieke sprite op basis van die waarde. Elke tegel kijkt naar de aangrenzende tegels om te bepalen welke sprite van de set aan zichzelf moet worden toegewezen.
Elke sprite in een tileset is genummerd en het bitmasking-proces retourneert een getal dat overeenkomt met de positie van een sprite in de tileset. Tijdens runtime wordt de bitmaskeringsprocedure uitgevoerd en wordt elke tegel bijgewerkt met de juiste sprite.
Het sprite-vel hierboven bestaat uit terreintegels met alle mogelijke randconfiguraties. De getallen op elke tegel vertegenwoordigen de bitmaskingwaarde, die we in de volgende sectie zullen leren berekenen. Voor nu is het belangrijk om te begrijpen hoe de bitmaskeringswaarde zich verhoudt tot de terrain tileset. De sprites worden sequentieel gerangschikt zodat een bitmaskerwaarde van 0
retourneert de eerste sprite, helemaal naar een waarde van 15
die de 16 teruggeeftth sprite.
Het berekenen van deze waarde is relatief eenvoudig. In dit voorbeeld gaan we uit van een enkel terreintype zonder hoekstukken.
Elke tegel controleert of er tegels zijn in het noorden, westen, oosten en zuiden en elke controle retourneert een Booleaanse waarde, waar 0
vertegenwoordigt een lege ruimte en 1
betekent de aanwezigheid van een andere terreintegel.
Dit Booleaanse resultaat wordt vervolgens vermenigvuldigd met de binaire richtingswaarde en toegevoegd aan het lopende totaal van de bitmaskingwaarde - het is gemakkelijker te begrijpen met enkele voorbeelden:
Het groene vierkant in de bovenstaande afbeelding vertegenwoordigt de terreinkaart die we berekenen. We beginnen met het controleren van een tegel naar het noorden. Er is geen tegel in het noorden, dus de Boolean-controle retourneert een waarde van 0
. We vermenigvuldigen 0 met de richtingswaarde voor Noord, 20 = 1, ons geven 1 * 0 = 0
.
Voor een terreintegel die volledig door lege ruimte wordt omgeven, keert elke Booleaanse cheque terug 0
, resulterend in het 4-bits binaire getal 0000
of 1 * 0 + 2 * 0 + 4 * 0 + 8 * 0 = 0
. Er zijn 16 totaal mogelijke combinaties, van 0 tot 15, dus de 1st sprite in de tileset zal worden gebruikt om dit type terreintegel te vertegenwoordigen met een waarde van 0
.
0001
, of 1 * 1 + 2 * 0 + 4 * 0 + 8 * 0 = 1
. De 2nd sprite in de tileset zal worden gebruikt om dit type terrein met een waarde van te vertegenwoordigen 1
.Een terreinkegel omzoomd door een tegel in het noorden en een tegel in het oosten geeft een binaire waarde van 0101
, of 1 * 1 + 2 * 0 + 4 * 1 + 8 * 0 = 5
. De zesde sprite in de tileset zal worden gebruikt om dit type terrein met een waarde van te vertegenwoordigen 5
.
Een terreinkegel omzoomd door een tegel in het oosten en een tegel in het westen geeft een binaire waarde van 0110
, of 1 * 0 + 2 * 1 + 4 * 1 + 8 * 0 = 6
. De 7e sprite in de tileset zal worden gebruikt om dit type terrein met een waarde van te vertegenwoordigen 6
.
Na het berekenen van de bitmaskingwaarde van een tile, wijzen we de juiste sprite toe uit de tileset. Deze laatste stap kan in realtime worden uitgevoerd terwijl de kaart wordt geladen, of het resultaat kan worden opgeslagen en in de gewenste tegeleditor worden geladen voor verdere bewerking.
Het cijfer links vertegenwoordigt een 4-bit, single-terrain tileset zoals die achter elkaar op een tegelpagina zou verschijnen. De figuur aan de rechterkant geeft weer hoe de tegels in het spel eruit zien nadat ze zijn geplaatst met behulp van de bitmaskeringsprocedure. Elke tegel is gemarkeerd met zijn bitmaskerwaarde om de relatie weer te geven tussen de volgorde van een tegel op het tegelblad en zijn positie in het spel.
Laten we als voorbeeld de tegel in de rechterbovenhoek van de afbeelding rechts bekijken. Deze tegel wordt begrensd door tegels naar het westen en Souh. De Boolean-controle retourneert een binaire waarde van 1010
, of 1 * 0 + 2 * 1 + 4 * 0 + 8 * 1 = 10
. Deze waarde komt overeen met de 11th sprite in de tegelpagina.
Het aantal vereiste Booleaanse richtingscontroles is afhankelijk van de beoogde complexiteit van uw tileset. Door hoekstukken te negeren, kunt u deze vereenvoudigde 4-bits oplossing gebruiken die slechts vier directionele binaire controles vereist.
Maar wat gebeurt er wanneer u meer visueel aantrekkelijk terrein wilt creëren? U moet rekening houden met het bestaan van hoektegels, waardoor de hoeveelheid sprites toeneemt van 16 tot 48. Het volgende 8-bits bitmaskeringsvoorbeeld vereist acht Booleaanse richtingscontroles per tegel.
Voor dit voorbeeld maken we een top-down tileset die grasachtig terrein in de buurt van de oceaan afbeeldt. In dit geval bestaat onze oceaan op een laag onder de terreintegels. Dit stelt ons in staat om een oplossing voor een enkel terrein te gebruiken, terwijl we toch de illusie houden dat twee terreintypen botsen.
Als het spel eenmaal is gestart en de bitmaskeringsprocedure is voltooid, zullen de sprites nooit veranderen. Dit is een naadloze, statische implementatie van bitmasking waarbij alles plaatsvindt voordat de speler de tegels ooit ziet.
We willen dat het terrein visueel interessanter is dan de vorige 4-bits oplossing, dus hoekstukken zijn vereist. Dit extra beetje visuele complexiteit vereist een exponentiële hoeveelheid extra werk voor de artiest, programmeur en het spel zelf. Door uit te breiden wat we hebben geleerd van de 4-bits oplossing, kunnen we snel begrijpen hoe we de 8-bits oplossing moeten benaderen.
Hier is de volledige sprite sheet voor onze terreintegels aan de oceaanzijde. Merk je iets vreemds aan het aantal tegels? Het 4-bit-voorbeeld van eerder resulteerde in 24 = 16 tegels, dus dit 8-bits voorbeeld zou zeker moeten resulteren in 28 = 256 tegels, maar er zijn duidelijk minder dan dat.
Hoewel het waar is dat deze 8-bits bitmaskeringsprocedure resulteert in 256 mogelijke binaire waarden, vereist niet elke combinatie een geheel unieke tegel. In het volgende voorbeeld wordt uitgelegd hoe 256 combinaties kunnen worden weergegeven met slechts 48 tegels.
Nu zijn we aan het maken acht Booleaanse directionele controles. De middelste tegel hierboven wordt begrensd door tegels naar het noorden, noordoosten en oosten, dus deze Boolean-controle retourneert een binaire waarde van 00010110
of 1 * 0 + 2 * 1 + 4 * 1 + 8 * 0 + 16 * 1 + 32 * 0 + 64 * 0 + 128 * 0 = 22
.
De tegel links hierboven is vergelijkbaar met de vorige tegel, maar wordt nu ook begrensd door tegels naar het zuidwesten en zuidoosten. Deze Booleaanse directionele controle moeten retourneer een binaire waarde van 10110110
, of 1 * 0 + 2 * 1 + 4 * 1 + 8 * 0 + 16 * 1 + 32 * 1 + 64 * 0 + 128 * 1 = 182
.
Deze waarde verschilt van de vorige tegel, maar beide tegels zouden eigenlijk visueel identiek zijn, dus wordt deze overbodig.
Om de overtolligheden te elimineren, voegen we een extra voorwaarde toe aan onze Booleaanse directionele controle: bij het controleren op de aanwezigheid van aangrenzende hoek tegels, we moeten ook controleren of er naburige tegels zijn in de vier windrichtingen (direct Noord, Oost, Zuid of West).
De tegel in het Noord-Oosten is bijvoorbeeld omringd door bestaande tegels, terwijl de tegels naar het Zuid-Westen en Zuid-Oosten dat niet zijn. Dit betekent dat de zuidwest- en zuidoosttegels niet zijn opgenomen in de bitmaskingberekening.
Met deze nieuwe voorwaarde retourneert deze Boolean-controle een binaire waarde van 00010110
of 1 * 0 + 2 * 1 + 4 * 1 + 8 * 0 + 16 * 1 + 32 * 0 + 64 * 0 + 128 * 0 = 22
Net als eerst. Nu kunt u zien hoe de 256 combinaties kunnen worden weergegeven met slechts 48 tegels.
Een ander probleem dat u misschien opmerkt is dat de waarden die zijn berekend met de 8-bits bitmaskeringsprocedure niet langer correleren met de volgorde van de tegels in het sprite-werkblad. Er zijn slechts 48 tegels, maar onze mogelijke berekende waarden variëren van 0 tot 255, dus we kunnen de berekende waarde niet langer gebruiken als een directe referentie bij het pakken van de juiste sprite.
Wat we daarom nodig hebben, is een datastructuur om de lijst met berekende waarden en hun bijbehorende tile-waarden te bevatten. Hoe u dit wilt implementeren, is aan u, maar bedenk dat de volgorde waarin u naar omliggende tegels zoekt, de volgorde bepaalt waarin uw tegels in het sprite-blad moeten worden geplaatst.
Voor dit voorbeeld controleren we de aangrenzende tegels in de volgende volgorde: Noordwest, Noord, Noordoost, West, Oost, Zuidwest, Zuid, Zuidoost.
Hieronder vindt u de complete set bitmaskingwaarden die betrekking hebben op de posities van tegels in onze sprite sheet (voel u vrij om deze waarden in uw project te gebruiken om tijd te besparen):
2 = 1, 8 = 2, 10 = 3, 11 = 4, 16 = 5, 18 = 6, 22 = 7, 24 = 8, 26 = 9, 27 = 10, 30 = 11, 31 = 12, 64 = 13, 66 = 14, 72 = 15, 74 = 16, 75 = 17, 80 = 18, 82 = 19, 86 = 20, 88 = 21, 90 = 22, 91 = 23, 94 = 24, 95 = 25 , 104 = 26, 106 = 27, 107 = 28, 120 = 29, 122 = 30, 123 = 31, 126 = 32, 127 = 33, 208 = 34, 210 = 35, 214 = 36, 216 = 37, 218 = 38, 219 = 39, 222 = 40, 223 = 41, 248 = 42, 250 = 43, 251 = 44, 254 = 45, 255 = 46, 0 = 47
Al onze eerdere voorbeelden gaan uit van een enkel terreintype, maar wat als we een tweede terrein introduceren in de vergelijking? We hebben een ... nodig 5bit bitmasking-oplossing, en we moeten onze twee terreintypen definiëren. We moeten ook een waarde toewijzen aan de middelste tegel die alleen wordt geteld onder specifieke voorwaarden. Vergeet niet dat we niet langer rekening houden met "lege ruimte" zoals in de vorige voorbeelden; tegels moeten nu aan alle kanten omringd zijn door een andere tegel.
De bovenstaande afbeelding toont een voorbeeld met twee typen terrein en geen hoektegels. Type 1 altijdgeeft een waarde van 0
wanneer het wordt gedetecteerd tijdens de directionele controle; de centertegelwaarde wordt berekend en gebruikt enkel en alleen als het terreintype 2 is.
De middelste tegel in het bovenstaande voorbeeld is omgeven door terreintype 2 naar het noorden, westen en oosten en door het terreintype 1 naar het zuiden. De middelste tegel is terreintype 1, dus deze wordt niet geteld. Deze Boolean-controle retourneert een binaire waarde van 00111
, of 1 * 1 + 2 * 1 + 4 * 1 + 8 * 0 + 16 * 0 = 7
.
In dit voorbeeld is onze middelste tegel terreintype 2, dus deze wordt meegeteld in de berekening. De middelste tegel is omgeven door terreintype 2 naar het noorden en het westen. Het is ook omringd door terreintype 1 naar het oosten en zuiden. Deze Boolean-controle retourneert een binaire waarde van 10011
, of 1 * 1 + 2 * 1 + 4 * 0 + 8 * 0 + 16 * 1 = 19
.
De bitmaskingberekening kan ook worden uitgevoerd tijdens het spelen van het spel, waardoor real-time veranderingen in plaatsing en uiterlijk van de tegel mogelijk worden. Dit is handig voor vernietigbare terreinen en spellen die geschikt zijn voor het maken en bouwen. De initiële bitmaskeringsprocedure is verplicht voor alle tegels, maar eventuele extra dynamische berekeningen mogen alleen worden uitgevoerd wanneer dit absoluut noodzakelijk is. Een vernietigde terreintegel zou bijvoorbeeld de bitmaskeringsberekening alleen activeren voor omliggende tegels.
Tile bitmasking is het perfecte voorbeeld van het bouwen van een werksysteem om je te helpen bij het ontwikkelen van games. Het is niet iets dat direct invloed heeft op de ervaring van de speler; in plaats daarvan biedt deze methode voor het automatiseren van een tijdrovend deel van het niveauontwerp een waardevol voordeel voor de ontwikkelaar. Eenvoudig gezegd: Bitmaskeren is een snelle manier om het spel je vuile werk te laten doen, zodat je je kunt concentreren op belangrijkere taken.