Usare le maschere di tessere per impostare un tipo di tessera in base alle sue tessere circostanti

Utilizzato comunemente nei giochi basati su tessere, le maschere di tessere consentono di modificare una tessera in base ai suoi vicini, consentendo di fondere terreni, sostituire tessere e altro. In questo tutorial, ti mostrerò un metodo scalabile e riutilizzabile per scoprire se i vicini immediati di una tessera corrispondono a un qualsiasi numero di pattern che hai impostato.

Nota: Sebbene questo tutorial sia scritto usando C # e Unity, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Cos'è una maschera per piastrelle?

Considera quanto segue: stai facendo un terrario e vuoi che i blocchi di sporcizia si trasformino in blocchi di fango se si trovano vicino all'acqua. Supponiamo che il tuo mondo abbia molta più acqua di quella sporca, quindi il modo più economico che puoi fare è controllare ogni blocco di sporcizia per vedere se è vicino all'acqua, e non viceversa. Questo è un buon momento per usare a maschera di tessere.

Le maschere di tessere sono vecchie come le montagne e si basano su un'idea semplice. In una griglia di oggetti 2D, una tessera può avere otto altre tessere direttamente adiacenti ad essa. Chiameremo questo il gamma locale di quella tessera centrale (Figura 1).


Figura 1. L'intervallo locale di una tessera.

Per decidere cosa fare con la tua tessera sporca, dovrai confrontare la sua gamma locale con una serie di regole. Ad esempio, potresti guardare direttamente sopra il blocco di sporcizia e vedere se c'è acqua lì (Figura 2). Il set di regole che utilizzi per valutare il tuo raggio d'azione locale è tuo maschera di tessere.


Figura 2. Maschera per piastrelle per l'identificazione di un blocco di sporco come blocco di fango. Le tessere grigie possono essere qualsiasi altra tessera.

Ovviamente vorrete anche controllare l'acqua nelle altre direzioni, quindi la maggior parte delle maschere di tessere ha più di una disposizione spaziale (Figura 3).


Figura 3. I quattro orientamenti tipici di una maschera per piastrelle: 0 °, 90 °, 180 °, 270 °.

E a volte scoprirai che persino le semplici maschere di tessere possono essere specchiate e non sovrapponibili (Figura 4).


Figura 4. Un esempio di chiralità in una maschera di riquadri. La riga superiore non può essere ruotata per corrispondere alla riga inferiore.

Strutture di archiviazione di una maschera per piastrelle

Memorizzare i suoi elementi

Ogni cella di una maschera tessera ha un elemento al suo interno. L'elemento in quella cella viene confrontato con l'elemento corrispondente nell'intervallo locale per vedere se corrispondono.

Uso gli elenchi come elementi. Liste mi consente di eseguire confronti di complessità arbitraria, e Linq di C # fornisce metodi molto utili per unire liste in elenchi più grandi, riducendo il numero di modifiche che posso potenzialmente dimenticare di fare.

Ad esempio, un elenco di piastrelle da parete può essere combinato con un elenco di piastrelle per pavimenti e un elenco di piastrelle di apertura (porte e finestre) per produrre un elenco di tessere strutturali, oppure è possibile combinare elenchi di piastrelle di mobili da singole stanze per creare una lista di tutti i mobili. Altri giochi potrebbero avere liste di tessere che possono essere bruciate o influenzate dalla gravità.

Conservare la stessa maschera per piastrelle

Il modo intuitivo per memorizzare una maschera di riquadri è come un array 2D, ma l'accesso agli array 2D può essere lento e lo farai molto. Sappiamo che un intervallo locale e una maschera tessera saranno sempre composti da nove elementi, quindi possiamo evitare quella penalità temporale usando un semplice array 1D, leggendo l'intervallo locale in esso dall'alto verso il basso, da sinistra a destra (Figura 4).


Figura 5. Memorizzazione di una maschera di tessere 2D in una struttura 1D.

Le relazioni spaziali tra gli elementi vengono mantenute se ne hai mai bisogno (non ne ho avuto bisogno fino ad ora), e gli array possono memorizzare praticamente qualsiasi cosa. I Tilemasks in questo formato possono anche essere facilmente ruotati mediante offset / scrittura.

Memorizzazione di informazioni rotazionali

In alcuni casi è possibile ruotare la piastrella centrale in base all'ambiente circostante, ad esempio quando si posiziona una parete accanto alle altre pareti. Mi piace usare gli array frastagliati per mantenere le quattro rotazioni di una maschera di tessere nello stesso punto, usando l'indice dell'array esterno per indicare la rotazione corrente (più su quello nel codice).


Requisiti del codice

Con le nozioni di base, possiamo delineare ciò che il codice deve fare:

  1. Definire e memorizzare maschere di tessere.
  2. Ruota automaticamente le maschere di tile, invece di dover mantenere manualmente quattro copie ruotate della stessa definizione.
  3. Prendi l'intervallo locale di una tessera.
  4. Valuta l'intervallo locale contro una maschera di riquadri.

Il seguente codice è scritto in C # per Unity, ma i concetti dovrebbero essere abbastanza portabili. L'esempio è uno dei miei lavori di conversione procedurale delle sole mappe di solo testo in 3D (posizionando una sezione diritta del muro, in questo caso).


Definire, ruotare e memorizzare le maschere di tessere

Io automatizzo tutto questo con una chiamata al metodo a DefineTilemask metodo. Ecco un esempio di utilizzo, con la dichiarazione del metodo di seguito.

 Elenco statico pubblico di sola lettura any = new List() ; Elenco statico pubblico di sola lettura ignorato = nuova lista() ", '_'; lista pubblica statica di sola lettura wall = new List() '#', 'D', 'W'; elenco statico pubblico[] [] outerWallStraight = MapHelper.DefineTilemask (any, ignorato, any, wall, any, wall, any, any, any);

Definisco la tilemask nella sua forma non ruotata. La lista chiamata ignorato memorizza caratteri che non descrivono nulla nel mio programma: spazi, che vengono saltati; e underscore, che uso per mostrare un indice di array non valido. Una tessera a (0,0) (angolo in alto a sinistra) di un array 2D non avrà nulla al suo Nord o Ovest, per esempio, quindi il suo range locale ottiene invece underscore. qualunqueè una lista vuota che viene sempre valutata come una corrispondenza positiva.

 elenco statico pubblico[] [] DefineTilemask (Lista nW, Elenco n, Elenco nE, Elenco w, Elenco centro, elenco e, Elenco sW, Elenco s, Elenco sE) Lista[] template = new List[9] nW, n, nE, w, center, e, sW, s, sE; ritorna nuova lista[4] [] RotateLocalRange (template, 0), RotateLocalRange (template, 1), RotateLocalRange (template, 2), RotateLocalRange (template, 3);  Elenco statico pubblico[] RotateLocalRange (Elenco[] localRange, int rotations) List[] rotatedList = new List[9] localRange [0], localRange [1], localRange [2], localRange [3], localRange [4], localRange [5], localRange [6], localRange [7], localRange [8]; per (int i = 0; i < rotations; i++)  List[] tempList = new List[9] rotatedList [6], rotatedList [3], rotatedList [0], rotatedList [7], rotatedList [4], rotatedList [1], rotatedList [8], rotatedList [5], rotatedList [2]; rotatedList = tempList;  return rotatedList; 

Vale la pena spiegare l'implementazione di questo codice. Nel DefineTilemask, Fornisco nove liste come argomenti. Questi elenchi vengono inseriti in un array 1D temporaneo e quindi ruotati in incrementi di + 90 ° scrivendo su un nuovo array in un ordine diverso. I tilemaschi ruotati vengono quindi memorizzati in una matrice frastagliata, la cui struttura viene utilizzata per trasmettere informazioni rotazionali. Se la tilemask dell'indice esterno 0 corrisponde, la tessera viene posizionata senza rotazione. Una corrispondenza all'indice esterno 1 conferisce alla tessera una rotazione di + 90 ° e così via.


Prendi l'intervallo locale di una tessera

Questo è semplice. Legge l'intervallo locale del riquadro corrente in un array di caratteri 1D, sostituendo qualsiasi indice non valido con caratteri di sottolineatura.

 / * Utilizzo: char [] localRange = GetLocalRange (blueprint, row, column); il progetto è l'array 2D che definisce l'edificio. riga e colonna sono gli indici di matrice della piastrella corrente che viene valutata. * / public static char [] GetLocalRange (char [,] thisArray, riga int, colonna int) char [] localRange = new char [9]; int localRangeCounter = 0; // Gli iteratori iniziano a contare da -1 per spostare la lettura verso l'alto e verso sinistra, posizionando l'indice richiesto al centro. per (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))) restituisce falso; se (colonna < thisArray.GetLowerBound (1) || column > (thisArray.GetUpperBound (1))) restituisce false; altrimenti restituisce vero; 

Valuta un intervallo locale con una maschera affiancata

Ed ecco dove avviene la magia! TrySpawningTile viene assegnato l'intervallo locale, una maschera tessera, il pezzo di muro da generare se la maschera della tessera corrisponde e la riga e la colonna della piastrella da valutare.

È importante sottolineare che il metodo che esegue il confronto effettivo tra intervallo locale e maschera di tile (TileMatchesTemplate) esegue il dump di una rotazione della maschera di tile non appena trova una mancata corrispondenza. Non mostrato è la logica di base che definisce quali maschere di tessere utilizzare per quali tipi di tessere (ad esempio, non si utilizzerebbe una maschera tessera su un mobile).

 / * Utilizzo: TrySpawningTile (localRange, TileIDs.outerWallStraight, outerWallWall, floorEdgingHalf, row, column); * / // Questi quaternioni hanno una rotazione di -90 lungo X perché i modelli devono essere // ruotati in Unity a causa del diverso asse su 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, List[] [] templateArray, GameObject tilePrefab, int row, int column) Quaternion horizontalRotation; if (TileMatchesTemplate (needleArray, templateArray, out horizontalRotation) == true) SpawnTile (tilePrefab, row, column, horizontalRotation); ritorna vero;  else return false;  public static bool TileMatchesTemplate (char [] needleArray, List[] [] tileMaskJaggedArray, out Quaternion horizontalRotation) horizontalRotation = ROTATE_NONE; per (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); 

Valutazione e Conclusione

Vantaggi di questa implementazione

  • Molto scalabile; aggiungi solo più definizioni di maschere di tile.
  • I controlli eseguiti da una maschera di riquadri possono essere complessi come preferisci.
  • Il codice è abbastanza trasparente e la sua velocità può essere migliorata eseguendo alcuni controlli extra nell'intervallo locale prima di iniziare a passare attraverso le maschere di tile.

Svantaggi di questa implementazione

  • Può essere potenzialmente molto costoso. Nel peggiore dei casi avrai (36 × n) + 1 accesso a array e chiamate a List.Contains () prima di trovare una partita, dove n è il numero di definizioni di maschere di tile che stai cercando. È fondamentale fare tutto il possibile per restringere la lista delle maschere di piastrelle prima di iniziare la ricerca.

Conclusione

Le maschere di tessere hanno usi non solo nella generazione o nell'estetica mondiale, ma anche negli elementi che influenzano il gameplay. Non è difficile immaginare un puzzle game in cui le maschere di tessere possano essere utilizzate per determinare lo stato della scacchiera o le potenziali mosse di pezzi, o gli strumenti di modifica potrebbero utilizzare un sistema simile per far scattare blocchi l'uno all'altro. Questo articolo ha dimostrato un'implementazione di base dell'idea e spero che tu l'abbia trovato utile.