Använda Tile Masks för att sätta en kakel typ baserat på dess omgivande plattor

Används vanligtvis i kakel-baserade spel, gör kakelmaskar att du kan ändra en kakel beroende på dess grannar, så att du kan blanda terränger, byta kakel och mer. I den här handledningen visar jag dig en skalbar, återanvändbar metod för att upptäcka om en kakelens närmaste grannar matchar ett av ett antal mönster som du ställer in.

Notera: Även om denna handledning skrivs med C # och Unity, borde du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst.


Vad är en kakelmask?

Tänk på följande: du gör en Terraria-liknande, och du vill ha smutsblock att bli till lera block om de är bredvid vatten. Låt oss anta att din värld har mycket mer vatten än det gör smuts, så det billigaste sättet du kan göra är att kontrollera varje smutsblock för att se om det ligger bredvid vatten och inte vice versa. Det här är en ganska bra tid att använda a kakelmask.

Tile masker är lika gamla som bergen, och bygger på en enkel idé. I ett 2D-galler av objekt kan en kakel ha åtta andra kakel direkt intill den. Vi ringer detta till lokalt utbud av den centrala plattan (Figur 1).


Figur 1. Det lokala utbudet av en kakel.

För att bestämma vad du ska göra med din smutsplatta, jämför du det lokala intervallet med en uppsättning regler. Till exempel kan du titta direkt ovanför smutsblocket och se om det finns vatten där (Figur 2). Den uppsättning regler du använder för att utvärdera ditt lokala sortiment är din kakelmask.


Figur 2. Kakelmask för att identifiera ett smutsblock som ett lera block. De grå kakel kan vara vilken som helst annan kakel.

Naturligtvis vill du också söka efter vatten i andra riktningar, så de flesta kakelmaskar har mer än ett rumsligt arrangemang (Figur 3).


Figur 3. De fyra typiska orienteringarna av en kakelmask: 0 °, 90 °, 180 °, 270 °.

Och ibland kommer du att upptäcka att även riktigt enkla kakelmaskarrangemang kan speglas och inte överbelastas (Figur 4).


Figur 4. Ett exempel på chiralitet i en kakelmask. Översta raden kan inte roteras för att matcha den nedre raden.

Lagringsstrukturer av en kakelmask

Lagra dess element

Varje cell i en kakelmask har ett element inuti det. Elementet i den cellen jämförs med motsvarande element i det lokala intervallet för att se om de matchar.

Jag använder listor som element. Listor gör det möjligt för mig att utföra jämförelser av godtycklig komplexitet, och C # s Linq ger mycket praktiska sätt att slå samman listor i större listor, vilket minskar antalet förändringar som jag potentiellt kan glömma att göra.

Till exempel kan en lista med väggplattor kombineras med en lista med golvplattor och en lista över öppningsplattor (dörrar och fönster) för att skapa en lista över strukturplattor, eller förteckningar över möbelplattorna från enskilda rum kan kombineras för att göra en lista över alla möbler. Andra spel kan ha listor med kakel som är brännbara eller påverkas av gravitation.

Lagring av kakelmasken själv

Det intuitiva sättet att lagra en kakelmask är som en 2D-array, men det går långsamt att få tillgång till 2D-arrays, och du kommer att göra det mycket. Vi vet att ett lokalt sortiment och en kakelmask alltid kommer att bestå av nio element, så vi kan undvika tidsbegränsning genom att använda en vanlig 1D-serie och läsa lokalområdet i det från topp till botten, åt vänster till höger (figur 4).


Figur 5. Lagring av en 2D-plattmask i en 1D-struktur.

De rumsliga relationerna mellan elementen bevaras om du någonsin behöver dem (jag har inte behövt dem hittills), och arrays kan lagra ganska mycket någonting. Tilemasks i detta format kan också lätt roteras genom offset läs / skriv.

Lagring av rotationsinformation

I vissa fall kan du rotera den centrala plattan i enlighet med omgivningen, till exempel när du placerar en vägg bredvid andra väggar. Jag tycker om att använda skarpa arrays för att hålla de fyra rotationerna i en kakelmask på samma plats, med hjälp av det yttre matrisindexet för att indikera den aktuella rotationen (mer om det i koden).


Kodkrav

Med de grundläggande förutsättningarna kan vi beskriva vad koden behöver göra:

  1. Definiera och lagra kakelmaskar.
  2. Rotera kakelmaskar automatiskt, istället för att vi måste manuellt upprätthålla fyra roterade kopior av samma definition.
  3. Ta tag i en kakel s lokala sortiment.
  4. Utvärdera lokalområdet mot en kakelmask.

Följande kod är skrivet i C # för Unity, men begreppen ska vara ganska bärbara. Exemplet är ett från mitt eget arbete för att procedurellt konvertera roguelike text-bara kartor till 3D (placera en rak del av väggen, i det här fallet).


Definiera, rotera och lagra kakelmaskerna

Jag automatiserar allt detta med ett metodsamtal till en DefineTilemask metod. Här är ett exempel på användning, med metoddeklaration nedan.

 offentlig statisk readonly lista någon = ny lista() ; offentlig statisk readonly lista ignoreras = ny lista() ", '_', statisk statisk läsbar lista vägg = ny lista() '#', 'D', 'W'; offentlig statisk lista[] [] outerWallStraight = MapHelper.DefineTilemask (någon, ignorerad, vilken som helst, vägg, vilken som helst, vägg, vilken som helst, vilken som helst);

Jag definierar tilemask i sin oroterade form. Listan heter ignoreras lagrar tecken som inte beskriver något i mitt program: mellanslag som hoppas över; och underskrifter, som jag använder för att visa ett ogiltigt arrayindex. En kakel vid (0,0) (övre vänstra hörnet) i en 2D-array kommer inte att ha något till dess nord eller väst, till exempel, så det lokala intervallet får understrykningar där istället. någraär en tom lista som alltid utvärderas som en positiv match.

 offentlig statisk lista[] [] DefinieraTilemask (Lista nW, lista n, lista nE, lista w, lista centrum, lista e, lista sW, lista s, lista sE) Lista[] mall = ny lista[9] nW, n, nE, w, centrum, e, sW, s, sE; returnera ny lista[4] [] RotateLocalRange (mall, 0), RotateLocalRange (mall, 1), RotateLocalRange (mall, 2), RotateLocalRange (mall, 3);  statisk statisk lista[] RotateLocalRange (Lista[] localRange, int rotations) Lista[] roteradLista = ny lista[9] [LocalRange [0], LocalRange [1], LocalRange [2], LocalRange [3], LocalRange [4], LocalRange [5], LocalRange [6], LocalRange [7], LocalRange [8]; för (int i = 0; i < rotations; i++)  List[] tempList = ny lista[9] [RoteradLista [6], RoteradLista [3], RoteradLista [0], RoteradLista [7], RoteradLista [4], RoteradLista [1], RoteradLista [8], RoteradLista [5], RoteradLista [2]; roteradLista = tempList;  returnera roterad lista; 

Det är värt att förklara genomförandet av denna kod. I DefineTilemask, Jag ger nio listor som argument. Dessa listor placeras i en temporär 1D-array och roteras sedan i + 90 ° steg genom att skriva till en ny array i en annan ordning. De roterade flaskorna lagras sedan i en skarp array, vars struktur jag använder för att förmedla rotationsinformation. Om tilemask vid yttre index 0 matchar, placeras kakan utan rotation. En match på yttre index 1 ger kakeln a + 90 ° rotation, och så vidare.


Ta tag i en kakel s lokala räckvidd

Den här är enkel. Den läser det lokala intervallet av nuvarande kakel i en 1D teckenuppsättning, och ersätter eventuella ogiltiga index med understreck.

 / * Användning: char [] localRange = GetLocalRange (ritning, rad, kolumn); Blueprint är 2D-serien som definierar byggnaden. rad och kolumn är arrayindexen för den aktuella plattan utvärderad. * / public static char [] GetLocalRange (char [,] thisArray, int rad, int kolumn) char [] localRange = new char [9]; int localRangeCounter = 0; // Iteratorer börjar räkna från -1 för att kompensera uppläsningen och till vänster, placera det efterfrågade indexet i mitten. för (inti = -1; i < 2; i++)  for (int j = -1; j < 2; j++)  int tempRow = row + i; int tempColumn = column + j; if (IsIndexValid (thisArray, tempRow, tempColumn) == true)  localRange[localRangeCounter] = thisArray[tempRow, tempColumn];  else  localRange[localRangeCounter] = '_';  localRangeCounter++;   return localRange;  public static bool IsIndexValid (char[,] thisArray, int row, int column)  // Must check the number of rows at this point, or else an OutOfRange exception gets thrown when checking number of columns. if (row < thisArray.GetLowerBound (0) || row > (thisArray.GetUpperBound (0))) returnera false; om (kolumn < thisArray.GetLowerBound (1) || column > (thisArray.GetUpperBound (1))) returnera false; annars återvända sant; 

Utvärdera en lokal räckvidd med en kakelmask

Och här är där magiken händer! TrySpawningTile ges det lokala sortimentet, en kakelmask, väggstycket att gissa om kakelmaskningen matchar, och raden och kolonnen på kakeln utvärderas.

Viktigt är att metoden som utför den faktiska jämförelsen mellan lokalområdet och kakelmasken (TileMatchesTemplate) dumpar en kakelmaskervridning så snart den finner en felaktig matchning. Inte visat är grundläggande logik som definierar vilka kakelmaskar som ska användas för vilka kakelstycken (du skulle inte använda en väggplattmask på en möbel, till exempel).

 / * Användning: TrySpawningTile (localRange, TileIDs.outerWallStraight, outerWallWall, floorEdgingHalf, rad, kolumn); * / // Dessa kvaternioner har en -90 rotation längs X eftersom modellerna måste // roteras i enhet på grund av den olika uppåtgående axeln i Blender. offentliga statiska readonly Quaternion ROTATE_NONE = Quaternion.Euler (-90, 0, 0); offentliga statiska readonly Quaternion ROTATE_RIGHT = Quaternion.Euler (-90, 90, 0); offentliga statiska readonly Quaternion ROTATE_FLIP = Quaternion.Euler (-90, 180, 0); offentliga statiska readonly Quaternion ROTATE_LEFT = Quaternion.Euler (-90, -90, 0); bool TrySpawningTile (char [] needleArray, List[] [] templateArray, GameObject kakelPrefab, int rad, int kolumn) Quaternion horizontalRotation; om (TileMatchesTemplate (needleArray, templateArray, out horizontalRotation) == true) SpawnTile (kakelPrefab, rad, kolumn, horizontalRotation); återvänd sant;  annars return false;  offentliga statiska bool TileMatchesTemplate (char [] needleArray, List[] [] tileMaskJaggedArray, ut Quaternion horizontalRotation) horizontalRotation = ROTATE_NONE; för (int i = 0; i < (tileMaskJaggedArray.Length); i++)  for (int j = 0; j < 9; j++)  if (j == 4) continue; // Skip checking the centre position (no need to ascertain that a block is what it says it is). if (tileMaskJaggedArray[i][j].Count != 0)  if (tileMaskJaggedArray[i][j].Contains (needleArray[j]) == false) break;  if (j == 8) // The loop has iterated nine times without stopping, so all tiles must match.  switch (i)  case 0: horizontalRotation = ROTATE_NONE; break; case 1: horizontalRotation = ROTATE_RIGHT; break; case 2: horizontalRotation = ROTATE_FLIP; break; case 3: horizontalRotation = ROTATE_LEFT; break;  return true;    return false;  void SpawnTile (GameObject tilePrefab, int row, int column, Quaternion horizontalRotation)  Instantiate (tilePrefab, new Vector3 (column, 0, -row), horizontalRotation); 

Bedömning och slutsats

Fördelar med denna implementering

  • Mycket skalbar; lägg bara till fler kakel masker definitioner.
  • Kontrollerna som en kakelmask utför kan vara så komplex som du vill.
  • Koden är ganska transparent och dess hastighet kan förbättras genom att utföra några extra kontroller på lokalområdet innan du börjar gå igenom kakelmaskar.

Nackdelar med denna implementering

  • Kan potentiellt vara mycket dyrt. I absolut värsta fall kommer du att ha (36 × n) + 1 array accesses och samtal till List.Contains () innan du hittar en match, var n är antalet kakelmaskinsdefinitioner du söker igenom. Det är viktigt att göra vad du kan för att begränsa listan över kakelmaskar ner innan du börjar söka.

Slutsats

Tile masker har användningar inte bara i världens generations eller estetik, men också i element som påverkar spel. Det är inte svårt att föreställa sig ett pusselspel där kakelmaskar kan användas för att bestämma styrelsens tillstånd eller de potentiella rörelserna i bitar, eller redigeringsverktyg kan använda ett liknande system för att knäppa block till varandra. Denna artikel visade en grundläggande implementering av idén, och jag hoppas att du tyckte att den var användbar.