Modèles procéduraux à utiliser avec Tilemaps, partie 2

Dans la première partie, nous avons vu comment créer des couches supérieures de manière procédurale à l'aide de différentes méthodes, comme le bruit de Perlin et la marche aléatoire. Dans cet article, nous allons examiner quelques-unes des façons de créer des grottes par génération procédurale, ce qui devrait vous donner une idée des variations possibles.
Tout ce dont nous allons parler dans cet article de blog est disponible dans ce projet. N'hésitez pas à télécharger les ressources et à essayer les algorithmes procéduraux.

Cet article de blog respecte les mêmes règles que la partie I. Pour rappel, ces règles sont :
- La façon dont nous distinguons le fait d'être une tuile ou non est d'utiliser le langage binaire. 1 étant activé et 0 étant désactivé.
- Nous stockons toutes nos cartes dans un tableau d'entiers 2D, qui est renvoyé à l'utilisateur à la fin de chaque fonction (sauf lors du rendu).
- J'utiliserai la fonction de tableau GetUpperBound() pour obtenir la hauteur et la largeur de la carte. Cela signifie qu'il y a moins de variables dans chaque fonction, ce qui permet d'avoir un code plus propre.
- J'utilise souvent Mathf.FloorToInt(), car le système de coordonnées de la carte commence en bas à gauche et l'utilisation de Mathf.FloorToInt() nous permet d'arrondir les nombres à un entier.
- Tout le code fourni dans ce billet de blog est en C#.
Dans l'article précédent, nous avons vu comment utiliser le bruit de Perlin pour créer des couches supérieures. Heureusement, nous pouvons également utiliser le bruit de Perlin pour créer une grotte. Pour ce faire, nous obtenons une nouvelle valeur de bruit de Perlin, qui prend en compte les paramètres de notre position actuelle multipliés par un modificateur. Le modificateur est une valeur comprise entre 0 et 1. Plus la valeur du modificateur est élevée, plus la génération de Perlin est désordonnée. Nous arrondissons ensuite cette valeur à un nombre entier de 0 ou de 1, que nous stockons dans le tableau map. Jetez un coup d'œil à la mise en œuvre :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
La raison pour laquelle nous utilisons un modificateur au lieu d'une graine est que les résultats de la génération Perlin sont meilleurs lorsque nous multiplions les valeurs par un nombre compris entre 0 et 0,5. Plus la valeur est faible, plus le résultat est figé. Jetez un coup d'œil à certains des résultats. Ce gif commence par une valeur de modificateur de 0,01 et va jusqu'à 0,25 par incréments.

Sur cette image, vous pouvez voir que la génération de Perlin ne fait qu'agrandir le motif à chaque tic-tac.
Dans l'article de blog précédent, nous avons vu que nous pouvons utiliser une pièce de monnaie pour déterminer si la plateforme va monter ou descendre. Dans ce billet, nous allons utiliser la même idée, mais avec deux options supplémentaires pour la gauche et la droite. Cette variante de l'algorithme de marche aléatoire permet de créer des grottes. Pour ce faire, nous obtenons une direction aléatoire, puis nous déplaçons notre position et retirons la tuile. Nous poursuivons ce processus jusqu'à ce que nous ayons atteint le nombre de sols à détruire. Pour l'instant, nous n'utilisons que 4 directions : haut, bas, gauche, droite.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Nous commençons la fonction par :
Trouver notre position de départ
Calculer le nombre de carreaux de sol à enlever
Retrait de la tuile à la position de départ
Ajout d'un étage à notre compteur
Nous passons ensuite à la boucle while. Cela créera la grotte pour nous :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Tout d'abord, nous décidons de la direction à prendre en utilisant un nombre aléatoire. Ensuite, nous vérifions la nouvelle direction à l'aide d'une instruction "switch case". Dans cette déclaration, nous vérifions si la position est un mur. Si ce n'est pas le cas, nous retirons le morceau de tuile du tableau. Nous continuons ainsi jusqu'à ce que nous atteignions le montant plancher requis. Le résultat final est illustré ci-dessous :

J'ai également créé une version personnalisée de cette fonction, qui inclut également les directions diagonales. Le code de cette fonction est un peu long, donc si vous voulez le voir, veuillez consulter le lien vers le projet au début de ce billet de blog.
Un tunnel directionnel commence à une extrémité de la carte et se dirige vers l'autre extrémité. Nous pouvons contrôler la courbe et la rugosité du tunnel en les entrant dans la fonction. Nous pouvons également déterminer la longueur minimale et maximale des parties du tunnel. Examinons la mise en œuvre ci-dessous :
public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness)
{
//Dans ce cas, avec une valeur de largeur de 1, la largeur du tunnel est de 3.
int tunnelWidth = 1 ;
//Set the start X position to the center of the tunnel (Définir la position X de départ au centre du tunnel)
int x = map.GetUpperBound(0) / 2;
//Set up our random with the seed
System.Random rand = new System.Random(Time.time.GetHashCode()) ;
/Créer la première partie du tunnel
for (int i = -tunnelWidth ; i <= tunnelWidth ; i++)
{
map[x + i, 0] = 0 ;
}
Nous commençons par définir une valeur de largeur. Cette valeur de largeur passera de sa contrepartie négative à sa valeur positive. Nous obtiendrons ainsi la taille réelle souhaitée. Dans ce cas, nous utilisons la valeur 1. Cela nous donnera une largeur totale de 3, car nous utiliserons les valeurs -1, 0, 1.
La prochaine chose à faire est de définir la position x de départ, en prenant le milieu de la largeur de la carte. Maintenant que nous avons mis en place ces deux premières valeurs, nous pouvons creuser le tunnel de la première partie de la carte.

Passons maintenant à la création du reste de la carte.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Générer un nombre aléatoire pour le comparer à notre valeur de rugosité, s'il est supérieur à la valeur, nous pouvons modifier la largeur du chemin. Nous vérifions également si la largeur n'est pas trop faible. Avec ce bout de code, nous nous frayons un chemin à travers la carte, en creusant des tunnels au fur et à mesure que nous avançons. À chaque étape, nous procédons comme suit :
Générer un nouveau nombre aléatoire pour le comparer à la valeur de notre courbe. Comme pour la vérification précédente, si elle est supérieure à la valeur, nous pouvons modifier le point central de la trajectoire. Nous vérifions également que nous ne sortons pas des limites de la carte.
Enfin, nous creusons le tunnel de la nouvelle section que nous avons créée.
Le résultat final de cette mise en œuvre est le suivant :

Les automates cellulaires utilisent un voisinage de cellules pour déterminer si la cellule actuelle est activée (1) ou désactivée (0). La base de ces quartiers utilise une grille de cellules générée de manière aléatoire. Dans notre cas, nous allons générer cette grille initiale à l'aide de la fonction Random.Next en C#.
Comme nous avons plusieurs implémentations différentes de Cellular Automata, j'ai créé une fonction distincte pour générer cette grille de base. La fonction se présente comme suit :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Dans cette fonction, nous pouvons également déterminer si nous voulons des murs sur notre grille. Pour le reste, c'est relativement simple. Nous comparons un nombre aléatoire à notre pourcentage de remplissage pour déterminer si la cellule actuelle est activée ou désactivée. Jetez un coup d'œil au résultat :

Le voisinage de Moore est utilisé pour aider à lisser la génération initiale d'automates cellulaires. Le quartier Moore se présente comme suit :

Les règles du quartier sont les suivantes :
- Vérifier dans chaque direction s'il y a un voisin.
- Si un voisin est une tuile active, ajoutez-en une aux tuiles qui l'entourent.
- Si un voisin n'est pas une tuile active, ne rien faire.
- Si la cellule est entourée de plus de 4 tuiles, elle devient une tuile active.
- Si la cellule a exactement 4 tuiles environnantes, laissez la tuile tranquille.
- Répétez l'opération jusqu'à ce que vous ayez essayé toutes les tuiles de la carte.
La fonction de vérification du voisinage de Moore est la suivante :
static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls)
{
/* Le voisinage de Moore ressemble à ceci ('T' est notre tuile, 'N' nos voisins)
*
* N N N
* N T N
* N N N
*
*/
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))
{
//Nous ne voulons pas compter la tuile dont nous vérifions l'environnement
if(neighbourX != x || neighbourY != y)
{
tileCount += map[neighbourX, neighbourY] ;
}
}
}
}
return tileCount ;
}
Après avoir vérifié notre tuile, nous procédons à l'utilisation des informations dans notre fonction de lissage. De nouveau, comme pour la génération initiale d'automates cellulaires, nous pouvons déterminer si les bords de la carte sont 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)))
{
//Définir l'arête comme étant un mur si l'option edgesAreWalls est vraie
map[x, y] = 1 ;
}
//La règle de Moore par défaut nécessite plus de 4 voisins
else if (surroundingTiles > 4)
{
map[x, y] = 1 ;
}
else if (surroundingTiles < 4)
{
map[x, y] = 0 ;
}
}
}
}
/Retourner la carte modifiée
retourner la carte ;
}
Un élément clé à noter dans cette fonction est le fait que nous disposons d'une boucle for pour parcourir la carte un certain nombre de fois. Le résultat est une carte plus belle.

Nous pourrions toujours modifier cet algorithme en reliant les chambres entre elles si, par exemple, il n'y a que 2 blocs entre elles.
Le voisinage de von Neumann est une autre méthode de mise en œuvre populaire pour les automates cellulaires. Pour cette génération, nous utilisons un voisinage plus simple que celui utilisé pour la génération Moore. Le quartier ressemble à ceci :

Les règles pour ce quartier sont les suivantes :
- Vérifier le pourtour de la tuile jusqu'aux voisins directs, à l'exclusion des diagonales.
- Si la cellule est active, nous ajoutons un à notre compte.
- Si la cellule est inactive, ne rien faire.
- Si nous avons plus de deux voisins, la cellule actuelle est active.
- Si nous avons moins de 2 voisins, nous rendons la cellule actuelle inactive.
- Si nous avons exactement 2 voisins, ne modifions pas la cellule actuelle.
Le deuxième résultat reprend les mêmes principes que le premier, mais en élargissant la zone de voisinage.
Nous vérifions la présence des voisins à l'aide de la fonction suivante :
static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls)
{
/* Le voisinage de von Neumann ressemble à ceci ('T' est notre tuile, 'N' est notre voisin)
*
* N
* N T N
* N
*
*/
int tileCount = 0 ;
//Conserver les bords comme des murs
if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1)))
{
tileCount++;
}
//s'assurer que nous ne touchons pas le côté gauche de la carte
if(x - 1 > 0)
{
tileCount += map[x - 1, y] ;
}
//s'assurer que nous ne touchons pas le fond de la carte
si(y - 1 > 0)
{
tileCount += map[x, y - 1] ;
}
//s'assurer que nous ne touchons pas le côté droit de la carte
if(x + 1 < map.GetUpperBound(0))
{
tileCount += map[x + 1, y] ;
}
//s'assurer que nous ne touchons pas le haut de la carte
if(y + 1 < map.GetUpperBound(1))
{
tileCount += map[x, y + 1] ;
}
return tileCount ;
}
Une fois que nous avons obtenu le résultat du nombre de voisins, nous pouvons passer au lissage du tableau. Comme précédemment, nous disposons d'une boucle for pour itérer à travers le lissage pour la quantité requise introduite.
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++)
{
//Imprimez les tuiles environnantes
int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls) ;
if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1)))
{
//Conserver nos bords comme des murs
map[x, y] = 1 ;
}
//Le voisinage de von Neuemann ne nécessite que 3 tuiles environnantes ou plus pour être transformé en une tuile.
else if (surroundingTiles > 2)
{
map[x, y] = 1 ;
}
else if (surroundingTiles < 2)
{
map[x, y] = 0 ;
}
}
}
}
/Retourner la carte modifiée
retourner la carte ;
}
Le résultat final est beaucoup plus complexe que le quartier de Moore, comme on peut le voir ci-dessous :

Là encore, comme pour le quartier Moore, nous pourrions faire en sorte qu'un script supplémentaire soit exécuté en plus de la génération afin d'améliorer les connexions entre les différentes zones de la carte.
J'espère vous avoir donné envie d'utiliser une forme ou une autre de génération procédurale dans vos projets. Si vous n'avez pas encore téléchargé le projet, vous pouvez le faire ici. Si vous souhaitez en savoir plus sur la génération procédurale de cartes, consultez le Wiki de la génération procédurale ou Roguebasin.com, qui sont tous deux d'excellentes ressources.
Si vous réalisez quelque chose de sympa en utilisant la génération procédurale, n'hésitez pas à me laisser un message sur Twitter ou à laisser un commentaire ci-dessous !
Vous souhaitez en savoir plus et obtenir une démonstration en direct ? Je parlerai également des Procedural Patterns à utiliser avec les Tilemaps à Unite Berlin, dans le mini-théâtre du hall d'exposition le 20 juin. Je serai dans les parages après la conférence si vous souhaitez discuter en personne !
