Creando roguelike en Unity desde cero

imagen

No hay muchos tutoriales sobre cómo crear roguelike en Unity, así que decidí escribirlo. No para alardear, sino para compartir conocimientos con aquellos que están en la etapa en la que ya estaba bastante tiempo.

Nota: No digo que esta sea la única forma de crear un roguelike en Unity. Él es solo uno de . Probablemente no sea el mejor y más efectivo, aprendí a través de prueba y error. Y aprenderé algunas cosas correctamente en el proceso de creación de un tutorial.

Supongamos que conoce al menos los conceptos básicos de Unity, por ejemplo, cómo crear un prefabricado o script, y similares. No esperes que te enseñe cómo crear hojas de sprites, hay muchos tutoriales geniales sobre esto. No me centraré en estudiar el motor, sino en cómo implementar el juego que crearemos juntos. Si tiene dificultades, diríjase a una de las increíbles comunidades de Discord y solicite ayuda:

Unity Developer Community

Roguelikes

Entonces, ¡comencemos!

Etapa 0 - planificación


Si eso es correcto. Lo primero que hay que crear es un plan. Será bueno para ti planificar el juego, y para mí, planificar el tutorial para que después de un tiempo no nos distraigamos del tema. Es fácil confundirse con las funciones del juego, al igual que en las mazmorras de roguelike.

Escribiremos roguelike. Seguiremos principalmente los sabios consejos del desarrollador de Cogmind Josh Ge aquí . Sigue el enlace, lee la publicación o mira el video, y luego regresa.

¿Cuál es el propósito de este tutorial? Obtenga un sólido roguelike básico simple, con el que luego puede experimentar. Debería tener generación de mazmorras, un jugador moviéndose en el mapa, niebla de visibilidad, enemigos y objetos. Solo lo más necesario. Entonces, el jugador debería poder bajar las escaleras varios pisos. digamos, por cinco, aumente su nivel, mejore y al final pelee con el jefe y derrótelo. O morir. Eso, de hecho, es todo.

Siguiendo los consejos de Josh Ge, construiremos las funciones del juego para que nos lleven a la meta. Por lo tanto, obtenemos el marco roguelike, que puede ampliarse aún más, agregar sus propios chips, creando singularidad. O arroje todo a la canasta, aproveche la experiencia adquirida y comience de cero. Será genial de todos modos.

No te daré ningún recurso gráfico. Dibújelos usted mismo o use los mosaicos gratuitos, que se pueden descargar aquí , aquí o buscando en Google. Solo no olvides mencionar a los autores de los gráficos en el juego.

Ahora enumeremos todas las funciones que estarán en nuestro roguelike en el orden de su implementación:

  1. Generación de mapas de mazmorras
  2. El personaje del jugador y su movimiento.
  3. Área de visibilidad
  4. Enemigos
  5. Busca un camino
  6. Lucha, salud y muerte
  7. Nivel de jugador arriba
  8. Artículos (armas y pociones)
  9. Trucos de consola (para pruebas)
  10. Pisos de mazmorras
  11. Guardar y cargar
  12. Jefe final

Después de implementar todo esto, tendremos un fuerte roguelike y aumentarás enormemente tus habilidades de desarrollo de juegos. En realidad, era mi forma de mejorar mis habilidades: crear código e implementar funciones. Por lo tanto, estoy seguro de que puede manejar esto también.

Etapa 1 - Clase MapManager


Este es el primer script que crearemos y se convertirá en la columna vertebral de nuestro juego. Es simple, pero contiene la mayor parte de la información importante para el juego.

Entonces, cree un script .cs llamado MapManager y ábralo.

Elimine ": MonoBehaviour" porque no heredará de él y no se adjuntará a ningún GameObject.

Elimine las funciones Start () y Update ().

Al final de la clase MapManager, cree una nueva clase pública llamada Tile.


La clase Tile contendrá toda la información de un solo mosaico. Hasta ahora, no necesitamos mucho, solo posiciones x e y, así como un objeto de juego ubicado en esta posición del mapa.


Entonces, tenemos información básica sobre mosaicos. Creemos un mapa a partir de este mosaico. Es simple, solo necesitamos una matriz bidimensional de objetos Tile. Suena complicado, pero no tiene nada de especial. Simplemente agregue la variable Tile [,] a la clase MapManager:


Voila! Tenemos un mapa!

Si, esta vacio. Pero este es un mapa. Cada vez que algo se mueve o cambia de estado en el mapa, la información en este mapa se actualizará. Es decir, si, por ejemplo, un jugador intenta cambiar a una nueva casilla, la clase verificará la dirección de la casilla de destino en el mapa, la presencia del enemigo y su permeabilidad. Gracias a esto, no tenemos que verificar miles de colisiones en cada turno, y no necesitamos colisionadores para cada objeto del juego, lo que facilitará y simplificará el trabajo con el juego.

El código resultante se ve así:

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 primera etapa se completa, pasemos a completar la tarjeta. Ahora comenzaremos a crear un generador de mazmorras.

Etapa 2: un par de palabras sobre la estructura de datos


Pero antes de comenzar, permítanme compartir los consejos que han surgido gracias a los comentarios recibidos después de la publicación de la primera parte. Al crear una estructura de datos, debes pensar desde el principio cómo mantendrás el estado del juego. De lo contrario, más tarde será mucho más caótico. El usuario de Discord st33d, el desarrollador de Star Shaped Bagel (puedes jugar este juego gratis aquí ), dijo que al principio creó el juego, pensando que no salvaría estados en absoluto. Poco a poco, el juego comenzó a hacerse más grande, y su fan le pidió apoyo para el mapa guardado. Pero debido al método elegido para crear la estructura de datos, fue muy difícil guardar los datos, por lo que no pudo hacerlo.


Realmente aprendemos de nuestros errores. A pesar de que puse la parte de guardar / cargar al final del tutorial, pienso en ellos desde el principio, y todavía no los he explicado. En esta parte hablaré un poco sobre ellos, pero para no sobrecargar a los desarrolladores inexpertos.

Guardaremos cosas como una matriz de variables de la clase Tile en la que se almacena el mapa. Guardaremos todos estos datos, excepto las variables de la clase GameObject, que están dentro de la clase Tile. ¿Por qué? Solo porque GameObjects no se puede serializar con Unity a los datos almacenados.

Por lo tanto, de hecho, no necesitamos guardar los datos almacenados dentro de GameObjects. Todos los datos se almacenarán en clases como Tile, y más tarde también Player, Enemy, etc. Luego tendremos GameObjects para simplificar el cálculo de cosas como la visibilidad y el movimiento, así como dibujar sprites en la pantalla. Por lo tanto, dentro de las clases habrá variables GameObject, pero el valor de estas variables no se guardará ni cargará. Al cargar, forzaremos a generar GameObject nuevamente a partir de los datos guardados (posición, sprite, etc.).

Entonces, ¿qué necesitamos hacer ahora? Bueno, solo agregue dos líneas a la clase Tile existente y una a la parte superior del script. Primero agregamos "usando el sistema"; al título del script, y luego [Serializable] delante de toda la clase y [No serializado] justo delante de la variable GameObject. Me gusta esto:



Te contaré más sobre esto cuando lleguemos a la parte del tutorial sobre guardar / cargar. Por ahora, dejemos todo y sigamos adelante.

Etapa 3: un poco más sobre la estructura de datos


Obtuve otra revisión sobre la estructura de datos que quiero compartir aquí.

De hecho, hay muchas formas de implementar datos en un juego. El primero que utilizo y que se implementará en este tutorial: todos los datos del mosaico están en la clase Tile, y todos se almacenan en una matriz. Este enfoque tiene muchas ventajas: es más fácil de leer, todo lo que necesita está en un solo lugar, los datos son más fáciles de manipular y exportar a un archivo guardado. Pero desde el punto de vista de la memoria, no es tan efectivo. Tendrás que asignar mucha memoria para variables que nunca se usarán en el juego. Por ejemplo, más adelante colocaremos la variable Enemy GameObject en la clase Tile para que podamos apuntar directamente desde el mapa al GameObject del enemigo que está parado en este mosaico para simplificar todos los cálculos relacionados con la batalla. Pero esto significará que cada mosaico en el juego tendrá espacio asignado en la memoria para la variable GameObject,incluso si no hay enemigo en este azulejo. Si hay 10 enemigos en un mapa de 2500 fichas, habrá 2490 vacías, pero variables de GameObject asignadas: puede ver cuánta memoria se desperdicia.

Un método alternativo sería usar estructuras para almacenar los datos básicos de los mosaicos (por ejemplo, posición y tipo), y todos los demás datos se almacenarían en hashmap-s, que se generarían solo si fuera necesario. Esto ahorraría mucha memoria, pero la recuperación sería una implementación un poco más complicada. En realidad, sería un poco más avanzado de lo que me gustaría en este tutorial, pero si lo desea, en el futuro puedo escribir una publicación más detallada al respecto.

Además, si desea leer una discusión sobre este tema, puede hacerlo en Reddit .

Etapa 4 - Algoritmo de generación de mazmorras


Sí, esta es otra sección en la que hablaré y no comenzaremos a programar nada. Pero esto es importante, una planificación cuidadosa de los algoritmos nos ahorrará mucho tiempo de trabajo en el futuro.

Hay varias formas de crear un generador de mazmorras. La que implementaremos juntos no es la mejor ni la más efectiva ... es solo una forma fácil e inicial. Es muy simple, pero los resultados serán bastante buenos. El principal problema serán muchos pasillos sin salida. Más tarde, si lo desea, puedo publicar otro tutorial sobre mejores algoritmos.

En general, el algoritmo que utilizamos funciona de la siguiente manera: digamos que tenemos un mapa completo lleno de valores cero, un nivel que consiste en una piedra. Al principio, cortamos una habitación en el centro. Desde esta sala, atravesamos el corredor en una dirección, y luego agregamos otros pasillos y salas, siempre comenzando de manera aleatoria desde una sala o corredor existente, hasta que alcanzamos el número máximo de corredores / salas dados al principio. O hasta que el algoritmo pueda encontrar un nuevo lugar para agregar una nueva habitación / corredor, lo que ocurra primero. Y así tenemos un calabozo.

Entonces, describamos esto de una manera más parecida a un algoritmo, paso a paso. Por conveniencia, llamaré a cada detalle del mapa (corredor o habitación) un elemento para que no tenga que decir "habitación / corredor" cada vez.

  1. Cortar la habitación en el centro del mapa.
  2. Selecciona aleatoriamente uno de los muros
  3. Atravesamos el corredor en este muro
  4. Seleccione aleatoriamente uno de los elementos existentes.
  5. Seleccione aleatoriamente uno de los muros de este elemento.
  6. Si el último elemento seleccionado es una habitación, generamos un corredor. Si es el corredor, elija al azar si el siguiente elemento será una habitación u otro corredor
  7. Compruebe si hay suficiente espacio en la dirección seleccionada para crear el elemento deseado.
  8. Si hay un lugar, cree un elemento, si no, regrese al paso 4
  9. Repita desde el paso 4

Eso es todo. Obtendremos un mapa simple de la mazmorra, en el que solo hay habitaciones y pasillos, sin puertas y elementos especiales, pero este será nuestro comienzo. Más tarde lo llenaremos con cofres, enemigos y trampas. E incluso puede personalizarlo: aprenderemos cómo agregar elementos interesantes que necesita.

Etapa 5 - corta la habitación


¡Finalmente proceda a la codificación! Cortemos nuestra primera habitación.

Primero, crea un nuevo script y llámalo DungeonGenerator. Heredará de Monobehaviour, por lo que deberá adjuntarlo a GameObject más adelante. Luego necesitaremos declarar varias variables públicas en la clase para que podamos establecer los parámetros de la mazmorra desde el inspector. Estas variables serán el ancho y la altura del mapa, la altura mínima y máxima y el ancho de las habitaciones, la longitud máxima de los corredores y el número de elementos que deben estar en el mapa.


Luego necesitamos inicializar el generador de mazmorras. Hacemos esto para inicializar las variables que serán pobladas por la generación. Por ahora, esto será solo un mapa. Y, y también elimine las funciones Start () y Update () que Unity genera para el nuevo script, no las necesitaremos.



Aquí inicializamos la variable de mapa de la clase MapManager (que creamos en el paso anterior), pasando el ancho y la altura del mapa, definidos por las variables anteriores como parámetros de las dos dimensiones de la matriz. Gracias a esto, tendremos un mapa de tamaño x horizontal (ancho) y tamaño y vertical (altura), y podemos acceder a cualquier celda del mapa ingresando MapManager.map [x, y]. Esto será muy útil al manipular la posición de los objetos.

Ahora crearemos una función para representar la primera sala. Lo llamaremos FirstRoom (). Hicimos InitializeDungeon () una función pública, ya que será lanzada por otro script (Game Manager, que pronto crearemos; centralizará la gestión de todo el proceso de lanzamiento del juego). No necesitamos ningún script externo para tener acceso a FirstRoom (), por lo que no lo hacemos público.

Ahora, para continuar, crearemos tres nuevas clases en el script de MapManager para que pueda crear una sala. Estas son las clases de entidad, muro y posición. La clase Posición contendrá las posiciones x e y para que podamos rastrear dónde está todo. El muro tendrá una lista de posiciones, la dirección en la que "se ve" en relación con el centro de la habitación (norte, sur, este u oeste), la longitud y la presencia de un nuevo elemento creado a partir de él. El elemento tendrá una lista de todas las posiciones que lo componen, el tipo de elemento (habitación o corredor), una matriz de variables de Muro y su ancho y alto.



Ahora vamos a la función FirstRoom (). Volvamos al script DungeonGenerator y creemos una función justo debajo de InitializeDungeon. Ella no necesitará recibir ningún parámetro, por lo que lo dejaremos simple (). A continuación, dentro de la función, primero debemos crear e inicializar la variable Room y su lista de variables de posición. Lo hacemos así:


Ahora configuremos el tamaño de la habitación. Recibirá un valor aleatorio entre la altura mínima y máxima y el ancho declarado al comienzo del script. Mientras estén vacíos, porque no les hemos establecido un valor en el inspector, pero no se preocupe, lo haremos pronto. Establecemos valores aleatorios como este:


A continuación, debemos declarar dónde se ubicará el punto de partida de la habitación, es decir, dónde se ubicará el punto de la habitación 0.0 en la cuadrícula del mapa. Queremos que comience en el centro del mapa (medio ancho y medio alto), pero tal vez no exactamente en el centro. Puede valer la pena agregar un pequeño aleatorizador para que se mueva ligeramente hacia la izquierda y hacia abajo. Por lo tanto, establecemos xStartingPoint como la mitad del ancho del mapa, y yStartingPoint como la mitad de la altura del mapa, y luego tomamos el valor de roomWidth y roomHeight, obtenemos un valor aleatorio de 0 a este ancho / altura, y lo restamos de las x e y iniciales. Me gusta esto:



A continuación, en la misma función agregaremos muros. Necesitamos inicializar la matriz de muros que se encuentran en la variable de habitación recién creada, y luego inicializar cada variable de pared dentro de esta matriz. Y luego inicialice cada lista de posiciones, establezca la longitud del muro en 0 e ingrese la dirección en la que cada muro "mirará".

Después de que se inicializa la matriz, recorremos cada elemento de la matriz en el bucle for (), inicializamos las variables de cada muro y luego usamos el interruptor, que nombra la dirección de cada muro. Se elige arbitrariamente, solo necesitamos recordar lo que significarán.


Ahora ejecutaremos dos bucles anidados para inmediatamente después de colocar las paredes. En el bucle externo, rodeamos todos los valores de y en la sala, y en el bucle anidado, todos los valores de x. De esta forma comprobaremos cada celda x en la fila y para que podamos implementarla.


Entonces, lo primero que debe hacer es encontrar el valor real de la posición de la celda en la escala del mapa desde la posición de la habitación. Esto es bastante simple: tenemos los puntos de partida x e y. Estarán en la posición 0,0 en la cuadrícula de la sala. Entonces, si necesitamos obtener el valor real de x, y de cualquier x local, y, entonces sumamos el x e y local con las posiciones iniciales x e y. Luego guardamos estos valores reales x, y en la variable Posición (de una clase creada previamente) y luego los agregamos a la Lista <> de las posiciones de la sala.


El siguiente paso es agregar esta información al mapa. Antes de cambiar los valores, recuerde inicializar la variable Tile.


Ahora haremos un cambio en la clase Tile. Vayamos al script de MapManager y agreguemos una línea a la definición de la clase Tile: "public string type;". Esto nos permitirá agregar una clase de mosaico al declarar que el mosaico en x, y es una pared, piso u otra cosa. Luego, volvamos al ciclo en el que hicimos el trabajo y agreguemos una gran construcción if-else, que nos permitirá no solo determinar cada muro, su longitud y todas las posiciones en este muro, sino también definir en el mapa global qué es un mosaico específico: un muro o género


Y ya hemos hecho algo. Si la variable y (control de la variable en el bucle externo) es 0, entonces el mosaico pertenece a la fila más baja de celdas en la habitación, es decir, es la pared sur. Si x (control de la variable del bucle interno) es 0, entonces el mosaico pertenece a la columna de celdas más a la izquierda, es decir, es el muro occidental. Y si está en la línea superior, entonces pertenece al muro norte, y en el extremo derecho, el muro este. Restamos 1 de las variables roomWidth y roomHeight, porque estos valores se calcularon a partir de 1, y las variables x e y del ciclo comenzaron desde 0, por lo que esta diferencia debe tenerse en cuenta. Y todas las células que no cumplen con las condiciones no son paredes, es decir, son el piso.


Genial, casi hemos terminado con la primera habitación. Está casi listo, solo necesitamos poner los últimos valores en la variable Característica que creamos. Salimos del bucle y finalizamos la función así:


¡Multa! Tenemos un cuarto!

Pero, ¿cómo entendemos que todo funciona? Necesito probar. ¿Pero cómo hacer la prueba? Podemos dedicar tiempo y agregar recursos para esto, pero será una pérdida de tiempo y también nos distraerá de completar el algoritmo. Hmm, pero esto se puede hacer usando ASCII! Si, buena idea! ASCII es una forma simple y de bajo costo de dibujar un mapa para que pueda ser probado. Además, si lo desea, puede omitir la parte con sprites y efectos visuales, que estudiaremos más adelante, y crear todo su juego en ASCII. Así que veamos cómo se hace esto.

Etapa 6 - dibujando la primera habitación


Lo primero que debe tener en cuenta al implementar una tarjeta ASCII es qué fuente elegir. El factor principal a considerar al elegir una fuente para ASCII es si es proporcional (ancho variable) o monoespaciado (ancho fijo). Necesitamos una fuente monoespaciada para que las tarjetas se vean según sea necesario (ver ejemplo a continuación). Por defecto, cualquier nuevo proyecto de Unity usa la fuente Arial, y no es monoespaciado, por lo que necesitamos encontrar otro. Windows 10 generalmente tiene fuentes monoespaciadas Courier New, Consolas y Lucida Console. Elija uno de estos tres o descargue cualquier otro en el lugar que necesita y colóquelo en la carpeta Fuentes dentro de la carpeta Activos del proyecto.


Vamos a preparar la escena para la salida ASCII. Para empezar, haga que el color de fondo de la cámara principal de la escena sea negro. Luego agregamos el objeto Canvas a la escena y le agregamos el objeto Text. Establezca la transformación del rectángulo de texto en el centro y en la posición 0,0,0. Establezca el objeto Texto para que use la fuente que elija y el color blanco, el desbordamiento horizontal y vertical (desbordamiento horizontal / vertical), seleccione Desbordamiento y centre la alineación vertical y horizontal. Luego cambie el nombre del objeto de texto a "ASCIITest" o algo similar.

Ahora de vuelta al código. En el script DungeonGenerator, cree una nueva función llamada DrawMap. Queremos que obtenga un parámetro que indique qué tarjeta generar: ASCII o sprite, así que cree un parámetro booleano y llámelo isASCII.


Luego verificaremos si el mapa representado es ASCII. En caso afirmativo (por ahora, consideraremos solo este caso), buscaremos un objeto de texto en la escena, le pasaremos el nombre dado como parámetro y obtendremos su componente Texto. Pero primero, debemos decirle a Unity que queremos trabajar con la interfaz de usuario. Agregue la línea usando UnityEngine.UI al encabezado del script:


Multa. Ahora podemos obtener el componente de texto del objeto. El mapa será una línea enorme, que se refleja en la pantalla como texto. Por eso es tan fácil de configurar. Así que creemos una cadena e inicialícela con el valor "".


Multa. Entonces, cada vez que se llama a DrawMap, necesitaremos informar si la tarjeta es un ASCII. Si esto es así (y siempre lo tendremos de esta manera, trabajaremos con otra cosa más adelante), entonces la función buscará en la jerarquía de la escena en busca de un objeto de juego llamado "ASCIITest". Si es así, recibirá su componente de Texto y lo guardará en la variable de pantalla, en la que luego podemos escribir fácilmente el mapa. Luego crea una cadena cuyo valor está inicialmente vacío. Completaremos esta línea con nuestro mapa marcado con símbolos.

Por lo general, damos la vuelta al mapa en un bucle, comenzando en 0 y llegando al final de su longitud. Pero para llenar la línea, comenzamos con la primera línea de texto, es decir, la línea superior. Por lo tanto, en el eje y, debemos movernos en un bucle en la dirección opuesta, yendo desde el final hasta el comienzo de la matriz. Pero el eje x de la matriz va de izquierda a derecha, al igual que el texto, por lo que esto nos conviene.


En este ciclo, verificamos cada celda del mapa para averiguar qué contiene. Hasta ahora, solo hemos inicializado las celdas como un nuevo Tile (), que cortamos para la sala, por lo que todos los demás devolverán un error al intentar acceder. Entonces, primero debemos verificar si hay algo en esta celda, y lo hacemos al verificar que la celda sea nula. Si no es nulo, entonces seguimos trabajando, pero si es nulo, entonces no hay nada dentro, por lo que podemos agregar espacio vacío al mapa.


Entonces, para cada celda no vacía, verificamos su tipo y luego agregamos el símbolo correspondiente. Queremos que las paredes se indiquen con el símbolo "#" y los pisos se indiquen con el ".". Y aunque solo tenemos estos dos tipos. Más tarde, cuando agreguemos el jugador, los monstruos y las trampas, todo será un poco más complicado.


Además, debemos realizar saltos de línea al llegar al final de la fila de la matriz, de modo que las celdas con la misma posición x estén directamente una debajo de la otra. Comprobaremos en cada iteración del ciclo si la celda es la última de la fila y luego agregaremos un salto de línea con el carácter especial "\ n".


Eso es todo. Luego salimos del bucle para poder agregar esta línea después de la finalización al objeto de texto en la escena.



¡Felicidades! Ha completado la secuencia de comandos que crea la sala y la muestra en la pantalla. Ahora solo necesitamos poner estas líneas en acción. No usamos Start () en el script DungeonGenerator, porque queremos tener un script separado para controlar todo lo que se realiza al comienzo del juego, incluida la generación de mapas, pero también la configuración del jugador, los enemigos, etc. Por lo tanto, este otro script contendrá la función Start () y, si es necesario, llamará a las funciones de nuestro script. El script DungeonGenerator tiene una función Initialize, que es pública, y FirstRoom y DrawMap no son públicos. Initialize simplemente inicializa las variables para configurar el proceso de generación de mazmorras, por lo que necesitamos otra función que llame al proceso de generación, que debe ser público para que pueda llamarse desde otros scripts.Por ahora, solo llamará a la función FirstRoom (), y luego a la función DrawMap (), pasándole un valor verdadero para que dibuje un mapa ASCII. Ah, o no, es aún mejor: creemos una variable pública isASCII, que se puede incluir en el inspector, y simplemente pase esta variable como parámetro a la función. Multa.


Entonces, ahora creemos un script GameManager. Será el mismo script que controla todos los elementos de alto nivel del juego, por ejemplo, creando un mapa y el curso de los movimientos. Eliminemos la función Update (), agreguemos una variable del tipo DungeonGenerator llamada dungeonGenerator y creemos una instancia de esta variable en la función Start ().


Después de eso, simplemente llamamos a las funciones InitializeDungeon () y GenerateDungeon () desde dungeonGenerator, en ese orden . Esto es importante: primero debe inicializar las variables y solo después de eso comenzar a construir sobre la base de ellas.


En esta parte con el código se completa. Necesitamos crear un objeto de juego vacío en el panel de jerarquía, cambiarle el nombre a GameManager y adjuntarle los scripts GameManager y DungeonGenerator. Y luego establezca los valores del generador de mazmorras en el inspector. Puede probar diferentes esquemas para el generador, y me decidí por esto:


¡Ahora solo haz clic en jugar y mira la magia! Deberías ver algo similar en la pantalla del juego:


¡Felicidades, ahora tenemos una habitación!

Quería que pusiéramos al personaje del jugador allí y lo hiciéramos mover, pero la publicación ya era bastante larga. Por lo tanto, en la siguiente parte, podemos proceder directamente a la implementación del resto del algoritmo de mazmorra, o podemos colocar al jugador en él y enseñarle cómo moverse. Vota lo que más te guste en los comentarios al artículo 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