Utilisation de masques de mosaïque pour définir le type de mosaïque en fonction des mosaïques environnantes

Habituellement utilisés dans les jeux basés sur des tuiles, les masques de tuiles vous permettent de modifier une tuile en fonction de ses voisins, ce qui vous permet de mélanger des terrains, de remplacer des tuiles, etc. Dans ce tutoriel, je vais vous montrer une méthode évolutive et réutilisable pour détecter si les voisins immédiats d'une tuile correspondent à l'un des nombreux modèles que vous avez définis..

Remarque: Bien que ce tutoriel ait été écrit en C # et Unity, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


Qu'est-ce qu'un masque de tuile?

Considérez ce qui suit: vous faites une sorte de Terraria et vous voulez que les blocs de terre se transforment en blocs de boue s’ils sont près de l’eau. Supposons que votre monde contient beaucoup plus d'eau que de saleté. Le moyen le plus économique de le faire est donc de vérifier chaque bloc de saleté pour voir s'il est à côté de l'eau et non l'inverse. C’est un bon moment pour utiliser un masque de tuile.

Les masques de tuile sont aussi vieux que les montagnes et sont basés sur une idée simple. Dans une grille d'objets 2D, une tuile peut avoir huit autres tuiles directement adjacentes. Nous appellerons cela la gamme locale de cette dalle centrale (Figure 1).


Figure 1. La plage locale d'une tuile.

Pour décider quoi faire de votre tuile de terre, comparez sa plage locale à un ensemble de règles. Par exemple, vous pouvez regarder directement au-dessus du bloc de terre et voir s'il y a de l'eau (Figure 2). L’ensemble de règles que vous utilisez pour évaluer votre plage locale est votre masque de tuile.


Figure 2. Masque en mosaïque permettant d'identifier un bloc de saleté en tant que bloc de boue. Les tuiles grises peuvent être n'importe quelle autre tuile.

Bien sûr, vous souhaiterez également vérifier la présence d’eau dans les autres directions également, de sorte que la plupart des masques en mosaïque ont plus d’une disposition spatiale (Figure 3)..


Figure 3. Les quatre orientations typiques d’un masque de tuile: 0 °, 90 °, 180 °, 270 °.

Et parfois, vous constaterez que même les arrangements de masque de mosaïque très simples peuvent être mis en miroir et non superposables (Figure 4)..


Figure 4. Exemple de chiralité dans un masque de mosaïque. La rangée supérieure ne peut pas être pivotée pour correspondre à la rangée inférieure.

Structures de stockage d'un masque de tuile

Stocker ses éléments

Chaque cellule d'un masque de mosaïque contient un élément. L'élément de cette cellule est comparé à l'élément correspondant dans la plage locale pour voir si elles correspondent.

J'utilise les listes comme des éléments. Les listes me permettent d'effectuer des comparaisons de complexité arbitraire, et Linq de C # fournit des moyens très pratiques de fusion de listes en listes plus grandes, réduisant ainsi le nombre de modifications que je peux potentiellement oublier de faire..

Par exemple, une liste de carreaux muraux peut être combinée à une liste de carreaux de sol et à une liste de carreaux ouvrants (portes et fenêtres) afin de produire une liste de carreaux structurels. Vous pouvez également combiner des listes de carreaux de meubles de pièces individuelles pour créer une liste de tous les meubles. D'autres jeux peuvent avoir des listes de tuiles pouvant être brûlées ou affectées par la gravité.

Stocker le masque de tuile lui-même

Le moyen intuitif de stocker un masque de mosaïque consiste à utiliser un tableau 2D, mais l'accès aux tableaux 2D peut être lent et vous allez le faire beaucoup. Nous savons qu’une plage locale et un masque de mosaïque seront toujours composés de neuf éléments. Nous pouvons donc éviter cette pénalité de temps en utilisant un tableau simple 1D, en lisant la plage locale de haut en bas, de gauche à droite (Figure 4)..


Figure 5. Stockage d'un masque de mosaïque 2D dans une structure 1D.

Les relations spatiales entre les éléments sont préservées si vous en avez besoin (je ne les ai pas utilisées jusqu'à présent), et les tableaux peuvent stocker à peu près n'importe quoi. Les mouchoirs en papier de ce format peuvent également être facilement pivotés par des lectures / écritures décalées.

Stockage des informations de rotation

Dans certains cas, vous pouvez faire pivoter le carreau central en fonction de son environnement, par exemple lorsque vous placez un mur à côté d'autres murs. J'aime utiliser des tableaux dentelés pour tenir les quatre rotations d'un masque de mosaïque au même endroit, en utilisant l'index du tableau extérieur pour indiquer la rotation actuelle (plus de détails dans le code).


Exigences du code

Avec les bases expliquées, nous pouvons décrire ce que le code doit faire:

  1. Définir et stocker les masques de tuiles.
  2. Faire pivoter les masques de mosaïque automatiquement, au lieu de devoir conserver manuellement quatre copies pivotées de la même définition.
  3. Saisir la portée locale d'une tuile.
  4. Évaluer la plage locale par rapport à un masque de mosaïque.

Le code suivant est écrit en C # pour Unity, mais les concepts doivent être relativement portables. Cet exemple est tiré de mon propre travail de conversion procédurale de cartes textuelles roguelike en 3D (placer une section de mur droite, dans ce cas)..


Définir, faire pivoter et stocker les masques de mosaïque

J'automatise tout cela avec un appel de méthode à un DefineTilemask méthode. Voici un exemple d'utilisation, avec déclaration de méthode ci-dessous.

 Liste statique publique en lecture seule any = nouvelle liste() ; Liste statique publique en lecture seule ignoré = nouvelle liste() ", '_'; public statique en lecture seule List mur = nouvelle liste() '#', 'D', 'W'; Liste statique publique[] [] outerWallStraight = MapHelper.DefineTilemask (any, ignored, any, wall, any, wall, any, any, any);

Je définis la tilemask dans sa forme non annotée. La liste appelée ignoré stocke les caractères qui ne décrivent rien dans mon programme: des espaces, qui sont sautés; et underscores, que j’utilise pour afficher un index de tableau non valide. Une tuile située à (0,0) (coin supérieur gauche) d'un tableau 2D n'aura rien au nord ou à l'ouest, par exemple, ainsi sa plage locale sera soulignée à la place.. toutest une liste vide qui est toujours évaluée comme une correspondance positive.

 Liste statique publique[] [] DefineTilemask (List nW, liste n, liste nE, liste w, liste centre, liste e, liste sW, liste s, liste sE) liste[] template = nouvelle liste[9] nW, n, nE, w, centre, e, sW, s, sE; retourne la nouvelle liste[4] [] RotateLocalRange (modèle, 0), RotateLocalRange (modèle, 1), RotateLocalRange (modèle, 2), RotateLocalRange (modèle, 3);  liste statique publique[] RotateLocalRange (List[] localRange, int rotations) List[] rotatedList = nouvelle liste[9] localRange [0], localRange [1], localRange [2], localRange [3], localRange [4], localRange [5], localRange [6], localRange [7], localRange [8]; pour (int i = 0; i < rotations; i++)  List[] tempList = nouvelle liste[9] rotatedList [6], rotatedList [3], rotatedList [0], rotatedList [7], rotatedList [4], rotatedList [1], rotatedList [8], rotatedList [5], rotatedList [1], rotatedList [1], rotatedList [1], rotatedList [4], rotatedList [2], rotatedList [2] rotatedList = tempList;  return rotatedList; 

Cela vaut la peine d'expliquer la mise en œuvre de ce code. Dans DefineTilemask, Je fournis neuf listes comme arguments. Ces listes sont placées dans un tableau 1D temporaire, puis pivotées par étapes de + 90 ° en écrivant dans un nouveau tableau dans un ordre différent. Les tilemasks en rotation sont ensuite stockés dans un tableau dentelé, dont je structure la structure pour transmettre des informations en rotation. Si le masque de masque à l'index externe 0 correspond, le pavé est placé sans rotation. Une correspondance à l'index extérieur 1 donne à la tuile une rotation de + 90 °, etc..


Saisir la portée locale d'une tuile

Celui-ci est simple. Il lit la plage locale de la mosaïque actuelle dans un tableau de caractères 1D, en remplaçant les index non valides par des traits de soulignement..

 / * Utilisation: char [] localRange = GetLocalRange (modèle, ligne, colonne); blueprint est le tableau 2D qui définit le bâtiment. ligne et colonne sont les indices de tableau de la tuile en cours d'évaluation. * / public static char [] GetLocalRange (char [,] thisArray, int row, int column) char [] localRange = new char [9]; int localRangeCounter = 0; // Les itérateurs commencent à compter à partir de -1 pour décaler la lecture vers le haut et à gauche, en plaçant l'index demandé au centre. pour (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; si (colonne < thisArray.GetLowerBound (1) || column > (thisArray.GetUpperBound (1))) renvoie la valeur false; sinon, retourne vrai; 

Évaluer une plage locale avec un masque de tuile

Et voici où la magie opère! TrySpawningTile se voit attribuer la plage locale, un masque de mosaïque, la pièce murale à apparaitre si le masque de mosaïque correspond, ainsi que la ligne et la colonne de la mosaïque en cours d'évaluation.

Il est important de noter que la méthode qui effectue la comparaison réelle entre la plage locale et le masque de mosaïque (TileMatchesTemplate) vide une rotation de masque de mosaïque dès qu’elle détecte une incompatibilité. La logique de base ne définit pas les masques de mosaïque à utiliser pour quels types de mosaïque (vous ne pouvez pas utiliser de masque de mosaïque de mur sur un meuble, par exemple)..

 / * Utilisation: TrySpawningTile (localRange, TileIDs.outerWallStraight, outerWallWall, floorEdgingHalf, ligne, colonne); * / // Ces quaternions ont une rotation de -90 le long de X car les modèles doivent être // tournés dans Unity en raison de l’axe ascendant différent dans Blender. public statique en lecture seule Quaternion ROTATE_NONE = Quaternion.Euler (-90, 0, 0); public statique en lecture seule Quaternion ROTATE_RIGHT = Quaternion.Euler (-90, 90, 0); public statique en lecture seule Quaternion ROTATE_FLIP = Quaternion.Euler (-90, 180, 0); public statique en lecture seule 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); retourne vrai;  else return false;  public statique bool TileMatchesTemplate (char [] needleArray, List[] [] tileMaskJaggedArray, hors Quaternion horizontalRotation) horizontalRotation = ROTATE_NONE; pour (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); 

Bilan et conclusion

Avantages de cette implémentation

  • Très évolutif; il suffit d'ajouter d'autres définitions de masques de tuile.
  • Les vérifications effectuées par un masque de mosaïque peuvent être aussi complexes que vous le souhaitez.
  • Le code est assez transparent et sa vitesse peut être améliorée en effectuant quelques vérifications supplémentaires sur la plage locale avant de commencer à utiliser des masques de mosaïque..

Inconvénients de cette implémentation

  • Peut potentiellement être très coûteux. Dans le pire des cas, vous aurez (36 × n) + 1 accès au tableau et appels vers List.Contains () avant de trouver un match, où n correspond au nombre de définitions de masque de mosaïque dans lesquelles vous effectuez une recherche. Il est essentiel de faire tout ce que vous pouvez pour réduire la liste des masques de mosaïque avant de commencer la recherche..

Conclusion

Les masques de tuile ont des utilisations non seulement dans la génération du monde ou dans l'esthétique, mais aussi dans des éléments qui affectent le gameplay. Il n’est pas difficile d’imaginer un jeu de casse-tête dans lequel des masques de mosaïque pourraient être utilisés pour déterminer l’état du tableau ou les déplacements potentiels de pièces, ou des outils d’édition pouvant utiliser un système similaire pour casser des blocs les uns aux autres. Cet article montre une implémentation de base de l'idée et j'espère que vous l'avez trouvée utile.