patterns procéduraux avec Tilemaps

De nombreux créateurs ont utilisé la génération procédurale pour ajouter de la diversité à leur jeu. Parmi les mentions notables, citons Minecraft ou, plus récemment, Enter the Gungeon et Descenders. Cet article explique certains des algorithmes que vous pouvez utiliser avec Tilemap, présentés comme une fonctionnalité 2D dans Unity 2017.2.

Avec les maps créées de manière procédurale, vous pouvez vous assurer qu'il n'y a pas deux versions de votre jeu identiques. Vous pouvez utiliser diverses entrées, telles que l'heure ou le niveau actuel du joueur pour vous assurer que le contenu change de façon dynamique même après la création du jeu.

Qu'allons-nous réaliser ?

Nous examinerons certaines des méthodes les plus courantes de création d'un monde procédural, ainsi que quelques variantes personnalisées. Voici un exemple de ce que vous pourrez créer après avoir lu cet article. Trois algorithmes travaillent ensemble pour créer une carte en utilisant un Tilemap et un RuleTile.

exemple de monde créer procéduralement
Exemple de monde créer procéduralement.

Lorsque nous générons une carte avec l'un des algorithmes, nous recevons un tableau int contenant toutes les nouvelles données. Nous pouvons alors prendre ces données et continuer à les modifier ou à les afficher sur un tilemap.

Bon à savoir avant de poursuivre la lecture :

  • La façon dont nous faisons la distinction entre ce qui est un tile et ce qui ne l'est pas est d'utiliser le binaire. 1 étant allumé et 0 étant éteint.
  • Nous allons stocker toutes nos maps dans un tableau d'entiers (int) 2D, qui est retourné à l'utilisateur à la fin de chaque fonction.
  • Nous utiliserons la fonction de tableau GetUpperBound() pour obtenir la hauteur et la largeur de chaque map afin que nous ayons moins de variables entrant dans chaque fonction, et du code plus propre.
  • Nous utilisons souvent Mathf.FloorToInt(), car le système de coordonnées Tilemap commence en bas à gauche et l'utilisation de Mathf.FloorToInt() nous permet d'arrondir les nombres à un nombre entier.
  • Tout le code réalisé ci-dessous est en C#.

Générer un tableau

GenerateArray crée un nouveau tableau int de la taille qui lui est attribuée. Nous pouvons également indiquer si le tableau doit être plein ou vide (1 ou 0).

C#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;
}

Render Map

Cette fonction est utilisée pour afficher notre map. Nous parcourons la largeur et la hauteur de la map, en plaçant uniquement les tiles si le tableau a un 1 à l'endroit que nous vérifions.

C#public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile)
{
    // Efface la map
    tilemap.ClearAllTiles(); 
    // Boucle à travers la largeur de la map
    for (int x = 0; x < map.GetUpperBound(0) ; x++) 
    {
        // Boucle à travers la hauteur de la 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); 
            }
        }
    }
}

Mettre à jour

Cette fonction est utilisée uniquement pour mettre à jour la map, plutôt que de la restituer. De cette façon, nous pouvons utiliser moins de ressources car nous ne redessinons pas chaque tile et ses données de tiles.

C#public static void UpdateMap(int[,] map, Tilemap tilemap)
{
    for (int x = 0; x < map.GetUpperBound(0); x++)
    {
        for (int y = 0; y < map.GetUpperBound(1); y++)
        {
            // Nous allons seulement mettre à jour la map, plutôt que de la restituer
            // C'est parce qu'il utilise moins de ressources pour mettre à jour les tiles à null
            // Par opposition à re-dessiner chaque tile (et les données de collision)
            if (map[x, y] == 0)
            {
                tilemap.SetTile(new Vector3Int(x, y, 0), null);
            }
        }
    }
}

Perlin Noise

Le Perlin Noise peut être utilisé de diverses manières. La première façon de l'utiliser est de créer une couche supérieure pour notre map. C'est aussi simple que d'obtenir un nouveau point en utilisant notre position x actuelle et une graine.

Simple

Cette génération prend la forme la plus simple d'implémentation de Perlin Noise dans la génération de niveau. Nous pouvons utiliser la fonction Unity pour Perlin Noise pour nous aider, donc il n'y a pas de programmation sophistiquée. Nous allons également nous assurer que nous avons des nombres entiers pour notre tilemap en utilisant la fonction Mathf.FloorToInt().

C#public static int[,] PerlinNoise(int[,] map, float seed)
{
    int newPoint;
    // Utilisé pour réduire la position du point Perlin
    float reduction = 0.5f;
    // Créer le Perlin
    for (int x = 0; x < map.GetUpperBound(0); x++)
    {
        newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1));
 
        // Assurez-vous que le noise commence près du point médian de la hauteur
        newPoint += (map.GetUpperBound(1) / 2); 
        for (int y = newPoint; y >= 0; y--)
        {
            map[x, y] = 1;
        }
    }
    return map;
}
rendu sur le tilemap
Rendu sur le tilemap.

Smoothed

Nous pouvons également prendre cette fonction et la lisser. Définir des intervalles pour enregistrer la hauteur Perlin, puis lisser entre les points. Cette fonction finit par être légèrement plus avancée, car nous devons prendre en compte des listes d'entiers pour nos intervalles.

C#public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval)
{
    // Lisse le noise et le stocker dans le tableau int
    if (interval > 1)
    {
        int newPoint, points;
        // Utilisé pour réduire la position du point Perlin
        float reduction = 0.5f;

        // Utilisé dans le processus de lissage
        Vector2Int currentPos, lastPos; 
        // Les points correspondants du lissage. Une liste pour x et une pour y
        List noiseX = new List();
        List noiseY = new List();

        // Génére le 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;

Pour la première partie de cette fonction, nous vérifions d'abord si l'intervalle est supérieur à un. Si c'est le cas, nous générons alors le noise. Nous faisons cela à intervalles pour permettre le lissage. La partie suivante consiste à travailler à lisser les points.

C#// Commence à 1 donc nous avons déjà une position précédente
            for (int i = 1; i < points; i++) 
            {
                // Obtenir la position actuelle
                currentPos = new Vector2Int(noiseX[i], noiseY[i]);
                // Obtenir également la dernière position
                lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]);

                // Trouver la différence entre les deux
                Vector2 diff = currentPos - lastPos;

                // Configurer quelle sera la valeur de changement de hauteur
                float heightChange = diff.y / interval;
                // Détermine la hauteur actuelle
                float currHeight = lastPos.y;

                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 passe par les étapes suivantes :

  • Obtenir la position actuelle et la dernière position.
  • Obtenir la différence entre les deux positions, l'information clé que nous voulons est la différence dans l'axe des y.
  • Ensuite, nous déterminons à quel point nous devrions changer le hit, cela se fait en divisant la différence y par la variable d'intervalle.
  • Maintenant, nous pouvons commencer à régler les positions. On va descendre jusqu'à zéro.
  • Lorsque nous touchons 0 sur l'axe des y, nous ajoutons le changement de hauteur à la hauteur actuelle et répétons le processus pour la prochaine position x.
  • Une fois que nous avons fait toutes les positions entre la dernière position et la position actuelle, nous passons au point suivant.

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

C# else
        {
            // Par défaut : Génération Perlin normale
            map = PerlinNoise(map, seed);
        }

        return map;
résultat du lissage
Résultat du lissage.

Random Walk

Random Walk Top

La façon dont cet algorithme fonctionne est de retourner une pièce de monnaie. Nous obtenons alors l'un des deux résultats. Si le résultat est des têtes, on monte d'un bloc, si le résultat est des tails, on descend d'un bloc.

C#public static int[,] RandomWalkTop(int[,] map, float seed)
{
    // Graine aléatoire
    System.Random rand = new System.Random(seed.GetHashCode()); 

    // Règle notre hauteur de départ
    int lastHeight = Random.Range(0, map.GetUpperBound(1));
        
    // Cycle à travers notre largeur
    for (int x = 0; x < map.GetUpperBound(0); x++) 
    {
        int nextMove = rand.Next(2);

        // Si heads, et nous ne sommes pas près du fond, moins une certaine hauteur
        if (nextMove == 0 && lastHeight > 2) 
        {
            lastHeight--;
        }
        // Si tails, et que nous ne sommes pas près du sommet, ajoutez de la hauteur
        else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2) 
        {
            lastHeight++;
        }

        // Faire un cercle de la dernière hauteur jusqu'au fond
        for (int y = lastHeight; y >= 0; y--) 
        {
            map[x, y] = 1;
        }
    }
    // Retourne la map
    return map; 
}

Cette génération nous donne une hauteur plus lisse par rapport à la génération de Perlin noise.

nouvelle génération
Nouvelle génération.

Random Walk Top Smoothed

Cette variation Random Walk permet une finition beaucoup plus lisse que la version précédente. Nous pouvons le faire en ajoutant deux nouvelles variables à notre fonction. La première variable est utilisée pour déterminer combien de temps nous avons maintenu notre hauteur actuelle. Il s'agit d'un nombre entier qui est réinitialisé lorsque nous changeons la hauteur. La deuxième variable est une entrée pour la fonction et est utilisée comme largeur de section minimale pour la hauteur. Cela aura plus de sens lorsque vous aurez vu la fonction.

C#public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth)
{
    // Graine aléatoire
    System.Random rand = new System.Random(seed.GetHashCode());

    // Détermine la position de départ
    int lastHeight = Random.Range(0, map.GetUpperBound(1));

    // Utilisé pour déterminer dans quelle direction aller
    int nextMove = 0;
    // Utilisé pour suivre la largeur des sections actuelles
    int sectionWidth = 0;

    // Boucle à travers la largeur du tableau
    for (int x = 0; x <= map.GetUpperBound(0); x++)
    {
        // Détermine le prochain mouvement
        nextMove = rand.Next(2);

        if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth)
        {
            lastHeight--;
            sectionWidth = 0;
        }
        else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth)
        {
            lastHeight++;
            sectionWidth = 0;
        }

        // Incrémente la largeur de la section
        sectionWidth++;

        // De la hauteur jusqu'à 0
        for (int y = lastHeight; y >= 0; y--)
        {
            map[x, y] = 1;
        }
    }

    // Retourne la map modifiée
    return map;
}

Comme vous pouvez le voir dans le gif ci-dessous, le lissage de l'algorithme de Random Walk permet d'obtenir de belles pièces plates à l'intérieur du niveau.

résultat de l'algorithme Random Walk
Résultat de l'algorithme Random Walk.

J'espère que cela vous a inspiré pour commencer à utiliser une certaine forme de génération procédurale dans vos projets. Si vous voulez en savoir plus sur les cartes génératrices de procédures, consultez le Wiki Génération procédurale ou Roguebasin.com, qui sont toutes deux d'excellentes ressources.

les réactions

Pour laisser un avis, vous devez être inscrit et connecté

Se connecter