Créer roguelike dans Unity à partir de zéro

image

Il n'y a pas beaucoup de tutoriels sur la création de roguelike dans Unity, j'ai donc décidé de l'écrire. Non pas pour me vanter, mais pour partager des connaissances avec ceux qui en sont au stade où j'étais déjà assez longtemps.

Remarque: je ne dis pas que c'est la seule façon de créer un roguelike dans Unity. Il est juste l' un des . Probablement pas le meilleur et le plus efficace, j'ai appris par essais et erreurs. Et j'apprendrai certaines choses dans le processus de création d'un tutoriel.

Supposons que vous connaissiez au moins les bases de Unity, par exemple, comment créer un préfabriqué ou un script, etc. Ne vous attendez pas à ce que je vous apprenne à créer des feuilles de sprites, il existe de nombreux tutoriels à ce sujet. Je ne me concentrerai pas sur l'étude du moteur, mais sur la façon de mettre en œuvre le jeu que nous allons créer ensemble. Si vous rencontrez des difficultés, rendez-vous dans l'une des superbes communautés de Discord et demandez de l'aide:

Unity Developer Community

Roguelikes

Alors, commençons!

Étape 0 - planification


Oui c'est vrai. La première chose à créer est un plan. Ce sera bien pour vous de planifier le jeu, et pour moi - de planifier le tutoriel afin qu'après un certain temps nous ne soyons pas distraits du sujet. Il est facile de se perdre dans les fonctions du jeu, tout comme dans les donjons roguelike.

Nous écrirons roguelike. Nous suivrons principalement les sages conseils du développeur de Cogmind Josh Ge ici . Suivez le lien, lisez l'article ou regardez la vidéo, puis revenez.

Quel est le but de ce tutoriel? Obtenez un solide roguelike de base simple, avec lequel vous pourrez ensuite expérimenter. Il devrait avoir une génération de donjon, un joueur se déplaçant sur la carte, un brouillard de visibilité, des ennemis et des objets. Seuls les plus nécessaires. Ainsi, le joueur devrait pouvoir descendre les escaliers sur plusieurs étages. disons, par cinq, augmentez votre niveau, améliorez et, à la fin, combattez le boss et battez-le. Ou mourir. En fait, c'est tout.

Suivant les conseils de Josh Ge, nous allons construire les fonctions du jeu pour qu'elles nous conduisent au but. Nous obtenons donc le cadre roguelike, qui peut être encore étendu, ajoutez vos propres puces, créant un caractère unique. Ou jetez tout dans le panier, profitez de l'expérience acquise et recommencez à zéro. Ce sera génial de toute façon.

Je ne vous donnerai aucune ressource graphique. Dessinez-les vous-même ou utilisez les ensembles de tuiles gratuits, qui peuvent être téléchargés ici , ici ou en effectuant une recherche dans Google. N'oubliez pas de mentionner les auteurs des graphismes du jeu.

Maintenant, listons toutes les fonctions qui seront dans notre roguelike dans l'ordre de leur implémentation:

  1. Génération de carte de donjon
  2. Le personnage du joueur et son mouvement
  3. Zone de visibilité
  4. Ennemis
  5. Rechercher un moyen
  6. Combat, santé et mort
  7. Niveau supérieur du joueur
  8. Objets (armes et potions)
  9. Cheats de la console (pour les tests)
  10. Planchers de donjon
  11. Sauvegarde et chargement
  12. Boss final

Après avoir implémenté tout cela, nous aurons un solide roguelike et vous améliorerez considérablement vos compétences en développement de jeux. En fait, c'était ma façon d'améliorer mes compétences: créer du code et implémenter des fonctions. Par conséquent, je suis sûr que vous pouvez également gérer cela.

Étape 1 - Classe MapManager


C'est le premier script que nous allons créer et il deviendra l'épine dorsale de notre jeu. C'est simple, mais contient la majeure partie des informations importantes pour le jeu.

Alors, créez un script .cs appelé MapManager et ouvrez-le.

Supprimez ": MonoBehaviour" car il n'en héritera pas et ne sera attaché à aucun GameObject.

Supprimez les fonctions Démarrer () et Mettre à jour ().

À la fin de la classe MapManager, créez une nouvelle classe publique appelée Tile.


La classe Tile contiendra toutes les informations d'une seule tuile. Jusqu'à présent, nous n'avons pas besoin de beaucoup, seulement des positions x et y, ainsi qu'un objet de jeu situé à cette position de la carte.


Nous avons donc des informations de base sur les tuiles. Créons une carte à partir de cette tuile. C'est simple, nous n'avons besoin que d'un tableau bidimensionnel d'objets Tile. Cela semble compliqué, mais il n'y a rien de spécial à ce sujet. Ajoutez simplement la variable Tile [,] à la classe MapManager:


Voila! Nous avons une carte!

Oui, c'est vide. Mais ceci est une carte. Chaque fois que quelque chose bouge ou change d'état sur la carte, les informations sur cette carte sont mises à jour. Autrement dit, si, par exemple, un joueur essaie de passer à une nouvelle tuile, la classe vérifiera l'adresse de la tuile de destination sur la carte, la présence de l'ennemi et sa perméabilité. Grâce à cela, nous n'avons pas à vérifier des milliers de collisions à chaque tour, et nous n'avons pas besoin de collisionneurs pour chaque objet de jeu, ce qui facilitera et simplifiera le travail avec le jeu.

Le code résultant ressemble à ceci:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MapManager 
{
    public static Tile[,] map; // the 2-dimensional map with the information for all the tiles
}

public class Tile { //Holds all the information for each tile on the map
    public int xPosition; // the position on the x axis
    public int yPosition; // the position on the y axis
    public GameObject baseObject; // the map game object attached to that position: a floor, a wall, etc.
}

La première étape est terminée, passons au remplissage de la carte. Nous allons maintenant commencer à créer un générateur de donjon.

Étape 2 - quelques mots sur la structure des données


Mais avant de commencer, permettez-moi de partager les conseils qui ont surgi grâce aux commentaires reçus après la publication de la première partie. Lors de la création d'une structure de données, vous devez penser dès le début à la manière dont vous maintiendrez l'état du jeu. Sinon, plus tard, ce sera beaucoup plus chaotique. L'utilisateur de Discord st33d, le développeur de Star Shaped Bagel (vous pouvez jouer à ce jeu gratuitement ici ), a déclaré qu'au début, il avait créé le jeu, pensant que cela ne sauverait pas du tout les États. Peu à peu, le jeu a commencé à s'agrandir, et son fan a demandé un soutien pour la carte enregistrée. Mais en raison de la méthode choisie pour créer la structure de données, il était très difficile de sauvegarder les données, il n'a donc pas pu le faire.


Nous apprenons vraiment de nos erreurs. Malgré le fait que je mette la partie sauvegarde / chargement à la fin du tutoriel, je pense à travers eux depuis le tout début, et je ne les ai pas encore expliqués. Dans cette partie, je vais en parler un peu, mais pour ne pas surcharger les développeurs inexpérimentés.

Nous allons enregistrer des choses comme un tableau de variables de la classe Tile dans laquelle la carte est stockée. Nous enregistrerons toutes ces données, à l'exception des variables de la classe GameObject, qui se trouvent à l'intérieur de la classe Tile. Pourquoi? Tout simplement parce que GameObjects ne peut pas être sérialisé avec Unity en données stockées.

Par conséquent, en fait, nous n'avons pas besoin de sauvegarder les données stockées dans GameObjects. Toutes les données seront stockées dans des classes telles que Tile, et plus tard également Player, Enemy, etc. Ensuite, nous aurons GameObjects pour simplifier le calcul de choses telles que la visibilité et le mouvement, ainsi que le dessin de sprites sur l'écran. Par conséquent, dans les classes, il y aura des variables GameObject, mais la valeur de ces variables ne sera pas enregistrée et chargée. Lors du chargement, nous forcerons à générer à nouveau GameObject à partir des données enregistrées (position, sprite, etc.).

Alors, que devons-nous faire maintenant? Eh bien, ajoutez simplement deux lignes à la classe Tile existante et une en haut du script. Nous ajoutons d'abord «using System;» au titre du script, puis [Serializable] devant toute la classe et [NonSerialized] juste devant la variable GameObject. Comme ça:



Je vous en dirai plus à ce sujet lorsque nous arriverons à la partie du didacticiel sur l'enregistrement / le chargement. Pour l'instant, laissons tout et continuons.

Étape 3 - un peu plus sur la structure des données


J'ai obtenu un autre examen de la structure des données que je souhaite partager ici.

En fait, il existe de nombreuses façons d'implémenter des données dans un jeu. La première que j'utilise et qui sera implémentée dans ce tutoriel: toutes les données de tuile sont dans la classe Tile, et toutes sont stockées dans un tableau. Cette approche présente de nombreux avantages: elle est plus facile à lire, tout ce dont vous avez besoin est au même endroit, les données sont plus faciles à manipuler et à exporter vers un fichier de sauvegarde. Mais du point de vue de la mémoire, ce n'est pas si efficace. Vous devrez allouer beaucoup de mémoire aux variables qui ne seront jamais utilisées dans le jeu. Par exemple, plus tard, nous mettrons la variable Enemy GameObject dans la classe Tile afin de pouvoir pointer directement de la carte vers le GameObject de l'ennemi se tenant sur cette tuile afin de simplifier tous les calculs liés à la bataille. Mais cela signifie que chaque tuile du jeu disposera d'un espace en mémoire pour la variable GameObject,même s'il n'y a pas d'ennemi sur cette tuile. S'il y a 10 ennemis sur une carte de 2500 tuiles, alors il y aura 2490 variables GameObject vides mais allouées - vous pouvez voir combien de mémoire est gaspillée.

Une autre méthode consisterait à utiliser des structures pour stocker les données de base des tuiles (par exemple, la position et le type), et toutes les autres données seraient stockées dans hashmap-s, qui ne seraient générées que si nécessaire. Cela économiserait beaucoup de mémoire, mais le retour sur investissement serait une implémentation légèrement plus compliquée. En fait, ce serait un peu plus avancé que je ne le souhaiterais dans ce tutoriel, mais si vous le souhaitez, à l'avenir, je pourrai écrire un article plus détaillé à ce sujet.

De plus, si vous souhaitez lire une discussion sur ce sujet, cela peut être fait sur Reddit .

Étape 4 - Algorithme de génération de donjon


Oui, c'est une autre section dans laquelle je parlerai et nous ne commencerons rien à programmer. Mais cela est important, une planification minutieuse des algorithmes nous permettra d'économiser beaucoup de temps de travail à l'avenir.

Il existe plusieurs façons de créer un générateur de donjon. Celui que nous allons mettre en œuvre ensemble n'est pas le meilleur ni le plus efficace ... c'est juste une manière simple et initiale. C'est très simple, mais les résultats seront assez bons. Le problème principal sera de nombreux couloirs sans issue. Plus tard, si vous le souhaitez, je pourrai publier un autre tutoriel sur de meilleurs algorithmes.

En général, l'algorithme que nous utilisons fonctionne comme suit: disons que nous avons une carte entière remplie de valeurs nulles - un niveau composé d'une pierre. Au début, nous avons découpé une pièce au centre. De cette pièce, nous traversons le couloir dans une direction, puis ajoutons d'autres couloirs et pièces, en commençant toujours au hasard à partir d'une pièce ou d'un couloir existant, jusqu'à ce que nous atteignions le nombre maximal de couloirs / pièces indiqué au tout début. Ou jusqu'à ce que l'algorithme puisse trouver un nouvel endroit pour ajouter une nouvelle pièce / couloir, selon la première éventualité. Et nous obtenons donc un donjon.

Décrivons donc cela d'une manière plus proche d'un algorithme, étape par étape. Pour plus de commodité, j'appellerai chaque détail de la carte (couloir ou pièce) un élément afin de ne pas avoir à dire «pièce / couloir» à chaque fois.

  1. Coupez la pièce au centre de la carte
  2. Sélectionnez au hasard l'un des murs
  3. Nous traversons le couloir dans ce mur
  4. Sélectionnez au hasard l'un des éléments existants.
  5. Sélectionnez au hasard l'un des murs de cet élément
  6. Si le dernier élément sélectionné est une pièce, nous générons un couloir. Si le couloir, choisissez au hasard si l'élément suivant sera une pièce ou un autre couloir
  7. Vérifiez s'il y a suffisamment d'espace dans la direction sélectionnée pour créer l'élément souhaité
  8. S'il y a un lieu, créez un élément, sinon, revenez à l'étape 4
  9. Répétez à partir de l'étape 4

C'est tout. Nous obtiendrons une carte simple du donjon, dans laquelle il n'y a que des salles et des couloirs, sans portes et éléments spéciaux, mais ce sera notre début. Plus tard, nous le remplirons de coffres, d'ennemis et de pièges. Et vous pouvez même le personnaliser: nous apprendrons à ajouter les éléments intéressants dont vous avez besoin.

Étape 5 - découpez la pièce


Procédez enfin au codage! Coupons notre première chambre.

Tout d'abord, créez un nouveau script et appelez-le DungeonGenerator. Il héritera de Monobehaviour, vous devrez donc le rattacher à GameObject plus tard. Ensuite, nous devrons déclarer plusieurs variables publiques dans la classe afin de pouvoir définir les paramètres du donjon à partir de l'inspecteur. Ces variables seront la largeur et la hauteur de la carte, la hauteur et la largeur minimales et maximales des pièces, la longueur maximale des couloirs et le nombre d'éléments qui devraient figurer sur la carte.


Ensuite, nous devons initialiser le générateur de donjon. Nous faisons cela pour initialiser les variables qui seront remplies par la génération. Pour l'instant, ce ne sera qu'une carte. Et, et supprimez également les fonctions Start () et Update () générées par Unity pour le nouveau script, nous n'en aurons pas besoin.



Ici, nous avons initialisé la variable de carte de la classe MapManager (que nous avons créée à l'étape précédente), en passant la largeur et la hauteur de la carte, définies par les variables ci-dessus comme paramètres des deux dimensions du tableau. Grâce à cela, nous aurons une carte de taille x horizontale (largeur) et de taille y verticale (hauteur), et nous pouvons accéder à n'importe quelle cellule de la carte en entrant MapManager.map [x, y]. Cela sera très utile lors de la manipulation de la position des objets.

Nous allons maintenant créer une fonction pour rendre la première pièce. Nous l'appellerons FirstRoom (). Nous avons fait d'InitializeDungeon () une fonction publique, car il sera lancé par un autre script (Game Manager, que nous allons bientôt créer; il centralisera la gestion de l'ensemble du processus de lancement du jeu). Nous n'avons pas besoin de scripts externes pour avoir accès à FirstRoom (), nous ne le rendons donc pas public.

Maintenant, pour continuer, nous allons créer trois nouvelles classes dans le script MapManager afin que vous puissiez créer une pièce. Il s'agit des classes d'objets, de murs et de positions. La classe Position contiendra les positions x et y afin que nous puissions suivre où tout se trouve. Le mur aura une liste de positions, la direction dans laquelle il "regarde" par rapport au centre de la pièce (nord, sud, est ou ouest), la longueur et la présence d'un nouvel élément créé à partir de celui-ci. L'élément aura une liste de toutes les positions qui le composent, le type de l'élément (pièce ou couloir), un tableau de variables de mur, ainsi que sa largeur et sa hauteur.



Passons maintenant à la fonction FirstRoom (). Revenons au script DungeonGenerator et créons une fonction juste en dessous d'InitializeDungeon. Elle n'aura pas besoin de recevoir de paramètres, nous allons donc laisser les choses simples (). Ensuite, à l'intérieur de la fonction, nous devons d'abord créer et initialiser la variable Room et sa liste de variables Position. Nous le faisons comme ceci:


Définissons maintenant la taille de la pièce. Il recevra une valeur aléatoire entre la hauteur et la largeur minimales et maximales déclarées au début du script. Bien qu'ils soient vides, car nous n'avons pas défini de valeur pour eux dans l'inspecteur, mais ne vous inquiétez pas, nous le ferons bientôt. Nous définissons des valeurs aléatoires comme ceci:


Ensuite, nous devons déclarer où le point de départ de la pièce sera situé, c'est-à-dire où le point de la pièce 0.0 sera situé dans la grille de la carte. Nous voulons le faire commencer au centre de la carte (mi-largeur et mi-hauteur), mais peut-être pas exactement au centre. Il pourrait être utile d'ajouter un petit randomiseur pour qu'il se déplace légèrement vers la gauche et vers le bas. Par conséquent, nous définissons xStartingPoint comme la moitié de la largeur de la carte, et yStartingPoint comme la moitié de la hauteur de la carte, puis prenons la roomWidth et roomHeight juste donnée, nous obtenons une valeur aléatoire de 0 à cette largeur / hauteur, et la soustrayons des x et y initiaux. Comme ça:



Ensuite, dans la même fonction, nous ajouterons des murs. Nous devons initialiser le tableau des murs qui se trouvent dans la variable de pièce nouvellement créée, puis initialiser chaque variable de mur à l'intérieur de ce tableau. Ensuite, initialisez chaque liste de positions, définissez la longueur du mur à 0 et entrez la direction dans laquelle chaque mur «se penchera».

Une fois le tableau initialisé, nous bouclons autour de chaque élément du tableau dans la boucle for (), initialisons les variables de chaque mur, puis utilisons le commutateur, qui nomme la direction de chaque mur. Il est choisi arbitrairement, il suffit de se rappeler ce qu'il signifiera.


Nous allons maintenant exécuter deux boucles imbriquées immédiatement après avoir placé les murs. Dans la boucle externe, nous contournons toutes les valeurs y dans la pièce, et dans la boucle imbriquée, toutes les valeurs x. De cette façon, nous vérifierons chaque cellule x de la ligne y afin de pouvoir l'implémenter.


Ensuite, la première chose à faire est de trouver la valeur réelle de la position de la cellule sur l'échelle de la carte à partir de la position de la pièce. C'est assez simple: nous avons les points de départ x et y. Ils seront positionnés 0,0 dans la grille de la pièce. Ensuite, si nous devons obtenir la valeur réelle de x, y à partir de n'importe quel x, y local, nous ajoutons les x et y locaux avec les positions initiales x et y. Ensuite, nous enregistrons ces valeurs réelles x, y dans la variable Position (à partir d'une classe précédemment créée), puis les ajoutons à la liste <> des positions de la pièce.


L'étape suivante consiste à ajouter ces informations à la carte. Avant de modifier les valeurs, n'oubliez pas d'initialiser la variable Tile.


Nous allons maintenant modifier la classe Tile. Passons au script MapManager et ajoutons une ligne à la définition de la classe Tile: «type de chaîne publique;». Cela nous permettra d'ajouter une classe de tuiles en déclarant que la tuile en x, y est un mur, un sol ou autre chose. Ensuite, revenons au cycle dans lequel nous avons fait le travail et ajoutons une grande construction if-else, qui nous permettra non seulement de déterminer chaque mur, sa longueur et toutes les positions dans ce mur, mais aussi de spécifier sur la carte globale ce qu'est une tuile spécifique - un mur ou le sexe.


Et nous avons déjà fait quelque chose. Si la variable y (contrôle de la variable dans la boucle externe) est 0, la tuile appartient à la rangée de cellules la plus basse de la pièce, c'est-à-dire qu'il s'agit du mur sud. Si x (contrôle de la variable de boucle intérieure) est égal à 0, alors la tuile appartient à la colonne de cellules la plus à gauche, c'est-à-dire qu'il s'agit du mur ouest. Et si c'est sur la ligne la plus haute, alors il appartient au mur nord, et tout à droite - le mur est. Nous soustrayons 1 des variables roomWidth et roomHeight, car ces valeurs ont été calculées à partir de 1 et les variables x et y du cycle ont commencé à 0, nous devons donc tenir compte de cette différence. Et toutes les cellules qui ne remplissent pas les conditions ne sont pas des murs, c'est-à-dire qu'elles sont le sol.


Super, nous avons presque fini avec la première chambre. Il est presque prêt, il suffit de mettre les dernières valeurs dans la variable Feature que nous avons créée. Nous quittons la boucle et terminons la fonction comme ceci:


Bien! Nous avons une chambre!

Mais comment comprendre que tout fonctionne? Besoin de tester. Mais comment tester? Nous pouvons passer du temps et ajouter des actifs pour cela, mais ce sera une perte de temps et nous distraira également de terminer l'algorithme. Hmm, mais cela peut être fait en utilisant ASCII! Oui, bonne idée! ASCII est un moyen simple et peu coûteux de dessiner une carte afin qu'elle puisse être testée. De plus, si vous le souhaitez, vous pouvez ignorer la partie avec des sprites et des effets visuels, que nous étudierons plus tard, et créer votre jeu entier en ASCII. Voyons donc comment cela se fait.

Étape 6 - dessiner la première salle


La première chose à considérer lors de la mise en œuvre d'une carte ASCII est la police à choisir. Le principal facteur à considérer lors du choix d'une police pour ASCII est de savoir si elle est proportionnelle (largeur variable) ou monospace (largeur fixe). Nous avons besoin d'une police monospace pour que les cartes aient l'air nécessaires (voir l'exemple ci-dessous). Par défaut, tout nouveau projet Unity utilise la police Arial, et il n'est pas à espacement fixe, nous devons donc en trouver un autre. Windows 10 a généralement des polices à espacement fixe Courier New, Consolas et Lucida Console. Choisissez l'un de ces trois ou téléchargez-en un autre à l'endroit dont vous avez besoin et placez-le dans le dossier Polices à l'intérieur du dossier Actifs du projet.


Préparons la scène pour la sortie ASCII. Pour commencer, rendez la couleur d'arrière-plan de la caméra principale de la scène noire. Ensuite, nous ajoutons l'objet Canvas à la scène et y ajoutons l'objet Text. Définissez la transformation du rectangle de texte au milieu du centre et à la position 0,0,0. Définissez l'objet Texte de sorte qu'il utilise la police que vous avez sélectionnée et la couleur blanche, débordement horizontal et vertical (débordement horizontal / vertical), sélectionnez Débordement et centrez l'alignement vertical et horizontal. Renommez ensuite l'objet Text en «ASCIITest» ou quelque chose de similaire.

Revenons maintenant au code. Dans le script DungeonGenerator, créez une nouvelle fonction appelée DrawMap. Nous voulons qu'elle obtienne un paramètre indiquant quelle carte générer - ASCII ou sprite, alors créez un paramètre booléen et appelez-le isCASII.


Ensuite, nous vérifierons si la carte rendue est ASCII. Si oui (pour l'instant, nous ne considérerons que ce cas), alors nous rechercherons un objet texte dans la scène, passerons le nom qui lui est donné en paramètre et obtiendrons son composant Text. Mais d'abord, nous devons dire à Unity que nous voulons travailler avec l'interface utilisateur. Ajoutez la ligne utilisant UnityEngine.UI à l'en-tête de script:


Bien. Nous pouvons maintenant obtenir le composant Text de l'objet. La carte sera une énorme ligne qui se reflétera sur l'écran sous forme de texte. C'est pourquoi il est si facile à installer. Créons donc une chaîne et initialisons-la avec la valeur "".


Bien. Ainsi, à chaque appel de DrawMap, nous devrons indiquer si la carte est un ASCII. Si c'est le cas (et nous l'aurons toujours de cette façon, nous travaillerons avec d'autres plus tard), alors la fonction cherchera la hiérarchie des scènes à la recherche d'un objet de jeu appelé "ASCIITest". Si c'est le cas, il recevra son composant Texte et l'enregistrera dans la variable d'écran, dans laquelle nous pourrons ensuite facilement écrire la carte. Il crée ensuite une chaîne dont la valeur est initialement vide. Nous remplirons cette ligne avec notre carte marquée de symboles.

Habituellement, nous parcourons la carte en boucle, commençant à 0 et allant jusqu'à la fin de sa longueur. Mais pour remplir la ligne, nous commençons par la première ligne de texte, c'est-à-dire la ligne la plus haute. Par conséquent, sur l'axe y, nous devons nous déplacer dans une boucle dans la direction opposée, allant de la fin au début du tableau. Mais l'axe des x du tableau va de gauche à droite, tout comme le texte, donc cela nous convient.


Dans ce cycle, nous vérifions chaque cellule de la carte pour découvrir ce qu'elle contient. Jusqu'à présent, nous avons seulement initialisé les cellules en tant que nouveau Tile (), que nous avons découpé pour la pièce, donc tout le monde retournera une erreur lors de la tentative d'accès. Nous devons donc d'abord vérifier s'il y a quelque chose dans cette cellule, et nous le faisons en vérifiant la cellule pour null. S'il n'est pas nul, alors nous continuons à travailler, mais s'il est nul, alors il n'y a rien à l'intérieur, nous pouvons donc ajouter de l'espace vide à la carte.


Ainsi, pour chaque cellule non vide, nous vérifions son type, puis ajoutons le symbole correspondant. Nous voulons que les murs soient indiqués par le symbole "#" et les sols par le ".". Et tandis que nous n'avons que ces deux types. Plus tard, lorsque nous ajouterons le joueur, les monstres et les pièges, tout sera un peu plus compliqué.


De plus, nous devons effectuer des sauts de ligne lorsque nous atteignons la fin de la ligne du tableau, de sorte que les cellules ayant la même position x se trouvent directement les unes sous les autres. Nous vérifierons à chaque itération de la boucle si la cellule est la dernière de la ligne, puis ajouterons un saut de ligne avec le caractère spécial "\ n".


C'est tout. Ensuite, nous quittons la boucle pour pouvoir ajouter cette ligne une fois terminée à l'objet texte dans la scène.



Toutes nos félicitations! Vous avez terminé le script qui crée la salle et l'affiche à l'écran. Il ne nous reste plus qu'à mettre ces lignes en action. Nous n'utilisons pas Start () dans le script DungeonGenerator, car nous voulons avoir un script séparé pour contrôler tout ce qui est effectué au début du jeu, y compris la génération de cartes, mais aussi les paramètres du joueur, les ennemis, etc. Par conséquent, cet autre script contiendra la fonction Start () et, si nécessaire, appellera les fonctions de notre script. Le script DungeonGenerator a une fonction Initialize, qui est publique, et FirstRoom et DrawMap ne sont pas publics. Initialiser initialise simplement les variables pour configurer le processus de génération de donjon, nous avons donc besoin d'une autre fonction qui appelle le processus de génération, qui doit être public afin qu'il puisse être appelé à partir d'autres scripts.Pour l'instant, elle n'appellera que la fonction FirstRoom (), puis la fonction DrawMap (), en lui passant une valeur vraie pour qu'elle dessine une carte ASCII. Oh, ou pas, c'est encore mieux - créons une variable publique isASCII, qui peut être incluse dans l'inspecteur, et passons simplement cette variable comme paramètre à la fonction. Bien.


Créons maintenant un script GameManager. Ce sera le script même qui contrôlera tous les éléments de haut niveau du jeu, par exemple, la création d'une carte et le déroulement des mouvements. Supprimons la fonction Update (), ajoutons une variable de type DungeonGenerator appelée dungeonGenerator et créons une instance de cette variable dans la fonction Start ().


Après cela, nous appelons simplement les fonctions InitializeDungeon () et GenerateDungeon () à partir de dungeonGenerator, dans cet ordre . Ceci est important - vous devez d'abord initialiser les variables, et seulement après cela commencer à construire sur leur base.


Sur cette partie avec le code est terminé. Nous devons créer un objet de jeu vide dans le panneau de hiérarchie, le renommer en GameManager et y attacher les scripts GameManager et DungeonGenerator. Et puis définissez les valeurs du générateur de donjon dans l'inspecteur. Vous pouvez essayer différents schémas pour le générateur, et je me suis installé sur ceci:


Maintenant, cliquez simplement sur jouer et regardez la magie! Vous devriez voir quelque chose de similaire sur l'écran du jeu:


Félicitations, nous avons maintenant une chambre!

Je voulais que nous y mettions le personnage du joueur et le fassions bouger, mais le message était déjà assez long. Par conséquent, dans la partie suivante, nous pouvons procéder directement à la mise en œuvre du reste de l'algorithme du donjon, ou nous pouvons y placer le joueur et lui apprendre à se déplacer. Votez ce que vous préférez dans les commentaires de l'article original.

MapManager.cs:

using System.Collections;
using System; // So the script can use the serialization commands
using System.Collections.Generic;
using UnityEngine;

public class MapManager {
    public static Tile[,] map; // the 2-dimensional map with the information for all the tiles
}

[Serializable] // Makes the class serializable so it can be saved out to a file
public class Tile { // Holds all the information for each tile on the map
    public int xPosition; // the position on the x axis
    public int yPosition; // the position on the y axis
    [NonSerialized]
    public GameObject baseObject; // the map game object attached to that position: a floor, a wall, etc.
    public string type; // The type of the tile, if it is wall, floor, etc
}

[Serializable]
public class Position { //A class that saves the position of any cell
    public int x;
    public int y;
}

[Serializable]
public class Wall { // A class for saving the wall information, for the dungeon generation algorithm
    public List<Position> positions;
    public string direction;
    public int length;
    public bool hasFeature = false;
}

[Serializable]
public class Feature { // A class for saving the feature (corridor or room) information, for the dungeon generation algorithm
    public List<Position> positions;
    public Wall[] walls;
    public string type;
    public int width;
    public int height;
}

DungeonGenerator.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DungeonGenerator : MonoBehaviour
{
    public int mapWidth;
    public int mapHeight;

    public int widthMinRoom;
    public int widthMaxRoom;
    public int heightMinRoom;
    public int heightMaxRoom;

    public int maxCorridorLength;
    public int maxFeatures;

    public bool isASCII;

    public void InitializeDungeon() {
        MapManager.map = new Tile[mapWidth, mapHeight];
    }

    public void GenerateDungeon() {
        FirstRoom();
        DrawMap(isASCII);
    }

    void FirstRoom() {
        Feature room = new Feature();
        room.positions = new List<Position>();

        int roomWidth = Random.Range(widthMinRoom, widthMaxRoom);
        int roomHeight = Random.Range(heightMinRoom, heightMaxRoom);

        int xStartingPoint = mapWidth / 2;
        int yStartingPoint = mapHeight / 2;

        xStartingPoint -= Random.Range(0, roomWidth);
        yStartingPoint -= Random.Range(0, roomHeight);

        room.walls = new Wall[4];

        for (int i = 0; i < room.walls.Length; i++) {
            room.walls[i] = new Wall();
            room.walls[i].positions = new List<Position>();
            room.walls[i].length = 0;

            switch (i) {
                case 0:
                    room.walls[i].direction = "South";
                    break;
                case 1:
                    room.walls[i].direction = "North";
                    break;
                case 2:
                    room.walls[i].direction = "West";
                    break;
                case 3:
                    room.walls[i].direction = "East";
                    break;
            }
        }

        for (int y = 0; y < roomHeight; y++) {
            for (int x = 0; x < roomWidth; x++) {
                Position position = new Position();
                position.x = xStartingPoint + x;
                position.y = yStartingPoint + y;

                room.positions.Add(position);

                MapManager.map[position.x, position.y] = new Tile();
                MapManager.map[position.x, position.y].xPosition = position.x;
                MapManager.map[position.x, position.y].yPosition = position.y;

                if (y == 0) {
                    room.walls[0].positions.Add(position);
                    room.walls[0].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (y == (roomHeight - 1)) {
                    room.walls[1].positions.Add(position);
                    room.walls[1].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (x == 0) {
                    room.walls[2].positions.Add(position);
                    room.walls[2].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (x == (roomWidth - 1)) {
                    room.walls[3].positions.Add(position);
                    room.walls[3].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else {
                    MapManager.map[position.x, position.y].type = "Floor";
                }
            }
        }

        room.width = roomWidth;
        room.height = roomHeight;
        room.type = "Room";
    }

    void DrawMap(bool isASCII) {
        if (isASCII) {
            Text screen = GameObject.Find("ASCIITest").GetComponent<Text>();

            string asciiMap = "";

            for (int y = (mapHeight - 1); y >= 0; y--) {
                for (int x = 0; x < mapWidth; x++) {
                    if (MapManager.map[x,y] != null) {
                        switch (MapManager.map[x, y].type) {
                            case "Wall":
                                asciiMap += "#";
                                break;
                            case "Floor":
                                asciiMap += ".";
                                break;
                        }
                    } else {
                        asciiMap += " ";
                    }

                    if (x == (mapWidth - 1)) {
                        asciiMap += "\n";
                    }
                }
            }

            screen.text = asciiMap;
        }
    }
}

GameManager.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    DungeonGenerator dungeonGenerator;
    
    void Start() {
        dungeonGenerator = GetComponent<DungeonGenerator>();

        dungeonGenerator.InitializeDungeon();
        dungeonGenerator.GenerateDungeon();
    }
}

All Articles