Motifs procéduraux pouvant être utilisés avec des cartes tuile

La génération procédurale est utilisée pour augmenter la variabilité des jeux. Des projets bien connus incluent Minecraft , Enter the Gungeon et Descenders . Dans cet article, je vais expliquer certains des algorithmes qui peuvent être utilisés lorsque vous travaillez avec le système Tilemap , qui est apparu comme une fonction 2D dans Unity 2017.2, et avec RuleTile .

Avec la création procédurale de cartes, chaque jeu de passes sera unique. Vous pouvez utiliser diverses données d'entrée, par exemple l'heure ou le niveau actuel du joueur, pour modifier dynamiquement le contenu même après l'assemblage du jeu.

De quoi parle ce post?


Nous examinerons certaines des façons les plus courantes de créer des mondes procéduraux, ainsi que quelques variantes que j'ai créées. Voici un exemple de ce que vous pouvez créer après avoir lu l'article. Trois algorithmes fonctionnent ensemble pour créer une carte à l'aide de Tilemap et RuleTile :


Dans le processus de génération d'une carte à l'aide de n'importe quel algorithme, nous obtenons un tableau int contenant toutes les nouvelles données. Vous pouvez continuer à modifier ces données ou à les rendre sur une carte de tuiles.

Avant de poursuivre la lecture, il serait bon de savoir ce qui suit:

  1. Nous distinguons ce qu'est une tuile et ce qui n'utilise pas de valeurs binaires. 1 est une tuile, 0 est son absence.
  2. Nous stockerons toutes les cartes dans un tableau entier bidimensionnel retourné à l'utilisateur à la fin de chaque fonction (sauf celle où le rendu est effectué).
  3. J'utiliserai la fonction de tableau GetUpperBound () pour obtenir la hauteur et la largeur de chaque carte, afin que la fonction reçoive moins de variables et que le code soit plus propre.
  4. J'utilise souvent Mathf.FloorToInt () , car le système de coordonnées Tilemap commence en bas à gauche et Mathf.FloorToInt () vous permet d'arrondir les nombres à un entier.
  5. Tout le code de cet article est Ă©crit en C #.

Génération de baies


GenerateArray crée un nouveau tableau int de la taille donnée. Nous pouvons également indiquer si le tableau doit être rempli ou vide (1 ou 0). Voici le code:

 public static int[,] GenerateArray(int width, int height, bool empty) { int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (empty) { map[x, y] = 0; } else { map[x, y] = 1; } } } return map; } 

Rendu de carte


Cette fonction est utilisée pour rendre une carte sur une carte de tuiles. Nous effectuons une boucle autour de la largeur et de la hauteur de la carte, en plaçant des tuiles uniquement lorsque le tableau au point testé a une valeur de 1.

 public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile) { //Clear the map (ensures we dont overlap) tilemap.ClearAllTiles(); //Loop through the width of the map for (int x = 0; x < map.GetUpperBound(0) ; x++) { //Loop through the height of the map for (int y = 0; y < map.GetUpperBound(1); y++) { // 1 = tile, 0 = no tile if (map[x, y] == 1) { tilemap.SetTile(new Vector3Int(x, y, 0), tile); } } } } 

Mise Ă  jour de la carte


Cette fonction est utilisée uniquement pour la mise à jour de la carte et non pour le nouveau rendu. Grâce à cela, nous pouvons utiliser moins de ressources sans redessiner chaque tuile et ses données de tuile.

 public static void UpdateMap(int[,] map, Tilemap tilemap) //Takes in our map and tilemap, setting null tiles where needed { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //We are only going to update the map, rather than rendering again //This is because it uses less resources to update tiles to null //As opposed to re-drawing every single tile (and collision data) if (map[x, y] == 0) { tilemap.SetTile(new Vector3Int(x, y, 0), null); } } } } 

Perlin de bruit


Le bruit Perlin peut être utilisé à diverses fins. Tout d'abord, nous pouvons l'utiliser pour créer la couche supérieure de notre carte. Pour ce faire, obtenez simplement un nouveau point en utilisant la position actuelle x et la graine.

Solution simple


Cette méthode de génération utilise la forme la plus simple de réalisation du bruit Perlin dans la génération de niveaux. Nous pouvons prendre la fonction Unity pour le bruit Perlin afin de ne pas écrire le code nous-mêmes. Nous utiliserons également uniquement des entiers pour la carte de tuiles, en utilisant la fonction Mathf.FloorToInt () .

 public static int[,] PerlinNoise(int[,] map, float seed) { int newPoint; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Create the Perlin for (int x = 0; x < map.GetUpperBound(0); x++) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1)); //Make sure the noise starts near the halfway point of the height newPoint += (map.GetUpperBound(1) / 2); for (int y = newPoint; y >= 0; y--) { map[x, y] = 1; } } return map; } 

Voici à quoi cela ressemble après le rendu sur une carte de tuiles:


Lissage


Vous pouvez également prendre cette fonction et la lisser. Définissez des intervalles pour fixer les hauteurs de Perlin, puis effectuez un lissage entre ces points. Cette fonction se révélera un peu plus compliquée, car pour les intervalles, vous devez considérer les listes de valeurs entières.

 public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval) { //Smooth the noise and store it in the int array if (interval > 1) { int newPoint, points; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Used in the smoothing process Vector2Int currentPos, lastPos; //The corresponding points of the smoothing. One list for x and one for y List<int> noiseX = new List<int>(); List<int> noiseY = new List<int>(); //Generate the noise for (int x = 0; x < map.GetUpperBound(0); x += interval) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, (seed * reduction))) * map.GetUpperBound(1)); noiseY.Add(newPoint); noiseX.Add(x); } points = noiseY.Count; 

Dans la première partie de cette fonction, nous vérifions d'abord si l'intervalle est supérieur à un. Si oui, générez du bruit. La génération est effectuée à intervalles afin que le lissage puisse être appliqué. La partie suivante de la fonction consiste à lisser les points.

 //Start at 1 so we have a previous position already for (int i = 1; i < points; i++) { //Get the current position currentPos = new Vector2Int(noiseX[i], noiseY[i]); //Also get the last position lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]); //Find the difference between the two Vector2 diff = currentPos - lastPos; //Set up what the height change value will be float heightChange = diff.y / interval; //Determine the current height float currHeight = lastPos.y; //Work our way through from the last x to the current x for (int x = lastPos.x; x < currentPos.x; x++) { for (int y = Mathf.FloorToInt(currHeight); y > 0; y--) { map[x, y] = 1; } currHeight += heightChange; } } } 

Le lissage s'effectue comme suit:

  1. Nous obtenons la position actuelle et la dernière
  2. Nous obtenons la différence entre deux points, l'information la plus importante dont nous avons besoin est la différence le long de l'axe y
  3. Ensuite, nous déterminons combien le changement doit être fait pour arriver au point, cela se fait en divisant la différence en y par la variable d'intervalle.
  4. Ensuite, nous commençons à définir des positions, allant jusqu'à zéro
  5. Lorsque nous atteignons 0 sur l'axe y, ajoutez le changement de hauteur à la hauteur actuelle et répétez le processus pour la position x suivante
  6. En terminant avec chaque position entre la dernière et la position actuelle, nous passons au point suivant

Si l'intervalle est inférieur à un, nous utilisons simplement la fonction précédente, qui fera tout le travail pour nous.

  else { //Defaults to a normal Perlin gen map = PerlinNoise(map, seed); } return map; 

Jetons un coup d'Ĺ“il au rendu:


Marche aléatoire


Haut de marche aléatoire


Cet algorithme effectue un lancer de pièce. Nous pouvons obtenir l'un des deux résultats. Si le résultat est "aigle", alors nous remontons d'un bloc, si le résultat est "queues", alors nous déplaçons le bloc vers le bas. Cela crée des hauteurs en se déplaçant constamment vers le haut ou vers le bas. Le seul inconvénient d'un tel algorithme est son encombrement très notable. Voyons comment cela fonctionne.

 public static int[,] RandomWalkTop(int[,] map, float seed) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Set our starting height int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Cycle through our width for (int x = 0; x < map.GetUpperBound(0); x++) { //Flip a coin int nextMove = rand.Next(2); //If heads, and we aren't near the bottom, minus some height if (nextMove == 0 && lastHeight > 2) { lastHeight--; } //If tails, and we aren't near the top, add some height else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2) { lastHeight++; } //Circle through from the lastheight to the bottom for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the map return map; } 


Random Walk Top avec anti-aliasing

Une telle génération nous donne des hauteurs plus douces par rapport à la génération de bruit Perlin.

Cette variation de Random Walk fournit un résultat beaucoup plus fluide par rapport à la version précédente. Nous pouvons l'implémenter en ajoutant deux autres variables à la fonction:

  • La première variable est utilisĂ©e pour dĂ©terminer combien de temps il faut pour maintenir la hauteur actuelle. Il est entier et est rĂ©initialisĂ© lorsque la hauteur change
  • La deuxième variable est entrĂ©e dans la fonction et est utilisĂ©e comme largeur de section minimale pour la hauteur. Cela deviendra plus clair lorsque nous examinerons la fonction.

Nous savons maintenant quoi ajouter. Jetons un coup d'Ĺ“il Ă  la fonction:

 public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Determine the start position int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Used to determine which direction to go int nextMove = 0; //Used to keep track of the current sections width int sectionWidth = 0; //Work through the array width for (int x = 0; x <= map.GetUpperBound(0); x++) { //Determine the next move nextMove = rand.Next(2); //Only change the height if we have used the current height more than the minimum required section width if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth) { lastHeight--; sectionWidth = 0; } else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth) { lastHeight++; sectionWidth = 0; } //Increment the section width sectionWidth++; //Work our way from the height down to 0 for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the modified map return map; } 

Comme vous pouvez le voir dans le gif ci-dessous, le lissage de l'algorithme de marche aléatoire vous permet d'obtenir de beaux segments plats au niveau.


Conclusion


J'espère que cet article vous inspirera à utiliser la génération procédurale dans vos projets. Si vous souhaitez en savoir plus sur les cartes générées de manière procédurale, explorez les excellentes ressources du wiki de génération procédurale ou de Roguebasin.com .

Dans la deuxième partie de l'article, nous utiliserons la génération procédurale pour créer des systèmes de grottes.

2e partie


Tout ce dont nous discuterons dans cette partie se trouve dans ce projet . Vous pouvez télécharger des ressources et essayer vos propres algorithmes procéduraux.


Perlin de bruit


Dans la partie précédente, nous avons examiné les moyens d'appliquer le bruit Perlin pour créer des couches supérieures. Heureusement, le bruit de Perlin peut également être utilisé pour créer une grotte. Ceci est réalisé en calculant la nouvelle valeur de bruit Perlin, qui reçoit les paramètres de la position actuelle multipliés par le modificateur. Le modificateur est une valeur de 0 à 1. Plus la valeur du modificateur est élevée, plus la génération Perlin est chaotique. Ensuite, nous arrondissons cette valeur à l'entier (0 ou 1), que nous stockons dans le tableau de cartes. Voyez comment cela est mis en œuvre:

 public static int[,] PerlinNoiseCave(int[,] map, float modifier, bool edgesAreWalls) { int newPoint; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (edgesAreWalls && (x == 0 || y == 0 || x == map.GetUpperBound(0) - 1 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; //Keep the edges as walls } else { //Generate a new point using Perlin noise, then round it to a value of either 0 or 1 newPoint = Mathf.RoundToInt(Mathf.PerlinNoise(x * modifier, y * modifier)); map[x, y] = newPoint; } } } return map; } 

Nous utilisons le modificateur au lieu de la graine car les résultats de la génération de Perlin sont meilleurs lorsqu'ils sont multipliés par un nombre compris entre 0 et 0,5. Plus la valeur est basse, plus le résultat sera polyédrique. Jetez un oeil à des exemples de résultats. Gif commence avec une valeur de modificateur de 0,01 et atteint progressivement une valeur de 0,25.


De ce gif, on peut voir que la génération Perlin avec chaque incrément augmente simplement le modèle.

Marche aléatoire


Dans la partie précédente, nous avons vu que vous pouvez utiliser un tirage au sort pour déterminer où la plate-forme se déplacera vers le haut ou vers le bas. Dans cette partie, nous utiliserons la même idée, mais
avec deux options supplémentaires pour le décalage gauche et droit. Cette variation de l'algorithme Random Walk nous permet de créer des grottes. Pour ce faire, nous sélectionnons une direction aléatoire, puis déplaçons notre position et supprimons la tuile. Nous continuons ce processus jusqu'à ce que nous atteignions le nombre requis de tuiles qui doivent être détruites. Jusqu'à présent, nous n'utilisons que 4 directions: haut, bas, gauche, droite.

 public static int[,] RandomWalkCave(int[,] map, float seed, int requiredFloorPercent) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Define our start x position int floorX = rand.Next(1, map.GetUpperBound(0) - 1); //Define our start y position int floorY = rand.Next(1, map.GetUpperBound(1) - 1); //Determine our required floorAmount int reqFloorAmount = ((map.GetUpperBound(1) * map.GetUpperBound(0)) * requiredFloorPercent) / 100; //Used for our while loop, when this reaches our reqFloorAmount we will stop tunneling int floorCount = 0; //Set our start position to not be a tile (0 = no tile, 1 = tile) map[floorX, floorY] = 0; //Increase our floor count floorCount++; 

La fonction commence par ce qui suit:

  1. Trouver la position de départ
  2. Calculez le nombre de carreaux de sol Ă  supprimer.
  3. Supprimer la tuile dans la position de départ
  4. Ajoutez-en une au nombre de tuiles.

Ensuite, nous passons à la while . Il va créer une grotte:

 while (floorCount < reqFloorAmount) { //Determine our next direction int randDir = rand.Next(4); switch (randDir) { //Up case 0: //Ensure that the edges are still tiles if ((floorY + 1) < map.GetUpperBound(1) - 1) { //Move the y up one floorY++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase floor count floorCount++; } } break; //Down case 1: //Ensure that the edges are still tiles if ((floorY - 1) > 1) { //Move the y down one floorY--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Right case 2: //Ensure that the edges are still tiles if ((floorX + 1) < map.GetUpperBound(0) - 1) { //Move the x to the right floorX++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Left case 3: //Ensure that the edges are still tiles if ((floorX - 1) > 1) { //Move the x to the left floorX--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; } } //Return the updated map return map; } 

Que faisons-nous ici?


Eh bien, tout d'abord, à l'aide d'un nombre aléatoire, nous choisissons la direction à suivre. Ensuite, nous vérifions la nouvelle direction avec l' switch case . Dans cette déclaration, nous vérifions si la position est un mur. Sinon, supprimez l'élément avec la tuile du tableau. Nous continuons à le faire jusqu'à ce que nous atteignions la surface de plancher souhaitée. Le résultat est présenté ci-dessous:


J'ai également créé ma propre version de cette fonction, qui comprend également des directions diagonales. Le code de la fonction est assez long, donc si vous voulez le regarder, téléchargez le projet à partir du lien au début de cette partie de l'article.

Tunnel directionnel


Un tunnel directionnel commence à un bord de la carte et atteint le bord opposé. Nous pouvons contrôler la courbure et la rugosité du tunnel en les passant à la fonction d'entrée. Nous pouvons également définir la longueur minimale et maximale des parties du tunnel. Jetons un coup d'œil à la mise en œuvre:

 public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness) { //This value goes from its minus counterpart to its positive value, in this case with a width value of 1, the width of the tunnel is 3 int tunnelWidth = 1; //Set the start X position to the center of the tunnel int x = map.GetUpperBound(0) / 2; //Set up our random with the seed System.Random rand = new System.Random(Time.time.GetHashCode()); //Create the first part of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, 0] = 0; } 

Que se passe-t-il?


Nous définissons d'abord la valeur de la largeur. La valeur de largeur passera de la valeur avec un moins à un positif. Grâce à cela, nous obtiendrons la taille dont nous avons besoin. Dans ce cas, nous utilisons la valeur 1, qui à son tour nous donnera une largeur totale de 3, car nous utilisons les valeurs -1, 0, 1.

Ensuite, nous définissons la position initiale en x, pour cela nous prenons le milieu de la largeur de la carte. Après cela, nous pouvons poser un tunnel dans la première partie de la carte.


Passons maintenant au reste de la carte.

  //Cycle through the array for (int y = 1; y < map.GetUpperBound(1); y++) { //Check if we can change the roughness if (rand.Next(0, 100) > roughness) { //Get the amount we will change for the width int widthChange = Random.Range(-maxPathWidth, maxPathWidth); //Add it to our tunnel width value tunnelWidth += widthChange; //Check to see we arent making the path too small if (tunnelWidth < minPathWidth) { tunnelWidth = minPathWidth; } //Check that the path width isnt over our maximum if (tunnelWidth > maxPathWidth) { tunnelWidth = maxPathWidth; } } //Check if we can change the curve if (rand.Next(0, 100) > curvyness) { //Get the amount we will change for the x position int xChange = Random.Range(-maxPathChange, maxPathChange); //Add it to our x value x += xChange; //Check we arent too close to the left side of the map if (x < maxPathWidth) { x = maxPathWidth; } //Check we arent too close to the right side of the map if (x > (map.GetUpperBound(0) - maxPathWidth)) { x = map.GetUpperBound(0) - maxPathWidth; } } //Work through the width of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, y] = 0; } } return map; } 

Nous générons un nombre aléatoire pour la comparaison avec la valeur de rugosité, et s'il est supérieur à cette valeur, alors la largeur du chemin peut être modifiée. Nous vérifions également la valeur afin de ne pas rendre la largeur trop petite. Dans la partie suivante du code, nous parcourons la carte. À chaque étape, les événements suivants se produisent:

  1. Nous générons un nouveau nombre aléatoire par rapport à la valeur de courbure. Comme dans le test précédent, si elle est supérieure à la valeur, nous changeons le point central du chemin. Nous effectuons également un contrôle afin de ne pas dépasser la carte.
  2. Enfin, nous posons un tunnel dans la partie nouvellement créée.

Les résultats de cette implémentation ressemblent à ceci:


Automates cellulaires


Les automates cellulaires utilisent des cellules voisines pour déterminer si la cellule actuelle est activée (1) ou désactivée (0). La base de détermination des cellules voisines est créée sur la base d'une grille de cellules générée aléatoirement. Nous allons générer cette grille source à l'aide de la fonction C # Random.Next .

Puisque nous avons quelques implémentations différentes d'automates cellulaires, j'ai écrit une fonction distincte pour générer cette grille de base. La fonction ressemble à ceci:

 public static int[,] GenerateCellularAutomata(int width, int height, float seed, int fillPercent, bool edgesAreWalls) { //Seed our random number generator System.Random rand = new System.Random(seed.GetHashCode()); //Initialise the map int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //If we have the edges set to be walls, ensure the cell is set to on (1) if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; } else { //Randomly generate the grid map[x, y] = (rand.Next(0, 100) < fillPercent) ? 1 : 0; } } } return map; } 

Dans cette fonction, vous pouvez également définir si notre grille a besoin de murs. À tous les autres égards, c'est assez simple. Nous vérifions un nombre aléatoire avec un pourcentage de remplissage pour déterminer si la cellule actuelle est activée. Jetez un œil au résultat:


Le quartier de Moore


Le quartier Moore est utilisé pour lisser la génération initiale d'automates cellulaires. Le quartier de Moore ressemble à ceci:


Les règles suivantes s'appliquent au quartier:

  • Nous vĂ©rifions le voisin dans chacune des directions.
  • Si le voisin est une tuile active, ajoutez-en une au nombre de tuiles environnantes.
  • Si le voisin est une tuile inactive, nous ne faisons rien.
  • Si une cellule a plus de 4 tuiles environnantes, activez-la.
  • Si la cellule a exactement 4 tuiles environnantes, nous n'en faisons rien.
  • RĂ©pĂ©tez jusqu'Ă  ce que nous vĂ©rifions chaque tuile de carte.

La fonction de vérification de voisinage de Moore est la suivante:

 static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* Moore Neighbourhood looks like this ('T' is our tile, 'N' is our neighbours) * * NNN * NTN * NNN * */ int tileCount = 0; for(int neighbourX = x - 1; neighbourX <= x + 1; neighbourX++) { for(int neighbourY = y - 1; neighbourY <= y + 1; neighbourY++) { if (neighbourX >= 0 && neighbourX < map.GetUpperBound(0) && neighbourY >= 0 && neighbourY < map.GetUpperBound(1)) { //We don't want to count the tile we are checking the surroundings of if(neighbourX != x || neighbourY != y) { tileCount += map[neighbourX, neighbourY]; } } } } return tileCount; } 

Après avoir vérifié la tuile, nous utilisons ces informations dans la fonction de lissage. Ici, comme dans la génération d'automates cellulaires, on peut indiquer si les bords de la carte doivent être des murs.

 public static int[,] SmoothMooreCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { int surroundingTiles = GetMooreSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == (map.GetUpperBound(0) - 1) || y == 0 || y == (map.GetUpperBound(1) - 1))) { //Set the edge to be a wall if we have edgesAreWalls to be true map[x, y] = 1; } //The default moore rule requires more than 4 neighbours else if (surroundingTiles > 4) { map[x, y] = 1; } else if (surroundingTiles < 4) { map[x, y] = 0; } } } } //Return the modified map return map; } 

Il est important de noter ici que la fonction possède une boucle for qui effectue le lissage le nombre de fois spécifié. Grâce à cela, une plus belle carte est obtenue.


Nous pouvons toujours modifier cet algorithme en connectant des pièces si, par exemple, il n'y a que deux blocs entre elles.

Quartier Von Neumann


Le quartier von Neumann est un autre moyen populaire d'implémenter des automates cellulaires. Pour une telle génération, nous utilisons un voisinage plus simple que dans la génération Moore. Le quartier ressemble à ceci:


Les règles suivantes s'appliquent au quartier:

  • Nous vĂ©rifions les voisins immĂ©diats de la tuile, sans tenir compte des diagonales.
  • Si la cellule est active, ajoutez-en une Ă  la quantitĂ©.
  • Si la cellule est inactive, ne faites rien.
  • Si la cellule a plus de 2 voisins, nous rendons la cellule actuelle active.
  • Si la cellule a moins de 2 voisins, nous rendons la cellule actuelle inactive.
  • S'il y a exactement 2 voisins, ne modifiez pas la cellule actuelle.

Le deuxième résultat utilise les mêmes principes que le premier, mais élargit la zone du quartier.

Nous vérifions les voisins avec la fonction suivante:

 static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* von Neumann Neighbourhood looks like this ('T' is our Tile, 'N' is our Neighbour) * * N * NTN * N * */ int tileCount = 0; //Keep the edges as walls if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1))) { tileCount++; } //Ensure we aren't touching the left side of the map if(x - 1 > 0) { tileCount += map[x - 1, y]; } //Ensure we aren't touching the bottom of the map if(y - 1 > 0) { tileCount += map[x, y - 1]; } //Ensure we aren't touching the right side of the map if(x + 1 < map.GetUpperBound(0)) { tileCount += map[x + 1, y]; } //Ensure we aren't touching the top of the map if(y + 1 < map.GetUpperBound(1)) { tileCount += map[x, y + 1]; } return tileCount; } 

Après avoir reçu le nombre de voisins, nous pouvons procéder au lissage du tableau. Comme précédemment, nous avons besoin d'une boucle forpour terminer le nombre d'itérations de lissage passées à l'entrée.

 public static int[,] SmoothVNCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //Get the surrounding tiles int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1))) { //Keep our edges as walls map[x, y] = 1; } //von Neuemann Neighbourhood requires only 3 or more surrounding tiles to be changed to a tile else if (surroundingTiles > 2) { map[x, y] = 1; } else if (surroundingTiles < 2) { map[x, y] = 0; } } } } //Return the modified map return map; } 

Comme vous pouvez le voir ci-dessous, le résultat final est beaucoup plus polyédrique que le quartier de Moore:


Ici, comme dans les environs de Moore, vous pouvez exécuter un script supplémentaire pour optimiser les connexions entre les parties de la carte.

Conclusion


J'espère que cet article vous inspirera à utiliser une sorte de génération procédurale dans vos projets. Si vous n'avez pas téléchargé le projet, vous pouvez le télécharger ici .

Source: https://habr.com/ru/post/fr471084/


All Articles