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

Vídeo con las últimas novedades

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.

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.

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.

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.

Google AI Studio

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

Integración directa con Google, ideal si ya usas Google Workspace (Docs, Drive, Classroom). Ventaja: se integra fácilmente en el ecosistema escolar. Inconveniente: depende de conexión a Google y la privacidad puede ser un tema sensible en colegios, donde se suele trabajar con herramientas Microsoft.

NotebookLM

Se puede acceder online desde https://notebooklm.google.com/.

Pensado para estudiar y resumir documentos. 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 generalistas.

Grok

Accesible desde https://grok.com/.

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

Accesible desde https://claude.ai/new. Más orientado a programadores.

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

Ejemplos de uso utilizando Qwen, Grok y Claude

¿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 series completas

Servicio proporcionado por Showrunner:

Generación de música

Las vocales en canción:

El baile del abecedario:

Modelo recomendado «Suno 4.5» de Suno.com:

Mundos virtuales

Modelo «Mirage 2» de DynamicsLab. Demostraciones disponibles aquí: https://demo.dynamicslab.ai/chaos.

Publicar libros en Amazon

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