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ø.
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).
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.
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).
Og noen ganger vil du oppdage at selv veldig enkle fliser-maskearrangementer kan speiles og ikke-superposable (figur 4).
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.
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).
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.
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).
Med det grunnleggende forklart, kan vi skissere hva koden må gjøre:
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).
Jeg automatiserer alt dette med en metodeanrop til en DefineTilemask
metode. Her er et eksempel på bruk, med metoden deklarasjon nedenfor.
offentlig statisk readonly listenoen = 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. noen
er 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.
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;
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);
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.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.