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.
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).
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.
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).
E a volte scoprirai che persino le semplici maschere di tessere possono essere specchiate e non sovrapponibili (Figura 4).
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à.
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).
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.
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).
Con le nozioni di base, possiamo delineare ciò che il codice deve fare:
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).
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 letturaany = 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.
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;
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);
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.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.