Laberinto, un juego arcade con música, nave, obstáculos, colisiones, enemigos, niveles, imagen de fondo y game over, hecho con Godot

Las imágenes y los ficheros de audio

Para nuestro juego necesitaremos varias imágenes y ficheros de audio. Para poder incluirlas en nuestro proyecto, primero debemos descargarlas en nuestro ordenador, y a continuación las arrastraremos a la zona de «Sistema de Archivos» de Godot. Las tienes disponibles en este archivo ZIP.

Pasos a seguir

Esqueleto del juego (todas las funciones vacías)

extends Node2D

# ==============================================================================
# CONFIGURACIÓN Y CONSTANTES
# ==============================================================================

# --- TAMAÑOS Y AJUSTES ---
const TAM_CELDA = 100    # Píxeles por bloque
const MARGEN    = 5      # Píxeles de separación interna

# --- VELOCIDADES ---
const VEL_JUGADOR = 350
const VEL_ENEMIGO = 150

# --- CARGA DE IMÁGENES ---
const TEX_FONDO     = preload("res://assets/fondo.png")
const TEX_JUGADOR   = preload("res://assets/jugador.png")
const TEX_ENEMIGO   = preload("res://assets/enemigo.png")
const TEX_PARED     = preload("res://assets/bloque.png")
const TEX_META      = preload("res://assets/meta.png")
const TEX_CERRAR    = preload("res://assets/boton_cerrar.png")
const TEX_REINICIAR = preload("res://assets/boton_reiniciar.png")
const TEX_GAMEOVER  = preload("res://assets/game_over.png")
const TEX_SIGUIENTE = preload("res://assets/siguiente_nivel.png")

# --- CARGA DE AUDIO ---
const AUDIO_FONDO   = preload("res://assets/musica_fondo.mp3")
const AUDIO_MUERTE  = preload("res://assets/sfx_muerte.mp3")

# ==============================================================================
# VARIABLES GLOBALES
# ==============================================================================

var pantalla: Vector2
var tam_tablero: Vector2
var celda_dim: Vector2

var paredes: Array[Rect2] = []
var enemigos: Array[Dictionary] = []
var jugador: Rect2
var meta: Rect2

var nivel_actual: int = 1
var game_over: bool = false
var siguiente_nivel: bool = false

var btn_cerrar: Rect2
var btn_reiniciar: Rect2

var music_player: AudioStreamPlayer
var sfx_player: AudioStreamPlayer

# ==============================================================================
# 1. HERRAMIENTAS BÁSICAS
# ==============================================================================

func _crear_rect(x_grid, y_grid, margen_interno):
	# Ayuda a convertir coordenadas de la cuadrícula (ej: 2,3) a píxeles reales en pantalla.
	pass

func _es_zona_segura(pos: Vector2):
	# Devuelve true si la coordenada es inicio, fin o esquinas clave.
	pass

func _colisiona_con_paredes(rect: Rect2):
	# Comprueba si un rectángulo intercepta alguna pared del array.
	pass

func _reproducir_sonido(stream):
	# Función auxiliar para reproducir un efecto de sonido puntual.
	pass

func _es_posicion_valida(rect: Rect2):
	# Valida si un objeto está dentro de la pantalla y libre de paredes.
	pass

# ==============================================================================
# 2. GENERACIÓN DE DATOS (Lógica de Nivel)
# ==============================================================================

func _generar_paredes(cantidad):
	# Coloca obstáculos aleatorios asegurándose de no bloquear el inicio o fin.
	pass

func _generar_enemigos(cantidad):
	# Genera enemigos solo en la mitad inferior para dar tiempo al jugador.
	pass

func _iniciar_nivel():
	# Resetea las variables y genera un nuevo mapa limpio.
	# Calcula la dificultad basada en el nivel actual.
	pass

# ==============================================================================
# 3. CONFIGURACIÓN Y VISUALIZACIÓN
# ==============================================================================

func _ready():
	# Inicialización básica: Configura el tamaño de pantalla, la cuadrícula y el audio.
	# Se ejecuta una única vez al arrancar la escena.
	pass

func _draw():
	# Dibuja texturas, enemigos, jugador e interfaz en pantalla.
	# IMPORTANTE: El orden importa. Lo primero que se dibuja queda al fondo.
	pass

# ==============================================================================
# 4. MOVIMIENTO Y FÍSICAS
# ==============================================================================

func _mover_jugador(delta):
	# Calcula el movimiento del jugador y evita que atraviese paredes.
	pass

func _mover_enemigos(delta):
	# Mueve enemigos verticalmente y los hace rebotar al chocar.
	pass

func _comprobar_colisiones():
	# Verifica condiciones de victoria o derrota por colisión.
	pass

# ==============================================================================
# 5. BUCLE PRINCIPAL E INPUT
# ==============================================================================

func _process(delta):
	# Bucle principal: Gestiona movimiento y colisiones en cada frame.
	pass

func _input(event):
	# Gestiona clics del ratón para botones y reinicio de juego.
	pass

crear_rect()

func _crear_rect(x_grid, y_grid, margen_interno) -> Rect2:
	# Ayuda a convertir coordenadas de la cuadrícula (ej: 2,3) a píxeles reales en pantalla.
	
	# Retorna un Rect2. La posición X es (columna * ancho_celda) + margen.
	# El tamaño es (ancho_celda - doble margen) para que quede centrado.
	return Rect2(
		x_grid * celda_dim.x + margen_interno,
		y_grid * celda_dim.y + margen_interno,
		celda_dim.x - margen_interno * 2,
		celda_dim.y - margen_interno * 2
	)

es_zona_segura()

func _es_zona_segura(pos: Vector2) -> bool:
	# Devuelve true si la coordenada es inicio, fin o esquinas clave.
	
	if pos == Vector2(0, 0): return true # Esquina Jugador
	if pos == Vector2(tam_tablero.x - 1, tam_tablero.y - 1): return true # Esquina Meta
	# Protegemos las otras dos esquinas para evitar bloqueos imposibles en mapas pequeños
	if pos == Vector2(tam_tablero.x - 1, 0): return true
	if pos == Vector2(0, tam_tablero.y - 1): return true
	return false

colisiona_con_paredes()

func _colisiona_con_paredes(rect: Rect2) -> bool:
	# Comprueba si un rectángulo intercepta alguna pared del array.
	
	for pared in paredes:
		# Usamos .grow(-1) para reducir un píxel el hitbox de chequeo.
		# Esto evita que detecte colisión si solo se están rozando lado con lado.
		if rect.grow(-1).intersects(pared): return true
	return false

reproducir_sonido()

func _reproducir_sonido(stream):
	# Función auxiliar para reproducir un efecto de sonido puntual.
	sfx_player.stream = stream
	sfx_player.play()

es_posicion_valida()

func _es_posicion_valida(rect: Rect2) -> bool:
	# Valida si un objeto está dentro de la pantalla y libre de paredes.
	
	# 'encloses' devuelve true si el rect está TOTALMENTE dentro de la pantalla
	if not get_viewport_rect().encloses(rect): return false
	
	# Si choca con pared, devuelve false (posición no válida)
	return not _colisiona_con_paredes(rect)

generar_paredes()

func _generar_paredes(cantidad):
	# Coloca obstáculos aleatorios asegurándose de no bloquear el inicio o fin.
	
	var intentos = 0
	# Usamos un while con límite de intentos para evitar bucles infinitos si no hay sitio
	while paredes.size() < cantidad and intentos < 1000:
		intentos += 1
		
		# Elegimos una columna y fila al azar
		var pos = Vector2(randi() % int(tam_tablero.x), randi() % int(tam_tablero.y))
		
		# Si la posición elegida es vital (inicio/fin), la descartamos y probamos otra
		if _es_zona_segura(pos): continue
		
		# Creamos el rectángulo provisional
		var nueva_pared = _crear_rect(pos.x, pos.y, 0)
		
		# Verificamos que no caiga encima de otra pared ya existente
		if not _colisiona_con_paredes(nueva_pared):
			paredes.append(nueva_pared)

generar_enemigos()

func _generar_enemigos(cantidad):
	# Genera enemigos solo en la mitad inferior para dar tiempo al jugador.
	
	var intentos = 0
	var fila_minima = int(tam_tablero.y / 2) # Calculamos la mitad del tablero
	
	while enemigos.size() < cantidad and intentos < 1000:
		intentos += 1
		
		# Coordenada X aleatoria (cualquier columna)
		var cx = randi() % int(tam_tablero.x)
		# Coordenada Y aleatoria (solo desde la mitad hacia abajo)
		var cy = randi_range(fila_minima, int(tam_tablero.y) - 1)
		
		if _es_zona_segura(Vector2(cx, cy)): continue
		
		var rect_temp = _crear_rect(cx, cy, 0)
		
		# Evitamos que un enemigo nazca dentro de una pared
		if _colisiona_con_paredes(rect_temp): continue
		
		# Añadimos el enemigo como un diccionario con sus propiedades
		enemigos.append({
			"rect": rect_temp.grow(-15), # Hacemos la hitbox más pequeña para perdonar roces
			"dir": 1 if randf() > 0.5 else -1, # 50% probabilidad de ir arriba o abajo
			"velocidad": VEL_ENEMIGO
		})

iniciar_nivel()

func _iniciar_nivel():
	# Resetea las variables y genera un nuevo mapa limpio.
	# Calcula la dificultad basada en el nivel actual.
	
	# Limpiamos los arrays para borrar el nivel anterior
	paredes.clear()
	enemigos.clear()
	
	# Reiniciamos estados lógicos
	game_over = false
	siguiente_nivel = false
	
	# Creamos al jugador en la posición (0,0) (Arriba-Izquierda)
	jugador = _crear_rect(0, 0, MARGEN)
	
	# Creamos la meta en la última celda disponible (Abajo-Derecha)
	meta = _crear_rect(tam_tablero.x - 1, tam_tablero.y - 1, 0)
	
	# Fórmula de dificultad: Más nivel = más paredes y enemigos
	var cant_paredes = 5 + (nivel_actual * 3)
	var cant_enemigos = 2 + (nivel_actual * 1)
	
	# Ejecutamos los bucles de generación
	_generar_paredes(cant_paredes)
	_generar_enemigos(cant_enemigos)

ready()

func _ready():
	# Inicialización básica: Configura el tamaño de pantalla, la cuadrícula y el audio.
	# Se ejecuta una única vez al arrancar la escena.
	
	# Inicializa la semilla aleatoria para que cada partida sea distinta
	randomize()
	
	# Obtenemos el tamaño visible de la ventana del juego
	pantalla = get_viewport_rect().size
	
	# Calculamos cuántas columnas y filas caben dividiendo el ancho/alto por el tamaño de celda
	var cols = round(pantalla.x / TAM_CELDA)
	var filas = round(pantalla.y / TAM_CELDA)
	tam_tablero = Vector2(cols, filas)
	
	# Recalculamos el tamaño exacto de la celda para que se ajuste perfectamente a la pantalla
	celda_dim = Vector2(pantalla.x / cols, pantalla.y / filas)
	
	# Definimos la geometría de los botones UI
	# Botón cerrar: Esquina superior derecha
	btn_cerrar = Rect2(pantalla.x - celda_dim.x, 0, celda_dim.x, celda_dim.y)
	# Botón reiniciar: Esquina inferior izquierda
	btn_reiniciar = Rect2(0, (tam_tablero.y - 1) * celda_dim.y, celda_dim.x, celda_dim.y)
	
	# Instanciamos y configuramos el nodo de música
	music_player = AudioStreamPlayer.new()
	music_player.stream = AUDIO_FONDO
	music_player.volume_db = -10 # Bajamos un poco el volumen
	add_child(music_player) # Lo añadimos al árbol de nodos
	music_player.play()
	
	# Instanciamos el nodo para efectos de sonido (SFX)
	sfx_player = AudioStreamPlayer.new()
	add_child(sfx_player)
	
	# Llamamos a la función que prepara el primer nivel
	_iniciar_nivel()

draw()

func _draw():
	# Dibuja texturas, enemigos, jugador e interfaz en pantalla.
	# IMPORTANTE: El orden importa. Lo primero que se dibuja queda al fondo.
	
	# 1. Fondo (Capa más baja)
	draw_texture_rect(TEX_FONDO, Rect2(Vector2.ZERO, pantalla), false)
	
	# 2. Objetivos
	draw_texture_rect(TEX_META, meta, false)
	
	# 3. Obstáculos
	for p in paredes:
		draw_texture_rect(TEX_PARED, p, false, Color(1,1,1,0.5)) # Transparencia 0.5
	
	# 4. Entidades (enemigos y jugador)
	for e in enemigos:
		draw_texture_rect(TEX_ENEMIGO, e.rect, false)
	
	draw_texture_rect(TEX_JUGADOR, jugador, false)
	
	# 5. Interfaz de Usuario (Capa superior)
	draw_texture_rect(TEX_CERRAR, btn_cerrar, false)
	draw_texture_rect(TEX_REINICIAR, btn_reiniciar, false)
	
	# Texto informativo
	draw_string(ThemeDB.fallback_font, Vector2(0, 50), "NIVEL " + str(nivel_actual), 
		HORIZONTAL_ALIGNMENT_CENTER, pantalla.x, 40)
	
	# 6. Pantallas Superpuestas (Overlays)
	if game_over:
		# Dibuja game over con un tinte ligeramente transparente (0.8)
		draw_texture_rect(TEX_GAMEOVER, Rect2(Vector2.ZERO, pantalla), false, Color(1,1,1,0.8))
	elif siguiente_nivel:
		draw_texture_rect(TEX_SIGUIENTE, Rect2(Vector2.ZERO, pantalla), false, Color(1,1,1,0.8))

mover_jugador()

func _mover_jugador(delta):
	# Calcula el movimiento del jugador y evita que atraviese paredes.
	
	# Obtiene un vector (-1, 0, 1) según las teclas pulsadas
	var dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	
	# Si no se pulsa nada, salimos para ahorrar cálculos
	if dir == Vector2.ZERO: return

	# Calculamos cuánto desplazarse en este frame (Dirección * Velocidad * Tiempo)
	var velocidad = dir * VEL_JUGADOR * delta
	
	# --- MOVIMIENTO EJE X ---
	# Creamos una copia temporal del jugador movida en X
	var test_x = jugador
	test_x.position.x += velocidad.x
	
	# Si la copia no choca, movemos al jugador real
	if _es_posicion_valida(test_x): jugador.position.x = test_x.position.x
	
	# --- MOVIMIENTO EJE Y ---
	# Hacemos lo mismo para el eje Y por separado. 
	# (Hacerlo separado permite deslizarse por las paredes en lugar de atascarse)
	var test_y = jugador
	test_y.position.y += velocidad.y
	
	if _es_posicion_valida(test_y): jugador.position.y = test_y.position.y

mover_enemigos()

func _mover_enemigos(delta):
	# Mueve enemigos verticalmente y los hace rebotar al chocar.
	
	for e in enemigos:
		# Calculamos el paso vertical
		var paso = e.dir * e.velocidad * delta
		e.rect.position.y += paso
		
		# Verificamos si se salió de la pantalla (Arriba < 0, Abajo > pantalla)
		var choca_borde = e.rect.position.y < 0 or e.rect.end.y > pantalla.y
		
		# Si toca borde o pared...
		if choca_borde or _colisiona_con_paredes(e.rect):
			e.rect.position.y -= paso # Revertimos el movimiento para que no se quede pegado
			e.dir *= -1               # Invertimos la dirección (1 a -1, o viceversa)

comprobar_colisiones()

func _comprobar_colisiones():
	# Verifica condiciones de victoria o derrota por colisión.
	
	# 1. VICTORIA: ¿El jugador toca la meta?
	if jugador.intersects(meta):
		siguiente_nivel = true
		_reproducir_sonido(AUDIO_FONDO)
	
	# 2. DERROTA: ¿El jugador toca algún enemigo?
	for e in enemigos:
		# Reducimos un poco el hitbox del enemigo (grow -4) para ser benévolos con el jugador
		if jugador.intersects(e.rect.grow(-4)):
			game_over = true
			music_player.stop()              # Silenciamos música
			_reproducir_sonido(AUDIO_MUERTE) # Sonido de choque
			return

process()

func _process(delta):
	# Bucle principal: Gestiona movimiento y colisiones en cada frame.
	
	# Si el juego acabó, salimos de la función inmediatamente (no procesamos nada más)
	if game_over or siguiente_nivel: return
	
	# Actualizamos la lógica
	_mover_jugador(delta)
	_mover_enemigos(delta)
	_comprobar_colisiones()
	
	# Solicitamos redibujar la pantalla (llama a _draw automáticamente)
	queue_redraw()

input()

func _input(event):
	# Gestiona clics del ratón para botones y reinicio de juego.
	
	# Solo nos interesan los eventos que sean Clics de ratón y que estén Presionados
	if not (event is InputEventMouseButton and event.pressed): return

	# Chequeo de botones de UI
	if btn_cerrar.has_point(event.position):
		get_tree().quit() # Cierra el juego
		return

	if btn_reiniciar.has_point(event.position):
		_iniciar_nivel() # Reinicia el nivel actual
		return

	# Lógica para continuar tras ganar o perder
	if game_over:
		music_player.play()
		_iniciar_nivel() # Reintenta el mismo nivel
	elif siguiente_nivel:
		nivel_actual += 1 # Sube la dificultad
		_iniciar_nivel()

El resultado

Desde Godot podemos exportar este mismo proyecto para poder jugar en cualquier navegador. Puedes ver el resultado y jugar directamente mediante el siguiente enlace: