Blog

Asteroides, un juego arcade muy básico con nave y colisiones, hecho con Godot

Introducción

En esta unidad proponemos desarrollar desde cero un juego tipo arcade muy sencillo con Godot, centrado en los conceptos fundamentales: crear una nave que se pueda controlar, generar obstáculos (asteroides) que se muevan, detectar colisiones e implementar una interfaz básica. A lo largo de los apartados siguientes aprenderás cómo estructurar el código, organizar la escena, gestionar la entrada del usuario, mostrar los gráficos correspondientes y añadir elementos como un marcador de tiempo o un texto de Game Over.

El objetivo no es construir un juego complejo, sino entender claramente cada pieza que lo conforma, de modo que puedas modificarlo o ampliarlo por ti mismo, afianzando los fundamentos esenciales del desarrollo de juegos 2D con Godot.

Nociones básicas

Con el siguiente vídeo te harás una idea en unos pocos minutos del trabajo que llevaremos a cabo en esta unidad. Además, se explican conceptos básicos de forma muy sencilla, y así podrás entender más fácilmente cómo se puede implementar el juego propuesto utilizando Godot.

Análisis previo del código fuente

En el siguiente enlace encontrarás una presentación estructurada con varias diapositivas explicando paso a paso el desarrollo del juego (qué hace cada función, cómo se implementa, qué efectos visuales tienen, etc.):

Las imágenes

Para nuestro juego necesitaremos las siguientes imágenes. 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.

Antes de empezar

  • Crea una escena con un nodo raíz Node2D y crea un script donde colocaremos toda la programación de nuestro juego.
  • Asegúrate de tener los ficheros res://jugador.png, res://asteroide.png y res://boton_cerrar.png en nuestra zona de Sistema de Archivos.
  • En Proyecto > Configuración del Proyecto > Mapa de Entrada, puedes configurar las teclas asociadas a las acciones ui_left, ui_right y ui_accept. Godot ya nos proporciona algunas teclas configuradas por defecto (cursores izquierda/derecha y Enter/Espacio), y nosotros podemos añadir las que creamos convenientes, como por ejemplo las teclas A y D.
  • Para poder ejecutar nuestro juego en dispositivos móviles y utilizar pantallas táctiles y ratón, controlaremos las acciones a realizar en la función _input().
  • En Proyecto > Configuración del Proyecto > Visualización > Ventana, puedes configurar cómo se ajusta la visualización del juego al tamaño de la ventana. Por ejemplo:
    • Resolución Full HD: Ancho del Viewport = 1920 y Alto del Viewport = 1080.
    • Abrir la aplicación directamente en pantalla completa: Modo = Fullscreen.
    • Escalar todo el viewport para llenar la pantalla: Estirar → Modo = viewport.
    • Evitar las barras negras cuando se ajuste el contenido: Estirar → Aspecto = expand.
    • Mantener proporciones de forma fluida al escalar: Estirar → Modo Escala = fractional.
    • Bloquear la orientación en horizontal: Manipulador → Orientación = Landscape.

Pasos a seguir

A continuación explicaremos el proceso de programación paso a paso, definiendo primero el esqueleto del código con constantes, variables (como jugador y asteroides) y funciones vacías. A continuación detallaremos la implementación de las funciones principales, cubriendo aspectos cruciales como la inicialización de la pantalla y el jugador, el movimiento del personaje mediante el parámetro delta, la creación y movimiento de obstáculos (asteroides), la detección de colisiones y el manejo de los estados como el Game Over y reinicio del juego.

Esqueleto del juego (todas las funciones vacías)

Cuando se empieza a programar un juego, primero debemos organizar el esqueleto del proyecto. Por eso en este primer paso crearemos constantes, variables y funciones vacías que solo contienen la palabra pass. De esta forma, dejamos preparado el esquema con todo lo que necesitaremos, tanto los parámetros o ajustes iniciales, como las acciones a realizar en cada momento: inicializar la pantalla, mover al jugador, crear asteroides, comprobar colisiones, mostrar el “Game Over”, etc., aunque las funciones todavía no estén implementadas. Es como escribir un índice antes de comenzar para definir las partes que debemos desarrollar.

En primer lugar utilizaremos constantes, que son valores que se definen una vez al inicio y que no cambian durante la partida. En el código del juego aparecen con la palabra clave const. Algunos ejemplos son los siguientes:

  • const TAM_JUGADOR = 64 → define el tamaño de la nave (ancho y alto del rectángulo del jugador).
  • const COLOR_FONDO = Color.BLACK → el color del fondo cuando estás jugando.
  • const COLOR_GAME_OVER = Color(1, 0, 0, 0.5) → el color rojizo y semitransparente que aparece cuando pierdes.
  • const TAM_BOTON_CERRAR = 48 → el tamaño del botón de cerrar en la esquina.
  • const TAM_TEXTO = 32 → el tamaño de la fuente para los textos (tiempo y “GAME OVER”).
  • const TEX_JUGADOR, const TEX_ASTEROIDE, const TEX_BOTON_CERRAR → rutas de las imágenes que se usan como texturas en el juego.

Además, definiremos variables, para establecer la configuración o los datos que cambian durante la partida. En este proyecto hay varias importantes:

  • pantalla: guarda el tamaño de la ventana del juego (ancho y alto). Sirve para colocar todo dentro de los límites visibles.
  • jugador: es un rectángulo (Rect2) que representa la nave, con su posición y tamaño.
  • asteroides: es una lista donde se van guardando todos los asteroides que aparecen.
  • tiempo_total: acumula los segundos que el jugador ha sobrevivido en la partida.
  • muerto: es un valor booleano (verdadero/falso) que indica si el jugador sigue vivo o ya ha perdido.
  • tocando_izquierda y tocando_derecha: valores booleanos que indicarán si el jugador está pulsando los controles para moverse en esas direcciones.

Las variables son la memoria del juego: sin ellas, Godot no tendría forma de saber dónde está el jugador, cuántos asteroides hay, si la partida sigue activa o cuánto tiempo ha pasado.

Por último definiremos una serie de funciones vacías con cada uno de los pasos que debemos seguir para completar nuestro juego. Además, estas funciones vacías también ayudan a evitar errores. En Godot, si el juego intenta llamar a una función que no existe, se bloquea con un error y no se inicia. En cambio, si la función existe aunque esté vacía, Godot la reconoce y el juego funciona sin problema. Así se puede ir construyendo el programa paso a paso, completando cada función cuando toque, sin miedo a que el proyecto falle por estar incompleto.

Además, al utilizar Godot dispondremos de unas funciones especiales, que actuarán como relojes internos, ejecutando el código correspondiente en cada momento. Gracias a ellas, el código del juego está bien organizado y cada tarea se ejecuta en el instante adecuado:

  • “La escena está lista” → _ready()
  • “Ha pasado un frame” → _process(delta)
  • “Hay que dibujar de nuevo” → _draw()
  • “El jugador ha pulsado algo” → _input(event)

Este el código que deberás copiar y pegar inicialmente en el fichero «script» creado dentro de nuestro proyecto. Luego iremos reemplazando paso a paso las funciones vacías, una a una, hasta completar toda la lógica del juego:

extends Node2D
# Este script controla un minijuego muy simple:
# - Una nave (el jugador) se mueve a izquierda/derecha en la parte inferior.
# - Aparecen asteroides desde arriba.
# - Si un asteroide choca con la nave: GAME OVER.
#
# ARQUITECTURA EN GODOT:
# - _ready() se ejecuta una vez al iniciar la escena (inicializaciones).
# - _process(delta) se ejecuta cada frame (lógica del juego “por frames”).
# - _draw() se ejecuta cuando se redibuja el nodo (dibujo 2D inmediato).
# - _input(event) recibe los eventos de entrada (teclado, ratón, táctil…).
#
# IMPORTANTE:
# - Mapea las acciones en Project → Project Settings → Input Map:
#   "ui_left", "ui_right" y "ui_accept".
# - Coloca las imágenes en las rutas indicadas (res://jugador.png, etc.).
# - Este ejemplo usa “dibujo inmediato” (draw_* en _draw) en lugar de nodos Sprite.

# -------------------------
# CONSTANTES DE CONFIGURACIÓN
# -------------------------

# Tamaños de jugador, asteroides, etc.
const TAM_JUGADOR = 128
const TAM_ASTEROIDE = 64
const FACTOR_HITBOX = 0.75   # Factor para reducir un poco las "cajas de choque" (colisiones menos “injustas”)
const TAM_BOTON_CERRAR = 32  # Tamaño del botón para cerrar el juego (arriba derecha)
const TAM_TEXTO = 25         # Tamaño del texto de las etiquetas (UI)

# Colores que se usan en el juego (Color(r, g, b, a) con valores 0..1)
const COLOR_FONDO = Color(0, 0, 0)                 # Fondo negro cuando jugamos
const COLOR_GAME_OVER = Color(0.2, 0.0, 0.0, 0.5)  # Capa roja semitransparente al perder (efecto “oscurecido”)

# Imágenes (texturas) del jugador, del asteroide y del botón cerrar.
# preload() carga el recurso al iniciar el juego (más eficiente que load() en tiempo de ejecución)
const TEX_JUGADOR = preload("res://jugador.png")
const TEX_ASTEROIDE = preload("res://asteroide.png")
const TEX_BOTON_CERRAR = preload("res://boton_cerrar.png")

# Velocidades y tiempos (valores “jugables”, se pueden tunear)
var vel_jugador = 500.0           # Pixels/segundo que se moverá el jugador
var vel_asteroides = 250.0        # Velocidad vertical de los asteroides (pixels/segundo)
var intervalo_asteroides = 0.75   # Cada cuánto aparece un nuevo asteroide (en segundos)

# -------------------------
# ESTADO DEL JUEGO
# -------------------------

# “Rect2” define posición y tamaño (position.x, position.y, size.x, size.y)
var boton_cerrar: Rect2
var jugador: Rect2
var asteroides: Array[Rect2] = []    # Lista de rectángulos de asteroides

# Variables de control
var tiempo_proximo_asteroide = 0.0   # Acumula el tiempo hasta crear el siguiente asteroide
var muerto = false                   # Marca si ya hemos perdido (pausa la lógica de juego)
var pantalla: Vector2                # Tamaño de la ventana/viewport (ancho, alto)
var tiempo_total = 0.0               # Cronómetro de la partida (en segundos)

# Etiquetas de texto (UI)
var etiqueta_game_over: Label
var etiqueta_tiempo: Label

# Controles del jugador (banderas para izquierda/derecha)
var tocando_izquierda = false
var tocando_derecha = false

# -------------------------
# CICLO DE VIDA PRINCIPAL
# -------------------------

func _ready():
	# Se llama una vez al cargar la escena. Preparamos todo.
	randomize() # Se utiliza una "semilla" distinta en cada partida para generar números aleatorios diferentes
	_inicializar_pantalla()
	_inicializar_jugador()
	_crear_ui_tiempo()
	_crear_boton_cerrar()
	_crear_ui_game_over()

func _process(delta: float):
	# delta = tiempo (en segundos) entre este frame y el anterior.
	# Si el jugador está muerto, detenemos la lógica (pero se puede seguir dibujando).
	if muerto: return

	# Orden típico de actualización por frames:
	_mover_jugador(delta)
	_crear_asteroides(delta)
	_mover_asteroides(delta)
	_comprobar_colision()
	_actualizar_tiempo(delta)
	_actualizar_dificultad(delta)

	# Pedimos que se llame a _draw() para redibujar (importante para ver cambios visuales).
	queue_redraw()

func _draw():
	# Todo lo que dibujamos aquí se "pinta" encima del lienzo del Node2D en este frame.
	_color_fondo_jugando()  # Capa de fondo (negro)
	_dibujar_asteroides()   # Asteroides (texturas)
	_dibujar_jugador()      # Nave del jugador (textura)
	_mostrar_boton_cerrar() # Botón "X" arriba a la derecha
	if muerto: _color_game_over()  # Capa roja semitransparente al perder (se dibuja al final)

func _input(event: InputEvent):
	# _input recibe eventos de teclado, ratón y táctiles tal cual ocurren.
	# Los separamos en dos funciones para mantener limpio el código.
	_comprobar_pantalla_tactil_y_raton(event)
	_comprobar_teclado(event)

# -------------------------
# FUNCIONES DE INICIO
# -------------------------

func _inicializar_pantalla():
	# Configura el tamaño de la ventana y otros ajustes iniciales de la pantalla
	pass

func _inicializar_jugador():
	# Crea y coloca al jugador en la posición inicial
	pass

func _dibujar_jugador():
	# Dibuja al jugador en la pantalla
	pass

# -------------------------
# CONTROLES
# -------------------------

func _comprobar_pantalla_tactil_y_raton(event: InputEvent):
	# Detecta y gestiona entradas de ratón o pantalla táctil
	pass

func _comprobar_teclado(event: InputEvent):
	# Detecta y gestiona las teclas pulsadas
	pass

# -------------------------
# MOVIMIENTO DEL JUGADOR
# -------------------------

func _mover_jugador(delta: float):
	# Actualiza la posición del jugador según la entrada de controles
	pass

# -------------------------
# BOTÓN PARA CERRAR LA APLICACIÓN
# -------------------------

func _crear_boton_cerrar():
	# Genera el botón de cerrar/salir del juego
	pass

func _mostrar_boton_cerrar():
	# Muestra el botón de cerrar para poder finalizar la ejecución del juego
	pass

# -------------------------
# INTERFAZ DE USUARIO
# -------------------------  

func _crear_ui_tiempo():
	# Crea el marcador visual que muestra el tiempo jugado
	pass

func _actualizar_tiempo(delta: float):
	# Incrementa y refresca el tiempo mostrado en la pantalla
	pass

func _crear_ui_game_over():
	# Prepara el mensaje de GAME OVER en la pantalla
	pass

# -------------------------
# ASTEROIDES
# -------------------------

func _crear_asteroides(delta: float):
	# Genera nuevos asteroides que caen desde arriba
	pass

func _mover_asteroides(delta: float):
	# Hace que los asteroides se desplacen hacia abajo
	pass

func _dibujar_asteroides():
	# Dibuja todos los asteroides en la pantalla
	pass

# -------------------------
# COLISIONES Y GAME OVER
# -------------------------

func _game_over():
	# Activa el estado de GAME OVER y detiene la partida
	pass

func _reiniciar_juego():
	# Reinicia la partida para volver a empezar
	pass

func _escalar_rect(r: Rect2):
	# Escala un rectángulo (útil para ajustar colisiones)
	pass

func _comprobar_colision():
	# Comprueba si el jugador choca con algún asteroide
	pass

# -------------------------
# DIBUJO DE FONDOS
# -------------------------

func _color_fondo_jugando():
	# Cambia el color de fondo cuando la partida está activa
	pass
		
func _color_game_over():
	# Cambia el color de fondo al llegar a GAME OVER
	pass

# -------------------------
# DIFICULTAD PROGRESIVA
# -------------------------

func _actualizar_dificultad(delta: float):
	# Ajusta la dificultad aumentando velocidad y reduciendo intervalos
	pass

Este primer código no hace que el juego “funcione” todavía, pero marca el punto de partida: Le dice a Godot que este será un juego 2D, y define la estructura básica para que luego sepamos dónde colocar cada parte de la lógica del juego (inicialización, movimiento, dibujo, controles).

De esta forma, establecemos el guion antes de comenzar a trabajar: todavía no vemos ningún resultado, pero sabemos cómo se va a organizar todo.

inicializar_pantalla()

Esta función sirve para que el juego “sepa” qué tamaño tiene la ventana en la que se va a mostrar. Cuando un juego se abre, Godot crea un espacio llamado viewport, que no es más que la zona rectangular donde se dibuja todo lo que aparece en pantalla. Esa zona tiene un ancho y un alto (por ejemplo, 1920 píxeles de ancho y 1080 de alto). Con esta función, el juego pregunta a Godot: “¿qué tamaño tiene la ventana ahora mismo?” y guarda esa información en una variable llamada pantalla. Esa variable no es más que un par de números: el primero indica el ancho y el segundo la altura.

¿Por qué es importante guardar esto? Porque a lo largo del juego necesitas saber los límites de la pantalla para que las cosas no se salgan. Por ejemplo, cuando el jugador mueve su nave, el código comprueba que no se vaya más allá de la izquierda o la derecha usando el ancho guardado. También, cuando se crean asteroides que caen desde arriba, el juego elige una posición aleatoria dentro del ancho de la pantalla para que siempre aparezcan dentro de los bordes visibles. Incluso el color de fondo o los botones de salir se dibujan ocupando todo el espacio según el tamaño que guardaste.

En resumen: esta función es como medir la pizarra antes de empezar a escribir en ella. Una vez que sabes sus dimensiones, puedes colocar correctamente todo lo que quieras pintar, mover o limitar dentro de tu “ventana de juego”.

func _inicializar_pantalla():
	# Guardamos el tamaño actual de la pantalla/viewport (Vector2(ancho, alto)).
	pantalla = get_viewport_rect().size

Por sí sola, esta función no “muestra” nada en la pantalla ni cambia nada visible al jugador. Por lo tanto, de momento no notarás ninguna diferencia en el juego.

inicializar_jugador()

Esta función se encarga de crear y colocar al jugador en su posición inicial dentro del juego. Para ello utiliza un objeto Rect2, que en Godot es básicamente un rectángulo definido por una posición (x, y) y un tamaño (ancho y alto). Ese rectángulo será la “caja” que representa al jugador: tanto para dibujarlo en pantalla como para detectar colisiones con asteroides.

En el código se indica que la posición horizontal del rectángulo es pantalla.x / 2 - TAM_JUGADOR / 2. Esto significa: toma el ancho total de la pantalla (pantalla.x), divídelo entre dos para ir al centro, y después resta la mitad del tamaño del jugador. El resultado es que el rectángulo queda perfectamente centrado en la parte inferior.

La posición vertical, en cambio, se calcula con pantalla.y - TAM_JUGADOR * 1.25. Aquí se parte de la altura total de la pantalla (pantalla.y) y se resta algo más que el tamaño del jugador. Ese “1.25” hace que no quede justo en el borde, sino un poco por encima, lo que da margen visual y evita que parezca que el jugador está pegado al suelo.

Finalmente, el ancho y el alto del rectángulo son simplemente TAM_JUGADOR, que es una constante que define el tamaño del jugador. Así se garantiza que siempre tenga la misma forma cuadrada.

func _inicializar_jugador():
	# Colocamos al jugador centrado horizontalmente y un poco por encima del borde inferior.
	jugador = Rect2(
		pantalla.x / 2 - TAM_JUGADOR / 2,     # x (centrado)
		pantalla.y - TAM_JUGADOR * 1.25,      # y (cerca del borde inferior)
		TAM_JUGADOR,                          # ancho
		TAM_JUGADOR                           # alto
	)

Cuando arranque la partida, el jugador aparecerá dibujado en el centro inferior de la pantalla, ligeramente elevado respecto al borde. Si esta función no existiera, el jugador no tendría un lugar inicial bien definido: podría aparecer fuera de la pantalla, en una esquina o ni siquiera estar visible. Con _inicializar_jugador(), en cambio, siempre empieza en el mismo punto, lo que da consistencia y claridad al juego.

dibujar_jugador()

Esta función se encarga de mostrar gráficamente al jugador en pantalla. Hasta ahora, en funciones como _inicializar_jugador(), solo se había definido la “caja” (Rect2) que representa al jugador en memoria, pero eso por sí solo no se ve. Aquí es donde realmente se dibuja la nave.

El código utiliza el método draw_texture_rect, que sirve para pintar una imagen (en Godot se llama textura) dentro de un rectángulo concreto. En este caso, la textura es TEX_JUGADOR, que seguramente es una variable que guarda la imagen de la nave, y el rectángulo es jugador, que ya tiene las coordenadas y el tamaño que definimos antes. El tercer parámetro (false) indica que no se estire la imagen para ajustarse al rectángulo más allá de lo necesario, respetando su escala.

func _dibujar_jugador():
	# Dibujamos la nave del jugador usando su rectángulo como destino.
	draw_texture_rect(TEX_JUGADOR, jugador, false)

Cuando se llame a _dibujar_jugador(), la nave aparecerá en pantalla justo en la posición que calculamos en _inicializar_jugador(). Antes de esta función, el jugador existía “en datos” pero era invisible; ahora, con el dibujo, se convierte en algo que el jugador puede ver y controlar. Sin esta función, por mucho que movieras el jugador o comprobaras colisiones, no verías ninguna nave en pantalla.

comprobar_pantalla_tactil_y_raton()

Esta función es la que conecta al juego con la interacción táctil o con el ratón, es decir, traduce lo que hace el jugador con los dedos o el clic en acciones dentro del juego.

El parámetro event representa lo que ha hecho el jugador (pulsar la pantalla, hacer clic con el ratón, soltar, etc.), y se utilizará de la siguiente forma:

  1. Filtrado del tipo de evento: Primero, la función comprueba si el evento es de tipo toque de pantalla (InputEventScreenTouch) o clic de ratón (InputEventMouseButton). Si no es uno de esos, no hace nada.
  2. Cuando se pulsa (event.pressed):
    • Lo primero que revisa es si la pulsación ocurrió dentro del rectángulo boton_cerrar, y entonces llama a get_tree().quit.call_deferred(), que es la manera segura de cerrar la aplicación en Godot. Con esto, el botón de la “X” funciona tanto con el ratón como al pulsar con el dedo.
    • Si el clic no se realizó sobre el botón de cerrar, entonces mira dónde pulsó el jugador respecto a la nave. Calcula el centro de la nave (centro_nave) y si el toque/clic está a la izquierda de ese centro, activa la variable tocando_izquierda, y si está a la derecha, activa tocando_derecha. Estas variables son las que usa luego la función _mover_jugador() para desplazar la nave en la dirección correcta.
  3. Cuando se suelta (else): Si el jugador levanta el dedo o suelta el clic, las variables tocando_izquierda y tocando_derecha se ponen en false. Eso significa que la nave deja de moverse y se queda quieta.
func _comprobar_pantalla_tactil_y_raton(event: InputEvent):
	# Control con pantalla táctil o ratón:
	# - Si pulsas sobre el botón de cerrar: se termina el juego.
	# - Si pulsas a la izquierda de la nave: mueves a la izquierda (y al revés).
	# - Al soltar: se detiene el movimiento.
	if event is InputEventScreenTouch or event is InputEventMouseButton:
		if event.pressed:
			# Si se ha pulsado el botón de cerrar el juego, finalizamos la ejecución de la aplicación de forma segura.
			if boton_cerrar.has_point(event.position):
				if get_tree(): 
					get_tree().quit.call_deferred()
				return
			
			# Decidimos la dirección según dónde has pulsado respecto al centro de la nave.
			var centro_nave = jugador.position.x + TAM_JUGADOR / 2
			if event.position.x < centro_nave:
				tocando_izquierda = true
				tocando_derecha = false
			else:
				tocando_derecha = true
				tocando_izquierda = false
		else:
			# Al soltar botón o dejar de tocar la pantalla, detenemos el movimiento.
			tocando_izquierda = false
			tocando_derecha = false

A partir de ahora dispondremos de la siguiente funcionalidad, que nos permitirá un poco más adelante mover nuestra nave horizontalmente:

  • Si tocas/haces clic en la parte izquierda de la pantalla (a la izquierda de la nave), activaremos una variable para que la nave se mueva a la izquierda.
  • Si lo haces en la parte derecha, activaremos otra variable para que la nave se mueve a la derecha.
  • Cuando levantas el dedo o sueltas el clic, la nave se detiene (ya que desactivaremos las variables de movimiento).
  • Si tocas/haces clic en la esquina superior derecha (el botón «X»), el juego se cierra.

Observaremos el resultado de esta función en breve.

comprobar_teclado()

Esta función hace lo mismo que la de pantalla táctil/ratón, pero con teclado. Es decir, escucha qué tecla ha pulsado el jugador y ajusta el comportamiento del juego en consecuencia. Principalmente traduce las pulsaciones de teclas en movimientos o reinicios, manteniendo el mismo sistema de variables booleanas (tocando_izquierda y tocando_derecha) que después usa _mover_jugador() para calcular el desplazamiento real:

  1. Reinicio tras el “Game Over”: Al principio se comprueba si el jugador ha perdido (muerto == true) y pulsa la acción "ui_accept" (que en Godot suele estar asociada a Enter o Espacio), y se llama a _reiniciar_juego(). Esto permite reiniciar la partida de forma rápida con una tecla, sin necesidad de ratón ni botones extra.
  2. Movimiento hacia la izquierda: Cuando se detecta que se ha pulsado la acción de moverse a la izquierda (normalmente la flecha izquierda o la tecla A), se activa la variable tocando_izquierda = true, y cuando se suelta esa tecla, se pone en false.
  3. Movimiento hacia la derecha: Igual que lo anterior, pero con "ui_right" (flecha derecha o tecla D). Mientras esté pulsada se pone a true ( tocando_derecha = true). Al soltarla, se pone a false.
func _comprobar_teclado(event: InputEvent):
	# Reiniciar el juego si hemos perdido: “ui_accept” suele ser Enter o Espacio.
	if muerto and event.is_action_pressed("ui_accept"):	
		_reiniciar_juego()
		return

	# Controles con teclado. Usamos acciones abstractas (Input Map) en lugar de teclas fijas.
	if event.is_action_pressed("ui_left"):
		tocando_izquierda = true
	if event.is_action_released("ui_left"):
		tocando_izquierda = false

	if event.is_action_pressed("ui_right"):
		tocando_derecha = true
	if event.is_action_released("ui_right"):
		tocando_derecha = false

A partir de ahora, dispondremos de la siguiente funcionalidad:

  • Si la partida está en “Game Over”, al pulsar Espacio o Enter se reinicia el juego.
  • Si mantienes pulsada la flecha izquierda (o la tecla asignada) mientras estás jugando, la nave se desplazará a la izquierda.
  • Al mantener pulsada la flecha derecha (o la tecla asignada) mientras estás jugando, la nave se moverá a la derecha.
  • Al soltar la tecla, la nave se detiene automáticamente.

En Proyecto > Configuración del Proyecto > Mapa de Entrada, puedes configurar las teclas asociadas a las acciones ui_leftui_right y ui_accept. Godot ya nos proporciona algunas teclas configuradas por defecto (cursores izquierda/derecha y Enter/Espacio), y nosotros podemos añadir las que creamos convenientes, como por ejemplo las teclas A y D.

A continuación vamos a implementar la función que nos permite mover el jugador dependiendo de las variables que estamos actualizando a medida que pulsamos las teclas correspondientes o pulsamos en la pantalla. Observaremos el resultado conseguido en el siguiente punto.

mover_jugador(delta)

Esta función es clave porque no solo define o dibuja al jugador, sino que le da movimiento, es decir, controla la posición horizontal de la nave del jugador. Aquí entra en juego un concepto importante en videojuegos: el parámetro delta. Ese número representa el tiempo transcurrido entre un frame y el siguiente. Usarlo asegura que la velocidad de movimiento de la nave y los asteroides no dependa de la velocidad de ejecución del juego en ordenadores o dispositivos diferentes.

Primero se define una variable dir que indica hacia dónde quiere moverse el jugador:

  • -1 significa izquierda,
  • 0 significa que está quieto,
  • +1 significa derecha.

Después, el código mira qué controles están activos (tocando_izquierda o tocando_derecha). Según eso, cambia el valor de dir, y con esa dirección ya decidida, se calcula el nuevo movimiento utilizando una sencilla fórmula (jugador.position.x + dir * vel_jugador * delta), donde:

  • vel_jugador es la velocidad base de la nave.
  • dir indica hacia dónde multiplicar esa velocidad (positivo a la derecha, negativo a la izquierda).
  • delta ajusta el movimiento según el tiempo real entre frames.

Ese valor se mete dentro de clamp(), que es una función para poner límites. En este caso, el límite inferior es 0 (borde izquierdo de la pantalla) y el superior es pantalla.x - TAM_JUGADOR (borde derecho menos el tamaño de la nave). Gracias a clamp, el jugador nunca se sale de la pantalla aunque insista en seguir moviéndose.

func _mover_jugador(delta: float):
	var dir = 0  # Dirección horizontal: -1 izquierda, 0 quieto, +1 derecha
	
	# Revisamos qué bandera de control está activa.
	if tocando_izquierda: dir = -1
	elif tocando_derecha: dir = +1

	# Sumamos desplazamiento = velocidad * tiempo * dirección.
	# clamp evita que el jugador se salga por los bordes (0 a pantalla.x - tamaño).
	jugador.position.x = clamp(
		jugador.position.x + dir * vel_jugador * delta,
		0, pantalla.x - TAM_JUGADOR
	)

Después de implementar esta función, cuando mantengas pulsada la tecla o el control de izquierda/derecha, la nave se desplazará en esa dirección. Si sueltas el control, dir vuelve a ser 0 y la nave se queda quieta. Además, siempre se mantiene dentro de los límites visibles de la pantalla: nunca se pierde por un lado ni aparece cortada.

Dicho comportamiento también se producirá cuando usemos el ratón o pantallas táctiles, pulsando a la izquierda o derecha de la nave, tal como establecimos en la función comprobar_pantalla_tactil_y_raton().

crear_boton_cerrar()

Esta función se encarga de definir el área del botón para cerrar el juego. Igual que antes con el jugador, aquí se utiliza un Rect2 (un rectángulo con posición y tamaño). Ese rectángulo será la “zona clicable” o el espacio donde luego se dibujará el botón:

  • pantalla.x - TAM_BOTON_CERRAR: coloca el rectángulo pegado al borde derecho de la pantalla, restando su propio tamaño para que encaje justo dentro.
  • 0: en el eje vertical, empieza en la parte más arriba de la pantalla (la esquina superior).
  • TAM_BOTON_CERRAR: tanto ancho como alto son iguales, de forma que queda un cuadrado.
func _crear_boton_cerrar():
	# Creamos un rectángulo en la esquina superior derecha con el tamaño indicado.
	boton_cerrar = Rect2(pantalla.x - TAM_BOTON_CERRAR, 0, TAM_BOTON_CERRAR, TAM_BOTON_CERRAR)

Después de esta función, tienes definido un área cuadrada en la esquina superior derecha de la pantalla. Esa será la posición reservada para el botón de “cerrar”. Por sí sola, esta función no dibuja nada todavía: solo establece el rectángulo donde luego se podrá pintar el botón y detectar si el jugador hace clic ahí.

En otras palabras: es como poner un marco invisible en la esquina superior derecha, que más adelante se convertirá en un botón visible y funcional.

mostrar_boton_cerrar()

Esta función se encarga de dibujar en pantalla el botón de cerrar que antes definimos en _crear_boton_cerrar(). Hasta ese momento, el rectángulo boton_cerrar existía en memoria, pero era invisible. Aquí es donde se le coloca una imagen para que el jugador la vea:

  • TEX_BOTON_CERRAR es la textura, es decir, la imagen del icono del botón (se suele utilizar un dibujo con una “X”).
  • boton_cerrar es el rectángulo (Rect2) que define la posición y el tamaño en la pantalla, creado antes.
  • false significa que la imagen se debe estirar para que ocupe todo el rectángulo definido para el botón.
func _mostrar_boton_cerrar():
	# Dibujamos el icono del botón de cerrar. Al hacer clic/tap encima, cerramos el juego.
	draw_texture_rect(TEX_BOTON_CERRAR, boton_cerrar, false)	

Gracias a esta función, en la esquina superior derecha de la pantalla aparecerá un pequeño botón con el icono que hayas puesto en TEX_BOTON_CERRAR. A partir de ese momento, el jugador puede reconocer visualmente dónde debe hacer clic si quiere cerrar el juego. Y como ya existe la comprobación en la función de entrada (_comprobar_pantalla_tactil_y_raton), hacer clic dentro de esa zona provocará que la aplicación se cierre.

En resumen: _crear_boton_cerrar() definió el área, y _mostrar_boton_cerrar() coloca la imagen encima para que ese área se vea como un botón real y funcional.

crear_ui_tiempo()

Esta función prepara el marcador que mostrará el tiempo que el jugador ha conseguido sobrevivir. Para ello, crea una nueva etiqueta de texto (Label.new()) y la guarda en la variable etiqueta_tiempo. Esa etiqueta comienza mostrando "0.0 s", lo que representa el punto de partida del cronómetro.

Además, se personaliza su aspecto para que sea fácilmente legible: en el código se ajusta el tamaño de la fuente con la constante TAM_TEXTO. De esta manera, aunque la etiqueta aparezca por defecto en la esquina superior izquierda de la pantalla, el texto no se verá demasiado pequeño. También se marca explícitamente que la etiqueta debe ser visible desde el principio, asegurando que el jugador vea el contador nada más empezar la partida.

Finalmente, se añade la etiqueta como un child de la escena principal mediante add_child(...). Esto es lo que hace que el texto realmente aparezca en pantalla. En resumen, al iniciar la partida aparecerá en la esquina superior izquierda un contador que marca 0.0 s, listo para ir actualizándose en cada frame gracias a otra función que lo hará avanzar con el tiempo.

func _crear_ui_tiempo():
	# Crea la etiqueta que muestra el tiempo de supervivencia.
	# Por defecto la Label aparece en (0, 0), es decir, en la esquina superior izquierda
	etiqueta_tiempo = Label.new()
	etiqueta_tiempo.text = "0.0 s"
	etiqueta_tiempo.set("theme_override_font_sizes/font_size", TAM_TEXTO)
	etiqueta_tiempo.visible = true
	add_child(etiqueta_tiempo)

Después de añadir esta función aparecerá un contador visible en la esquina superior izquierda, que empieza en 0.0 s y luego se irá actualizando con otra función. Esto le da al juego un elemento extra de retroalimentación: el jugador ya no solo intenta sobrevivir, sino que también puede medir cuánto tiempo ha resistido, o competir con otros para ver quién dura más.

actualizar_tiempo(delta)

Esta función es la encargada de que el contador de supervivencia vaya avanzando mientras dura la partida. En Godot, el parámetro delta representa el tiempo que ha pasado desde el último frame, normalmente una fracción de segundo. Usar delta en lugar de sumar simplemente 1 hace que el cronómetro sea exacto aunque el juego vaya más rápido o más lento en distintos ordenadores o dispositivos.

Dentro de la función, primero se comprueba si el jugador no está muerto. Si sigue vivo, se suma delta a la variable tiempo_total. De esa manera, el cronómetro acumula el tiempo real que pasa durante la partida. Si el jugador ha perdido, ya no se actualiza más, y el contador se queda congelado mostrando el último tiempo conseguido.

Finalmente, se actualiza el texto de la etiqueta de tiempo. Aquí entra en juego snappedf(tiempo_total, 0.1): esta función redondea el número para mostrarlo con un paso de 0.1 segundos, es decir, con un solo decimal. De esa forma, en la pantalla no aparecen números con demasiados decimales, sino algo legible como 3.4 s, 12.7 s, etc. Ese valor se convierte en texto y se muestra en etiqueta_tiempo.

func _actualizar_tiempo(delta: float):
	# Si seguimos vivos, sumamos delta al cronómetro.
	if not muerto:
		tiempo_total += delta
	# snappedf(x, step) redondea con un “paso”. Aquí dejamos 1 decimal.
	etiqueta_tiempo.text = str(snappedf(tiempo_total, 0.1)) + " s"

Antes de esta función, el contador creado en _crear_ui_tiempo() estaba siempre fijo en 0.0 s. Después de añadir _actualizar_tiempo(), el marcador se va incrementando en tiempo real, dándole al jugador una referencia clara de cuánto ha durado su partida. Además, al morir, el tiempo se congela, permitiendo comparar resultados entre partidas.

crear_ui_game_over()

Esta función prepara la etiqueta de texto que mostrará el mensaje de “GAME OVER” cuando el jugador pierda la partida. Igual que con el cronómetro, se utiliza un nodo Label para mostrar texto en pantalla.

Primero se crea la etiqueta (Label.new()) y se le ajusta el tamaño de la fuente usando la constante TAM_TEXTO, para que el mensaje sea lo bastante grande y visible. A diferencia del cronómetro, aquí la etiqueta empieza oculta (visible = false). Esto es importante: el mensaje no debe aparecer desde el inicio, sino únicamente cuando el jugador muera.

Finalmente, se añade a la escena con add_child(...), lo que significa que la etiqueta ya está preparada en memoria y lista para mostrarse en el momento adecuado, aunque por ahora siga invisible.

func _crear_ui_game_over():
	# Crea la etiqueta de "GAME OVER" (oculta hasta que perdamos).
	etiqueta_game_over = Label.new()
	etiqueta_game_over.set("theme_override_font_sizes/font_size", TAM_TEXTO)
	etiqueta_game_over.visible = false
	add_child(etiqueta_game_over)

Antes de esta función, al perder simplemente se detenía la partida, pero no había un mensaje claro que indicara lo ocurrido. Después de añadirla, existe una etiqueta especial que puede activarse para decirle al jugador que la partida actual ha finalizado. Esto hace que la experiencia sea más clara y más parecida a un juego completo, donde el Game Over es un elemento esperado.

crear_asteroides(delta)

Esta función es la responsable de ir generando nuevos asteroides que «caen» desde la parte superior de la pantalla. Para controlar la frecuencia con la que aparecen, se usa un contador (tiempo_proximo_asteroide) que va acumulando el tiempo transcurrido en cada frame gracias a delta. Cuando ese contador supera el valor de intervalo_asteroides (por ejemplo, cada 1 segundo, o 0.5 según la dificultad), se “resetea” a 0 y se crea un nuevo asteroide.

Cada asteroide se genera con cierta aleatoriedad para que la partida sea más interesante:

  • Primero, se calcula una posición horizontal aleatoria (x = randi_range(0, pantalla.x - TAM_ASTEROIDE)), asegurándose de que el asteroide esté dentro de los límites de la pantalla.
  • Después, se le asigna un tamaño también aleatorio, que puede ser desde la mitad hasta el doble del tamaño base (tam = randi_range(TAM_ASTEROIDE / 2.0, TAM_ASTEROIDE * 2.0)). Eso significa que en la partida habrá asteroides más pequeños y fáciles de esquivar, y otros mucho más grandes y complicados.
  • Finalmente, se crea un Rect2 con esas dimensiones y se coloca justo por encima de la pantalla (y = -tam). Esto hace que el asteroide “entre” desde fuera del área visible, cayendo hacia abajo.
func _crear_asteroides(delta: float):
	# Cada “intervalo_asteroides” segundos, creamos un asteroide nuevo arriba.
	tiempo_proximo_asteroide += delta
	if tiempo_proximo_asteroide >= intervalo_asteroides:
		tiempo_proximo_asteroide = 0.0

		# Posición X aleatoria dentro de los límites de la pantalla.
		var x = randi_range(0, pantalla.x - TAM_ASTEROIDE)

		# Tamaño aleatorio entre la mitad y el doble del tamaño base.
		var tam = randi_range(TAM_ASTEROIDE / 2.0, TAM_ASTEROIDE * 2.0)

		# Colocamos el rectángulo justo por encima de la pantalla (y negativo), y así “entra” cayendo.
		var asteroide = Rect2(x, -tam, tam, tam)
		asteroides.append(asteroide)

Al añadir la función _crear_asteroides(), el juego empieza a generar asteroides de forma automática cada cierto tiempo, con posiciones horizontales aleatorias y tamaños variables, lo que introduce variedad y dificultad progresiva. Aunque en este punto aún no se ven en pantalla (solo se almacenan en la lista asteroides), pronto conseguiremos que el juego tenga un “ritmo de obstáculos” que irán cayendo y que después podrán moverse, dibujarse y comprobar colisiones con el jugador.

mover_asteroides(delta)

Esta función se encarga de dar vida y movimiento a los asteroides que fueron creados con la función anterior. Hasta ese momento, los asteroides existían pero estaban fijos en la parte superior, esperando. Con esta función, comienzan a caer.

El primer bloque del código recorre la lista de asteroides (for i in asteroides.size():) y a cada uno le incrementa su posición vertical (position.y) multiplicando la velocidad vel_asteroides por delta. Esto asegura que todos se muevan hacia abajo de forma suave y proporcional al tiempo real, sin importar los frames por segundo del ordenador. Así, el jugador ve cómo los asteroides entran desde arriba y caen hacia la zona donde se encuentra su nave.

El segundo bloque se ocupa de la limpieza: elimina los asteroides que ya se han salido por la parte inferior de la pantalla. Lo hace con filter, que crea una nueva lista solo con aquellos cuya posición vertical sigue siendo menor que la altura de la pantalla (pantalla.y). Gracias a esto, no se acumulan objetos invisibles fuera del área de juego, lo que evita que el rendimiento baje innecesariamente.

func _mover_asteroides(delta: float):
	# Los asteroides caen hacia abajo a velocidad constante (vel_asteroides).
	for i in asteroides.size():
		asteroides[i].position.y += vel_asteroides * delta

	# Eliminamos los que ya salieron de la pantalla (su y es mayor que la altura).
	# filter crea una nueva lista con los elementos que cumplan la condición.
	asteroides = asteroides.filter(func(o): return o.position.y < pantalla.y)

Antes de esta función, los asteroides simplemente aparecían arriba y se quedaban quietos. Después de añadirla, realmente caen con velocidad, se desplazan por toda la pantalla y desaparecen al llegar abajo, convirtiéndose en los obstáculos que el jugador debe esquivar. En otras palabras, es aquí donde los asteroides dejan de ser “estáticos” y se transforman en un peligro real.

dibujar_asteroides()

Esta función se encarga de mostrar en pantalla todos los asteroides que existen en ese momento. Hasta aquí, los asteroides se habían creado con _crear_asteroides() y habían empezado a moverse con _mover_asteroides(), pero aún eran solo datos en memoria (rectángulos con posiciones y tamaños). Con esta función se convierten en imágenes visibles para el jugador.

El código recorre la lista asteroides y, para cada uno, usa draw_texture_rect(TEX_ASTEROIDE, asteroide, false). Eso significa que se dibuja la textura asociada al asteroide (TEX_ASTEROIDE) dentro del rectángulo que ya define su posición y tamaño. El false asegura que la textura se estire y se dibuje ocupando toda el área de cada rectángulo.

func _dibujar_asteroides():
	# Dibujamos cada asteroide con su textura y su rectángulo correspondiente.
	for asteroide in asteroides:
		draw_texture_rect(TEX_ASTEROIDE, asteroide, false)

Aunque los asteroides “existían” y se movían antes de añadir esta función, el jugador no podía verlos. Después de añadirla, los asteroides aparecen claramente en pantalla mientras caen, con su tamaño aleatorio y desde distintas posiciones, convirtiéndose en los obstáculos visibles que hay que esquivar.

game_over()

Con esta función conseguimos que el juego reaccione al momento en que el jugador pierde: se marca el estado de derrota con la variable muerto = true (así el resto de funciones dejan de mover al jugador o crear asteroides), se desactivan los controles (tocando_izquierda y tocando_derecha a false), y en la interfaz se oculta el contador de tiempo para mostrar en su lugar el mensaje “GAME OVER” junto con el tiempo que el jugador ha sobrevivido. En otras palabras, pasamos de la acción a la pantalla final que indica claramente que la partida ha terminado.

func _game_over():
	# Marcamos estado de derrota y actualizamos UI.
	muerto = true
	tocando_izquierda = false
	tocando_derecha = false	
	etiqueta_game_over.text = "GAME OVER (" + etiqueta_tiempo.text + ")"
	etiqueta_tiempo.visible = false
	etiqueta_game_over.visible = true

Antes de esta función, al chocar con un asteroide no había un final claro: el jugador podía seguir moviéndose y no se mostraba ningún mensaje. Después de añadirla, la partida termina de forma ordenada: la nave se bloquea y aparece un mensaje de “GAME OVER” y el tiempo sobrevivido, dejando claro al jugador que esa ronda ha terminado.

reiniciar_juego()

Esta función es la encargada de volver a empezar la partida desde cero después de un Game Over. Su lógica es muy sencilla: recarga toda la escena actual con get_tree().reload_current_scene().

En Godot, la escena es como el “contenedor” de todo lo que forma el juego en ese momento: el jugador, los asteroides, las etiquetas de UI, etc. Al recargarla, se destruye la escena en curso y se crea una nueva copia desde el estado inicial definido en el script. Eso significa que todas las variables (muerto, tiempo_total, asteroides, etc.) vuelven automáticamente a los valores originales con los que empezó la partida.

func _reiniciar_juego():
	# Recargamos la escena actual (vuelta a empezar).
	# Todas las variables vuelven a su valor inicial definido en el script.
	get_tree().reload_current_scene()

Antes de tener esta función, al finalizar una partida no teníamos forma de volver a jugar sin cerrar y abrir el juego de nuevo. Tras añadirla, el jugador puede reiniciar al instante: la nave aparece otra vez en su posición inicial, los asteroides desaparecen, el contador vuelve a cero y el marcador de Game Over se oculta. En resumen, el juego se resetea como si acabara de iniciarse.

escalar_rect(r)

Esta función sirve para ajustar la zona de colisión del jugador o de los asteroides, lo que normalmente se llama la hitbox. En los juegos, la imagen (sprite) de un objeto suele ser un poco más grande o tener bordes irregulares, y si usáramos su tamaño exacto para detectar colisiones, sería demasiado artificial: parecería que te golpean incluso cuando los objetos apenas se rozan visualmente.

Lo que hace el código es recibir un rectángulo r (el área original del objeto) y devolver una versión un poco más pequeña. Para eso utiliza el método grow, que puede agrandar o encoger un rectángulo en todas sus direcciones. Si le pasamos un número positivo, el rectángulo crece; si es negativo, se encoge.

La clave está en este cálculo: (FACTOR_HITBOX - 1.0) * r.size.x / 2. Aquí se usa una constante que definimos al principio de nuestro código, llamada FACTOR_HITBOX. Si vale, por ejemplo, 0.8, el resultado será negativo y el rectángulo se hará un 20 % más pequeño en cada lado. De ese modo, la zona que cuenta para la colisión ya no es el borde exacto del sprite, sino una versión reducida que da más sensación de justicia al jugador.

func _escalar_rect(r: Rect2):
	# Hacemos la “hitbox” un poco más pequeña que el sprite real para
	# que las colisiones estén más ajustadas.
	# Rect2.grow(margen) crece (margen > 0) o encoge (margen < 0) por todos los lados.
	# Aquí calculamos un margen negativo proporcional al ancho.
	return r.grow((FACTOR_HITBOX - 1.0) * r.size.x / 2)

Antes de esta función, cualquier roce mínimo entre el sprite del jugador y un asteroide contaba como choque. Después de añadirla, el área efectiva de colisión es más pequeña, y el jugador tiene un poco más de margen: puede esquivar mejor, aunque visualmente las imágenes parezcan rozarse. Esto hace que el juego sea menos frustrante y más equilibrado.

comprobar_colision()

Esta función es la encargada de decidir si el jugador ha chocado con algún asteroide. Para hacerlo más justo, no compara los rectángulos originales, sino versiones reducidas gracias a la función _escalar_rect().

Primero, coge el rectángulo del jugador y lo pasa por _escalar_rect(), creando jugador_escalado, que es básicamente la hitbox ajustada del jugador. Luego, recorre cada asteroide en la lista y hace lo mismo: genera un asteroide_escalado con una hitbox más pequeña que la del sprite real.

Después, utiliza el método intersects(), que comprueba si dos rectángulos se solapan. Si alguno de los asteroides escalados se cruza con el rectángulo escalado del jugador, significa que han colisionado, y en ese momento se llama a _game_over(), terminando la partida.

func _comprobar_colision():
	# Comprobamos si el rect del jugador (reducido) intersecta con alguno de los asteroides.
	var jugador_escalado = _escalar_rect(jugador)
	for asteroide in asteroides:
		var asteroide_escalado = _escalar_rect(asteroide)
		if jugador_escalado.intersects(asteroide_escalado):
			_game_over()

Antes de esta función, no había forma de detectar un choque: el jugador y los asteroides podían ocupar el mismo espacio sin consecuencias. Después de añadirla, el juego ya sabe cuándo se produce un impacto, y reacciona mostrando el Game Over. Además, gracias a que se usan hitbox reducidas, los choques resultan más realistas: parece que la nave esquiva “por los pelos”, aunque en realidad los rectángulos reales que ocupan las imágenes ya se están tocando.

color_fondo_jugando()

Esta función se encarga de dibujar el fondo del juego mientras la partida está en marcha. En este caso, el fondo no es una imagen, sino simplemente un rectángulo pintado con un color liso.

El código crea un Rect2 que empieza en la esquina superior izquierda de la pantalla (Vector2.ZERO, es decir, coordenada (0,0)) y que ocupa todo el ancho y el alto guardados en la variable pantalla. Luego, con draw_rect(..., COLOR_FONDO, true) se rellena ese rectángulo del color definido en COLOR_FONDO. El true indica que el rectángulo debe pintarse sólido, no solo con el borde.

func _color_fondo_jugando():
	# Pintamos un rectángulo del tamaño de la pantalla con color de fondo.
	draw_rect(Rect2(Vector2.ZERO, pantalla), COLOR_FONDO, true)

Antes de esta función, el fondo podía quedar transparente o mostrar un color por defecto, sin coherencia visual. Después de añadirla, la pantalla completa se pinta con el color elegido para el estado de “jugando” (por ejemplo, un azul oscuro o un negro espacial). Esto da uniformidad a la partida y diferencia claramente el área del juego.

color_game_over()

Esta función se encarga de dar un efecto visual cuando el jugador pierde la partida. En lugar de cambiar completamente la pantalla, pinta una capa de color encima de todo lo que ya estaba dibujado.

El código crea, igual que antes, un rectángulo que cubre toda la pantalla (Rect2(Vector2.ZERO, pantalla)) y lo rellena con COLOR_GAME_OVER. Este color no suele ser totalmente opaco, sino que se define con algo de transparencia (por ejemplo, un rojo semitransparente). Eso hace que, al dibujarse, todavía se pueda ver el juego debajo, pero como “oscurecido” o teñido de rojo.

func _color_game_over():
	# Dibujamos una capa roja semitransparente por encima al perder.
	# Truco visual: se ve el juego “oscurecido” debajo.
	draw_rect(Rect2(Vector2.ZERO, pantalla), COLOR_GAME_OVER, true)

Antes de esta función, al perder solo aparecía el texto de “GAME OVER”, sin un cambio visual que transmitiera impacto. Después de añadirla, la pantalla se tiñe con un rojo translúcido, creando una atmósfera de derrota. Es un truco visual que no borra lo que ya estaba en pantalla, sino que lo cubre con un filtro de color. Así, el jugador sigue viendo los asteroides y la nave congelados en su sitio, pero bajo una capa que marca el final de la partida.

actualizar_dificultad(delta)

Esta función es la que hace que el juego se vuelva más complicado conforme pasa el tiempo. La clave está en que usa el parámetro delta (el tiempo que ha pasado desde el último frame) para aplicar cambios de manera suave y proporcional al tiempo real.

Por un lado, aumenta la velocidad de caída de los asteroides: vel_asteroides += 10 * delta. Eso significa que, cuanto más tiempo sobrevivas, más deprisa caerán, obligándote a reaccionar más rápido. Por otro lado, reduce el intervalo entre apariciones (intervalo_asteroides), haciendo que cada vez haya más asteroides en pantalla. Para que no se vuelva imposible, se usa un max(0.1, ...), lo que garantiza que nunca se generen más rápido que una décima de segundo.

func _actualizar_dificultad(delta: float):
	# Aumenta poco a poco la velocidad de caída de los asteroides
	vel_asteroides += 10 * delta
	
	# Disminuye el tiempo entre asteroides (aparecen más rápido)
	# Usamos max() para poner un límite inferior y que nunca baje de 0.1 s.
	intervalo_asteroides = max(0.1, intervalo_asteroides - 0.01 * delta)

Antes de esta función, el juego se mantenía siempre con la misma dificultad: los asteroides caían a la misma velocidad y con la misma frecuencia. Después de añadirla, la partida se vuelve dinámica: al principio es más tranquila, pero poco a poco la pantalla se llena de asteroides más rápidos y que aparecen con más frecuencia. Esto introduce una curva de dificultad progresiva que hace que cada partida tenga tensión creciente y que la supervivencia sea un verdadero reto.

Resumiendo

La eficacia del modelo de Godot para este tipo de juegos es evidente en la clara separación de responsabilidades entre las funciones del ciclo de vida. _ready() gestiona la configuración inicial, _process(delta) se encarga de la lógica evolutiva del estado del juego, _draw() traduce ese estado en una representación visual e _input(event) maneja las interacciones del usuario de forma asíncrona.

Estructura del proyecto y configuración inicial

La base del juego se establece sobre una escena simple cuyo nodo raíz es un Node2D. A este nodo se le asocia un único script que contendrá toda la programación, centralizando la lógica en un solo lugar. Para la gestión de los recursos gráficos, se pre-cargan las imágenes del jugador (jugador.png), los asteroides (asteroide.png) y la interfaz (boton_cerrar.png). El uso de preload() es una decisión clave para garantizar una experiencia de usuario fluida, ya que asegura que todos los recursos gráficos estén cargados en memoria antes de que la partida comience.

Los componentes de configuración más relevantes definidos mediante la interfaz de Godot para el proyecto son los siguientes:

  • Mapa de Entrada: Define las acciones abstractas (ui_left, ui_right, ui_accept) y su mapeo a teclas físicas (cursores, A/D, Enter/Espacio), desacoplando el código de las entradas específicas del hardware.
  • Ventana de Visualización: Configura la resolución (por ejemplo 1920x1080), el modo de pantalla (Fullscreen) y el comportamiento de escalado (viewport, expand) para asegurar una experiencia visual consistente en diferentes dispositivos.

Una vez definidos estos parámetros estáticos, el siguiente paso es definir los datos que gestionarán el estado dinámico de cada partida.

Gestión del estado del juego

El rol de las constantes (const) es definir parámetros de configuración que no cambian durante la partida. Actúan como un «panel de ajustes» centralizado que permite modificar el comportamiento del juego sin alterar la lógica del código. Entre las constantes clave se encuentran los tamaños de los objetos (TAM_JUGADOR, TAM_ASTEROIDE), los colores de la interfaz (COLOR_FONDO, COLOR_GAME_OVER) y las rutas a los recursos de texturas (TEX_JUGADOR, TEX_ASTEROIDE), cargadas eficientemente con preload().

Por otro lado, las variables actúan como la memoria viva del juego, almacenando toda la información que cambia dinámicamente durante la partida:

  • pantalla (Vector2): Almacena las dimensiones del viewport (ancho y alto). Su función es esencial para el posicionamiento de entidades y la validación de los límites de la pantalla, asegurando que los objetos permanezcan dentro del área visible.
  • jugador (Rect2): Representa la posición y el tamaño de la nave del jugador. Actúa como su entidad lógica y física, utilizada tanto para el dibujo como para la detección de colisiones.
  • asteroides (Array[Rect2]): Es una colección dinámica que contiene todas las instancias de asteroides activos en la escena. Esta lista se modifica constantemente a medida que se crean nuevos asteroides y se eliminan los que salen de la pantalla.
  • muerto (bool): Una variable booleana que controla el flujo principal del juego. Al pasar a true, detiene la lógica de la partida (movimiento, creación de asteroides, etc.), indicando que se ha alcanzado el estado de «Game Over».
  • tiempo_total (float): Funciona como un cronómetro de supervivencia, acumulando los segundos que el jugador ha permanecido con vida.
  • tocando_izquierda / tocando_derecha (bool): Implementan un patrón de sondeo de estado. En lugar de ejecutar el movimiento directamente desde el evento de entrada, desacoplan la detección (en _input) de la lógica de movimiento (en _process), garantizando que la física del juego se actualice de forma consistente dentro del bucle principal y no de manera esporádica basada en eventos de hardware.

Una vez que la estructura de datos que define el estado del juego está establecida, el siguiente paso es analizar cómo el motor Godot orquesta la manipulación de este estado a lo largo del tiempo a través de su ciclo de vida.

Ciclo de vida en Godot

El modelo de ejecución de Godot se puede entender como un sistema de «relojes internos» o funciones de callback que el motor invoca en momentos específicos. Este modelo estructura el flujo de un programa en tiempo real, organizando el código en fases lógicas y predecibles. Para esta implementación de «Asteroides», el ciclo de vida se gestiona a través de cuatro funciones principales, cada una con una responsabilidad claramente definida.

Fase de inicialización

La función _ready() es el punto de entrada que se ejecuta una única vez cuando se crea la escena. Su propósito es preparar el estado inicial del juego, asegurando que todos los componentes estén en una condición válida y conocida antes de que comience la partida.

La secuencia de operaciones de inicialización es la siguiente:

  1. randomize(): Se inicializa el generador de números aleatorios con una nueva semilla. Esto garantiza que la aparición y el tamaño de los asteroides varíen en cada partida.
  2. _inicializar_pantalla(): Se capturan y almacenan las dimensiones del viewport en la variable pantalla.
  3. _inicializar_jugador(): Se crea el Rect2 del jugador y se posiciona en su ubicación inicial, centrado en la parte inferior de la pantalla.
  4. _crear_ui_tiempo(), _crear_boton_cerrar(), _crear_ui_game_over(): Instanciación, configuración y adición de los nodos de la interfaz de usuario (Label y áreas de Rect2) al árbol de escenas principal a través de add_child().

Bucle principal

La función _process(delta) es el corazón del juego, ejecutándose en cada frame. Es responsable de actualizar el estado del juego en función del tiempo y las interacciones. El parámetro delta es crucial, ya que representa el tiempo transcurrido (en segundos) desde el frame anterior, permitiendo que el movimiento y otras lógicas basadas en el tiempo sean independientes de la tasa de frames del dispositivo.

El flujo de control dentro de esta función está gobernado por la variable muerto. Una instrucción return detiene la ejecución de la lógica principal si el jugador ha perdido, congelando el estado del juego. Si la partida está activa, se ejecutan las siguientes actualizaciones en un orden específico:

  • _mover_jugador(delta)
  • _crear_asteroides(delta)
  • _mover_asteroides(delta)
  • _comprobar_colision()
  • _actualizar_tiempo(delta)
  • _actualizar_dificultad(delta)

Finalmente, se llama a queue_redraw(), un mecanismo que solicita al motor que invoque la función _draw() en el ciclo de renderizado actual, asegurando que los cambios de estado se reflejen visualmente.

Renderizado

La función _draw() es la responsable del dibujo inmediato de todas las entidades visuales del juego en cada frame en el que se ha solicitado un redibujado. En esta arquitectura, se utiliza para traducir el estado del juego (las variables de posición, color y texturas) a una representación gráfica en la pantalla.

El orden de las llamadas a las funciones de dibujo es crítico, ya que establece un sistema de capas visuales implícito. La secuencia es la siguiente:

  1. _color_fondo_jugando(): Se dibuja el fondo negro, que actúa como la capa más profunda.
  2. _dibujar_asteroides(): Se dibujan todos los asteroides.
  3. _dibujar_jugador(): Se dibuja la nave del jugador sobre los asteroides y el fondo.
  4. _mostrar_boton_cerrar(): Se dibuja el botón de la interfaz.
  5. _color_game_over(): Si el jugador ha perdido, se dibuja una capa roja semitransparente por encima de todo. Este es un ‘truco visual’ efectivo que comunica el fin de la partida sin destruir la escena, permitiendo al jugador ver el estado final congelado (su nave y los asteroides) bajo un filtro de color.

Eventos de entrada

La función _input() actúa como el gestor de eventos de entrada asíncronos. Se invoca cada vez que el usuario realiza una acción, como presionar una tecla, hacer clic con el ratón o tocar la pantalla. Para mantener el código organizado, la función delega la lógica de procesamiento a dos sub-funciones: _comprobar_pantalla_tactil_y_raton() y _comprobar_teclado().

El patrón de diseño clave empleado aquí es el desacoplamiento entre la detección de la entrada y la acción resultante. La función _input no ejecuta el movimiento del jugador directamente. En su lugar, modifica variables de estado (tocando_izquierda y tocando_derecha). Estas variables se consultan posteriormente en el bucle _process(), que es el responsable de calcular y aplicar el movimiento.

Componentes principales del juego

El juego se compone de cuatro elementos clave: la nave del jugador, que se mueve dentro de los límites de la pantalla; los asteroides, generados y desplazados de forma aleatoria; un sistema de colisiones que determina el fin de la partida y permite reiniciar; y la interfaz con dificultad progresiva, que muestra la información al jugador y aumenta el reto con el tiempo.

El jugador (nave)

La gestión del jugador está distribuida en una cadena de funciones que manejan su ciclo de vida completo. _inicializar_jugador() establece su estado inicial, posicionándolo centrado en la parte inferior de la pantalla. _dibujar_jugador() se encarga de su representación visual, dibujando la textura correspondiente en la posición definida por su Rect2.

El movimiento se implementa en _mover_jugador(). Esta función lee las variables de estado tocando_izquierda y tocando_derecha para determinar una dirección de movimiento (dir con valor -1, 0 o 1). La nueva posición se calcula con la fórmula posición_actual + dir * vel_jugador * delta, garantizando un desplazamiento suave e independiente del framerate. Finalmente, la función clamp() restringe la posición resultante dentro de los límites horizontales de la pantalla, evitando que la nave se salga del área de juego.

Los obstáculos (asteroides)

El ciclo de vida de los asteroides es gestionado por tres funciones principales. La función _crear_asteroides() utiliza un temporizador (tiempo_proximo_asteroide) para controlar la frecuencia de aparición de nuevos obstáculos. Cada vez que se genera un nuevo asteroide, se le asigna una posición horizontal (randi_range) y un tamaño aleatorios, lo que introduce variedad y jugabilidad.

Una vez creados, _mover_asteroides() actualiza la posición vertical de cada asteroide en la lista asteroides, haciendo que se muevan por la pantalla. Esta función también incluye un importante mecanismo de optimización: utiliza asteroides.filter() para crear una nueva lista que contiene únicamente los asteroides que todavía se encuentran dentro de los límites de la pantalla. Debemos aclarar que aunque este enfoque es fácil de implementar, en escenarios de alto rendimiento con miles de entidades, la creación de un nuevo array en cada frame no sería óptimo. Para un juego de esta escala es válido, pero en proyectos más complejos se podría considerar un enfoque que modifique la lista in-situ para minimizar la asignación de memoria. Finalmente, _dibujar_asteroides() itera sobre la lista y renderiza cada uno en la pantalla.

Sistema de colisiones y transición de estado

La detección de colisiones se centraliza en _comprobar_colision(). Para que las colisiones sean más «justas» y menos frustrantes, el sistema no utiliza los rectángulos visuales de los objetos. En su lugar, emplea el concepto de «hitbox», una zona de impacto efectiva más pequeña que el sprite. Esto se implementa a través de la función _escalar_rect(), que recibe un Rect2 y devuelve una versión reducida basada en la constante FACTOR_HITBOX. La función comprueba si la hitbox del jugador se superpone (intersects()) con la de algún asteroide. Esta técnica de desacoplar la ‘hitbox’ del sprite visible es un pilar fundamental en el diseño de juegos de acción, ya que impacta directamente en la ‘sensación de juego’, primando la percepción del jugador sobre la precisión física.

Si se detecta una colisión, se invocan las funciones de transición de estado. _game_over() es la responsable de cambiar el estado del juego: establece la variable muerto a true, desactiva los controles del jugador y actualiza la interfaz de usuario para mostrar el mensaje de fin de partida. Para volver a jugar, la función _reiniciar_juego() utiliza el método get_tree().reload_current_scene(), para restablecer completamente el estado del juego, ya que al recargar la escena, todas las variables (muerto, tiempo_total, asteroides, etc.) se reinicializan automáticamente a sus valores por defecto definidos en el script, eliminando la necesidad de una función de reseteo manual.

Interfaz de usuario (UI) y dificultad progresiva

La gestión de la interfaz de usuario se basa en la creación dinámica de nodos Label. Funciones como _crear_ui_tiempo() y _crear_ui_game_over() instancian estos nodos, configuran sus propiedades (tamaño de fuente, visibilidad) y los añaden a la escena. La actualización del cronómetro se realiza en _actualizar_tiempo(), que incrementa un contador y refresca el texto del Label correspondiente en cada frame.

Para mantener el interés del jugador, el juego implementa una curva de dificultad progresiva en la función _actualizar_dificultad(). Este mecanismo ajusta dinámicamente dos parámetros clave con el paso del tiempo: aumenta gradualmente la velocidad de caída de los asteroides (vel_asteroides) y reduce el intervalo de tiempo entre sus apariciones (intervalo_asteroides). El resultado es un desafío que se intensifica de forma constante, manteniendo la tensión a lo largo de la partida. Para evitar que la dificultad escale hasta un punto injugable, se impone un límite inferior de 0.1 segundos al intervalo de aparición mediante el uso de max(), garantizando que la cadencia de obstáculos nunca supere un umbral predefinido.

Ejercicios propuestos

A continuación te proponemos una serie de ejercicios prácticos para que experimentes creando tu propia versión del juego Asteroides en Godot. La idea es que cambies el código fuente del juego y las imágenes, y observes cómo los distintos cambios afectan a la jugabilidad y a la experiencia del jugador. Te sugerimos que ajustes los parámetros que establecen la dificultad del juego (velocidad de los asteroides, precisión de las colisiones o ritmo de progresión) y además también es muy importante que cambies aspectos estéticos (paleta de colores, tamaño de los objetos o recursos gráficos) según tus propios gustos y preferencias.

Algunas de las páginas web que te pueden ayudar a completar estos ejercicios son las siguientes:

  • OpenGameArt (https://opengameart.org/): repositorio de gráficos y recursos libres para videojuegos.
  • Kenney (https://kenney.nl/): colección de assets gratuitos y de calidad profesional para juegos.
  • Google AI Studio (https://aistudio.google.com/): entorno online gratuito con inteligencia artificial para generar imágenes, obtener sugerencias de colores, etc.
  • I love IMG (https://www.iloveimg.com/es): herramienta práctica para recortar, redimensionar, quitar fondo o convertir imágenes.

Modificar la velocidad inicial de los asteroides

Cambia el valor de la variable vel_asteroides para que los asteroides empiecen cayendo más rápido o más lento. Comprueba cómo afecta esto a la dificultad de las partidas.

Modificar el incremento de la dificultad

En la función _actualizar_dificultad(), haz que la velocidad de los asteroides aumente más o menos rápido, o que el intervalo entre asteroides baje más despacio o más rápido. ¿Qué configuración te parece la más divertida?

Cambiar los colores de fondo

Edita las constantes COLOR_FONDO (cuando juegas) y COLOR_GAME_OVER (cuando pierdes). Prueba colores llamativos o incluso combina tonos que creen un efecto distinto al perder (por ejemplo, verde o azul en lugar de rojo).

Ajustar el tamaño de la nave y los asteroides

Modifica las constantes TAM_JUGADOR y TAM_ASTEROIDE para que los objetos se vean más grandes o más pequeños. Observa cómo un cambio de tamaño afecta a la dificultad: ¿es más fácil esquivar los asteroides cuando son pequeños?

Modificar la detección de colisiones

Cambia el valor de FACTOR_HITBOX (que se utiliza en la función _escalar_rect()) para ajustar el tamaño de la «hitbox». Si lo haces más grande, las colisiones serán más estrictas; si lo haces más pequeño, será más fácil esquivar los asteroides. Encuentra el valor que te parezca más equilibrado.

Cambiar las imágenes del juego

Busca o genera con IA (o dibuja tú mismo) una nueva imagen para la nave y otra para los asteroides. Si cambias el nombre de los ficheros, actualiza las variables TEX_JUGADOR y TEX_ASTEROIDE para utilizar el nombre de tus imágenes y juega para ver cómo cambia la estética del juego.

Cambiar la posición inicial del jugador

En la función _inicializar_jugador(), modifica la fórmula de la posición para que la nave no empiece centrada, sino más a la izquierda, a la derecha o incluso más arriba. Comprueba si esto hace el juego más fácil o más complicado.

El juego completo

Desde el siguiente enlace te puedes descargar un ZIP con todo el código del proyecto:

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:

Y en el siguiente vídeo puedes observar cómo el juego se ejecuta desde Godot a pantalla completa, y cómo la dificultad del juego se va incrementando hasta que finalmente el jugador colisiona con un asteroide. En ese momento se utiliza el botón de cerrar para volver al entorno de desarrollo:

Inteligencia artificial

¿Cómo ponerte al día?

Canales de YouTube recomendados

  • Jon Hernández. Divulgación semanal sobre IA. Publica vídeos nuevos cada lunes por la noche. Proporciona actualizaciones constantes sobre tecnologías emergentes, combinando noticias y explicaciones accesibles.
  • Carlos Santana (Dot CSV). Explicaciones profundas sobre IA y aprendizaje automático, presentadas de manera clara y amena. Uno de los divulgadores en español más reconocidos de IA. Mezcla precisión técnica con narrativa accesible, ideal para quienes buscan profundidad sin perder claridad.
  • Xavier Mitjana. Tutoriales que conectan IA con creatividad y producción, mostrando aplicaciones prácticas e inspiradoras. Orientado a quienes desean integrar IA en procesos creativos o narrativos, como cine, diseño o automatización visual.
  • Alejavi Rivera. IA explicada de forma práctica, directa y con casos reales de uso. Combina teoría con aplicación concreta, ideal para emprendedores o quienes buscan resultados inmediatos y efectivos con IA.
  • Javi Galué. Herramientas, estrategias, hacks y formas de monetizar usando IA. Muy orientado a la productividad, la creatividad aplicada y la generación de ingresos reales mediante IA.
  • Elena Santos (ChicaGeek). Consejos tecnológicos prácticos (apps, trucos, gadgets y tutoriales) pensados para facilitar la vida digital. Muy accesible y útil para el día a día; ideal para público general que busca mejorar su uso de tecnología sin tecnicismos.

Comparación de los canales recomendados:

CanalEnfoque principalA destacar
Jon HernándezActualizaciones semanales de IAFrecuencia constante; enfoque claro y accesible
Carlos Santana (Dot CSV)IA explicada en profundidadEquilibrio entre rigor técnico y divulgación clara
Xavier MitjanaIA aplicada a creatividad y negociosEnfoque profesional, creativo y muy visual
Alejavi RiveraCasos prácticos de IAAplicabilidad directa; visión de marketing digital
Javi GaluéHacks, monetización y productividad con IAOrientación a resultados reales y uso eficiente
Elena Santos (ChicaGeek)Trucos tecnológicos cotidianosConsejos muy prácticos para la vida digital diaria

Blogs recomendados

  • Microsiervos.com. Temática muy variada (tecnología, ciencia, Internet), pero también humor, astronomía, aviación, conspiraciones, puzzles, reseñas de libros o películas y experiencias personales.
  • Xataka.com. Noticias, análisis, reseñas extensas de dispositivos (móviles, gadgets, informática), exploración espacial, energías renovables, etc., cubriendo toda la actualidad tecnológica.

Comparación de los blogs recomendados:

CaracterísticaMicrosiervos.comXataka.com
OrígenesBlog personal de un grupo de amigosBlog profesional
TemáticaMuy variada: tecnología, ciencia, humor, curiosidadesTecnología aplicada, noticias, análisis, gadgets
EstiloInformal, personal, humorísticoInformativo, técnico, bien estructurado
FormatoPosts sueltos, blog estilo “diario”Publicación periódica con secciones y verticales
EquipoPequeño grupo de autores (Alvy, Wicho y colaboraciones)Equipo amplio y diversificado de periodistas y editores

¿Qué IA debo utilizar?

ChatGPT

Se puede acceder online (https://chatgpt.com) y con la app:

Muy versátil, entiende bien el lenguaje natural, ideal para redacción, resúmenes, creación de materiales didácticos y resolver dudas de alumnos. Ventaja: calidad y fluidez. Inconveniente: no siempre gratuito en sus versiones más nuevas.

Gemini

Se puede acceder online (https://gemini.google.com/app) y con la app:

Muy versátil, entiende bien el lenguaje natural, ideal para redacción, resúmenes, creación de materiales didácticos y resolver dudas de alumnos. Ventaja: calidad y fluidez. Inconveniente: no siempre gratuito en sus versiones más nuevas.

Qwen

Se puede acceder online (https://chat.qwen.ai) y con la app:

Excelentes resultados en razonamiento matemático y técnico, con versiones grandes y pequeñas. Ideal para programación y ciencias. Ventaja: muy bueno en lógica y coste relativamente bajo. Inconveniente: menos natural en la redacción creativa.

NotebookLM

Se puede acceder online desde https://notebooklm.google.com/ y con la app:

Pensado para estudiar, resumir documentos y crear infografías, presentaciones y vídeos. Muy útil para alumnos y maestros que quieren convertir apuntes o libros en resúmenes y cuestionarios. Ventaja: especializado en aprendizaje. Inconveniente: no es tan flexible como otros modelos de uso general.

Z.ai

Se puede acceder online (https://chat.z.ai).

Destaca por ser ligero, rápido y económico, con buen rendimiento en chino, inglés y español. Ventaja: eficiencia y bajo coste, y generación excelente de aplicaciones informáticas y presentaciones con diapositivas. Inconveniente: menos precisión que modelos grandes en explicaciones complejas.

Deepseek

Se puede acceder online (https://www.deepseek.com/en) y con la app:

Especializado en eficiencia y bajo consumo de recursos, pensado para análisis de datos y asistentes técnicos. Ventaja: rapidez y buena relación coste-rendimiento. Inconveniente: menos pulido en lenguaje pedagógico o creativo.

Mistral

Se puede acceder online (https://chat.mistral.ai/chat) y con la app:

Modelos europeos, abiertos y de calidad, muy buenos para tareas multilingües y razonamiento. Ventaja: muy buenos resultados con idiomas europeos. Inconveniente: no dispone de generación de presentaciones de manera automática.

Grok

Se puede acceder online (https://grok.com/) y con la app:

Integrado en la red social X, con estilo más informal. Ventaja: útil para comunicación desenfadada y creatividad. Inconveniente: menos serio y todavía limitado en educación formal.

Claude

Este modelo de IA está más orientado a programadores y empresas de desarrollo de software. Se puede acceder online (https://claude.ai/new) y con la app:

Destaca en ética, seguridad, comprensión de textos largos, y programación de aplicaciones informáticas. Ideal para trabajar con documentos extensos y entornos donde se busca una IA más enfocada al mundo empresarial. Ventaja: maneja textos muy largos y cuida el tono. Inconveniente: menos creativo que ChatGPT.

Hugging Face

Accesible desde https://huggingface.co/spaces.

No es un modelo en sí, sino una plataforma que reúne cientos de modelos de IA abiertos. Ventaja: enorme flexibilidad y recursos gratuitos para experimentar. Inconveniente: requiere más conocimientos técnicos para elegir y usar el modelo correcto (orientado a usuarios expertos).

Comparación de los diversos modelos

IAFortalezasDebilidadesUso ideal en educación
ChatGPTMuy fluido y versátil en lenguaje; excelente para redacción, resúmenes y explicaciones creativasVersiones avanzadas pueden tener costo; menos personalizable a nivel técnicoRedacción de actividades, generación de ejemplos, guías didácticas
Z.ai (GLM‑4.5)Modelo unificado con razonamiento, codificación y agentes; código abierto y coste competitivo (Business Insider, z.ai)Comunidad menor en españolBúsquedas en Internet, razonamiento avanzado, creación de presentaciones y aplicaciones informáticas
Qwen (Alibaba)Gran variedad multimodal (texto, audio, imagen/video); razonamiento avanzado; multilingüe (Wikipedia, Wikipedia, Prismetric)Ecosistema menos conocido fuera de ChinaRecursos multimedia, explicación con voz e imagen, programación de aplicaciones informáticas
DeepSeekAltamente eficiente y abierto; razonamiento potente a muy bajo coste (The Wall Street Journal, Business Insider, Wikipedia)Censura en temas sensibles según versión; consideraciones de privacidad (The Times, Wikipedia)Para recursos locales o de investigación avanzada, cuando se tiene soporte técnico y precaución con contenido censurable
MistralOpen source, flexible, desplegable localmente, razonamiento multilingüe eficiente (kalm.works, Medium, Appvizer)Comunidad menor en educaciónPersonalización local, modelado lingüístico adaptado al contexto local
Google AI Studio / GeminiIntegración con Workspace (Docs, Drive, Classroom); resumen de archivos, notas automáticas (Android Central)Depende del ecosistema Google; preocupaciones de privacidad institucionalIntegración en flujos escolares existentes, generación de notas y resúmenes automáticos
NotebookLM (Google)Asistente de investigación: resúmenes, guías de estudio, podcasts automáticos en español (Cinco Días, Wikipedia, blog.google, Revolgy)Menor flexibilidad para otros usos creativos fuera de investigaciónPreparación de guías, podcasts educativos, interacción con documentos complejos
Grok (xAI)Integración en X y búsquedas en tiempo real, razonamiento avanzado (Grok 4) (Tom’s Guide, Wikipedia, Business Insider)Historial de respuestas controvertidas; riesgo de sesgos; requiere supervisión crítica (The Wall Street Journal)Uso en comunicación dinámica, debates o actividades creativas (con precaución)
Claude (Anthropic)Usado ampliamente a nivel profesional y creación de aplicaciones informáticas (swiftask.ai, anthropic.com)Menor creatividad espontánea; acceso limitado; ecosistema cerrado (reddit.com)Crear cuestionarios, diseño curricular, aplicaciones informáticas, uso empresarial
Hugging FacePlataforma con acceso a muchos modelos abiertos; ideal para explorar y experimentarNecesita conocimientos técnicos para elegir y probar modelos adecuadosExploración de nuevas IA y prototipos para usos específicos educativos

¿Qué IA es la mejor?

Ranking mundial

En la página web LMArena podemos utilizar los modelos de inteligencia artificial más populares. Es una plataforma abierta y colaborativa que permite realizar comparaciones directas mediante “batallas” anónimas: el usuario introduce un prompt, recibe dos respuestas generadas por modelos distintos y elige cuál le parece mejor; luego se revelan las identidades de los modelos y el resultado se suma a un ranking público. Creada por investigadores de la Universidad de California en Berkeley, se ha convertido en un referente porque combina transparencia (sus datos se publican para investigación), participación comunitaria y alcance global, evaluando desde modelos de código abierto hasta prototipos pre-lanzamiento de gigantes como OpenAI, Google o Anthropic.

Por ejemplo, en los siguientes enlaces se puede consultar el ranking mundial de los modelos de IA más populares para generación de texto en español, para resolución de problemas matemáticos, para escritura creativa, y de generación y edición de imágenes, según las valoraciones de los usuarios:

Además, se pueden probar todos los modelos de forma gratuita a través de este enlace: https://lmarena.ai/?mode=direct

Casos prácticos

Traducciones

Traductor de Google desde su app:

Generación y edición de imágenes e infografías completas

Ejemplos generados con ChatGPT utilizando imágenes reales (en diciembre del 2024, y agosto del 2025 respectivamente):

Ejemplo generados con «Nano Banana» desde Google AI Studio con los siguientes prompts:

  • Crea un dibujo de un niño corriendo porque llega tarde al colegio y sus padres están detrás, con estilo manga.
  • Cambia la torre del reloj por la Torre Eiffel.

Modelo recomendado «Nano Banana» accesible desde Google AI Studio (es mejor modelo de generación de imágenes que el proporcionado por ChatGPT 5):

Generación de presentaciones con diapositivas

Presentación de ejemplo sobre la Cabalgata de los Reyes Magos de Alcoy generada desde Z.ai:

Presentación de ejemplo para aprender nombres de animales en inglés, generada automáticamente desde kimi.com:

Modelos recomendados:

  • «GLM» desde Z.ai:

Generación de podcasts (o resúmenes)

Ejemplo generado automáticamente a partir de la información disponible en la Wikipedia (https://es.wikipedia.org/wiki/Cabalgata_de_Reyes_Magos_de_Alcoy):

Accesible desde https://notebooklm.google.com/:

Generación de vídeos explicativos

Ejemplo generado automáticamente a partir de la información disponible en la Wikipedia (https://es.wikipedia.org/wiki/Cabalgata_de_Reyes_Magos_de_Alcoy):

Accesible desde https://notebooklm.google.com/:

Libros ilustrados

Modelo recomendado «Gemini» desde Gemini Storybook:

Generación de música

Las vocales en canción:

El baile del abecedario:

Modelo recomendado «Suno 4.5» de Suno.com:

Publicar libros en Amazon

Servicio accesible desde https://kdp.amazon.com/:

Instant buttons con HTML+CSS+Javascript

En esta unidad crearemos una aplicación con una galería interactiva de botones de audio, diseñada para reproducir sonidos de Halloween y Navidad. La interfaz está organizada en dos tablas temáticas: una para Halloween y otra para Navidad, cada una con imágenes representativas que, al hacer clic, activan la reproducción de un sonido específico.

Imágenes y sonidos

A continuación se muestran posibles imágenes y audios que puedes usar. Además, puedes hacer clic en estos enlaces para descargarte un zip con todas las imágenes y todos los sonidos.

Halloween

Navidad

Fichero «index.html»

El archivo index.html define la estructura básica de la aplicación, que es una galería de botones de audio con imágenes interactivas. A continuación explicamos cada parte del archivo.

Cabecera (<head>)

El código que se encuentra en esta primera sección del archivo se encarga de lo siguiente:

  • Define el título de la página («Instant buttons»).
  • Incluye enlaces a fuentes personalizadas de Google Fonts:
    • Creepster: utilizada para el tema de Halloween.
    • Mountains of Christmas: utilizada para el tema de Navidad.
  • Vincula la hoja de estilos style.css, donde están los estilos para el fondo, las fuentes, y los efectos visuales de las imágenes.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Instant buttons</title>

    <!-- Fuentes personalizadas desde Google Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Creepster&family=Mountains+of+Christmas:wght@400;700&display=swap" rel="stylesheet">

    <!-- Archivo de estilos CSS -->
    <link rel="stylesheet" href="style.css">
</head>
<body>
    ...
</body>
</html>

Cuerpo de la página (<body>)

Dentro del cuerpo, se encuentran dos tablas temáticas: una de Halloween y otra de Navidad.

Tabla de Halloween

  • Utiliza la clase halloween para aplicar estilos específicos desde style.css, como el color rojo y la fuente Creepster.
  • Cada celda (<td>) contiene una imagen y tiene un evento onclick que ejecuta la función play con dos parámetros: el nombre del archivo de sonido y el volumen de reproducción.
  • Los archivos de imagen se encuentran en una carpeta llamada img y están nombrados de forma consecutiva para cada botón (por ejemplo, halloween1.webp, halloween2.webp, etc.).
<table class="halloween">
    <caption>Halloween</caption>
    <!-- Fila 1 de botones de Halloween -->
    <tr>
        <td onclick="play('halloween1', 0.5)"><img src="img/halloween1.webp"></td>
        <td onclick="play('halloween2', 0.7)"><img src="img/halloween2.webp"></td>
        <td onclick="play('halloween3', 0.7)"><img src="img/halloween3.webp"></td>
    </tr>
    <!-- Fila 2 de botones de Halloween -->
    <tr>
        <td onclick="play('halloween4', 0.7)"><img src="img/halloween4.webp"></td>
        <td onclick="play('halloween5', 0.7)"><img src="img/halloween5.webp"></td>
        <td onclick="play('halloween6', 0.7)"><img src="img/halloween6.webp"></td>
    </tr>
    <!-- Fila 3 de botones de Halloween -->
    <tr>
        <td onclick="play('halloween7', 0.7)"><img src="img/halloween7.webp"></td>
        <td onclick="play('halloween8', 1.0)"><img src="img/halloween8.webp"></td>
        <td onclick="play('halloween9', 1.0)"><img src="img/halloween9.webp"></td>
    </tr>
</table>

Tabla de Navidad

  • Sigue la misma estructura que la tabla de Halloween, pero está estilizada con la clase christmas, que aplica un color azul y la fuente Mountains of Christmas.
  • Las imágenes de Navidad también están organizadas en una cuadrícula de tres filas y tres columnas, cada una con un archivo de audio distinto.
<table class="christmas">
    <caption>Christmas</caption>
    <tr>
        <td onclick="play('christmas1', 0.7)"><img src="img/christmas1.webp"></td>
        <td onclick="play('christmas2', 1.0)"><img src="img/christmas2.webp"></td>
        <td onclick="play('christmas3', 0.7)"><img src="img/christmas3.webp"></td>
    </tr>
    <tr>
        <td onclick="play('christmas4', 0.7)"><img src="img/christmas4.webp"></td>
        <td onclick="play('christmas5', 0.7)"><img src="img/christmas5.webp"></td>
        <td onclick="play('christmas6', 1.0)"><img src="img/christmas6.webp"></td>
    </tr>
    <tr>
        <td onclick="play('christmas7', 0.7)"><img src="img/christmas7.webp"></td>
        <td onclick="play('christmas8', 1.0)"><img src="img/christmas8.webp"></td>
        <td onclick="play('christmas9', 1.0)"><img src="img/christmas9.webp"></td>
    </tr>
</table>

Elemento de audio y JavaScript

Antes de cerrar el elemento <body> de nuestra página web, deberemos incluir un elemento <audio> que utilizaremos para reproducir los sonidos, y también deberemos enlazar el fichero con el código JavaScript:

...
<body>
    ...
    <audio id="player"></audio>
    <script src="script.js"></script>
</body>
</html>
  • Elemento <audio>: Este elemento, con el id="player", se usa para reproducir los archivos de sonido. Al hacer clic en cada imagen, la función JavaScript play modifica su fuente (src) y volumen, y luego reproduce el audio seleccionado.
  • JavaScript: Al final del archivo se carga el script script.js, que contiene la función play para controlar la reproducción de audio cuando se hace clic en una imagen.

En resumen…

El archivo index.html estructura la página, definiendo tablas temáticas interactivas para reproducir sonidos festivos de Halloween y Navidad mediante el uso de JavaScript y estilos en CSS para mejorar la experiencia visual y sonora.

El archivo script.js es un código JavaScript sencillo que controla la reproducción de sonidos al hacer clic en las imágenes de la galería. A continuación, explicamos la función principal de este archivo.

Fichero «script.js»

Nuestra aplicación de instant buttons utiliza sólo una sencilla función javascript para reproducir los audios con ajuste de volumen incluido. A continuación se muestra todo el código necesario agrupado en la función play:

function play(soundName, soundVolume) {
    // Obtener el elemento de audio
    const player = document.getElementById("player");

    // Cambiar el archivo de audio a reproducir
    player.src = `audio/${soundName}.mp3`;

    // Cambiar el volumen del audio
    player.volume = soundVolume;

    // Reproducir el audio
    player.play();
}

Explicación detallada de la función play

Esta función play recibe dos parámetros, soundName y soundVolume, y se ejecuta cada vez que se hace clic en una de las imágenes en el archivo index.html. La función realiza los siguientes pasos:

  1. Obtener el elemento de audio:
   const player = document.getElementById("player");

Utiliza document.getElementById("player") para obtener el elemento de audio <audio id="player"> en el archivo HTML. Este elemento de audio es el que realmente reproduce los sonidos.

  1. Cambiar el archivo de audio:
   player.src = `audio/${soundName}.mp3`;

Modifica la propiedad src del elemento player para cambiar el archivo de audio que se va a reproducir. La función construye la ruta del archivo de audio usando el nombre del archivo pasado como parámetro soundName (por ejemplo, halloween1, christmas2, etc.). Así, si soundName es halloween1, la función buscará y cargará el archivo audio/halloween1.mp3.

  1. Ajustar el volumen:
   player.volume = soundVolume;

Cambia el volumen de reproducción del audio estableciendo la propiedad volume del elemento player. Este valor se toma del parámetro soundVolume, que es un número entre 0 (silencio) y 1 (volumen máximo). Por ejemplo, algunos sonidos tienen un volumen de 0.7 y otros de 1.0 en función de la experiencia que se quiera dar al usuario.

  1. Reproducir el sonido:
   player.play();

Finalmente, llama al método play() del elemento player para iniciar la reproducción del archivo de audio con el archivo y volumen especificados.

En resumen…

La función play es un controlador de reproducción de sonido que:

  • Cambia el archivo de audio a reproducir.
  • Ajusta el volumen de la reproducción.
  • Inicia la reproducción del archivo de audio.

Esta función permite que, al hacer clic en las imágenes de la galería, cada imagen reproduzca un sonido específico asociado a la festividad (Halloween o Navidad) con el volumen predefinido en la función onclick del HTML.

Fichero «style.css»

El archivo style.css contiene los estilos visuales de la aplicación, desde el fondo animado hasta los efectos visuales para cada imagen y tabla temática. A continuación, explicamos cada sección del código CSS.

Fondo animado con gradiente

body {
    background: linear-gradient(90deg, #1a1a1a, #003c5a, #0086c9, #9001d7);
    background-size: 200% 100%;
    animation: aurora 2.5s infinite alternate;
}
  • Gradiente de colores: El fondo de la página es un gradiente lineal que pasa por varios tonos oscuros y fríos (negro, azul oscuro, azul brillante y morado). Esto crea un ambiente visual similar a una aurora boreal.
  • Tamaño del fondo: background-size: 200% 100%; hace que el gradiente se duplique en ancho, lo que permite que el fondo se mueva de lado a lado.
  • Animación: animation: aurora 2.5s infinite alternate; activa una animación de ida y vuelta cada 2.5 segundos, creando el efecto de movimiento en el fondo.
@keyframes aurora {
    0% { background-position: 0% 50%; }
    100% { background-position: 100% 50%; }
}
  • Animación aurora: Define una transición que mueve el gradiente de izquierda a derecha en el 100% del ancho, y luego vuelve al inicio, lo que da el efecto de aurora en movimiento.

Estilos para las imágenes de los botones

img {
    width: 100%;
    max-width: 250px;
    transition: 0.5s ease;
}

img:active {
    transform: scale(1.5);
    filter: brightness(0.75) contrast(1.25);
    box-shadow: 0 0 20px black;
}
  • Ancho de las imágenes: width: 100% asegura que las imágenes ocupen todo el ancho de la celda. max-width: 250px limita el tamaño máximo de las imágenes en pantallas grandes.
  • Transición suave: transition: 0.5s ease aplica una transición suave al hacer clic en las imágenes.
  • Efecto al hacer clic (:active): Cuando una imagen es presionada, se agranda un 50% (transform: scale(1.5)), se oscurece ligeramente (brightness(0.75)) y aumenta el contraste (contrast(1.25)), además de aplicar una sombra negra para resaltar el efecto de interacción.

Estilos para las tablas de Halloween y Navidad

Cada tabla tiene estilos específicos que incluyen una fuente, color y sombra para dar un ambiente acorde a la festividad.

Tabla de Halloween

.halloween {
    font-size: 250%;
    color: red;
    font-family: 'Creepster', cursive;
    text-shadow: 3px 3px 4px black, -3px -3px 4px #550000;
}
  • Fuente: La tabla de Halloween usa la fuente Creepster, que tiene un estilo tenebroso.
  • Color: El color del texto es rojo (color: red;).
  • Sombra de texto: Aplica una sombra oscura y difusa (text-shadow: 3px 3px 4px black, -3px -3px 4px #550000;), dando un efecto de terror al texto.

Tabla de Navidad

.christmas {
    font-size: 250%;
    font-weight: bold;
    color: blue;
    font-family: 'Mountains of Christmas', cursive;
    text-shadow: 2px 2px 5px #00aaff, -2px -2px 5px #ffffff;
}
  • Fuente: Utiliza Mountains of Christmas, que tiene un estilo festivo.
  • Color: El color del texto es azul (color: blue;).
  • Sombra de texto: Usa sombras suaves en azul claro y blanco (text-shadow: 2px 2px 5px #00aaff, -2px -2px 5px #ffffff;) para dar un efecto festivo de brillo.

Estilos generales para las tablas

table {
    margin: auto;
}
  • Centrado de las tablas: La propiedad margin: auto; centra las tablas horizontalmente en la página.

En resumen…

Este archivo CSS define la estética de la aplicación:

  • Un fondo animado tipo aurora boreal.
  • Efectos interactivos para las imágenes de los botones.
  • Estilos específicos para cada tabla (Halloween y Navidad) con colores y fuentes temáticas.

Estos estilos ayudan a crear una experiencia visual inmersiva y coherente con cada festividad.

Ejercicios propuestos

Cambiar la orientación de la animación de fondo

Prueba a cambiar la orientación de fondo para que el efecto de aurora boreal se produzca con un movimiento de arriba hacia abajo y viceversa. Para ello basta con que modifiques el ángulo del gradiente a 180 grados, el tamaño del fondo al doble de alto, y el porcentaje de la animación en el eje Y de 0% a 100%:

/* Fondo con gradiente animado vertical tipo aurora */
body {
    /* Cambiamos el ángulo del gradiente a 180 grados para que vaya de arriba hacia abajo */
    background: linear-gradient(180deg, #1a1a1a, #003c5a, #0086c9, #9001d7);
    /* Ajustamos el tamaño del fondo para que solo se duplique en altura */
    background-size: 100% 200%;
    /* Mantiene la misma animación */
    animation: aurora 2.5s infinite alternate;
}

/* Animación de gradiente tipo aurora en vertical */
@keyframes aurora {
    /* Fijamos la posición horizontal en 50% y animamos solo la posición vertical */
    0% { background-position: 50% 0%; }
    100% { background-position: 50% 100%; }
}

Animación de fondo radial

Prueba a cambiar la orientación de fondo para que el efecto de aurora boreal sea de tipo radial con movimiento diagonal. Para ello basta con que definas el gradiente con radial-gradient(circle, ...), y luego dupliques el tamaño del fondo al doble de ancho y de alto, y en último lugar cambies los porcentajes de la animación en ambos ejes de 0% a 100%:

/* Fondo con gradiente animado tipo aurora */
body {
    /* Cambiamos a un gradiente radial para un efecto más llamativo */
    background: radial-gradient(circle, #1a1a1a, #003c5a, #0086c9, #9001d7);
    /* Ajustamos el tamaño del gradiente para que cubra más área */
    background-size: 200% 200%;
    /* Mantiene la misma animación */
    animation: aurora 2.5s infinite alternate;
}

/* Animación de gradiente tipo aurora */
@keyframes aurora {
    /* Modificamos la posición inicial y final para adaptarla al gradiente radial */
    0% { background-position: 0% 0%; }
    100% { background-position: 100% 100%; }
}

Diferentes colores de fondo

Prueba a cambiar los colores de la animación de fondo. Para ello solo debes cambiar los colores de la propiedad background dentro del selector body. Por ejemplo:

/* Fondo con gradiente animado tipo aurora de Halloween */
body {
    /* Usamos colores típicos de Halloween en el gradiente radial */
    background: radial-gradient(circle, black, purple, orange, darkred);
    ...
}
...

Utiliza tus propias imágenes y graba tus propios audios

Prueba a usar otras imágenes y otros audios. Puedes encontrar imágenes y audios gratuitos en los siguientes enlaces:

También puedes grabar tus audios e incluirlos directamente en la aplicación. Puedes utilizar tu propio móvil o alguna página web online similar a la siguiente:

Compilar la aplicación para móvil

Prueba a compilar la aplicación para móvil, y así podrás pulsar sobre las imágenes como si fueran botones. Puedes seguir las instrucciones detalladas aquí.

Ten en cuenta que el tamaño máximo para la aplicación en formato móvil es de 10MB, por lo que no podrás utilizar imágenes y audios demasiado grandes. Recomendamos el formato webp para imágenes y mp3 para audio. Puedes cambiar el formato o bajar la calidad de los ficheros con alguna herramienta online. Por ejemplo:

Imágenes:

Audios:

Añadir efecto de vibración al pulsar sobre cada imagen

Puedes añadir efecto de vibración muy fácilmente añadiendo la siguiente línea dentro de la función play del fichero script.js:

function play(soundName, soundVolume) {
    ...
    // Si la vibración está disponible (en el móvil por ejemplo), vibrar durante 100 ms
    if (navigator.vibrate) navigator.vibrate(100);
}

El resultado

En este enlace puedes probar esta versión de la aplicación instant buttons.

Cookie Clicker con HTML+CSS+Javascript (parte 10): Comprando fábricas

En esta parte del proyecto, vamos a añadir al juego la posibilidad de comprar Fábricas que produzcan más galletas para nosotros cada segundo sin tener que hacer clic.

Imagen para activar la mejora

Al pulsar sobre esta imagen activarás la mejora de las fábricas. Puedes elegir cualquier imagen para tu juego, teniendo en cuenta simplemente que deberás colocarla dentro de la carpeta img. A continuación te proporcionamos una imagen de ejemplo:

Modificaciones del fichero «index.html»

Vamos a añadir un div adicional a nuestro código html para poder activar la mejora. De esta forma, el jugador podrá comprar fábricas pulsando sobre la imagen. Por cada fábrica que adquiera el jugador, se generarán más galletas automáticamente cada segundo:

...
<body>
    ...
    <div class="caja-mejora" onclick="comprarMejora('fabricas')">
        <img src="img/fabrica.png" width="125">
        <div id="textoFabricas"></div>
    </div>
    ...
</body>
</html>

Modificaciones del fichero «variables.js»

Añadimos la mejora de fabricas al objeto juego, con un precio inicial de 500 galletas. Cada fábrica comprada generará 20 galletas por segundo:

// Objeto que almacena toda la información relativa al progreso y configuración del juego
let juego = {
    galletas: 0, // Cantidad total de galletas conseguidas
    mejoras: {
        ...
        fabricas: { cantidad: 0, precio: 500, descripcion: "Fábricas que producen 20 galletas por segundo" } // Mejora de Fábricas
    }
}

Modificaciones del fichero «script.js»

Para conseguir que las fábricas produzcan galletas sin que tengamos que hacer clic, deberemos incrementar la cantidad de galletas de forma automática cada segundo. Para ello ya disponemos de la función «producirAutomaticamente()», que creamos en una unidad anterior, dentro del fichero «script.js». Añadiendo una sola línea a esta función podremos conseguir que por cada fábrica que hayamos comprado, consigamos 20 galletas más de forma automática cada segundo:

...

// Función para producir galletas automáticamente
// Se ejecuta cada segundo y suma galletas según las mejoras compradas
function producirAutomaticamente() {
    ...
    juego.galletas += juego.mejoras.fabricas.cantidad * 20;  // Suma las galletas por Fábricas
    guardarProgreso();  // Guarda el progreso automáticamente
}

Cookie Clicker con HTML+CSS+Javascript (parte 9): Comprando granjas

En esta parte del proyecto vamos a añadir al juego la posibilidad de comprar Granjas. De esta forma podremos conseguir más galletas cada segundo sin tener que hacer clic.

Imagen para activar la mejora

Al pulsar sobre esta imagen activarás la mejora de las granjas. Puedes elegir cualquier imagen para tu juego, teniendo en cuenta simplemente que deberás colocarla dentro de la carpeta img. A continuación te proporcionamos una imagen de ejemplo:

Modificaciones del fichero «index.html»

Vamos a añadir un div adicional a nuestro código html para poder activar la mejora. De esta forma, el jugador podrá comprar granjas pulsando sobre la imagen. Por cada granja que adquiera el jugador, se generarán más galletas automáticamente:

...
<body>
    ...
    <div class="caja-mejora" onclick="comprarMejora('granjas')">
        <img src="img/granja.png" width="125">
        <div id="textoGranjas"></div>
    </div>
    ...
</body>
</html>

Modificaciones del fichero «variables.js»

Añadimos la mejora de granjas al objeto juego, con un precio inicial de 400 galletas por cada granja que compremos. Cada una de ellas generará 10 galletas por segundo:

// Objeto que almacena toda la información relativa al progreso y configuración del juego
let juego = {
    galletas: 0, // Cantidad total de galletas conseguidas
    mejoras: {
        ...
        granjas: { cantidad: 0, precio: 400, descripcion: "Granjas que producen 10 galletas por segundo" }, // Mejora de Granjas
    }
}

Modificaciones del fichero «script.js»

Para conseguir que las granjas produzcan galletas sin que tengamos que hacer clic, deberemos incrementar la cantidad de galletas de forma automática cada segundo. Para ello ya disponemos de la función «producirAutomaticamente()», que creamos en una unidad anterior, dentro del fichero «script.js». Añadiendo una sola línea a esta función podremos conseguir que por cada granja que hayamos comprado, consigamos 10 galletas más de forma automática cada segundo:

...

// Función para producir galletas automáticamente
// Se ejecuta cada segundo y suma galletas según las mejoras compradas
function producirAutomaticamente() {
    ...
    juego.galletas += juego.mejoras.granjas.cantidad * 10;  // Suma las galletas por Granjas
    guardarProgreso();  // Guarda el progreso automáticamente
}

Cookie Clicker con HTML+CSS+Javascript (parte 8): Comprando abuelas

En esta parte del proyecto, vamos a añadir al juego la posibilidad de comprar Abuelas que cocinen varias galletas para nosotros cada segundo. De esta forma podremos conseguir incrementar nuestra cantidad de galletas sin tener que hacer clic.

Imagen para activar la mejora

Al pulsar sobre esta imagen activarás la mejora de las abuelas. Puedes elegir cualquier imagen para tu juego, teniendo en cuenta simplemente que deberás colocarla dentro de la carpeta img. A continuación te proporcionamos una imagen de ejemplo:

Modificaciones del fichero «index.html»

Vamos a añadir un div adicional a nuestro código html para poder activar la mejora. De esta forma, el jugador podrá comprar abuelas pulsando sobre la imagen. Por cada abuela que adquiera el jugador, se generarán más galletas automáticamente por segundo:

...
<body>
    ...
    <div class="caja-mejora" onclick="comprarMejora('abuelas')">
        <img src="img/abuela.png" width="150">
        <div id="textoAbuelas"></div>
    </div>
    ...
</body>
</html>

Modificaciones del fichero «variables.js»

Añadimos la mejora de abuelas al objeto juego, con un precio inicial de 300 galletas. Cada abuela comprada generará 5 galletas por segundo:

// Objeto que almacena toda la información relativa al progreso y configuración del juego
let juego = {
    galletas: 0, // Cantidad total de galletas conseguidas
    mejoras: {
        ...
        abuelas: { cantidad: 0, precio: 300, descripcion: "Abuelas que cocinan 5 galletas por segundo" }, // Mejora de Abuelas
    }
}

Modificaciones del fichero «script.js»

Para conseguir que las abuelas cocinen y produzcan galletas sin que tengamos que hacer clic, deberemos incrementar la cantidad de galletas de forma automática cada segundo. Para ello ya disponemos de la función «producirAutomaticamente()», que creamos en la unidad anterior, dentro del fichero «script.js». Añadiendo una sola línea a esta función podremos conseguir que por cada abuela que hayamos comprado, consigamos 5 galletas más de forma automática cada segundo:

...

// Función para producir galletas automáticamente
// Se ejecuta cada segundo y suma galletas según las mejoras compradas
function producirAutomaticamente() {
    ...
    juego.galletas += juego.mejoras.abuelas.cantidad * 5;  // Suma las galletas por Abuelas
    guardarProgreso();  // Guarda el progreso automáticamente
}

Cookie Clicker con HTML+CSS+Javascript (parte 7): Auto clickers

En esta parte del proyecto, vamos a añadir una mejora conocida como Auto Clicker, que permite al jugador ganar galletas automáticamente cada segundo sin tener que hacer click.

Imagen para activar la mejora

Al pulsar sobre esta imagen activarás la mejora de clicks automáticos. Puedes elegir cualquier imagen para tu juego, teniendo en cuenta simplemente que deberás colocarla dentro de la carpeta img. A continuación te proporcionamos una imagen de ejemplo:

Modificaciones del fichero «index.html»

Vamos a añadir un div adicional a nuestro código html para poder activar la mejora. De esta forma, el jugador podrá comprar Auto Clickers pulsando sobre la imagen. Por cada Auto Clicker que adquiera el jugador, se generará una galleta automáticamente por segundo:

...
<body>
    ...
    <div class="caja-mejora" onclick="comprarMejora('autoClickers')">
        <img src="img/mano.png" width="100">
        <div id="textoAutoClickers"></div>
    </div> 
    ...
</body>
</html>

Modificaciones del fichero «variables.js»

Añadimos la mejora de Auto Clickers al objeto juego, con un precio inicial de 200 galletas. Cada Auto Clicker comprado generará una galleta por segundo:

// Objeto que almacena toda la información relativa al progreso y configuración del juego
let juego = {
    galletas: 0, // Cantidad total de galletas conseguidas
    mejoras: {
        ...
        autoClickers: { cantidad: 0, precio: 200, descripcion: "Auto click cada segundo" }, // Mejora de AutoClicker
    }
}

// Variable global para controlar el intervalo de la producción automática
let intervalo = null;

Modificaciones del fichero «script.js»

Deberemos crear una nueva función de JavaScript dentro del fichero «script.js», y la ejecutaremos automáticamente cada segundo:

  • Función reiniciarJuego(): Esta función se encargaba de reiniciar el progreso del juego. Ahora además deberá encargarse de cancelar el intervalo de producción de galletas automáticamente. Para ello deberemos añadir la siguiente línea:
    • clearInterval(intervalo); // Detiene la producción automática de galletas
  • Función producirAutomaticamente(): Esta función se ejecuta cada segundo y añade galletas según la cantidad de Auto Clickers comprados.
  • Inicialización: En la función inicializar(), deberemos configurar un intervalo que ejecute producirAutomaticamente() cada segundo. Para ello utilizaremos la función nativa de JavaScript setInterval.
...

// Función para reiniciar el juego: ¡Sólo debemos añadir la línea con la llamada a 'clearInterval()'!
function reiniciarJuego() {
    // Pregunta al usuario si realmente quiere reiniciar, y si dice que sí, borra el progreso y reinicia la página
    if (confirm("...")) {
        clearInterval(intervalo);  // Detiene la producción automática de galletas
        localStorage.removeItem('juego');  // Elimina el progreso guardado
        location.reload();  // Recarga la página
    }
}

...

// Función para producir galletas automáticamente
// Se ejecuta cada segundo y suma galletas según las mejoras compradas
function producirAutomaticamente() {
    juego.galletas += juego.mejoras.autoClickers.cantidad;  // Suma las galletas por AutoClicker
    guardarProgreso();  // Guarda el progreso automáticamente
}

// Función para inicializar el juego
function inicializar() {
    cargarProgreso();  // Carga el progreso guardado al iniciar el juego
    intervalo = setInterval(producirAutomaticamente, 1000);  // Cada segundo, produce galletas automáticamente
}

inicializar();

Cookie Clicker con HTML+CSS+Javascript (parte 6): Compilando la aplicación para móvil

En esta unidad aprenderemos cómo podemos obtener la aplicación en formato «apk» para instalarla en nuestros propios dispositivos móviles.

Utilizaremos la plataforma VoltBuilder, que facilita la creación de aplicaciones nativas para Android e iOS a partir de proyectos web hechos con HTML, CSS y JavaScript. Sólo tendremos que subir un archivo zip con nuestro proyecto, y la herramienta se encargará de convertirlo en una app nativa. Además, como está disponible en la nube, no tendremos que instalar ningún tipo de software en nuestros equipos.

A continuación se enumeran los pasos que deberemos seguir para compilar nuestra aplicación y obtener el fichero de instalación «.apk».

Icono de la aplicación

Podemos utilizar el mismo icono tanto para mostrarlo en la pestaña del navegador, como para compilar e instalar nuestra aplicación en dispositivos móviles. En nuestro caso, utilizaremos la misma imagen, pero podría ser otra diferente:

Pantalla de bienvenida

La imagen que utilicemos para la pantalla de bienvenida deberá tener cierto margen alrededor, ya que se mostrará en el centro de la pantalla, y la plataforma VoltBuilder la recortará para asegurar que se muestra de manera correcta en cualquier dispositivo y orientación.

Crear fichero de configuración «config.xml»

Primero deberemos crear el archivo «config.xml» en el directorio raiz de la aplicación. A continuación se muestra un ejemplo básico (puedes cambiar el nombre, la descripción, el icono y la pantalla de bienvenida a tu gusto):

<?xml version='1.0' encoding='utf-8'?>
<widget id="com.fernandoruizrico.clicker" xmlns="http://www.w3.org/ns/widgets" version="1.0.1">
  <name>Cookie Clicker</name>
  <description>Juego Cookie Clicker</description>

  <icon src="img/icono.png" />
  <splash src="img/pantalla-bienvenida.png" />

  <preference name="SplashScreenDelay" value="2000" />
  <preference name="AutoHideSplashScreen" value="true" />
  <preference name="AndroidWindowSplashScreenAnimatedIcon" value="img/pantalla-bienvenida.png" />
  <preference name="AndroidWindowSplashScreenBackground" value="#FFFFFF" />

  <plugin name="cordova-plugin-device" />
  <plugin name="cordova-plugin-splashscreen" />
  <plugin name="cordova-plugin-vibration" />

  <preference name="Orientation" value="portrait" /> 

  <access origin="*" />
</widget>

Comprimir la aplicación en formato «.zip»

La página web VoltBuilder necesita que le proporcionemos nuestra aplicación en formato ZIP. Por ello, deberemos comprimir toda nuestra aplicación en un solo fichero ZIP, que incluirá el fichero «config.xml», y todos los ficheros html, CSS y JavaScript. No importa el nombre que tenga el fichero comprimido.

Compilar la «.apk»

El último paso que deberemos seguir para obtener nuestra aplicación en formato «apk» será subir nuestro fichero «.zip» a la página web VoltBuilder. El proceso es muy sencillo:

  1. Acceder a la página de inicio de VoltBuilder: https://volt.build/
  2. Iniciar sesión (con tu cuenta de gmail por ejemplo) haciendo click en la opción «Login/Sign Up».
  3. Hacer click en la opción «Upload».
  4. Enviar nuestra aplicación arrastrando el fichero ZIP directamente al cuadro de «Android». También se puede hacer click sobre dicho cuadro y seleccionar nuestro fichero después.
  5. Una vez la aplicación se haya compilado, podremos descargar el fichero «.apk» generado a través de un código QR, y ya podremos instalarlo en nuestro propio móvil.

Cookie Clicker con HTML+CSS+Javascript (parte 5): Añadiendo icono y estilos

En esta parte vamos a añadir un icono al juego y aplicaremos los primeros estilos CSS. Esto nos permitirá personalizar el juego, haciéndolo visualmente más atractivo e interactivo, lo que ofrece una mejor experiencia para el jugador.

Icono de la aplicación

Puedes elegir cualquier imagen para el icono de tu juego, teniendo en cuenta simplemente que deberás colocarla dentro de la carpeta img. A continuación te proporcionamos una imagen de ejemplo:

Modificar el archivo «index.html»

Realizaremos las siguientes modificaciones sobre el fichero «index.html»:

  • Añadir un icono: El icono aparecerá en la pestaña del navegador, y posteriormente también lo utilizaremos como icono de aplicación cuando instalemos el fichero «apk» en nuestros dispositivos móviles. Utilizaremos la etiqueta <link> para enlazar el archivo de imagen.
  • Fichero con estilos CSS: Usamos un archivo externo con código CSS («style.css») para aplicar estilos a la página.
  • Clases CSS: Añadiremos las correspondientes clases para aplicar estilos específicos a cada caja. Utilizaremos el atributo class sobre cada bloque div:
    • caja-galleta: Caja que contiene la galleta sobre la que hacemos click para incrementar el contador.
    • caja-mejora: Cajas que activan cada una de las mejoras del juego.
    • caja-opciones: Caja que contendrá opciones como reiniciar el juego, o más adelante, exportar e importar el progreso.
<!DOCTYPE html>
<html lang="es">
<head>
    ...
    <link rel="icon" type="image/png" href="img/icono.png">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Cookie Clicker</h1>

    <div class="caja-galleta" onclick="incrementarContador()">
        ...
    </div>

    <div class="caja-mejora" onclick="comprarMejora('galletasExtra')">
        ...
    </div>
 
    <div class="caja-opciones">
        <button onclick="reiniciarJuego()">Reiniciar el juego</button>
    </div>
 
    ...
</body>

Archivo «style.css»

Crearemos el fichero «style.css» que contendrá todos los estilos que utilizaremos para personalizar nuestro juego:

  • Estilos básicos: Centramos el contenido, aplicamos una fuente legible y hacemos que el contador de galletas destaque.
  • Estilos avanzados: Añadiremos bordes, sombras, y efectos de clic para que las cajas sean más interactivas y agradables visualmente.
body {
  text-align: center; /* Centra el contenido horizontalmente */
  font-family: Arial, sans-serif; /* Fuente básica para mantener legibilidad */
}

#contador {
  color: #991919; /* Rojo oscuro para destacar el contador, simboliza "galletas acumuladas" */
  font-weight: bold; /* El texto en negrita ayuda a que sea más legible y destaque */
  font-size: 250%; /* Tamaño grande para captar la atención del usuario */
}

/* Agrupación de estilos compartidos para cajas de galleta, mejora y opciones */
.caja-galleta, .caja-mejora, .caja-opciones {
  border-radius: 10px; /* Consistencia visual en los bordes redondeados */
  box-shadow: 2px 2px 10px; /* Sombra para simular profundidad */
  padding: 10px; /* Espacio interno suficiente para evitar que el contenido quede pegado al borde */
  margin: 20px 10px; /* Espaciado que separa visualmente las cajas, sin sobrecargar el espacio */
}

.caja-galleta {
  border: 2px solid #7837d4; /* Borde morado, un color alegre que representa el clic principal del juego */
  cursor: pointer; /* Indica que es interactivo */
}

.caja-galleta:active {
  transform: scale(0.98); /* Efecto visual de que la caja está siendo presionada */
  box-shadow: 2px 2px 5px; /* Sombra más pequeña para dar sensación de "presión" */
}

.caja-mejora {
  border: 2px solid #d4af37; /* Borde dorado para simbolizar un "logro" o mejora premium */
  cursor: pointer; /* Interactivo */
}

.caja-mejora:active {
  transform: scale(0.98); /* Efecto de clic */
  box-shadow: 2px 2px 5px; /* Sombra reducida al hacer clic */
}

.caja-opciones {
  border: 2px solid #d45e37; /* Borde naranja, un color cálido para representar acciones de configuración */
  padding: 20px; /* Mayor espacio interno para acomodar los botones y opciones de manera cómoda */
}

Cookie Clicker con HTML+CSS+Javascript (parte 4): Reiniciando el progreso del juego

En esta parte aprenderemos a reiniciar el progreso del juego. Esto permitirá que el jugador borre todo su progreso y comience desde cero si lo desea. Este enfoque enseña cómo manipular el almacenamiento local del navegador y cómo interactuar con el usuario mediante ventanas de confirmación.

Modificar el archivo «index.html»

Vamos a agregar un botón en la página que le dará al jugador la opción de reiniciar su juego. Cuando el jugador haga clic en el botón, se ejecutará la función reiniciarJuego():

...
</body>
    ...

    <div>
        <button onclick="reiniciarJuego()">Reiniciar el juego</button>
    </div>

    ...
</body>
</html>

Modificar el archivo «script.js»

Añadiremos una nueva función JavaScript que agrupará el código que se debe ejecutar cuando el usuario desee reiniciar el juego:

  • Función reiniciarJuego(): Esta función se ejecuta cuando el jugador hace clic en el botón de reiniciar. Primero, se le pregunta al jugador si realmente quiere reiniciar (usando confirm()). Si el jugador acepta, la función borra el progreso guardado en localStorage y recarga la página:
...

// Función para reiniciar el juego
function reiniciarJuego() {
    // Pregunta al usuario si realmente quiere reiniciar, y si dice que sí, borra el progreso y reinicia la página
    if (confirm("¿Estás seguro de borrar todo el progreso? (Esto no se puede deshacer)")) {
        localStorage.removeItem('juego');  // Elimina el progreso guardado
        location.reload();  // Recarga la página
    }
}