Tegels gebruiken om het type tegel in te stellen op basis van de omliggende tegels

Veel gebruikt in tegel-gebaseerde spellen, tegelmaskers kunt u een tegel afhankelijk van de buren, zodat u terreinen kunt mengen, tegels vervangen en meer. In deze zelfstudie laat ik je een schaalbare, herbruikbare methode zien om te detecteren of de directe buren van een tegel overeenkomen met een van een willekeurig aantal patronen dat je hebt ingesteld.

Notitie: Hoewel deze tutorial geschreven is met C # en Unity, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Wat is een tegelmasker?

Overweeg het volgende: je maakt een Terraria-achtig uiterlijk en je wilt dat blokken zand in modderblokken veranderen als ze zich naast water bevinden. Laten we aannemen dat jouw wereld veel meer water heeft dan vuil, dus de goedkoopste manier om dit te doen is door elk vuilblok te controleren om te zien of het naast water is, en niet andersom. Dit is een goed moment om een ​​a te gebruiken tegelmasker.

Tegeltableaus zijn zo oud als de bergen en zijn gebaseerd op een eenvoudig idee. In een 2D-raster van objecten kan een tegel acht andere tegels hebben die er direct naast liggen. We zullen dit het noemen lokaal bereik van die centrale tegel (figuur 1).


Figuur 1. Het lokale bereik van een tegel.

Als u wilt beslissen wat u met uw vuiltegel moet doen, vergelijkt u het lokale bereik met een reeks regels. U kunt bijvoorbeeld recht boven het blok vuil kijken en zien of er water is (Figuur 2). De set regels die u gebruikt om uw lokale bereik te evalueren, is uw tegelmasker.


Figuur 2. Tegelmasker voor het identificeren van een vuilblok als een modderblok. De grijze tegels kunnen elke andere tegel zijn.

Natuurlijk wilt u ook in de andere richtingen naar water zoeken, dus de meeste tegeldempers hebben meer dan één ruimtelijke indeling (Figuur 3).


Figuur 3. De vier typische oriëntaties van een tegeldrager: 0 °, 90 °, 180 °, 270 °.

En soms zul je zien dat zelfs hele eenvoudige tegelmasker-arrangementen gespiegeld en niet-superposabel kunnen zijn (figuur 4).


Figuur 4. Een voorbeeld van chiraliteit in een tegeldamasker. De bovenste rij kan niet worden gedraaid om overeen te komen met de onderste rij.

Opslagstructuren van een tegelmasker

De elementen opslaan

Elke cel van een tegelmasker heeft een element erin. Het element in die cel wordt vergeleken met het overeenkomstige element in het lokale bereik om te zien of ze overeenkomen.

Ik gebruik lijsten als elementen. Lijsten stellen me in staat vergelijkingen te maken van willekeurige complexiteit, en C # 's Linq biedt erg handige manieren om lijsten samen te voegen tot grotere lijsten, waardoor het aantal wijzigingen dat ik mogelijk kan vergeten te verkleinen.

Een lijst met wandtegels kan bijvoorbeeld worden gecombineerd met een lijst met vloertegels en een lijst met openende tegels (deuren en ramen) om een ​​lijst met structurele tegels te produceren, of lijsten met meubeltegels uit afzonderlijke kamers kunnen worden gecombineerd om een lijst met alle meubels. Andere spellen hebben mogelijk lijsten met tegels die kunnen worden verbrand of die worden beïnvloed door de zwaartekracht.

Het tegelmasker zelf opslaan

De intuïtieve manier om een ​​tegelmasker op te slaan is als een 2D-array, maar toegang tot 2D-arrays kan traag zijn en u zult het veel doen. We weten dat een lokaal bereik en een tegeldiagrammasker altijd uit negen elementen zullen bestaan, dus we kunnen die tijdwinst vermijden door een gewone 1D-array te gebruiken, waarbij het lokale bereik erin van boven naar beneden, van links naar rechts wordt gelezen (Figuur 4).


Figuur 5. Een 2D-tegeld masker opslaan in een 1D-structuur.

De ruimtelijke relaties tussen elementen worden behouden als je ze ooit nodig hebt (ik heb ze tot nu toe niet nodig), en arrays kunnen vrijwel alles opslaan. Tulamaps in dit formaat kunnen ook eenvoudig worden gedraaid door middel van offset-lees / schrijfbewerkingen.

Roterende informatie opslaan

In sommige gevallen kunt u de centrale tegel naar de omgeving ervan draaien, bijvoorbeeld wanneer u een muur naast andere muren plaatst. Ik gebruik graag gekartelde arrays om de vier rotaties van een tegeldiagrammasker op dezelfde plaats te houden, met behulp van de index van de buitenste array om de huidige rotatie aan te geven (meer hierover in de code).


Code Vereisten

Met de basis uitgelegd, kunnen we beschrijven wat de code moet doen:

  1. Tegelmaskers definiëren en opslaan.
  2. Roteer tegelmaskers automatisch, in plaats van dat u vier geroteerde kopieën van dezelfde definitie handmatig moet onderhouden.
  3. Grijp het lokale bereik van een tegel.
  4. Evalueer het lokale bereik tegen een tegelmasker.

De volgende code is geschreven in C # voor eenheid, maar de concepten moeten redelijk draagbaar zijn. Het voorbeeld is een van mijn eigen werk in het procedureel converteren van roguelike tekst-alleen kaarten naar 3D (het plaatsen van een recht deel van de muur, in dit geval).


Definieer, roteer en bewaar de tegelmaskers

Ik automatiseer dit allemaal met één methode aanroep naar a DefineTilemask methode. Hier is een voorbeeld van gebruik, met onderstaande methodeaangifte.

 openbare statische alleen-lezen lijst any = nieuwe lijst() ; openbare statische alleen-lezen lijst genegeerd = nieuwe lijst() ", '_'; openbare statische alleen-lezen lijst muur = nieuwe lijst() '#', 'D', 'W'; openbare statische lijst[] [] outerWallStraight = MapHelper.DefineTilemask (ongeacht, genegeerd, any, wall, any, wall, any, any, any);

Ik definieer het tilemask in zijn niet-geroteerde vorm. De lijst heeft gebeld genegeerd slaat tekens op die niets in mijn programma beschrijven: spaties die worden overgeslagen; en underscores, die ik gebruik om een ​​ongeldige array-index weer te geven. Een tegel op (0,0) (linkerbovenhoek) van een 2D-array heeft bijvoorbeeld niets in het noorden of westen, dus het lokale bereik krijgt daar onderstrepingstekens. iederis een lege lijst die altijd wordt geëvalueerd als een positieve match.

 openbare statische lijst[] [] DefineTilemask (lijst nW, lijst n, lijst nE, lijst w, lijst midden, lijst e, lijst sW, lijst s, lijst sE) Lijst[] template = nieuwe lijst[9] nW, n, nE, w, center, e, sW, s, sE; keer nieuwe lijst terug[4] [] RotateLocalRange (sjabloon, 0), RotateLocalRange (sjabloon, 1), RotateLocalRange (sjabloon, 2), RotateLocalRange (sjabloon, 3);  openbare statische lijst[] RotateLocalRange (lijst[] localRange, int rotaties) Lijst[] rotatedList = nieuwe lijst[9] localRange [0], localRange [1], localRange [2], localRange [3], localRange [4], localRange [5], localRange [6], localRange [7], localRange [8]; voor (int i = 0; i < rotations; i++)  List[] tempList = nieuwe lijst[9] geroteerdeLijst [6], geroteerdLijst [3], geroteerdLijst [0], geroteerdLijst [7], geroteerdLijst [4], geroteerdLijst [1], geroteerdLijst [8], geroteerdLijst [5], geroteerdLijst [2]; rotatedList = tempList;  geroteerdeLijst terugkeert; 

Het is de moeite waard om de implementatie van deze code uit te leggen. In DefineTilemask, Ik geef negen lijsten als argumenten. Deze lijsten worden in een tijdelijke 1D-array geplaatst en vervolgens in stappen van + 90 ° geroteerd door in een andere volgorde naar een nieuwe array te schrijven. De geroteerde tilemasks worden vervolgens opgeslagen in een gekartelde array, waarvan ik de structuur gebruik om de rotatie-informatie over te brengen. Als het tilemask op de buitenindex 0 overeenkomt, wordt de tegel zonder rotatie geplaatst. Een overeenkomst op de buitenste index 1 geeft de tegel een rotatie van + 90 °, enzovoort.


Grijp het lokale bereik van een tegel

Deze is eenvoudig. Het leest het lokale bereik van de huidige tegel in een 1D-tekenreeks, waarbij ongeldige indexen worden vervangen door onderstrepingstekens.

 / * Gebruik: char [] localRange = GetLocalRange (blauwdruk, rij, kolom); blueprint is de 2D-array die het gebouw definieert. rij en kolom zijn de array-indices van de huidige tegel die wordt geëvalueerd. * / public static char [] GetLocalRange (char [,] thisArray, int row, int column) char [] localRange = new char [9]; int localRangeCounter = 0; // Iterators beginnen te tellen vanaf -1 om het aflezen en naar links te compenseren, waarbij de gevraagde index in het midden wordt geplaatst. voor (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; if (kolom < thisArray.GetLowerBound (1) || column > (thisArray.GetUpperBound (1))) return false; anders ga terug naar waar; 

Evalueer een lokaal bereik met een tegelmasker

En hier gebeurt de magie! TrySpawningTile krijgt het lokale bereik, een tegelmasker, het wandstuk om te spawnen als het tegeldiagram past en de rij en kolom van de tegel die wordt geëvalueerd.

Belangrijk is de methode die de daadwerkelijke vergelijking tussen het lokale bereik en het tegeldimma uitvoert (TileMatchesTemplate) dumpt een rotatie van het tegeldiagram zodra het een mismatch vindt. Niet weergegeven is basislogica die definieert welke tegeldempings moeten worden gebruikt voor welke tegeltypes (u zou bijvoorbeeld geen wandtegelmasker op een meubelstuk gebruiken).

 / * Gebruik: TrySpawningTile (localRange, TileIDs.outerWallStraight, outerWallWall, floorEdgingHalf, row, column); * / // Deze quaternions hebben een rotatie van -90 langs X omdat modellen moeten worden geroteerd in Unity vanwege de verschillende opwaartse as in Blender. public static readonly Quaternion ROTATE_NONE = Quaternion.Euler (-90, 0, 0); public static readonly Quaternion ROTATE_RIGHT = Quaternion.Euler (-90, 90, 0); public static readonly Quaternion ROTATE_FLIP = Quaternion.Euler (-90, 180, 0); public static readonly Quaternion ROTATE_LEFT = Quaternion.Euler (-90, -90, 0); bool TrySpawningTile (char [] needleArray, lijst[] [] templateArray, GameObject tilePrefab, int row, int column) Quaternion horizontalRotation; if (TileMatchesTemplate (needleArray, templateArray, out horizontalRotation) == true) SpawnTile (tilePrefab, row, column, horizontalRotation); geef waar terug;  else return false;  public static bool TileMatchesTemplate (char [] needleArray, lijst[] [] tileMaskJaggedArray, uit Quaternion horizontalRotation) horizontalRotation = ROTATE_NONE; voor (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); 

Beoordeling en conclusie

Voordelen van deze implementatie

  • Zeer schaalbaar; voeg gewoon de definities van meerdere maskers toe.
  • De controles die een tile-masker uitvoert, kunnen zo complex zijn als u wilt.
  • De code is redelijk transparant en de snelheid kan worden verbeterd door een paar extra controles uit te voeren op het lokale bereik voordat u begint door tegeldmaskers te gaan.

Nadelen van deze implementatie

  • Kan mogelijk erg duur zijn. In het ergste geval heb je (36 × n) + 1 array-toegangen en oproepen naar List.Contains () voor het vinden van een match, waar n is het aantal tegelmaskerdefinities waarnaar u zoekt. Het is essentieel om te doen wat u kunt om de lijst met tegeldmaskers te verkleinen voordat u begint met zoeken.

Conclusie

Tegeltableaus worden niet alleen gebruikt in de wereldgeneratie of esthetiek, maar ook in elementen die van invloed zijn op het spelen van het spel. Het is niet moeilijk je een puzzelspel voor te stellen waarbij tegeldmaskers kunnen worden gebruikt om de status van het bord of de mogelijke bewegingen van stukken te bepalen, of bewerkingstools kunnen een soortgelijk systeem gebruiken om blokken op elkaar te klikken. Dit artikel demonstreerde een basisimplementatie van het idee en ik hoop dat je het nuttig hebt gevonden.