Pour beaucoup, la génération procédurale est un concept magique hors de portée. Seuls les développeurs expérimentés savent comment créer un jeu capable de créer ses propres niveaux… non? Cela pourrait sembler comme la magie, mais PCG (génération de contenu procédural) peut être appris par les développeurs de jeux débutants. Dans ce tutoriel, je vais vous montrer comment générer de manière procédurale un système de grotte de donjon.
Voici une démo SWF qui montre le type de disposition de niveau que cette technique peut générer:
Apprendre les bases signifie généralement beaucoup de recherche et d’expérimentation sur Google. Le problème est, il y a très peu de simple guides sur la façon de commencer. Pour référence, voici d’excellentes sources d’information sur le sujet que j’ai étudiées:
Avant d'entrer dans les détails, c'est une bonne idée d'examiner comment nous allons résoudre le problème. Voici quelques morceaux faciles à digérer que nous allons utiliser pour garder cette chose simple:
Une fois que nous avons étudié les exemples suivants, vous devez avoir les compétences nécessaires pour expérimenter PCG dans vos propres jeux. Excitant, hein?
La première chose que nous allons faire est de placer au hasard les pièces d’un niveau de donjon généré de manière procédurale..
Afin de suivre, il est judicieux de bien comprendre le fonctionnement des cartes de tuiles. Si vous avez besoin d’un aperçu rapide ou d’un rappel, consultez ce didacticiel sur les cartes de tuiles. (Il est conçu pour Flash, mais même si vous n’êtes pas familier avec Flash, cela reste utile pour comprendre l’essentiel des cartes de tuiles.)
Avant de commencer, nous devons remplir notre carte avec des carreaux muraux. Tout ce que vous avez à faire est de parcourir tous les points de votre carte (un tableau 2D, idéalement) et de placer la mosaïque..
Nous devons également convertir les coordonnées en pixels de chaque rectangle en coordonnées de grille. Si vous souhaitez passer de pixels à l'emplacement de la grille, divisez la coordonnée en pixels par la largeur de la mosaïque. Pour passer de la grille aux pixels, multipliez les coordonnées par la largeur de la tuile..
Par exemple, si nous voulons placer le coin supérieur gauche de notre pièce à (5, 8)
sur notre grille et nous avons une largeur de carreaux de 8
pixels, nous aurions besoin de placer ce coin à (5 * 8, 8 * 8)
ou (40, 64)
en coordonnées de pixel.
Créons un Pièce
classe; cela pourrait ressembler à ceci dans le code Haxe:
class Room includes Sprite // ces valeurs contiennent les coordonnées de la grille pour chaque coin de la salle public var x1: Int; public var x2: Int; public var y1: Int; public var y2: Int; // largeur et hauteur de la pièce en termes de grille public var w: Int; public var h: Int; // centre de la pièce public var center: Point; // constructeur pour créer de nouvelles salles public function new (x: Int, y: Int, w: Int, h: Int) super (); x1 = x; x2 = x + w; y1 = y; y2 = y + h; this.x = x * Main.TILE_WIDTH; this.y = y * Main.TILE_HEIGHT; this.w = w; this.h = h; center = new Point (Math.floor ((x1 + x2) / 2), Math.floor ((y1 + y2) / 2)); // return true si cette pièce intersecte la fonction fournie par la fonction publique intersection (room: Room): Bool return (x1 <= room.x2 && x2 >= room.x1 && y1 <= room.y2 && room.y2 >= room.y1);
Nous avons des valeurs pour la largeur, la hauteur, la position du point central et des quatre coins de chaque pièce, ainsi qu'une fonction qui nous indique si cette pièce en croise une autre. Notez également que tout sauf les valeurs x et y se trouve dans notre système de coordonnées de grille. En effet, il est beaucoup plus facile d’utiliser de petits nombres chaque fois que nous accédons aux valeurs de la pièce..
D'accord, nous avons le cadre pour une salle en place. Maintenant, comment pouvons-nous générer et placer une pièce de manière procédurale? Eh bien, grâce aux générateurs de nombres aléatoires intégrés, cette partie n'est pas trop difficile.
Tout ce que nous avons à faire est de fournir des valeurs aléatoires x et y pour notre pièce dans les limites de la carte et de donner des valeurs aléatoires de largeur et de hauteur dans une plage prédéterminée..
Étant donné que nous utilisons des emplacements et des dimensions aléatoires pour nos salles, nous allons nécessairement chevaucher des salles créées précédemment au fur et à mesure que nous remplirons notre donjon. Eh bien, nous avons déjà codé un simple intersections ()
méthode afin de nous aider à résoudre le problème.
Chaque fois que nous essayons de placer une nouvelle pièce, nous appelons simplement intersections ()
sur chaque paire de pièces de la liste complète. Cette fonction retourne une valeur booléenne: vrai
si les pièces se chevauchent, et faux
autrement. Nous pouvons utiliser cette valeur pour décider quoi faire de la pièce que nous venons d'essayer de placer.
intersections ()
une fonction. Vous pouvez voir comment les valeurs x et y se chevauchent et retournent vrai
. fonction privée placeRooms () // crée un tableau de stockage pour un accès facile rooms = new Array (); // valeurs aléatoires pour chaque pièce pendant (r dans 0… maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH - w - 1) + 1; var y = Std.random (MAP_HEIGHT - h - 1) + 1; // crée une pièce avec des valeurs aléatoires var newRoom = new Pièce (x, y, w, h); var failed = false; for (otherRoom dans les chambres) if (newRoom.intersects (otherRoom)) failed = true; Pause; if (! failed) // fonction locale pour créer une nouvelle pièce createRoom (newRoom); // insère une nouvelle pièce dans un tableau array rooms.push (newRoom)
La clé ici est la échoué
Booléen; il est réglé sur la valeur de retour de intersections ()
, Et il en est de même vrai
si (et seulement si) vos pièces se chevauchent. Une fois que nous sortons de la boucle, nous vérifions cela échoué
variable et, si elle est fausse, nous pouvons découper la nouvelle salle. Sinon, nous nous contentons de jeter la pièce et d’essayer de nouveau jusqu’à atteindre le nombre maximum de pièces..
La grande majorité des jeux qui utilisent du contenu généré par des procédures s'efforcent de rendre tout ce contenu accessible par le joueur, mais quelques personnes pensent que ce n'est pas nécessairement la meilleure décision de conception. Et si vous aviez dans votre donjon des pièces que le joueur ne pourrait que rarement se rendre mais qu'il pourrait toujours voir? Cela pourrait ajouter une dynamique intéressante à votre donjon.
Bien sûr, peu importe de quel côté de l'argument vous êtes, c'est toujours une bonne idée de vous assurer que le joueur peut toujours progresser dans le jeu. Ce serait assez frustrant si vous arriviez à un niveau du donjon du jeu et que la sortie était complètement bloquée.
Considérant que la plupart des jeux tournent pour un contenu accessible à 100%, nous nous en tiendrons à cela.
A présent, vous devriez avoir une carte de tuiles opérationnelle et il devrait y avoir un code en place pour créer un nombre variable de pièces de différentes tailles. Regarde ça; vous avez déjà des salles de donjons intelligentes générées de manière procédurale!
Maintenant, l’objectif est de connecter chaque salle afin que nous puissions traverser notre donjon et éventuellement atteindre une sortie qui mène au niveau suivant. Nous pouvons accomplir cela en creusant des couloirs entre les pièces.
Nous devrons ajouter un point
variable au code pour garder une trace du centre de chaque pièce créée. Chaque fois que nous créons et plaçons une pièce, nous en déterminons le centre et nous le connectons au centre de la pièce précédente..
Tout d'abord, nous allons implémenter les corridors:
fonction privée hCorridor (x1: Int, x2: Int, y) pour (x dans Std.int (Math.min (x1, x2))… Std.int (Math.max (x1, x2)) + 1) // supprime les tuiles pour "sculpter" la carte de corridor [x] [y] .parent.removeChild (map [x] [y]); // place une nouvelle carte de tuiles non bloquée [x] [y] = nouvelle tuile (Tile.DARK_GROUND, false, false); // ajoute une tuile comme nouvel objet de jeu addChild (map [x] [y]); // définit l'emplacement de la tuile de façon appropriée map [x] [y] .setLoc (x, y); // crée un couloir vertical pour connecter des salles fonction privée vCorridor (y1: Int, y2: Int, x) pour (y dans Std.int (Math.min (y1, y2))…… Std.int (Math.max (y1, y2)) + 1) // détruit les tuiles pour "sculpter" la carte de corridor [x] [y] .parent.removeChild (map [x] [y]); // place une nouvelle carte de tuiles non bloquée [x] [y] = nouvelle tuile (Tile.DARK_GROUND, false, false); // ajoute une tuile comme nouvel objet de jeu addChild (map [x] [y]); // définit correctement l'emplacement de la mosaïque map [x] [y] .setLoc (x, y);
Ces fonctions agissent à peu près de la même manière, mais l’une se découpe horizontalement et l’autre verticalement..
La connexion de la première pièce à la seconde nécessite unvCorridor
Et un hCorridor
. Pour ce faire, nous avons besoin de trois valeurs. Pour les corridors horizontaux, nous avons besoin de la valeur x de départ, de la valeur x de fin et de la valeur y actuelle. Pour les corridors verticaux, nous avons besoin des valeurs y de départ et d'arrivée ainsi que de la valeur x actuelle.
Puisque nous nous déplaçons de gauche à droite, nous avons besoin des deux valeurs x correspondantes, mais seulement d'une valeur y car nous ne pourrons pas monter ou descendre. Lorsque nous nous déplaçons verticalement, nous aurons besoin des valeurs y. dans le pour
boucle au début de chaque fonction, nous itérons de la valeur de départ (x ou y) à la valeur de fin jusqu'à ce que nous ayons découpé tout le corridor.
Maintenant que nous avons le code de corridor en place, nous pouvons changer notre placeRooms ()
fonction et appelez nos nouvelles fonctions de corridor:
fonction privée placeRooms () // stocke des pièces dans un tableau pour en faciliter l’accès = new Array (); // variable pour le centre de suivi de chaque pièce var newCenter = null; // valeurs aléatoires pour chaque pièce pendant (r dans 0… maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH - w - 1) + 1; var y = Std.random (MAP_HEIGHT - h - 1) + 1; // crée une pièce avec des valeurs aléatoires var newRoom = new Pièce (x, y, w, h); var failed = false; for (otherRoom dans les chambres) if (newRoom.intersects (otherRoom)) failed = true; Pause; if (! failed) // fonction locale pour créer une nouvelle pièce createRoom (newRoom); // centre de magasin pour la nouvelle salle newCenter = newRoom.center; if (rooms.length! = 0) // centre du magasin de la pièce précédente var prevCenter = rooms [chambres.length - 1] .center; // délimitez les corridors entre les pièces en fonction de leur centre // commencez au hasard par des couloirs horizontaux ou verticaux si ), Std.int (prevCenter.y)); vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (newCenter.x)); else vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (prevCenter.x)); hCorridor (Std.int (prevCenter.x), Std.int (newCenter.x), Std.int (newCenter.y)); if (! failed) rooms.push (newRoom);
Dans l'image ci-dessus, vous pouvez suivre la création du couloir de la première à la quatrième pièce: rouge, vert, puis bleu. Vous pouvez obtenir des résultats intéressants en fonction de l'emplacement des pièces. Par exemple, deux couloirs l'un à côté de l'autre forment un couloir double largeur..
Nous avons ajouté quelques variables pour suivre le centre de chaque pièce et nous avons attaché les pièces avec des couloirs entre leurs centres. Maintenant, il y a plusieurs salles et couloirs qui ne se chevauchent pas et qui permettent de connecter tout le niveau du donjon. Pas mal.
Vous avez parcouru un long chemin pour construire votre premier niveau de donjon généré par des procédures, et j'espère que vous avez compris que PCG n'est pas une bête magique que vous n'aurez jamais l'occasion de tuer..
Nous avons expliqué comment placer de manière aléatoire du contenu autour de votre niveau de donjon avec de simples générateurs de nombres aléatoires et quelques plages prédéterminées pour conserver votre contenu à la bonne taille et plus ou moins au bon endroit. Ensuite, nous avons découvert un moyen très simple de déterminer si votre placement aléatoire avait un sens en vérifiant si des pièces se chevauchent. Enfin, nous avons parlé un peu des avantages de garder votre contenu accessible et nous avons trouvé un moyen de nous assurer que votre lecteur puisse accéder à toutes les pièces de votre donjon..
Les trois premières étapes de notre processus en quatre étapes sont terminées, ce qui signifie que vous avez les éléments de base d’un grand donjon pour votre prochain match. La dernière étape revient à vous: vous devez parcourir ce que vous avez appris pour créer davantage de contenu généré de manière procédurale pour une rejouabilité sans fin..
La méthode de calcul des niveaux de donjon simples de ce didacticiel ne fait qu’effleurer la surface de PCG. Il existe d’autres algorithmes simples que vous pouvez facilement saisir..
Mon défi pour vous est de commencer à expérimenter les débuts de votre jeu que vous avez créés ici et de faire des recherches sur davantage de méthodes pour changer vos donjons..
Une excellente méthode pour créer des niveaux de grotte consiste à utiliser des automates cellulaires, qui offrent une infinité de possibilités pour personnaliser les niveaux de donjon. Une autre excellente méthode d’apprentissage est le partitionnement d’espace binaire (BSP), qui crée des niveaux de donjon ressemblant à une grille..
J'espère que cela vous a donné un bon départ dans la génération de contenu procédural. Si vous avez des questions, n'hésitez pas à commenter ci-dessous. J'aimerais voir quelques exemples de ce que vous créez avec PCG..
Articles Similaires