Creando un jueguito Roguelike. Parte 6

Breve explicación
Sin entrar en demasiados detalles, la generación de los cuartos es un proceso de tres pasos:
- Determinar dónde serán colocados los cuartos.
- Generar los cuartos de acuerdo al paso anterior.
- Informar a cada cuarto acerca de sus vecinos para generar puertas o barreras de acuerdo con ello.
Paso 1
Usaremos arreglos 2D (arrays), los cuales son una estructura de datos que manejan múltiples valores en una rejilla de dos dimensiones. Godot no tiene de momento una manera explícita o prefabricada de manejar arreglos 2D, pero crearemos los nuestros, que van a manejar valores lógicos para indicar presencia o ausencia de vecinos.
Configurando el nodo generativo
En la escena Nivel_01 haremos dos cosas: eliminar PlantillaCuarto y anidar como hijo de Principal un nodo Node y lo subimos por encima de los demás nodos. Le añadiremos un script que guardaremos como generacion_cuartos.gd en su respectiva carpeta.
Ahí definiremos una clase y algunas variables:
class_name GeneracionCuarto
extends Node
@export var talla_mapa : int = 7
@export var cuartos_a_generar : int = 12
var conteo_cuartos : int = 0
var mapa : Array[bool]
var cuartos : Array[Cuarto]
var cuartos_pos_desfase : float = 160
var primer_cuarto_x : int = 3
var primer_cuarto_y : int = 3
var primer_cuarto : Cuarto
# Cargamos nuestra plantilla de cuarto
var escena_cuarto : PackedScene = preload("res://Escenas/Cuartos/plantilla_cuarto.tscn")
@export var jugador : CharacterBody2DEstas variables expresan lo siguiente:
talla_mapa: Define la anchura y altura de nuestro mapa 2D.cuartos_a_generar: Es el número de cuartos que queremos generar.conteo_cuartos: Registra el número actual de cuartos generados.mapa: Un arreglo 2D que define la presencia de cuartos.cuartos: Un arreglo que registra los cuartos generados.cuartos_pos_desfase: El espacio “físico” que hay entre cuartos.primer_cuarto_xyprimer_cuarto_y: La posición inicial del primer cuarto.primer_cuarto: Lleva el registro del primer cuarto, una vez ya instanciado.jugador: una variable para seleccionar al caracter del jugador.
Toca crear unas cuantas funciones que no definiremos del todo aún:
# Inicializa el proceso de generación
func _ready():
pass
# Configura el mapa e inicia la generación de cuartos
func _generar():
pass
# Verifica y coloca cuartos recursivamente
func _checar_cuarto(x : int, y : int, direccion_deseada : Vector2, primer_cuarto : bool = false):
pass
# Instancia los cuartos
func _instanciar_cuartos():
passGeneración
Lo primero que haremos será llamar a la función _generar() en nuestra _ready():
func _ready():
_generar()Ahora debemos asegurarnos que el conteo de cuartos quede en 0 al inicio, ya que no hemos generado alguno aún. Entonces definiremos el tamaño del mapa, lo cual queda definido por talla_mapa.
Luego llamaremos a _checar_cuarto(), enviando la posición de nuestro primer cuarto para que la generación arranque en el centro de nuestra malla. Finalmente llamamos a _instanciar_cuartos(), tal que _checar_cuarto() gestione lo principal de nuestra generación y rellene el arreglo.
Definamos la función _generar():
# Configura el mapa e inicia la generación de cuartos
func _generar():
# Iniciamos conteo en 0
conteo_cuartos = 0
# Redimensionamos el mapa
mapa.resize(talla_mapa * talla_mapa)
# Inicia el chequeo de cuartos en el centro de la malla
_checar_cuarto(primer_cuarto_x, primer_cuarto_y, Vector2.ZERO, true)
# Una vez definido el mapa, instancia cuartos
_instanciar_cuartos()Condiciones de retorno
La función _checar_cuarto() deberá verificar que ciertas condiciones se cumplan para devolver algo tempranamente, esto es, antes de generar algún cuarto.
- Si el conteo de cuartos es mayor a los cuartos a generar.
- Si las coordenadas están fuera de los límites del mapa.
- El cuarto ya existe.
Vamos a implementar estos puntos.
Los primeros dos se consiguen con lo siguiente dentro de la función _checar_cuarto():
func _checar_cuarto(x : int, y : int, direccion_deseada : Vector2, primer_cuarto : bool = false):
# Conteo excedido
if conteo_cuartos >= cuartos_a_generar:
return
# Tamaño del mapa sobrepasado
if x < 0 or x > talla_mapa - 1 or y < 0 or y > talla_mapa - 1:
returnEl tercero sí necesita mayores arreglos. Vamos a crear dos funciones auxiliares que 1. Tomará un valor x y y y devolverá un true o un false, dependiendo de si la coordenada se encuentra dentro del arreglo del mapa; y 2. Tomará también un valor x y y, además de un booleano con base en si el cuarto existe o no. Entonces coloca el cuarto en el arreglo mapa. Ponemos ambas funciones al final.
func _obtener_mapa(x : int, y : int) -> bool:
return mapa[x + y * talla_mapa]
func _poner_mapa(x : int, y : int, value : bool):
mapa[x + y * talla_mapa] = valueVolvemos a _checar_cuarto() y agregamos estas funciones. Además, incrementaremos el conteo de cuartos, para dejar el código como sigue:
func _checar_cuarto(x : int, y : int, direccion_deseada : Vector2, primer_cuarto : bool = false):
# Conteo excedido
if conteo_cuartos >= cuartos_a_generar:
return
# Tamaño del mapa sobrepasado
if x < 0 or x > talla_mapa - 1 or y < 0 or y > talla_mapa - 1:
return
# Si el cuarto ya existe
if _obtener_mapa(x, y) == true:
return
conteo_cuartos += 1
_poner_mapa(x, y, true)Decidiendo dirección de la expansión
La función que decide en cuál dirección se puede expandir el cuarto. Usará un número aleatorio y un operador para determinar la probabilidad de expandirse en cada dirección. Configuremos la dirección norte primero.
En la misma función _checar_cuarto() agregamos las siguientes líneas:
var hacia_norte : bool = randf() > (0.2 if direccion_deseada == Vector2.UP else 0.8)
var hacia_oeste : bool = randf() > (0.2 if direccion_deseada == Vector2.LEFT else 0.8)
var hacia_este : bool = randf() > (0.2 if direccion_deseada == Vector2.RIGHT else 0.8)
var hacia_sur : bool = randf() > (0.2 if direccion_deseada == Vector2.DOWN else 0.8)
if hacia_norte or primer_cuarto:
_checar_cuarto(x, y - 1, Vector2.UP if primer_cuarto else direccion_deseada)
if hacia_oeste or primer_cuarto:
_checar_cuarto(x - 1, y, Vector2.LEFT if primer_cuarto else direccion_deseada)
if hacia_este or primer_cuarto:
_checar_cuarto(x + 1, y, Vector2.RIGHT if primer_cuarto else direccion_deseada)
if hacia_sur or primer_cuarto:
_checar_cuarto(x, y - 1, Vector2.DOWN if primer_cuarto else direccion_deseada)Instanciando los cuartos
Ahora que tenemos el mapa generado, vamos a instanciar los cuartos. Usaremos un ciclo que verificará los cuartos con base en sus coordenadas. Si no podemos encontrar un cuarto, pasaremos al siguiente. Si uno es encontrado, lo agregaremos a la escena, lo colocaremos y lo adjuntaremos al arreglo. El código que lo permite es el siguiente:
# Instancia los cuartos
func _instanciar_cuartos():
for x in range(talla_mapa):
for y in range(talla_mapa):
if _obtener_mapa(x,y) == false:
continue
var cuarto : Cuarto = escena_cuarto.instantiate()
get_tree().root.add_child.call_deferred(cuarto)
cuartos.append(cuarto)
cuarto.global_position = Vector2(x,y) * cuartos_pos_desfasePodemos ejecutar la escena, pero probablemente no veamos nada. Toca modificar el zoom de Camera2D a un valor aproximado al 0.5. Listo, ya hemos generado con éxito nuestro nivel. Sin embargo, falta pulir varios detalles, lo cual será abordado en otra entrada.