Esta vez nos sumergiremos en la implementación del algoritmo del generador de mazmorras. En el último artículo, creamos la primera sala, y ahora generaremos el resto del nivel de mazmorra.Pero antes de comenzar, me gustaría corregir un error de una publicación anterior. De hecho, en las últimas semanas he aprendido algo nuevo, por lo que parte del trabajo que he realizado está desactualizado y quiero hablar sobre ello.¿Recuerdas la clase de posición que creamos? De hecho, Unity ya tiene una clase incorporada que realiza exactamente las mismas funciones, pero con un control ligeramente mejor: es más fácil de declarar y procesar. Esta clase se llama Vector2Int. Por lo tanto, antes de comenzar, eliminaremos la clase Position de MapManager.cs y reemplazaremos cada variable de Position con la variable Vector2Int.Lo mismo debe hacerse en varios lugares en el script DungeonGenerator.cs. Ahora vamos al resto del algoritmo.Etapa 7 - generación de sala / sala
Comenzaremos con un pequeño cambio en la función FirstRoom () creada la última vez. En lugar de crear otra función para generar todos los demás elementos del mapa y duplicar un montón de código, simplemente transformamos esta función, convirtiéndola en una GenerateFeature () generalizada. Por lo tanto, cambie el nombre de FirstRoom a GenerateFeature.Ahora necesitaremos pasar parámetros a esta función. En primer lugar, debe saber qué función genera: una habitación o un pasillo. Podemos pasar una cadena llamada tipo. Luego, la función necesita conocer el punto de partida del elemento, es decir, de qué pared proviene (porque siempre creamos un nuevo elemento a partir de la pared del elemento más antiguo), y para esto, pasar como argumento de Wall es suficiente. Finalmente, la primera sala que se creará tiene características especiales, por lo que necesitamos una variable bool opcional que indique si el elemento es la primera sala. Por defecto, es falso: bool isFirst = false. Entonces el título de la función cambiará de esto:en este:Multa. El siguiente paso es cambiar la forma de calcular el ancho y la altura del elemento. Mientras los calculamos, obtenemos un valor aleatorio entre los valores mínimo y máximo de la altura y el ancho de las habitaciones; esto es ideal para las habitaciones, pero no funcionará para los corredores. Entonces, hasta ahora tenemos lo siguiente:Pero los corredores tendrán un tamaño constante de 3 de ancho o alto, dependiendo de la orientación. Por lo tanto, debemos verificar cuál es el elemento: una habitación o un corredor, y luego realizar los cálculos apropiados.Entonces. Verificamos si el artículo es una habitación. En caso afirmativo, hacemos lo mismo que antes: obtenemos un número aleatorio en el intervalo entre mínimo y máximo de altura y anchura. Pero ahora en lo mismo si necesita hacer algo un poco diferente. Necesitamos verificar la orientación del corredor. Afortunadamente, al generar un muro, guardamos información sobre en qué dirección se dirige, por lo que lo usamos para obtener la orientación del corredor.Pero aún no hemos declarado la variable minCorridorLength. Debe volver a las declaraciones de variables y declararlo, justo encima de maxCorridorLength.Ahora volvamos a nuestras declaraciones de cambio condicional. Lo que estamos haciendo aquí: obtenemos el valor de la dirección de la pared, es decir, dónde está mirando la pared, desde donde irá el corredor. La dirección solo puede tener cuatro valores posibles: Sur, Norte, Oeste y Este. En el caso del sur y el norte, el corredor tendrá un ancho de 3 (dos paredes y un piso en el medio) y una altura variable (longitud). Para el oeste y el este, todo será al revés: la altura será constantemente igual a 3, y el ancho tendrá una longitud variable. Hagamoslo.Guau. Y ahí es donde terminamos dimensionando el nuevo elemento. Ahora debe decidir dónde colocarlo. Colocamos la primera habitación en un lugar aleatorio dentro de los valores de umbral relativos al centro del mapa.Pero para todos los demás elementos, esto no funcionará. Deben comenzar al lado del punto aleatorio en la pared desde el cual se genera el elemento. Entonces cambiemos el código. Primero, debemos verificar si el elemento es la primera habitación. Si esta es la primera habitación, definimos los puntos de partida de la misma manera que antes, como la mitad del ancho y la altura del mapa.De lo contrario, si el elemento no es la primera habitación, obtenemos un punto aleatorio en la pared desde el cual se genera el elemento. Primero, debemos verificar si el muro tiene un tamaño de 3 (esto significará que es el punto final del corredor), y si es así, entonces el punto medio siempre se seleccionará, es decir, el índice 1 del conjunto de muros (con 3 elementos, el conjunto tiene índices 0, 1, 2). Pero si el tamaño no es igual a 3 (el muro no es el punto final del corredor), entonces tomamos un punto aleatorio entre el punto 1 y la longitud del muro menos 2. Esto es necesario para evitar pasajes creados en la esquina. Es decir, por ejemplo, en una pared con una longitud de 6, excluimos los índices 0 y 5 (primero y último), y seleccionamos un punto aleatorio entre los puntos 1, 2, 3 y 4.
Etapa 8: verifique si hay un lugar
Ahora creemos una nueva función justo después de que se complete la función GenerateFeature (). Necesita dos argumentos: la posición en la que comienza el elemento y la posición en la que termina. Puede usar dos variables Vector2Int como ellas. La función debe devolver un valor bool para que pueda usarse si se busca espacio.Está subrayado en rojo, porque hasta ahora no ha devuelto nada. Pronto lo arreglaremos, pero por ahora no prestaremos atención. En esta función, recorreremos todas las posiciones entre el principio y el final del elemento, y comprobaremos si la posición actual en MapManager.map es nula o si ya hay algo allí. Si hay algo allí, entonces detenemos la función y devolvemos falso. Si no, entonces continúa. Si la función llega al final del ciclo sin encontrar los lugares llenos, entonces devuelve verdadero.Además, antes de verificar la posición para nulo, necesitamos una línea para verificar si la posición está dentro del mapa. Porque de lo contrario, podemos obtener un error de índice de matriz y un bloqueo del juego.Multa. Ahora regrese al lugar donde insertamos esta función dentro de la función GenerateFeature (). Necesitamos arreglar esta llamada porque no pasa los argumentos necesarios.Aquí queremos insertar una declaración if para verificar si hay suficiente espacio para el elemento. Si el resultado es falso, finalizamos la función sin insertar un nuevo elemento en MapManager.map.Necesitamos pasar los argumentos requeridos, es decir, dos variables Vector2Int. Con el primero, todo es simple, esta es la posición con las coordenadas x e y del punto de inicio del elemento.El segundo es más difícil, pero no mucho. Este es el punto de partida más la altura para y y el ancho para x, restando 1 de ambos (porque el inicio ya se ha tenido en cuenta).Ahora pasemos al siguiente paso: crear un algoritmo para llamar a la función GenerateFeature ().Etapa 9: elementos generados por la llamada
Volver a la función GenerateDungeon () creada en la parte anterior del artículo. Ahora debería verse así:La llamada a FirstRoom () está subrayada en rojo porque cambiamos el nombre de esta función. Entonces llamemos a la primera generación de salas.Pasamos los argumentos necesarios: "Room" como tipo, porque la primera habitación siempre será Room, new Wall (), porque la primera habitación no se creará de ninguna otra, por lo que simplemente pasamos nulo, y esto es bastante normal. En lugar de nuevo Wall (), puede sustituir nulo , esto es una cuestión de preferencia personal. El último argumento determina si el nuevo elemento es la primera habitación, por lo que en nuestro caso pasamos verdadero .Ahora llegamos al punto principal. Usamos un bucle for que se ejecutará 500 veces; sí, intentaremos agregar elementos 500 veces. Pero si el número de elementos creados (variable countFeatures) es igual al número máximo especificado de elementos (variable maxFeatures), entonces interrumpimos este ciclo.El primer paso en este ciclo es declarar el elemento a partir del cual se creará el nuevo elemento. Si hemos creado solo un elemento (la primera sala), entonces será el original. De lo contrario, seleccionamos aleatoriamente uno de los elementos ya creados.Ahora elegiremos qué muro de este elemento se usará para crear el nuevo elemento.Tenga en cuenta que todavía no tenemos esta función ChoseWall (). Escribámoslo rápido. Baja hasta el final de la función y créala. Debería devolver un muro y usar un elemento como argumento, para que la función pueda seleccionar el muro de este elemento.Lo creé entre las funciones CheckIfHasSpace () y DrawMap (). Tenga en cuenta que si está trabajando en Visual Studio, que está instalado con Unity, puede usar los campos - / + a la izquierda para contraer / expandir partes del código para simplificar el trabajo.En esta función encontraremos el muro desde el cual el elemento aún no se ha creado. A veces obtendremos elementos con uno o más muros de los cuales otros elementos ya están unidos, por lo que debemos verificar una y otra vez si alguno de los muros aleatorios está libre. Para hacer esto, usamos un bucle for repetido diez veces: si después de estas diez veces no se encuentra un muro libre, entonces la función devuelve nulo.Ahora regrese a la función GenerateDungeon () y pase el elemento original como parámetro a la función ChoseWall ().La línea if (wall == null) continue;significa que si la función de búsqueda de muro devuelve falso, entonces el elemento original no puede generar un nuevo elemento, por lo tanto, la función continuará el ciclo, es decir, no podría crear un nuevo elemento y pasará a la siguiente iteración del ciclo.Ahora necesitamos seleccionar el tipo para el siguiente elemento. Si el elemento fuente es una habitación, la siguiente debe ser un pasillo (no queremos que la habitación conduzca directamente a otra habitación sin un pasillo entre ellos). Pero si este es un corredor, entonces debemos crear la probabilidad de que otro corredor o habitación sea el próximo.Multa. Ahora solo necesitamos llamar a la función GenerateFeature (), pasarle la pared y escribir como parámetros.Finalmente, vaya al inspector de Unity, seleccione el objeto GameManager y cambie los valores a lo siguiente:Si ahora hace clic en el botón de reproducción, ¡ya verá los resultados!Como dije, esta no es la mejor mazmorra. Tenemos muchos callejones sin salida. Pero es completamente funcional y garantiza que no tendrá una habitación que no esté conectada a ninguna otra.¡Espero que lo hayan disfrutado! En la próxima publicación, crearemos un jugador que se moverá a través de la mazmorra, y luego convertiremos el mapa de ASCII en sprite.using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Post3 : MonoBehaviour {
    public int mapWidth;
    public int mapHeight;
    public int widthMinRoom;
    public int widthMaxRoom;
    public int heightMinRoom;
    public int heightMaxRoom;
    public int minCorridorLength;
    public int maxCorridorLength;
    public int maxFeatures;
    int countFeatures;
    public bool isASCII;
    public List<Feature> allFeatures;
    public void InitializeDungeon() {
        MapManager.map = new Tile[mapWidth, mapHeight];
    }
    public void GenerateDungeon() {
        GenerateFeature("Room", new Wall(), true);
        for (int i = 0; i < 500; i++) {
            Feature originFeature;
            if (allFeatures.Count == 1) {
                originFeature = allFeatures[0];
            }
            else {
                originFeature = allFeatures[Random.Range(0, allFeatures.Count - 1)];
            }
            Wall wall = ChoseWall(originFeature);
            if (wall == null) continue;
            string type;
            if (originFeature.type == "Room") {
                type = "Corridor";
            }
            else {
                if (Random.Range(0, 100) < 90) {
                    type = "Room";
                }
                else {
                    type = "Corridor";
                }
            }
            GenerateFeature(type, wall);
            if (countFeatures >= maxFeatures) break;
        }
        DrawMap(isASCII);
    }
    void GenerateFeature(string type, Wall wall, bool isFirst = false) {
        Feature room = new Feature();
        room.positions = new List<Vector2Int>();
        int roomWidth = 0;
        int roomHeight = 0;
        if (type == "Room") {
            roomWidth = Random.Range(widthMinRoom, widthMaxRoom);
            roomHeight = Random.Range(heightMinRoom, heightMaxRoom);
        }
        else {
            switch (wall.direction) {
                case "South":
                    roomWidth = 3;
                    roomHeight = Random.Range(minCorridorLength, maxCorridorLength);
                    break;
                case "North":
                    roomWidth = 3;
                    roomHeight = Random.Range(minCorridorLength, maxCorridorLength);
                    break;
                case "West":
                    roomWidth = Random.Range(minCorridorLength, maxCorridorLength);
                    roomHeight = 3;
                    break;
                case "East":
                    roomWidth = Random.Range(minCorridorLength, maxCorridorLength);
                    roomHeight = 3;
                    break;
            }
        }
        int xStartingPoint;
        int yStartingPoint;
        if (isFirst) {
            xStartingPoint = mapWidth / 2;
            yStartingPoint = mapHeight / 2;
        }
        else {
            int id;
            if (wall.positions.Count == 3) id = 1;
            else id = Random.Range(1, wall.positions.Count - 2);
            xStartingPoint = wall.positions[id].x;
            yStartingPoint = wall.positions[id].y;
        }
        Vector2Int lastWallPosition = new Vector2Int(xStartingPoint, yStartingPoint);
        if (isFirst) {
            xStartingPoint -= Random.Range(1, roomWidth);
            yStartingPoint -= Random.Range(1, roomHeight);
        }
        else {
            switch (wall.direction) {
                case "South":
                    if (type == "Room") xStartingPoint -= Random.Range(1, roomWidth - 2);
                    else xStartingPoint--;
                    yStartingPoint -= Random.Range(1, roomHeight - 2);
                    break;
                case "North":
                    if (type == "Room") xStartingPoint -= Random.Range(1, roomWidth - 2);
                    else xStartingPoint--;
                    yStartingPoint ++;
                    break;
                case "West":
                    xStartingPoint -= roomWidth;
                    if (type == "Room") yStartingPoint -= Random.Range(1, roomHeight - 2);
                    else yStartingPoint--;
                    break;
                case "East":
                    xStartingPoint++;
                    if (type == "Room") yStartingPoint -= Random.Range(1, roomHeight - 2);
                    else yStartingPoint--;
                    break;
            }
        }
         if (!CheckIfHasSpace(new Vector2Int(xStartingPoint, yStartingPoint), new Vector2Int(xStartingPoint + roomWidth - 1, yStartingPoint + roomHeight - 1))) {
            return;
        }
        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<Vector2Int>();
            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++) {
                Vector2Int position = new Vector2Int();
                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";
                }
                if (y == (roomHeight - 1)) {
                    room.walls[1].positions.Add(position);
                    room.walls[1].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                if (x == 0) {
                    room.walls[2].positions.Add(position);
                    room.walls[2].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                if (x == (roomWidth - 1)) {
                    room.walls[3].positions.Add(position);
                    room.walls[3].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                if (MapManager.map[position.x, position.y].type != "Wall") {
                    MapManager.map[position.x, position.y].type = "Floor";
                }
            }
        }
        if (!isFirst) {
            MapManager.map[lastWallPosition.x, lastWallPosition.y].type = "Floor";
            switch (wall.direction) {
                case "South":
                    MapManager.map[lastWallPosition.x, lastWallPosition.y - 1].type = "Floor";
                    break;
                case "North":
                    MapManager.map[lastWallPosition.x, lastWallPosition.y + 1].type = "Floor";
                    break;
                case "West":
                    MapManager.map[lastWallPosition.x - 1, lastWallPosition.y].type = "Floor";
                    break;
                case "East":
                    MapManager.map[lastWallPosition.x + 1, lastWallPosition.y].type = "Floor";
                    break;
            }
        }
        room.width = roomWidth;
        room.height = roomHeight;
        room.type = type;
        allFeatures.Add(room);
        countFeatures++;
    }
    bool CheckIfHasSpace(Vector2Int start, Vector2Int end) {
        for (int y = start.y; y <= end.y; y++) {
            for (int x = start.x; x <= end.x; x++) {
                if (x < 0 || y < 0 || x >= mapWidth || y >= mapHeight) return false;
                if (MapManager.map != null) return false;
            }
        }
        return true;
    }
    Wall ChoseWall(Feature feature) {
        for (int i = 0; i < 10; i++) {
            int id = Random.Range(0, 100) / 25;
            if (!feature.walls[id].hasFeature) {
                return feature.walls[id];
            }
        }
        return null;
    }
    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;
        }
    }
}