Bruke Tile Masks å sette en flis type basert på sine omgivende fliser

Vanlig brukt i fliser basert spill, gjør fliser masker deg endre en flis avhengig av naboene, slik at du kan blande terreng, erstatte fliser og mer. I denne opplæringen vil jeg vise deg en skalerbar, gjenbrukbar metode for å oppdage om en flis nærmeste naboer samsvarer med et hvilket som helst antall mønstre du har satt inn.

Merk: Selv om denne opplæringen er skrevet med C # og Unity, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.


Hva er en flisemask?

Tenk på følgende: du lager en Terraria-lignende, og du vil at smussblokker skal bli til slamblokker hvis de er ved siden av vann. La oss anta at verden din har mye mer vann enn det gjør smuss, så den billigste måten du kan gjøre dette på, er å sjekke hver smussblokk for å se om den er ved siden av vann, og ikke omvendt. Dette er en ganske god tid å bruke a fliser maske.

Tile masker er like gamle som fjellene, og er basert på en enkel ide. I et 2D-rutenett av objekter kan en flis ha åtte andre fliser rett ved siden av den. Vi ringer dette til lokal rekkevidde av den sentrale flisen (figur 1).


Figur 1. Lokal rekkevidde av en flis.

For å bestemme hva du skal gjøre med smussflisen din, sammenligner du lokalområdet med et sett med regler. For eksempel kan du se direkte over smussblokken og se om det er vann der (figur 2). Sett med regler du bruker til å evaluere ditt lokale utvalg er din fliser maske.


Figur 2. Tile maske for å identifisere en smuss blokk som en slam blokk. De grå fliser kan være andre fliser.

Selvfølgelig vil du også sjekke for vann i de andre retningene, så de fleste fliser masker har mer enn ett romlig arrangement (figur 3).


Figur 3. De fire typiske orienteringene til en flisemaske: 0 °, 90 °, 180 °, 270 °.

Og noen ganger vil du oppdage at selv veldig enkle fliser-maskearrangementer kan speiles og ikke-superposable (figur 4).


Figur 4. Et eksempel på kiralitet i en flisemaske. Toppraden kan ikke roteres for å matche den nederste raden.

Lagring Strukturer av en flisemask

Lagre sine elementer

Hver celle i en flisemaske har et element inne i det. Elementet i den cellen er sammenlignet med det tilsvarende elementet i det lokale området for å se om de samsvarer.

Jeg bruker Lister som elementer. Lister tillater meg å utføre sammenligninger av vilkårlig kompleksitet, og C # 's Linq gir svært nyttige måter å slå sammen lister i større lister, og redusere antallet endringer som jeg potensielt kan glemme å gjøre.

For eksempel kan en liste over veggfliser kombineres med en liste med gulvfliser og en liste over åpningstegler (dører og vinduer) for å lage en liste over bygningsfliser, eller lister over møblerte fliser fra individuelle rom kan kombineres for å lage en liste over alle møbler. Andre spill kan ha lister over fliser som er brennbare eller påvirket av tyngdekraften.

Lagring av flisemasken selv

Den intuitive måten å lagre en fliser på, er som en 2D-serie, men tilgang til 2D-arrays kan være sakte, og du skal gjøre det mye. Vi vet at et lokalt område og en flisemaske alltid vil bestå av ni elementer, slik at vi kan unngå tidsbegrensning ved å bruke en vanlig 1D-rekkefølge, og leser det lokale området inn fra topp til bunn, fra venstre til høyre (figur 4).


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

De romlige forhold mellom elementene blir bevart hvis du noen gang trenger dem (jeg har ikke trengte dem så langt), og arrays kan lagre stort sett alt. Tilemasks i dette formatet kan også enkelt roteres av offset read / writes.

Lagring av rotasjonsinformasjon

I noen tilfeller kan du rotere sentralflisen i henhold til omgivelsene, for eksempel når du plasserer en vegg ved siden av andre vegger. Jeg liker å bruke skarpe arrays for å holde de fire rotasjonene til en flisermaske på samme sted, ved hjelp av den ytre array-indeksen for å indikere den nåværende rotasjonen (mer om det i koden).


Kode Krav

Med det grunnleggende forklart, kan vi skissere hva koden må gjøre:

  1. Definer og lagre fliser masker.
  2. Roter fliser masker automatisk, i stedet for at vi må manuelt opprettholde fire roterte kopier av samme definisjon.
  3. Ta tak i en flis lokal rekkevidde.
  4. Vurder det lokale området mot en flisemaske.

Følgende kode er skrevet i C # for Unity, men konseptene skal være ganske bærbare. Eksemplet er en fra mitt eget arbeid i prosedyrisk konvertering av roguelike tekst-bare kart til 3D (plasserer en rett del av veggen, i dette tilfellet).


Definer, roter og lagre flisemaskene

Jeg automatiserer alt dette med en metodeanrop til en DefineTilemask metode. Her er et eksempel på bruk, med metoden deklarasjon nedenfor.

 offentlig statisk readonly liste noen = ny liste() ; offentlig statisk readonly liste ignorert = ny liste() ", '_', statisk, statisk, lesbar liste vegg = ny liste() '#', 'D', 'W'; offentlig statisk liste[] [] outerWallStraight = MapHelper.DefineTilemask (noen, ignorert, hvilken som helst, vegg, hvilken som helst, vegg, hvilken som helst, hvilken som helst);

Jeg definerer tilemask i sin uroterte form. Listen ringte ignorert lagrer tegn som ikke beskriver noe i mitt program: mellomrom som hoppes over; og understreker, som jeg bruker til å vise en ugyldig arrayindeks. En flis på (0,0) (øverste venstre hjørne) av et 2D-array vil ikke ha noe til Nord eller Vest, for eksempel, slik at lokalområdet får understreker der i stedet. noener en tom liste som alltid vurderes som en positiv kamp.

 offentlig statisk liste[] [] DefinerTilemask (Liste nW, Liste n, Liste nE, Liste w, liste senter, liste e, liste sW, Liste s, liste sE) Liste[] mal = ny liste[9] nW, n, nE, w, senter, e, sW, s, sE; returner ny liste[4] [] RotateLocalRange (template, 0), RotateLocalRange (template, 1), RotateLocalRange (template, 2), RotateLocalRange (template, 3);  offentlig statisk liste[] RotateLocalRange (Liste[] localRange, int rotasjoner) Liste[] rotertListe = ny liste[9] [LocalRange [0], LocalRange [1], LocalRange [2], LocalRange [3], LocalRange [4], LocalRange [5], LocalRange [6], LocalRange [7], LocalRange [8]; for (int i = 0; i < rotations; i++)  List[] tempList = ny liste[9] [rotertListe [6], rotertListe [3], rotertListe [0], rotertListe [7], rotertListe [4], rotertListe [1], rotertListe [8], rotertListe [5], rotertListe [2]; rotertListe = tempList;  tilbake rotertListe; 

Det er verdt å forklare implementeringen av denne koden. I DefineTilemask, Jeg gir ni lister som argumenter. Disse lister er plassert i et midlertidig 1D-array, og deretter rotert i + 90 ° trinn ved å skrive til et nytt array i en annen rekkefølge. De roterte tilemaskene lagres deretter i et skråt array, hvis struktur jeg bruker til å formidle rotasjonsinformasjon. Hvis tilemask på ytre indeks 0 stemmer, blir flisen plassert uten rotasjon. En kamp på ytre indeks 1 gir flisen a + 90 ° rotasjon, og så videre.


Ta tak i et flis lokalområde

Denne er grei. Den leser det lokale spekteret av gjeldende fliser i en 1D-tegnserie, og erstatter eventuelle ugyldige indekser med understreker.

 / * Bruk: char [] localRange = GetLocalRange (blåkopi, rad, kolonne); Blåkopi er 2D-arrayet som definerer bygningen. rad og kolonne er matriseindeksene for gjeldende fliser evaluert. * / offentlig statisk char [] GetLocalRange (char [,] thisArray, int rad, int kolonne) char [] localRange = new char [9]; int localRangeCounter = 0; // Iteratorer begynner å telle fra -1 for å kompensere lesingen og til venstre, og plassere den etterspurte indeksen i sentrum. for (int i = -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))) return false; hvis (kolonne < thisArray.GetLowerBound (1) || column > (thisArray.GetUpperBound (1))) returnere false; ellers returnere sant; 

Vurder en lokal rekkevidde med en flisemask

Og her er hvor magien skjer! TrySpawningTile Gis det lokale området, en flisermaske, veggstykket å gyte hvis flisemasken passer, og raden og kolonnen til flisen blir evaluert.

Viktig er at metoden som utfører den faktiske sammenligningen mellom lokal rekkevidde og flisermaske (TileMatchesTemplate) dumper en flismaskerotasjon så snart den finner en feilstilling. Ikke vist er grunnleggende logikk som definerer hvilke fliser masker som skal brukes for hvilke fliser typer (du vil ikke bruke en vegg fliser maske på et møbel, for eksempel).

 / * Bruk: TrySpawningTile (localRange, TileIDs.outerWallStraight, outerWallWall, floorEdgingHalf, rad, kolonne); * / // Disse quaternions har en -90 rotasjon langs X fordi modeller må // roteres i enhet på grunn av den forskjellige oppaksen i Blender. offentlig statisk readonly Quaternion ROTATE_NONE = Quaternion.Euler (-90, 0, 0); offentlig statisk readonly Quaternion ROTATE_RIGHT = Quaternion.Euler (-90, 90, 0); offentlig statisk readonly Quaternion ROTATE_FLIP = Quaternion.Euler (-90, 180, 0); offentlig statisk readonly Quaternion ROTATE_LEFT = Quaternion.Euler (-90, -90, 0); bool TrySpawningTile (char [] needleArray, List[] [] templateArray, GameObject tilePrefab, int rad, int kolonne) Quaternion horizontalRotation; hvis (TileMatchesTemplate (needleArray, templateArray, ut horizontalRotation) == true) SpawnTile (flisebelegg, rad, kolonne, horizontalRotation); returnere sant;  ellers return false;  offentlig statisk bool TileMatchesTemplate (char [] needleArray, List[] [] tileMaskJaggedArray, ut Quaternion horizontalRotation) horizontalRotation = ROTATE_NONE; for (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); 

Vurdering og konklusjon

Fordeler med denne gjennomføringen

  • Veldig skalerbar; bare legg til flere fliser masker definisjoner.
  • Kontrollene som en flisemaske utfører kan være så kompleks som du vil.
  • Koden er ganske gjennomsiktig, og hastigheten kan forbedres ved å utføre noen ekstra kontroller på lokalområdet før du begynner å gå gjennom fliser.

Ulemper ved denne gjennomføringen

  • Kan potensielt være veldig dyrt. I absolutt verste fall vil du ha (36 × n) + 1 array tilgang og samtaler til List.Contains () før du finner en kamp, ​​hvor n er antall fliser maske definisjoner du søker gjennom. Det er viktig å gjøre det du kan for å begrense listen over fliser masker ned før du begynner å søke.

Konklusjon

Tile masker har bruk ikke bare i verdens generasjon eller estetikk, men også i elementer som påvirker gameplay. Det er ikke vanskelig å forestille seg et puslespill der fliser masker kan brukes til å bestemme styrets tilstand eller potensielle bevegelser av stykker, eller redigeringsverktøy kan bruke et lignende system for å knipe blokker til hverandre. Denne artikkelen viste en grunnleggende implementering av ideen, og jeg håper du fant det nyttig.