Générer des niveaux de cavité aléatoires à l'aide d'automates cellulaires

Les générateurs de contenu procédural sont des fragments de code écrits dans votre jeu qui peuvent créer de nouveaux éléments de contenu de jeu à tout moment, même lorsque le jeu est en cours d'exécution! Les développeurs de jeux ont essayé de générer de manière procédurale tout, des mondes 3D aux bandes sonores musicales. Ajouter une génération à votre jeu est un excellent moyen d'ajouter de la valeur supplémentaire: les joueurs l'adorent car ils reçoivent un nouveau contenu imprévisible et excitant à chaque jeu..

Dans ce didacticiel, nous allons examiner une excellente méthode pour générer des niveaux aléatoires et essayer d’étendre les limites de ce que vous pensez pouvoir générer..

Articles Similaires

Si vous souhaitez en savoir plus sur la génération de contenu procédural, la conception de niveaux, l'IA ou les automates cellulaires, veillez à consulter ces autres articles:

  • Créer la vie: le jeu de la vie de Conway
  • Portal 2 Level Design: Créer des puzzles pour mettre vos joueurs au défi
  • StarCraft II Level Design: Astuces pour la conception esthétique et la rédaction
  • Codage d'un générateur de séquence personnalisé pour restituer un paysage Starscape
  • Glossaire Gamedev: Générateurs de séquence et générateurs de nombres pseudo-aléatoires

Bienvenue dans les grottes!

Dans ce tutoriel, nous allons construire un générateur de cavernes. Les grottes sont parfaites pour toutes sortes de genres et de paramètres de jeu, mais elles me rappellent particulièrement les vieux donjons dans les jeux de rôle.

Jetez un coup d’œil à la démo ci-dessous pour voir le type de sortie que vous pourrez obtenir. Cliquez sur 'Nouveau monde' pour créer une nouvelle grotte à regarder. Nous parlerons de ce que les différents paramètres font en temps voulu.


Ce générateur nous renvoie en réalité un grand tableau bidimensionnel de blocs, chacun étant plein ou vide. Donc, en fait, vous pouvez utiliser ce générateur pour toutes sortes de jeux en plus des robots de donjons: niveaux aléatoires pour les jeux de stratégie, tilemaps pour les jeux de plateforme, peut-être même comme arènes pour un jeu de tir multijoueur! Si vous regardez bien, retourner les blocs pleins et vides constitue également un générateur d'îlots. Tout utilise le même code et le même résultat, ce qui en fait un outil vraiment flexible.

Commençons par poser une question simple: qu'est-ce qu'un automate cellulaire??


Débuter avec les cellules

Dans les années 1970, un mathématicien appelé John Conway a publié une description du jeu de la vie, parfois simplement appelée Life. La vie n'était pas vraiment un jeu; cela ressemblait plus à une simulation qui prenait une grille de cellules (pouvant être vivantes ou mortes) et leur appliquait des règles simples.

Quatre règles ont été appliquées à chaque cellule à chaque étape de la simulation:

  1. Si une cellule vivante a moins de deux voisins vivants, elle meurt.
  2. Si une cellule vivante a deux ou trois voisins vivants, elle reste en vie.
  3. Si une cellule vivante a plus de trois voisins vivants, elle meurt.
  4. Si une cellule morte a exactement trois voisins vivants, elle devient vivante.

Sympa et simple! Pourtant, si vous essayez différentes combinaisons de grilles de départ, vous pouvez obtenir des résultats très étranges. Boucles infinies, machines à cracher des formes, etc. Le jeu de la vie est un exemple de automate cellulaire - une grille de cellules régie par certaines règles.

Nous allons mettre en œuvre un système très similaire à Life, mais au lieu de produire des motifs et des formes amusantes, il créera des systèmes de grottes incroyables pour nos jeux..


Implémentation d'un automate cellulaire

Nous allons représenter notre grille cellulaire comme un tableau à deux dimensions de booléen (vrai ou faux) valeurs. Cela nous convient parce que nous ne cherchons qu'à déterminer si une tuile est solide ou non..

Voici nous initialisant notre grille de cellules:

booléen [] [] cellmap = nouveau booléen [largeur] [hauteur];

Pointe: Notez que le premier index est la coordonnée x du tableau et que le deuxième index est la coordonnée y. Cela rend l'accès au tableau plus naturel dans le code.

Dans la plupart des langages de programmation, ce tableau sera initialisé avec toutes ses valeurs définies sur faux. C'est bien pour nous! Si un index de tableau (x, y) est faux, nous dirons que la cellule est vide; si c'est vrai, cette tuile sera solide rock.

Chacune de ces positions de tableau représente l'une des «cellules» de notre grille cellulaire. Nous devons maintenant configurer notre réseau pour pouvoir commencer à construire nos grottes..

Nous allons commencer par définir au hasard chaque cellule soit morte soit vivante. Chaque cellule aura la même chance aléatoire d’être rendue vivante, et vous devez vous assurer que cette valeur de chance est définie dans une variable quelque part, car nous voudrons certainement la modifier plus tard et la placer dans un endroit facile d’accès nous aidera à cette. Je vais utiliser 45% pour commencer.

float chanceToStartAlive = 0.45f; public boolean [] [] initialiseMap (boolean [] [] map) pour (int x = 0; x 
Notre grotte aléatoire avant toute étape de simulation d'un automate cellulaire.

Si nous exécutons ce code, nous nous retrouvons avec une grande grille de cellules comme celle ci-dessus qui sont aléatoirement vivantes ou mortes. C'est en désordre, et cela ne ressemble certainement pas à un système de grottes que j'ai jamais vu. Alors quelle est la prochaine?


Cultiver nos grottes

Rappelez-vous les règles qui régissaient les cellules dans The Game Of Life? Chaque fois que la simulation se déroulait d'une étape à la fois, chaque cellule vérifiait les règles de la vie et voyait si elle deviendrait vivante ou morte. Nous allons utiliser exactement la même idée pour construire nos caves: nous allons maintenant écrire une fonction qui parcourt chaque cellule de la grille et applique quelques règles de base pour décider si elle vit ou meurt..

Comme nous le verrons plus tard, nous allons utiliser ce morceau de code plus d'une fois. Par conséquent, le mettre dans sa propre fonction signifie que nous pouvons l'appeler autant ou autant de fois que nous le voulons. Nous allons lui donner un joli nom informatif comme doSimulationStep (), aussi.

Qu'est-ce que la fonction doit faire? Tout d’abord, nous allons créer une nouvelle grille dans laquelle nous pourrons insérer nos valeurs de cellule mises à jour. Pour comprendre pourquoi nous devons le faire, rappelez-vous que pour calculer la nouvelle valeur d’une cellule dans la grille, nous devons examiner ses huit voisins:

Mais si nous avons déjà calculé la nouvelle valeur de certaines cellules et les avons replacées dans la grille, notre calcul sera alors un mélange de données nouvelles et anciennes, comme ceci:

Oops! Ce n'est pas ce que nous voulons du tout. Ainsi, chaque fois que nous calculons une nouvelle valeur de cellule, au lieu de la replacer dans l'ancienne carte, nous l'écrirons dans une nouvelle..

Commençons par écrire que doSimulationStep () fonction, alors:

public doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = new boolean [largeur] [hauteur]; //… 

Nous voulons examiner chaque cellule de la grille et compter combien de ses voisins sont vivants et morts. Compter vos voisins dans un tableau est l’un de ces bits de code que vous devrez écrire un million de fois. Voici une implémentation rapide dans une fonction que j'ai appelée countAliveNeighbours ():

// Retourne le nombre de cellules d'un anneau autour (x, y) qui sont en vie. public countAliveNeighbours (booléen [] [] map, int x, int y) int count = 0; pour (int i = -1; i<2; i++) for(int j=-1; j<2; j++) int neighbour_x = x+i; int neighbour_y = y+j; //If we're looking at the middle point if(i == 0 && j == 0) //Do nothing, we don't want to add ourselves in!  //In case the index we're looking at it off the edge of the map else if(neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= map.length || neighbour_y> = map [0] .length) count = count + 1;  // Sinon, vérification normale du voisin if (map [neighbour_x] [neighbour_y]) count = count + 1; 

Quelques choses à propos de cette fonction:

Premièrement le pour Les boucles sont un peu bizarres si vous n’avez jamais fait quelque chose comme ça auparavant. L'idée est que nous voulons regarder toutes les cellules qui sont autour du point (x, y). Si vous regardez l'illustration ci-dessous, vous pouvez voir comment les indices que nous voulons sont un de moins, égal à et un de plus que l'indice d'origine. Nos deux pour les boucles nous donnent juste ça, à partir de -1, et en boucle à travers +1. Nous ajoutons ensuite cela à l'index d'origine à l'intérieur du pour boucle pour trouver chaque voisin.

Deuxièmement, remarquez que si nous vérifions une référence de grille qui n’est pas réelle (par exemple, elle se trouve en dehors de la carte), nous la comptons comme un voisin. Je préfère ceci pour la génération de grottes, car cela a tendance à remplir les bords de la carte, mais vous pouvez expérimenter en ne le faisant pas si vous le souhaitez..

Alors maintenant, revenons à notre doSimulationStep () fonction et ajouter un peu plus de code:

public boolean [] [] doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = new boolean [largeur] [hauteur]; // Boucle sur chaque ligne et colonne de la carte pour (int x = 0; x birthLimit) newMap [x] [y] = true;  else newMap [x] [y] = false;  return newMap; 

Ceci parcourt toute la carte, appliquant nos règles à chaque cellule de la grille pour calculer la nouvelle valeur et la plaçant dans nouveauCarte. Les règles sont plus simples que le jeu de la vie - nous avons deux variables spéciales, une pour la naissance de cellules mortes (naissanceLimite) et un pour tuer des cellules vivantes (deathLimit). Si les cellules vivantes sont entourées de moins de deathLimit cellules meurent, et si les cellules mortes sont proches au moins naissanceLimite cellules ils deviennent vivants. Gentil et simple!

Tout ce qui reste à la fin est une touche finale pour renvoyer la carte mise à jour. Cette fonction représente une seule étape des règles de notre automate cellulaire. La prochaine étape consiste à comprendre ce qui se passe lorsque nous l'appliquons une, deux ou plusieurs fois à notre carte de départ initiale..


Réglage et mise au point

Regardons maintenant à quoi ressemble le code de la génération principale, en utilisant le code que nous avons écrit jusqu'à présent.

public boolean [] [] generateMap () // Crée une nouvelle carte boolean [] [] cellmap = new boolean [largeur] [hauteur]; // Configurer la carte avec des valeurs aléatoires cellmap = initialiseMap (cellmap); // Et maintenant, lancez la simulation pour un nombre défini d'étapes pour (int i = 0; i 

Le seul morceau de code vraiment nouveau est un pour boucle qui exécute notre méthode de simulation un nombre défini de fois. Encore une fois, insérez-le dans une variable pour que nous puissions la modifier, car nous allons commencer à jouer avec ces valeurs!

Jusqu'à présent, nous avons défini ces variables:

  • chanceToStartAlive définit la densité de la grille initiale en cellules vivantes.
  • famineLimite est la limite inférieure de voisinage à laquelle les cellules commencent à mourir.
  • overpopLimit est la limite de voisinage supérieure à laquelle les cellules commencent à mourir.
  • birthNumber est le nombre de voisins à l'origine de la vie d'une cellule morte.
  • nombre d'étapes est le nombre de fois que nous effectuons l'étape de simulation.

Notre caverne aléatoire après deux étapes de simulation d'automate cellulaire.

Vous pouvez jouer avec ces variables dans la démo en haut de la page. Chaque valeur changera la démo de façon spectaculaire, alors jouez et voyez ce qui convient.

L’un des changements les plus intéressants que vous puissiez apporter est la nombre d'étapes variable. Lorsque vous exécutez la simulation pour plusieurs étapes, la rugosité de la carte disparaît et les îles disparaissent. J'ai ajouté un bouton pour que vous puissiez appeler la fonction manuellement et voir les effets. Expérimentez un peu et vous trouverez une combinaison de paramètres qui conviennent à votre style et au type de niveaux dont votre jeu a besoin.


Notre caverne aléatoire après six étapes de simulation d'automate cellulaire.

Avec ça, vous avez terminé. Félicitations - vous venez de créer un générateur de niveau de procédure, bravo! Asseyez-vous, exécutez et relancez votre code et souriez aux systèmes de grottes étranges et merveilleux qui en sortent. Bienvenue dans le monde de la génération procédurale.


Prenant plus loin

Si vous regardez votre belle génératrice de grottes et vous demandez ce que vous pouvez faire de plus, voici quelques idées de mission "crédit supplémentaire":

Utiliser un remplissage pour effectuer un contrôle de qualité

Flood Fill est un algorithme très simple que vous pouvez utiliser pour rechercher tous les espaces d'un tableau qui se connectent à un point particulier. Comme son nom l’indique, l’algorithme fonctionne un peu comme si vous versiez un seau d’eau dans votre niveau: il s’étend du point de départ et remplit tous les coins..

Le remplissage par inondation est idéal pour les automates cellulaires, car vous pouvez l'utiliser pour voir la taille d'une grotte. Si vous exécutez la démo à quelques reprises, vous remarquerez que certaines cartes sont constituées d'une grande grotte, tandis que d'autres comportent quelques grottes plus petites séparées les unes des autres. Le remplissage d'inondation peut vous aider à déterminer la taille d'une grotte, puis à régénérer le niveau s'il est trop petit ou à décider où vous voulez que le joueur commence si vous pensez que sa taille est suffisante. Il y a un grand contour de remplissage d'inondation sur Wikipedia.

Placement simple et rapide des trésors

Placer des trésors dans des zones froides nécessite parfois beaucoup de code, mais nous pouvons en fait écrire un morceau de code assez simple pour placer des trésors à l'écart dans nos systèmes de grottes. Nous avons déjà notre code qui compte le nombre de voisins par carré, alors en bouclant sur notre système de grottes fini, nous pouvons voir à quel point un carré est entouré de murs.

Si une cellule vide de la grille est entourée de beaucoup de murs solides, c'est probablement tout au bout d'un couloir ou cachée dans les murs du système de grottes. C’est un bon endroit pour cacher un trésor - alors, en vérifiant simplement nos voisins, nous pouvons glisser un trésor dans les coins et dans les ruelles..

public void placeTreasure (booléen [] [] monde) // Comment caché un endroit doit-il être pour le trésor? // Je trouve que 5 ou 6 c'est bien. 6 pour un trésor très rare. int treasureHiddenLimit = 5; pour (int x = 0; x < worldWidth; x++) for (int y=0; y < worldHeight; y++) if(!world[x][y]) int nbs = countAliveNeighbours(world, x, y); if(nbs >= treasureHiddenLimit) lieuTreasure (x, y); 

Ce n'est pas parfait. Cela met parfois des trésors dans des trous inaccessibles dans le système de grottes, et parfois les taches seront tout aussi évidentes. Mais, à la rigueur, c’est un excellent moyen de disperser des objets de collection autour de votre niveau. Essayez-le dans la démo en appuyant sur le placeTreasure () bouton!


Conclusions et lectures complémentaires

Ce tutoriel vous a montré comment créer un générateur de procédures basique mais complet. En quelques étapes simples, nous avons écrit un code qui permet de créer de nouveaux niveaux en un clin d'œil. J'espère que cela vous a donné un aperçu du potentiel de création de générateurs de contenu procédural pour vos jeux.!

Si vous voulez en savoir plus, Roguebasin est une excellente source d’information sur les systèmes de génération procédurale. Il se concentre principalement sur les jeux fantastiques, mais bon nombre de ses techniques peuvent être utilisées dans d'autres types de jeux, et il y a beaucoup d'inspiration pour générer de manière procédurale d'autres parties d'un jeu.!

Si vous voulez en savoir plus sur la génération de contenu procédural ou les automates cellulaires, voici une version en ligne géniale de The Game Of Life (bien que je vous recommande vivement de taper "Le jeu de la vie de Conway" dans Google). Vous pourriez aussi aimer Wolfram Tones, une charmante expérience d'utilisation d'automates cellulaires pour générer de la musique!