Générateur de grottes bidimensionnel aléatoire

Préface


Si vous êtes trop paresseux pour prendre votre temps, faire un niveau pour votre jeu, alors vous êtes au bon endroit.

Cet article vous expliquera en détail comment vous pouvez utiliser l'une des nombreuses autres méthodes de génération en utilisant l'exemple des hautes terres et des grottes. Nous considérerons l'algorithme Aldous-Broder et comment rendre la grotte générée plus belle.

À la fin de la lecture de l'article, vous devriez obtenir quelque chose comme ceci:

Résumé


Théorie


Montagne


Pour être honnête, la grotte peut être générée à partir de zéro, mais sera-t-elle en quelque sorte laide? Dans le rôle de "plate-forme" pour le placement des mines, j'ai choisi une chaîne de montagnes.
Cette montagne est générée tout simplement: ayons un tableau à deux dimensions et une hauteur variable , initialement égale à la moitié de la longueur du tableau dans la deuxième dimension; nous allons simplement parcourir ses colonnes et remplir quelque chose avec toutes les lignes de la colonne à une valeur de hauteur variable , en la changeant avec une chance aléatoire vers le haut ou vers le bas.

Cave


Pour générer les donjons eux-mêmes, j'ai choisi - il me semble - un excellent algorithme. En termes simples, cela peut être expliqué comme suit: même si nous avons deux (peut-être dix) variables X et Y , et un tableau bidimensionnel de 50 par 50, nous donnons à ces variables des valeurs aléatoires dans notre tableau, par exemple, X = 26 et Y = 28 . Après cela, nous faisons les mêmes actions plusieurs fois: nous obtenons un nombre aléatoire de zéro à

Nombredevariables2

, dans notre cas, jusqu'à quatre ; puis, en fonction du nombre d'abandons, nous changeons
nos variables:

switch (Random.Range(0, 4)) { case 0: X += 1; break; case 1: X -= 1; break; case 2: Y += 1; break; case 3: Y -= 1; break; } 

Ensuite, bien sûr, nous vérifions si une variable est tombée en dehors des limites de notre champ:

  X = X < 0 ? 0 : (X >= 50 ? 49 : X); Y = Y < 0 ? 0 : (Y >= 50 ? 49 : Y); 

Après toutes ces vérifications, nous faisons quelque chose dans les nouvelles valeurs X et Y pour notre tableau (par exemple: en ajouter une à l'élément) .

 array[X, Y] += 1; 

La préparation


Pour simplifier la mise en œuvre et la visualisation de nos méthodes, allons-nous dessiner les objets résultants? Je suis tellement content que ça ne te dérange pas! Nous le ferons avec Texture2D .

Pour fonctionner, nous n'avons besoin que de deux scripts:
ground_libray est ce que l'article tournera autour. Ici, nous générons, nettoyons et dessinons
ground_generator est ce que notre ground_libray utilisera
Que le premier soit statique et n'hérite de rien:

 public static class ground_libray 

Et le second est normal, seulement nous n'aurons pas besoin de la méthode Update .

Créons également un objet de jeu sur scène, avec le composant SpriteRenderer

Partie pratique


En quoi cela consiste-t-il?


Pour travailler avec des données, nous utiliserons un tableau à deux dimensions. Vous pouvez prendre un tableau de différents types, d' octet ou d' int , à Color , mais je crois que ce sera mieux fait:

Nouveau type
Nous écrivons cette chose dans ground_libray .

 [System.Serializable] public class block { public float[] color = new float[3]; public block(Color col) { color = new float[3] { col.r, col.g, col.b }; } } 


Je vais expliquer cela par le fait que cela nous permettra à la fois de sauvegarder notre tableau et de le modifier si nécessaire.

Massif


Désignons, avant de commencer à générer la montagne, désigner l'endroit où nous allons la stocker .

Dans le script ground_generator, j'ai écrit ceci:

  public int ground_size = 128; ground_libray.block[,] ground; Texture2D myT; 

ground_size - la taille de notre champ (c'est-à-dire que le tableau sera composé de 16384 éléments).
ground_libray.block [,] ground - c'est notre domaine de génération.
Texture2D myT est ce sur quoi nous allons nous appuyer .

Comment ça va fonctionner?
Le principe de travail avec nous sera le suivant - nous appellerons quelques méthodes ground_libray de ground_generator , donnant au premier notre champ sol .

Créons la première méthode dans le script ground_libray:

Faire de la montagne
  public static float mount_noise = 0.02f; public static void generate_mount(ref block[,] b) { int h_now = b.GetLength(1) / 2; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < h_now; y++) { b[x, y] = new block(new Color(0.7f, 0.4f, 0)); h_now += Random.value > (1.0f - mount_noise) ? (Random.value > 0.5 ? 1 : -1) : 0; } } 

Et tout de suite, nous allons essayer de comprendre ce qui se passe ici: comme je l'ai dit, nous passons simplement sur les colonnes de notre tableau b , en changeant en même temps la variable de hauteur h_now , qui était à l'origine égale à la moitié 128 (64) . Mais il y a encore quelque chose de nouveau - mount_noise . Cette variable est responsable de la possibilité de changer h_now , car si vous changez la hauteur très souvent, la montagne ressemblera à un peigne .

La couleur
J'ai immédiatement mis une couleur légèrement brunâtre , que ce soit au moins une partie - à l'avenir, nous n'en aurons plus besoin.

Passons maintenant à ground_generator et écrivons ceci dans la méthode Start :

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); 

Nous initialisons le terrain variable une fois qu'il a dû être fait .
Après, sans explication, envoyez-le à ground_libray .
Nous avons donc généré la montagne.

Pourquoi ne puis-je pas voir ma montagne?


Tirons maintenant ce que nous avons!

Pour le dessin, nous écrirons la méthode suivante dans notre ground_libray :

Dessin
  public static void paint(block[,] b, ref Texture2D t) { t = new Texture2D(b.GetLength(0), b.GetLength(1)); t.filterMode = FilterMode.Point; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) { t.SetPixel(x, y, new Color(0, 0, 0, 0)); continue; } t.SetPixel(x, y, new Color( b[x, y].color[0], b[x, y].color[1], b[x, y].color[2] ) ); } t.Apply(); } 

Ici, nous ne donnerons plus à quelqu'un notre domaine, nous n'en donnerons qu'une copie (bien que, en raison du mot classe, nous en ayons donné un peu plus qu'une simple copie) . Nous donnerons également notre Texture2D à cette méthode.

Les deux premières lignes: nous créons notre texture à la taille du champ et supprimons le filtrage .

Après cela, nous parcourons tout notre champ de tableau et où nous n'avons rien créé (la classe doit être initialisée) - nous dessinons un carré vide, sinon, s'il n'est pas vide, nous dessinons ce que nous avons enregistré dans l'élément.

Et, bien sûr, une fois terminé, nous allons à ground_generator et ajoutons ceci:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); //   ground_libray.paint(ground, ref myT); GetComponent<SpriteRenderer>().sprite = Sprite.Create(myT, new Rect(0, 0, ground_size, ground_size), Vector3.zero ); 

Mais peu importe combien nous dessinons sur notre texture, dans le jeu, nous ne pouvons la voir qu'en plaçant cette toile sur quelque chose:

SpriteRenderer n'accepte Texture2D nulle part , mais rien ne nous empêche de créer un sprite à partir de cette texture - Sprite.Create ( texture , rectangle avec les coordonnées du coin inférieur gauche et supérieur droit , les coordonnées de l'axe ).

Ces lignes seront appelées les plus récentes, nous ajouterons le reste au-dessus de la méthode de peinture !

Le mien


Maintenant, nous devons remplir nos champs de grottes aléatoires. Pour de telles actions, nous allons également créer une méthode distincte dans ground_libray . Je voudrais expliquer immédiatement les paramètres de la méthode:

 ref block[,] b -     . int thick -    int size -         Color outLine -   

Cave
  public static void make_cave(ref block[,] b, int thick, int size, Color outLine) { int xNow = Random.Range(0, b.GetLength(0)); int yNow = Random.Range(0, b.GetLength(1) / 2); for (int i = 0; i < size; i++) { b[xNow, yNow] = null; make_thick(ref b, thick, new int[2] { xNow, yNow }, outLine); switch (Random.Range(0, 4)) { case 0: xNow += 1; break; case 1: xNow -= 1; break; case 2: yNow += 1; break; case 3: yNow -= 1; break; } xNow = xNow < 0 ? 0 : (xNow >= b.GetLength(0) ? b.GetLength(0) - 1 : xNow); yNow = yNow < 0 ? 0 : (yNow >= b.GetLength(1) ? b.GetLength(1) - 1 : yNow); } } 

Pour commencer, nous avons déclaré nos variables X et Y , mais je les ai juste appelées respectivement xNow et yNow .

Le premier, à savoir xNow , obtient une valeur aléatoire de zéro à la taille du champ dans la première dimension.

Et le second - yNow - obtient également une valeur aléatoire: de zéro au milieu du champ dans la deuxième dimension. Pourquoi? Nous générons notre montagne à partir du milieu, la chance qu'elle atteigne le "plafond" n'est pas grande . Sur cette base, je ne considère pas pertinent de générer des grottes dans l'air.

Après cela, une boucle va immédiatement, dont le nombre de ticks dépend du paramètre de taille . Chaque fois que nous mettons à jour le champ aux positions xNow et yNow , et seulement ensuite nous les mettons à jour nous-mêmes (les mises à jour du champ peuvent être mises à la fin - vous ne sentirez pas la différence)

Il existe également une méthode make_thick , dans les paramètres dont nous passons notre champ , la largeur du trait de la grotte , la position actuelle de mise à jour de la grotte et la couleur du trait :

AVC
  static void make_thick (ref block[,] b, int t, int[] start, Color o) { for (int x = (start[0] - t); x < (start[0] + t); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - t); y < (start[1] + t); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) continue; b[x, y] = new block(o); } } } 

La méthode prend la coordonnée de départ qui lui est transmise, et autour d'elle à distance t repeint tous les blocs en couleur o - tout est très simple!


Ajoutons maintenant cette ligne à notre générateur de terrain :

 ground_libray.make_cave(ref ground, 2, 10000, new Color(0.3f, 0.3f, 0.3f)); 

Vous pouvez installer le script ground_generator en tant que composant sur notre objet et vérifier son fonctionnement!



En savoir plus sur les grottes ...
  • Pour créer plus de grottes, vous pouvez appeler la méthode make_cave plusieurs fois (utilisez une boucle)
  • La modification du paramètre de taille n'augmente pas toujours la taille de la grotte, mais elle devient souvent plus grande
  • En modifiant le paramètre épais , vous augmentez considérablement le nombre d'opérations:
    si le paramètre est 3, alors le nombre de carrés dans un rayon de 3 sera 36 , donc avec la taille du paramètre = 40 000 , le nombre d'opérations sera 36 * 40 000 = 1440000


Correction de la grotte




Avez-vous remarqué que de ce point de vue, la grotte n'a pas le meilleur aspect? Trop de détails supplémentaires (peut-être pensez-vous différemment) .

Pour nous débarrasser des inclusions de certains # 4d4d4d, nous allons écrire cette méthode dans ground_libray :

Plus propre
  public static void clear_caves(ref block[,] b) { for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) continue; if (solo(b, 2, 13, new int[2] { x, y })) b[x, y] = null; } } 

Mais il sera difficile de comprendre ce qui se passe ici si vous ne savez pas ce que fait la fonction solo :

  static bool solo (block[,] b, int rad, int min, int[] start) { int cnt = 0; for (int x = (start[0] - rad); x <= (start[0] + rad); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - rad); y <= (start[1] + rad); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) cnt += 1; else continue; if (cnt >= min) return true; } } return false; } 

Dans les paramètres de cette fonction, notre champ , le rayon de la vérification du point , le «seuil de destruction» et les coordonnées du point à contrôler doivent être présents.
Voici une explication détaillée de ce que fait cette fonction:
int cnt est le compteur du "seuil" actuel
Viennent ensuite deux cycles qui vérifient tous les points autour de celui dont les coordonnées sont passées pour commencer . S'il y a un point vide , alors nous en ajoutons un à cnt , lorsque nous atteignons le «seuil de destruction», nous renvoyons la vérité - le point est superflu . Sinon, on ne la touche pas.

J'ai défini le seuil de destruction à 13 points vides et le rayon de vérification est 2 (c'est-à-dire qu'il vérifiera 24 points, sans le central)
Exemple
Celui-ci restera indemne, car il ne reste que 9 points vides.



Mais celui-ci n'a pas eu de chance - jusqu'à 14 points vides



Une brève description de l'algorithme: nous parcourons tout le champ et vérifions tous les points pour voir s'ils sont nécessaires.

Ensuite, nous ajoutons simplement la ligne suivante à notre ground_generator :

 ground_libray.clear_caves(ref ground); 

Résumé


Comme nous pouvons le voir, la plupart des particules inutiles ont tout simplement disparu.

Ajoutez de la couleur


Notre montagne a l'air très monotone, je la trouve ennuyeuse.

Ajoutons de la couleur. Ajoutez la méthode level_paint à ground_libray :

Peinture sur les montagnes
  public static void level_paint(ref block[,] b, Color[] all_c) { for (int x = 0; x < b.GetLength(0); x++) { int lvl_div = -1; int counter = 0; int lvl_now = 0; for (int y = b.GetLength(1) - 1; y > 0; y--) { if (b[x, y] != null && lvl_div == -1) lvl_div = y / all_c.Length; else if (b[x, y] == null) continue; b[x, y] = new block(all_c[lvl_now]); lvl_now += counter >= lvl_div ? 1 : 0; lvl_now = (lvl_now >= all_c.Length) ? (all_c.Length - 1) : lvl_now; counter = counter >= lvl_div ? 0 : (counter += 1); } } } </ <cut />source>           .    ,       ,   .       ,      .          <b>Y </b>  ,      . </spoiler>     <b>ground_generator </b> : <source lang="cs"> ground_libray.level_paint(ref ground, new Color[3] { new Color(0.2f, 0.8f, 0), new Color(0.6f, 0.2f, 0.05f), new Color(0.2f, 0.2f, 0.2f), }); 

J'ai choisi seulement 3 couleurs: vert , rouge foncé et gris foncé .
Bien sûr, vous pouvez modifier à la fois le nombre de couleurs et les valeurs de chacune. Il s'est avéré comme ceci:

Résumé


Mais ça a l'air trop strict pour ajouter un peu d'aléatoire aux couleurs, on va écrire cette propriété dans ground_libray :

Couleurs aléatoires
  public static float color_randomize = 0.1f; static float crnd { get { return Random.Range(1.0f - color_randomize, 1.0f + color_randomize); } } 

Et maintenant dans les méthodes level_paint et make_thick , dans les lignes où nous assignons des couleurs, par exemple dans make_thick :

 b[x, y] = new block(o); 

Nous écrirons ceci:

 b[x, y] = new block(o * crnd); 

Et dans level_paint

 b[x, y] = new block(all_c[lvl_now] * crnd); 


En fin de compte, tout devrait ressembler à ceci:

Résumé



Inconvénients


Supposons que nous ayons un champ de 1024 par 1024, nous devons générer 24 grottes, dont l'épaisseur des bords sera de 4 et la taille est de 80 000.

1024 * 1024 + 24 * 64 * 80 000 = 5 368 832 000 000 opérations.

Cette méthode ne convient que pour générer de petits modules pour le monde du jeu, il est impossible de générer quelque chose de très grand à la fois .

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


All Articles