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:
- Nous distinguons ce qu'est une tuile et ce qui n'utilise pas de valeurs binaires. 1 est une tuile, 0 est son absence.
- 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é).
- 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.
- 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.
- 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) {
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)
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;
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) {
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.
Le lissage s'effectue comme suit:
- Nous obtenons la position actuelle et la dernière
- 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
- 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.
- Ensuite, nous commençons à définir des positions, allant jusqu'à zéro
- 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
- 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 {
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) {
Random Walk Top avec anti-aliasingUne 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) {
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;
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) {
La fonction commence par ce qui suit:
- Trouver la position de départ
- Calculez le nombre de carreaux de sol Ă supprimer.
- Supprimer la tuile dans la position de départ
- Ajoutez-en une au nombre de tuiles.
Ensuite, nous passons Ă la
while
. Il va créer une grotte:
while (floorCount < reqFloorAmount) {
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) {
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.
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:
- 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.
- 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) {
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) { 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)) {
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))) {
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) { int tileCount = 0;
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 for
pour 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++) {
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 .