9

Generador de mazmorras al azar con Love2D

dungeon_portada“La Valkyria desenvainó su espada, inhaló aire y lentamente descendió a las oscuras profundidades de las mazmorras. Su determinación era férrea; su objetivo, simple: encontrar al Brujo, matarlo, robarle el amuleto de Yen-D’hor y restablecer el orden en el Sacro Imperio Genérico.

Aunque no sin antes tener que atravesar un enorme laberinto de mazmorras que parecían cambiar su distribución caprichosamente, con infinidad de trampas letales y plagadas de horrores indescriptibles ansiosos por probar la carne humana…”

Saludos, homo fabers. Como algunos sabéis, soy una gran aficionada a los juegos Roguelike como Nethack o Slash’EM. Lo que más me fascina de estos juegos es la rejugabilidad que tienen. Esto se debe a que los niveles están generados al azar mediante algoritmos, ¡Por lo que nunca habrá dos niveles iguales!

Hay muchas técnicas diferentes para generar mapas al azar. Algunos juegos empiezan dibujando las habitaciones en el mapa y después las unen con pasillos. Otros, empiezan con pasillos y colocan las habitaciones encima.

Hoy voy a explicaros mi forma de construir niveles al azar, para conseguir algo al estilo de Nethack.

Puede que no sea la forma más elegante ni eficiente para conseguirlo, pero tiene un aspecto muy positivo:

  1. Funciona.

Vamos a implementar este algoritmo con el motor de juegos Love2D, del que hablé hace unos días en uno de mis fabulosos artículos (lo sé, mis creadores se olvidaron de programarme la humildad). Al terminar este tutorial, conseguiremos generar mapas como este:

roguelike_capture

La mayoría de tiles son de www.opengameart.org


La melodía del día es Béla Bartók – Cuerda, percusión y celesta, para ponernos en situación en el ambiente oscuro y lúgubre de las mazmorras…

I – El algoritmo

La idea es conseguir un nivel al azar el cual tenga un cierto número de habitaciones de dimensiones aleatorias. Por supuesto, todas ellas accesibles. Necesitaremos conocimientos avanzados de estadística, integración, derivación de distintas variables y gestión de memoria.

¡Es broma! Nada de eso. La idea es muy simple. Los pasos a seguir serán:

1- Iniciar el mapa y crear una habitación al azar. Las paredes están en negro y el suelo/zonas despejadas en blanco.

Paso1

2- Seleccionar una casilla al azar (rojo) que esté en contacto con un bloque despejado.

Paso2

3- Elegir unas dimensiones al azar para la nueva habitación. A partir del bloque rojo, comprobar que hay suficiente espacio para colocarla (la zona en gris claro representa la posible ubicación de la nueva habitación. La zona en gris oscuro es un espacio adicional para asegurar que la habitación no está en contacto con otra).

Paso3

4- Si hay suficiente espacio, colocar la nueva habitación. Sino, repetir el paso 3 con otras dimensiones.

Paso4

5- Repetir el paso 2.

Paso5

6- No hay que crear sólo habitaciones. Se pueden generar pasillos, habitaciones especiales… al final quedará algo así:

Paso6

7- Finalmente, añadimos algunos cofres y objetos al azar (léase: monstruos, cofres, trampas…)

Paso7


II – Preparación

Primero hay que crear un directorio dónde se guardarán todas las imágenes, scripts y assets. Yo lo he bautizado como love_2d_dungeon.

Dentro del directorio hay que crear dos ficheros de texto: main.lua y mapa.lua. El primero será el fichero main necesario para ejecutar un código con Love2d. El segundo fichero contendrá el código del generador.

También hay que descargar estas imágenes y guardarlas en el directorio que acabamos de crear. De arriba a abajo: la salida/entrada, un cofre, una pared y baldosas del suelo.

passage chest wall floor


III – Let’s do it

Empezamos por mapa.lua. Necesitaremos estas funciones (perdón por la mezcla inglés-español de los nombres):

  • pickWall(): en el apartado I, es la función que elige los bloques rojos en el paso 2. Es decir, buscará un bloque que esté en contacto con una casilla despejada. Actualiza la posición X, Y del bloque rojo.
  • buildRoom(): a partir de la posición del bloque rojo, construye una habitación de dimensiones X, Y.
  • checkSpace(): dadas unas dimensiones y la posición del bloque rojo, comprueba que no hay ningún obstáculo y puede colocarse una habitación.
  • decide(): elige unas dimensiones para la nueva habitación, y llama las funciones buildRoom() y checkSpace() para colocarla.
  • creaCofres(): llena el mapa con cofres al azar.
  • creaMapa(): combina todas las funciones anteriores para crear el mapa.
  • drawMap(): dibuja el mapa en pantalla.
  • checkContacto(): devuelve true si el objeto de las coordenadas X, Y está en contacto con un cierto elemento. Será útil para dibujar las paredes de las habitaciones.

Empezamos. Lo primero será definir las variables globales que utilizaremos, llamar librerías, etc.


require "math" -- necesitamos math.lua para algunos calculos

mapMax = 60 -- dimensiones maximas del mapa (60x60)

maxX = 5 --margen de posicion x a partir del cual pueden crearse habitaciones
maxY = 5 --lo mismo con el eje y

cofreMax = 10 -- numero maximo de cofres

habMax = 15 -- habitaciones maximas

-- Coordenadas del bloque rojo que estamos analizando para colocar una nueva habitacion
wallX = 0
wallY = 0

--Dimensiones maximas de las habitaciones (8x8 es suficiente)
habInicialX = 0
habInicialY = 0

--Cuando elegimos una nueva posicion para construir una habitacion, hay que saber donde debe construirse: arriba, abajo, derecha o izquierda. Esta variable se actualizara para saberlo: t(top), b(bottom), r(right), l(left)
orientation = 'b'

Los comentarios deberían explicar bien este fragmento, pero por si queda alguna duda voy a ampliar la explicación.

require “math” llama a la librería math (¡qué sorpresa!). Servirá para calcular divisiones enteras más adelante.

mapMax y habMax limitan el máximo de habitaciones que puede tener el nivel. Un mapa de 60×60 con 15 habitaciones es de tamaño medio.

wallX y wallY son las coordenadas de la posición dónde debe crearse una nueva habitación (bloque rojo en las imágenes de la explicación del principio).

Necesitamos saber las coordenadas de la habitación inicial para colocar una salida/entrada a la mazmorra. habInicialX y habInicialY serán sus cordenadas, que se llenarán al crearse la primera habitación.

orientation sirve para saber dónde debe crearse la nueva habitación. Si nos fijamos en esta imagen otra vez:

Paso2Está claro que la nueva habitación debe crearse a la derecha del bloque rojo. orientation tomará los valores b (bottom), t(top), r(right) o l(left) para saber dónde debe crearse la nueva habitación.

Ahora creamos un mapa vacío y lo llenamos con ceros. Los ceros representarán las casillas dónde hay un muro. Hay que escribir un bucle que añada nuevas filas a la matriz map y las llene:


map = {} -- La matriz mapa

for row = 1,mapMax do
    table.insert(map, { }) -- Crea una fila nueva
    for column = 1,mapMax do
        table.insert(map[row], 0) -- Escribe un '0'
    end
end

Ahora viene la función pickWall(). Irá eligiendo casillas al azar hasta dar con una que esté en contacto con una zona despejada. También, según la posición de la zona despejada en relación a la casilla que se ha elegido, se actualizará la variable orientation.

Algo importante antes de continuar: según me contaron cuándo aprendí Love2D, es costumbre cuando se quiere acceder a una tabla o matriz girar el eje x e y. Por ejemplo, en vez de escribir map[x][y] se escribe map[y][x]. En este tutorial he seguido esta costumbre, pero en realidad no tiene la menor importancia.


function pickWall()

    --Selecciona una casilla impasable que este en contacto con una zona despejada
    --Actualiza wallX, wallY
    --Actualiza orientation

    valid = false --Cuando la posicion sea valida (wallX, wallY se corresponda a una casilla impasable que este al lado de una zona despejada) cambiara su valor a true

    while not valid do --Mientras la casilla no sea valida
    
        --Se elige una posicion al azar
        wallX = love.math.random( 1+maxX, mapMax-maxX-1 )
        wallY = love.math.random( 1+maxY, mapMax-maxY-1 )

        --Valid se establece por defecto a true
        valid = true

        --Si wallX,Y se corresponde a una casilla impasable que este en contacto con una zona despejada, se actualiza orientation
        --De lo contrario, valid = false y vuelve a empezar el bucle
        if map[wallY][wallX] == 0 and map[wallY-1][wallX] ~= 0 then
            orientation = 'b'

        elseif map[wallY][wallX] == 0 and map[wallY+1][wallX] ~= 0 then
            orientation = 't'
        
        elseif map[wallY][wallX] == 0 and map[wallY][wallX+1] ~= 0 then
            orientation = 'r'
        
        elseif map[wallY][wallX] == 0 and map[wallY][wallX-1] ~= 0 then
            orientation = 'l'
        else
            --Si wallX,Y no esta en contacto con un espacio vacio, entonces es una posicion no valida
            valid = false
        end
    end

end

Ahora viene la función buildRoom(). Ahora escribo la función completa, pero la analizaremos por partes:

function buildRoom(x, y)

    map[wallY][wallX] = 1 -- Convertimos la posicion wallX,Y en un pasillo despejado

    --Queremos empezar a construir todas las habitaciones desde la esquina inferior izquierda

    if orientation == 'b' then
        Yinicial = wallY+y
        Xinicial = wallX-math.floor(x/2)

    elseif orientation == 'l' then
        Yinicial = wallY+math.floor(y/2)
        Xinicial = wallX+1
    
    elseif orientation == 'r' then
        Yinicial = wallY + math.floor(y/2)
        Xinicial = wallX - x

    elseif orientation == 't' then
        Yinicial = wallY-1
        Xinicial = wallX-math.floor(x/2)
    end

    --Bucle para crear la habitacion
    --va iterando hasta llenar un cuadrado de x*y
    i = Xinicial
    j = Yinicial
    while i < Xinicial+x do         
       while j > Yinicial-y do
            map[j][i] = 1
            j = j-1
        end
        j = Yinicial
        i = i+1
    end
end

¿Qué hace este código? La función recibe dos parámetros x, y que serán las dimensiones de la nueva habitación que se debe construir.

Lo primero es convertir la casilla wallX,Y en una zona despejada, ¡Sino, el jugador no podría pasar de una habitación a la otra!

Queremos empezar a construir la habitación desde la esquina inferior izquierda, sin importar si debe construirse arriba, debajo, a la derecha o a la izquierda de wallX, wallY. ¿Y eso por qué? Sino, habría que escribir un bucle distinto que construyera la habitación para cada orientación, y eso sería mucho trabajo. Es más fácil, según el valor de orientation, cambiar la posición inicial de la habitación y después construirla con el mismo bucle.

Por ejemplo, si debe crearse una habitación 5×5 debajo de wallX, wallY la esquina se calcula con wallY+y, wallX + int(x/2), dónde int() es la división entera entre dos números:

esquema_habitacion

Con las otras orientaciones es equivalente.

En el bucle se inician dos contadores i, j a la posición inicial de la habitación (recordad: la esquina inferior izquierda) y van iterando hasta llenar un cuadrado que llegue a la posición inicial + x (-y para el otro eje).

Ahora viene la función checkSpace(). Esta función comprueba que en un espacio de x*y pueda colocarse una habitación de (x-2)*(y-2). ¿Por qué x-2? Así se deja un margen para colocar una pared y ninguna habitación toca con otra.

Su funcionamiento es igual al de la función buildRoom(), sólo que en vez de llenar un área, devuelve false si detecta que alguna de las casillas no es un espacio impasable.


function checkSpace(x, y)

    -- Encontrar el punto inferior izquierdo de la habitacion
    if orientation == 'b' then
        Yinicial = wallY+y-1
        Xinicial = wallX-math.floor(x/2)

    elseif orientation == 'l' then
        Yinicial = wallY+math.floor(y/2)
        Xinicial = wallX
    
    elseif orientation == 'r' then
        Yinicial = wallY + math.floor(y/2)
        Xinicial = wallX

    elseif orientation == 't' then
        Yinicial = wallY
        Xinicial = wallX-math.floor(x/2)
    end

    -- A partir de este punto, ver que esté todo libre
    i = Xinicial
    j = Yinicial
    while i < Xinicial+x do         
        while j > Yinicial-y do
            -- Si hay algo que no es un muro, significa que en el espacio hay un pasillo/habitacion
            -- Por lo tanto, return false
            if map[j][i] ~= 0 then
                return false
            end
            j = j-1
        end
        j = Yinicial
        i = i+1
    end
    return true
end

La función decide() elige un elemento al azar (un pasillo o una habitación), elige un tamaño máximo y llama la función checkSpace() para ver si puede colocarla. Si puede, llama a buildRoom() para construirla y resta 1 al máximo de habitaciones. Sino, no hace nada.


function decide()

        -- Elige un elemento al azar e intenta colocarlo en el mapa

        --Elegir un elemento al azar
        -- [1] Habitacion
        -- [2] Passadis

        element = love.math.random( 1, 2 )

        if element == 1 then --Si el elemento es una habitacion, escoger unas dimensiones

            sX = love.math.random( 3, 7 )
            sY = love.math.random( 3, 7 )

            -- El booleano val dira si hay suficiente espacio para la habitacion y los margenes o no
            val = checkSpace(sX+2,sY+2)
            -- Si lo hay, se constru
            if val then
                buildRoom(sX,sY)
                habMax = habMax - 1
            end
        
        --Y lo mismo con los pasillos. Seran habitaciones con tamano 1 de ancho (o largo, dependiendo de la orientacion)
        elseif element == 2 then

            if orientation == 'b' or orientation == 't' then
                sX = 1
                sY = math.floor(love.math.random( 4, 10 )/2)
                val = checkSpace(sX+2,sY+2)
                if val then
                    buildRoom(sX,sY)
                    habMax = habMax - 1
                end
            else
                sX = math.floor(love.math.random( 6, 14 )/2)
                sY = 1
                val = checkSpace(sX+2, sY+2)
                if val then
                    buildRoom(sX, sY)
                    habMax = habMax - 1
                end
            end

        end        
            
        
end

creaCofres() elige una posición al azar en el mapa y si es una zona despejada (pasillo o habitación) coloca un cofre. Podremos encontrarnos cofres en medio de los pasillos pero, ¿quién dijo que los monstruos de las mazmorras eran gente ordenada? ¿Alguna vez ha habido algun brujo malvado satisfecho con su ejército de las tinieblas?


function creaCofres()

	while cofreMax > 0 do -- Mientras quede algun cofre por colocar
                --Elige una posicion
		cofreX = love.math.random( 1, mapMax )
		cofreY = love.math.random( 1, mapMax )

                --Si esta despejada, anade un cofre y resta 1 al maximo de cofres
		if map[cofreY][cofreX] == 1 then
			map[cofreY][cofreX] = 3
			cofreMax = cofreMax-1
		end
	end
end

La función creaMapa() hace algo inesperado: ¡crea un mapa! En tres pasos: primero construye una habitación inicial y coloca una salida/entrada en su centro. Después va creando habitaciones hasta cumplir el cupo (llamando a la función decide() )

También guardará las coordenadas de la habitación inicial, así más adelante será posible centrar la cámara encima.


function creaMapa()

--PASO 0: Crear la habitacion inicial
        --elegir una posicion al azar
        wallX = love.math.random( 20, mapMax-20 )
        wallY = love.math.random( 20, mapMax-20 )
        

        --construir una habitacion de dimensiones aleatorias
        valX = love.math.random( 3, 5 )
        valY = love.math.random( 3, 5 )
        buildRoom(valX, valY)

        --marcar el centro de la habitacion
        map[wallY+2][wallX] = 5

        --Esto nos servira para saber donde esta la habitacion y centrar la camara
        habInicialX = wallX
        habInicialY = (wallY+2)

--PASO 1: Crear x elementos (habitaciones, pasillos...)

    while habMax > 0 do
        pickWall()
        decide()
    end

--PASO 2: Cofres
    creaCofres()

end

checkContacto() será una función que recibirá tres parámetros (fila, columna y elemento) y devolverá true si map[columna][fila] está en contacto con una casilla de tipo elemento. Por ejemplo, si recibe los parámetros (5, 12, 1) devolverá true si map[12][5] está en contacto con una casilla de tipo 1.


function checkContacto(row, column, element)
    if (row > element and column > element and column < mapMax and row < mapMax) and (map[row+1][column+1] == element or map[row-1][column-1] == element or map[row+1][column] == element or map[row-1][column] == element or map[row][column+1] == element or map[row][column-1] == element or map[row-1][column+1] == element or map[row+1][column-1] == element) then
        return true
    else
        return false
    end
end

Y por último queda la función drawMap(). Esta función será llamada desde main.lua para dibujar el mapa y recibirá tres parámetros. offx y offy es el desplazamiento de la cámara, así será posible moverse por el mapa y verlo todo. tileSize es el tamaño de las baldosas, que ajustaremos también en el fichero main.lua (para este tutorial serán de 32).

Esta función lee cada una de las casillas del mapa y dibuja baldosas, paredes y suelo según convenga. Las imágenes se importan en el fichero main.lua .


function drawMap(offx, offy, tileSize)
    -- Dibujamos el mapa
    for row = 1,mapMax do
            for column = 1,mapMax do
                if map[row][column] == 1 then
                love.graphics.draw(floor, row*tileSize+offy, column*tileSize+offx)
            elseif map[row][column] == 2 then
                love.graphics.draw(floor, row*tileSize+offy, column*tileSize+offx)
            elseif map[row][column] == 3 then
                love.graphics.draw(floor, row*tileSize+offy, column*tileSize+offx)
                love.graphics.draw(cofre, row*tileSize+offy, column*tileSize+offx)
            elseif map[row][column] == 5 then
                love.graphics.draw(passage, row*tileSize+offy, column*tileSize+offx)
            
            else
                if (checkContacto(row, column, 1) or checkContacto(row, column, 3)) then
                love.graphics.draw(wall, row*tileSize+offy, column*tileSize+offx)
                end
            end
            end
    end
end

Ahora hay que escribir el fichero main.lua. Consistirá únicamente en las tres funciones básicas de love2D: love.load(), love.update() y love.draw().

Primero llamamos la librería math y el fichero mapa.lua .


require "math"
require "mapa"

La función love.load() inicializa todos los parámetros: offx y offy será la posición de la cámara y speed su velocidad. tileSize el tamaño de las baldosas (en este ejemplo es 32, pero puede ampliarse si las imágenes de las baldosas son mayores). También importará las imágenes (ver apartado II), creará el mapa llamando la función creaMapa() y centrará offx, offy en la habitación inicial.


function love.load()

    -- offset para el desplazamiento de la pantalla y velocidad del mismo
    offx = 0
    offy = 0
    speed = 300

    tileSize = 32 --Todos los graficos son de 32*32 px

    --Carga todas las imagenes de los muros, suelo, cofres...
    wall = love.graphics.newImage("wall.png")
    floor = love.graphics.newImage("floor.png")
    passage = love.graphics.newImage("passage.png")
    cofre = love.graphics.newImage("chest.png")

    creaMapa() -- Llama la funcion creaMapa()

    -- Hay que centrar la camara en la habitacion inicial
    -- como la ventana por defecto es de 800x600, si no se sumase 300 y 400 la habitacion quedaria en una esquina
    offx = -habInicialX*tileSize+300
    offy = -habInicialY*tileSize+400
    
    
end

love.update() recibe como parámetro el tiempo actual y servirá para desplazar la cámara cambiando el valor de offx y offy.


function love.update(dt)
    

    if love.keyboard.isDown("up") then
        offx = offx + (speed * dt)
    end
        if love.keyboard.isDown("down") then
              offx = offx - (speed * dt)
       end

       if love.keyboard.isDown("left") then
              offy = offy + (speed * dt)
       end
       if love.keyboard.isDown("right") then
              offy = offy - (speed * dt)
       end
end

Por último, la función love.draw() llamará a drawMap() para dibujar todo el mapa a cada frame.


function love.draw()
	drawMap(offx, offy, tileSize)
end

¿Aún estáis aquí? Pues enhorabuena, ¡el generador de mazmorras está completado! En el siguiente apartado aparece el código completo de main.lua y mapa.lua y como ejecutarlo en Linux.


IV – Código completo

El código de mapa.lua:


require "math" -- necesitamos math.lua para algunos calculos
 
mapMax = 60 -- dimensiones maximas del mapa (60x60)
 
maxX = 5 --margen de posicion x a partir del cual pueden crearse habitaciones
maxY = 5 --lo mismo con el eje y
 
cofreMax = 10 -- numero maximo de cofres
 
habMax = 15 -- habitaciones maximas
 
-- Coordenadas del bloque rojo que estamos analizando para colocar una nueva habitacion
wallX = 0
wallY = 0
 
--Dimensiones maximas de las habitaciones (8x8 es suficiente)
habInicialX = 0
habInicialY = 0
 
--Cuando elegimos una nueva posicion para construir una habitacion, hay que saber donde debe construirse: arriba, abajo, derecha o izquierda. Esta variable se actualizara para saberlo: t(top), b(bottom), r(right), l(left)
orientation = 'b'

map = {} -- La matriz mapa
 
for row = 1,mapMax do
    table.insert(map, { }) -- Crea una fila nueva
    for column = 1,mapMax do
        table.insert(map[row], 0) -- Escribe un '0'
    end
end

function pickWall()
 
    --Selecciona una casilla impasable que este en contacto con una zona despejada
    --Actualiza wallX, wallY
    --Actualiza orientation
 
    valid = false --Cuando la posicion sea valida (wallX, wallY se corresponda a una casilla impasable que este al lado de una zona despejada) cambiara su valor a true
 
    while not valid do --Mientras la casilla no sea valida
     
        --Se elige una posicion al azar
        wallX = love.math.random( 1+maxX, mapMax-maxX-1 )
        wallY = love.math.random( 1+maxY, mapMax-maxY-1 )
 
        --Valid se establece por defecto a true
        valid = true
 
        --Si wallX,Y se corresponde a una casilla impasable que este en contacto con una zona despejada, se actualiza orientation
        --De lo contrario, valid = false y vuelve a empezar el bucle
        if map[wallY][wallX] == 0 and map[wallY-1][wallX] ~= 0 then
            orientation = 'b'
 
        elseif map[wallY][wallX] == 0 and map[wallY+1][wallX] ~= 0 then
            orientation = 't'
         
        elseif map[wallY][wallX] == 0 and map[wallY][wallX+1] ~= 0 then
            orientation = 'r'
         
        elseif map[wallY][wallX] == 0 and map[wallY][wallX-1] ~= 0 then
            orientation = 'l'
        else
            --Si wallX,Y no esta en contacto con un espacio vacio, entonces es una posicion no valida
            valid = false
        end
    end
 
end

function buildRoom(x, y)
 
    map[wallY][wallX] = 1 -- Convertimos la posicion wallX,Y en un pasillo despejado
 
    --Queremos empezar a construir todas las habitaciones desde la esquina inferior izquierda
 
    if orientation == 'b' then
        Yinicial = wallY+y
        Xinicial = wallX-math.floor(x/2)
 
    elseif orientation == 'l' then
        Yinicial = wallY+math.floor(y/2)
        Xinicial = wallX+1
     
    elseif orientation == 'r' then
        Yinicial = wallY + math.floor(y/2)
        Xinicial = wallX - x
 
    elseif orientation == 't' then
        Yinicial = wallY-1
        Xinicial = wallX-math.floor(x/2)
    end
 
    --Bucle para crear la habitacion
    --va iterando hasta llenar un cuadrado de x*y
    i = Xinicial
    j = Yinicial
    while i < Xinicial+x do
        while j > Yinicial-y do
            map[j][i] = 1
            j = j-1
        end
        j = Yinicial
        i = i+1
    end
end

function checkSpace(x, y)
 
    -- Encontrar el punto inferior izquierdo de la habitacion
    if orientation == 'b' then
        Yinicial = wallY+y-1
        Xinicial = wallX-math.floor(x/2)
 
    elseif orientation == 'l' then
        Yinicial = wallY+math.floor(y/2)
        Xinicial = wallX
     
    elseif orientation == 'r' then
        Yinicial = wallY + math.floor(y/2)
        Xinicial = wallX
 
    elseif orientation == 't' then
        Yinicial = wallY
        Xinicial = wallX-math.floor(x/2)
    end
 
    -- A partir de este punto, ver que esté todo libre
    i = Xinicial
    j = Yinicial
    while i < Xinicial+x do
        while j > Yinicial-y do
            -- Si hay algo que no es un muro, significa que en el espacio hay un pasillo/habitacion
            -- Por lo tanto, return false
            if map[j][i] ~= 0 then
                return false
            end
            j = j-1
        end
        j = Yinicial
        i = i+1
    end
    return true
end

function decide()
 
        -- Elige un elemento al azar e intenta colocarlo en el mapa
 
        --Elegir un elemento al azar
        -- [1] Habitacion
        -- [2] Passadis
 
        element = love.math.random( 1, 2 )
 
        if element == 1 then --Si el elemento es una habitacion, escoger unas dimensiones
 
            sX = love.math.random( 3, 7 )
            sY = love.math.random( 3, 7 )
 
            -- El booleano val dira si hay suficiente espacio para la habitacion y los margenes o no
            val = checkSpace(sX+2,sY+2)
            -- Si lo hay, se constru
            if val then
                buildRoom(sX,sY)
                habMax = habMax - 1
            end
         
        --Y lo mismo con los pasillos. Seran habitaciones con tamano 1 de ancho (o largo, dependiendo de la orientacion)
        elseif element == 2 then
 
            if orientation == 'b' or orientation == 't' then
                sX = 1
                sY = math.floor(love.math.random( 4, 10 )/2)
                val = checkSpace(sX+2,sY+2)
                if val then
                    buildRoom(sX,sY)
                    habMax = habMax - 1
                end
            else
                sX = math.floor(love.math.random( 6, 14 )/2)
                sY = 1
                val = checkSpace(sX+2, sY+2)
                if val then
                    buildRoom(sX, sY)
                    habMax = habMax - 1
                end
            end
 
        end        
             
         
end

function creaCofres()
 
    while cofreMax > 0 do -- Mientras quede algun cofre por colocar
                --Elige una posicion
        cofreX = love.math.random( 1, mapMax )
        cofreY = love.math.random( 1, mapMax )
 
                --Si esta despejada, anade un cofre y resta 1 al maximo de cofres
        if map[cofreY][cofreX] == 1 then
            map[cofreY][cofreX] = 3
            cofreMax = cofreMax-1
        end
    end
end

function creaMapa()
 
--PASO 0: Crear la habitacion inicial
        --elegir una posicion al azar
        wallX = love.math.random( 20, mapMax-20 )
        wallY = love.math.random( 20, mapMax-20 )
         
 
        --construir una habitacion de dimensiones aleatorias
        valX = love.math.random( 3, 5 )
        valY = love.math.random( 3, 5 )
        buildRoom(valX, valY)
 
        --marcar el centro de la habitacion
        map[wallY+2][wallX] = 5
 
        --Esto nos servira para saber donde esta la habitacion y centrar la camara
        habInicialX = wallX
        habInicialY = (wallY+2)
 
--PASO 1: Crear x elementos (habitaciones, pasillos...)
 
    while habMax > 0 do
        pickWall()
        decide()
    end
 
--PASO 2: Cofres
    creaCofres()
 
end

function checkContacto(row, column, element)
    if (row > element and column > element and column < mapMax and row < mapMax) and (map[row+1][column+1] == element or map[row-1][column-1] == element or map[row+1][column] == element or map[row-1][column] == element or map[row][column+1] == element or map[row][column-1] == element or map[row-1][column+1] == element or map[row+1][column-1] == element) then
        return true
    else
        return false
    end
end

function drawMap(offx, offy, tileSize)
    -- Dibujamos el mapa
    for row = 1,mapMax do
            for column = 1,mapMax do
                if map[row][column] == 1 then
                love.graphics.draw(floor, row*tileSize+offy, column*tileSize+offx)
            elseif map[row][column] == 2 then
                love.graphics.draw(floor, row*tileSize+offy, column*tileSize+offx)
            elseif map[row][column] == 3 then
                love.graphics.draw(floor, row*tileSize+offy, column*tileSize+offx)
                love.graphics.draw(cofre, row*tileSize+offy, column*tileSize+offx)
            elseif map[row][column] == 5 then
                love.graphics.draw(passage, row*tileSize+offy, column*tileSize+offx)
             
            else
                if (checkContacto(row, column, 1) or checkContacto(row, column, 3)) then
                love.graphics.draw(wall, row*tileSize+offy, column*tileSize+offx)
                end
            end
            end
    end
end

Y el código para main.lua:


require "math"
require "mapa"

function love.load()

    -- offset para el desplazamiento de la pantalla y velocidad del mismo
    offx = 0
    offy = 0
    speed = 300

    tileSize = 32 --Todos los graficos son de 32*32 px

    --Carga todas las imagenes de los muros, suelo, cofres...
    wall = love.graphics.newImage("wall.png")
    floor = love.graphics.newImage("floor.png")
    passage = love.graphics.newImage("passage.png")
    cofre = love.graphics.newImage("chest.png")

    creaMapa() -- Llama la funcion creaMapa()

    -- Hay que centrar la camara en la habitacion inicial
    -- como la ventana por defecto es de 800x600, si no se sumase 300 y 400 la habitacion quedaria en una esquina
    offx = -habInicialX*tileSize+300
    offy = -habInicialY*tileSize+400

end

function love.update(dt)

	if love.keyboard.isDown("up") then
		offx = offx + (speed * dt)
	end
        if love.keyboard.isDown("down") then
      		offx = offx - (speed * dt)
   	end

   	if love.keyboard.isDown("left") then
      		offy = offy + (speed * dt)
   	end
   	if love.keyboard.isDown("right") then
      		offy = offy - (speed * dt)
   	end
end

function love.draw()
	drawMap(offx, offy, tileSize)

end

Para compilar, abrimos la Terminal y escribimos:

 love directorio_donde_tengo_los_ficheros/

¡Ya está! Ahora aparecerá una mazmorra nuevecita, lista para ser poblada con monstruos, tesoros y trampas con algunas variaciones del código. Pero esto da para otro tutorial y muchas horas de trabajo.

Si estáis interesados en desarrollar juegos Roguelike en Love2D o cualquier otro motor (o picando código a mano, como los héroes de antaño) os recomiendo una página web muy interesante llamada Roguebasin. ¡Es la meca de los Roguelikes! Contiene mucha información de este tipo de juegos, dirigida tanto a jugadores como a desarrolladores.

Si tenéis alguna duda o sugerencia podéis dejarme un comentario más abajo. Y si os ha gustado este tutorial, ¡no dudéis en suscribiros! Así estaréis al tanto de todas mis publicaciones. Bueno, y también las de mis compañeros.

Gl4r3

Brillante, luminosa y cegadora a veces, Glare es tan artista como técnica. Le encanta dar rienda suelta a sus módulos de imaginación y desdibujar los ya de por si delgados límites que separan el mundo de la electrónica y el arte. Su mayor creación hasta la fecha es un instrumento capaz de convertir los colores y la luz en música. Cuándo sus circuitos no están trabajando en una nueva obra electrónica, le gusta dedicar sus ciclos a la lectura o a documentar sus invenciones para beneficio de los humanos. Sus artilugios favoritos son aquellos que combinan una funcionalidad práctica con un diseño elegante y artístico.

Antes de comentar, por favor, lee las Normas

9 Comentarios en "Generador de mazmorras al azar con Love2D"

avatar
Ordenar por:   más nuevos primero | más antiguos primero
Eleazar
Humano

Hola! Buen aporte. Pero por alguna razon cuando ejecuto el codigo en el motor de love2d se queda con la pantalla en blanco.. puede ser eto un error de codigo?

dalmemail
Humano

Muy buen tutorial. Aunque a mi me da error: map.lua:39: attempt to perform arithmetic on global ‘maxX’ (a nil value).

¿A que se puede deber?

N4n0
Admin

Hola! No soy un experto en Lua, aquí la que sabe es Glare, la autora del tutorial… pero según me parece, el problema está en que la variable maxX no está declarada.

Creo que esta variable sirve para establecer un margen máximo a partir del cuál pueden empezar a crearse las habitaciones. Intenta escribir, al principio del programa:

maxX = 2
maxY = 2

¡Suerte!

dalmemail
Humano

Ya funciona!! Gracias, a ser posible editad el post y añadirlo para que funcione a la primera 😉

N4n0
Admin

Hmmm… el error está en que cofreMax no está declarado. Añade esto al principio del programa:

cofreMax = 5

(sustituye 5 por el número máximo de cofres que quieras)

dalmemail
Humano

Humm. Por lo que estoy viendo ahora quizás eso ya esté solucionado y sea otro problema:

Falla aquí: (Entre otros)
while cofreMax > 0 do — Mientras quede algun cofre por colocar
–Elige una posicion
cofreX = love.math.random( 1, mapMax )
cofreY = love.math.random( 1, mapMax )

dalmemail
Humano

Parece que no 🙁
Dice: Attempt to compare a number with nil

Lo que vendría a decir que he intentado comparar un número con un valor nulo.

¿Que tengo que hacer? (Perdona, es que soy nuevo LUA y Love2D)

wpDiscuz