Serpiente v2, un juego con música, efectos de sonido, colisiones, comida, imagen de fondo y game over, hecho con Godot

Assets

Pasos a seguir

Esqueleto del juego

extends Node2D

# ----------------------------------------
# SERPIENTE (SNAKE) v2 GODOT 4.x
# ----------------------------------------
#
# - Incorpora música de fondo, efectos de sonido (comer, morir).
# - Fondo de pantalla (imagen).
# - Imágenes para los segmentos de la serpiente (cabeza, cuerpo, cola) y la comida.
# - Pantalla de "Game Over" con imagen.
# - Botón de cerrar el juego.
# - Estructura de funciones similar al ejemplo de Asteroides.
#
# INPUT MAP necesario (Proyecto → Configuración del Proyecto → Mapa de Entrada):
#   "ui_left" (← / A), "ui_right" (→ / D), "ui_up" (↑ / W), "ui_down" (↓ / S)
#   "ui_accept" (Enter/Espacio) para reiniciar.
#
# Imágenes y sonidos en la carpeta 'assets':
#   cabeza_serpiente.png, cuerpo_serpiente.png, cola_serpiente.png, comida_manzana.png
#   fondo_jungla.png, game_over_snake.png, boton_cerrar.png
#   musica_fondo_snake.mp3, sfx_comer.mp3, sfx_muerte_snake.mp3

# -------------------------
# CONSTANTES DE CONFIGURACIÓN
# -------------------------
const TAM_CUADRICULA = 75    # Tamaño de cada "cuadrado" de la cuadrícula en píxeles
const TAM_CABEZA = 90        # Tamaño de la cabeza en píxeles
const TAM_COMIDA = 90        # Tamaño de la comida en píxeles
const VELOCIDAD_JUEGO = 0.2  # Segundos entre cada movimiento (más bajo = más rápido)
const TAM_BOTON_CERRAR = 50
const TAM_TEXTO = 40
const PAUSA_GAME_OVER = 1

# Colores (para el texto de la UI)
const COLOR_TEXTO = Color(1, 1, 1, 0.8)

# Imágenes (pon los archivos en la carpeta 'assets')
const TEX_FONDO: Texture2D = preload("res://assets/fondo.png")
const TEX_CABEZA_SERPIENTE: Texture2D = preload("res://assets/cabeza.png")
const TEX_CUERPO_SERPIENTE: Texture2D = preload("res://assets/cuerpo.png")
const TEX_COLA_SERPIENTE: Texture2D = preload("res://assets/cola.png")
const TEX_COMIDA: Texture2D = preload("res://assets/comida.png")
const TEX_GAME_OVER: Texture2D = preload("res://assets/game_over.png")
const TEX_BOTON_CERRAR: Texture2D = preload("res://assets/boton_cerrar.png")

# Audios (pon los archivos en la carpeta 'assets')
const MUSICA_FONDO: AudioStream = preload("res://assets/musica_fondo.mp3")
const SFX_COMER: AudioStream = preload("res://assets/sfx_comer.mp3")
const SFX_MUERTE: AudioStream = preload("res://assets/sfx_muerte.mp3")

# -------------------------
# ESTADO DEL JUEGO
# -------------------------
var pantalla: Vector2
var cuadricula_ancho: int
var cuadricula_alto: int

# La serpiente es un array de posiciones en la *cuadrícula* (no píxeles)
# La cabeza es el *último* elemento (serpiente.back())
var serpiente: Array[Vector2] = []
var comida: Vector2 # Posición de la comida en la cuadrícula
var direccion: Vector2 = Vector2.RIGHT
var proxima_direccion: Vector2 = Vector2.RIGHT

var muerto: bool = false
var crecer_serpiente: bool = false
var puntuacion: int = 0

# Control del "tick" del juego usando _process
var tiempo_acumulado: float = 0.0

# UI
var etiqueta_puntuacion: Label
var boton_cerrar: Rect2

# Audio players
var musica_fondo_player: AudioStreamPlayer
var sfx_comer_player: AudioStreamPlayer
var sfx_muerte_player: AudioStreamPlayer

# -------------------------
# CICLO DE VIDA PRINCIPAL
# -------------------------
func _ready():
	# Ejecutar al iniciar la escena
	randomize()
	_inicializar_pantalla()
	_crear_ui()
	_crear_boton_cerrar()
	_inicializar_audio()
	_reproducir_audio(musica_fondo_player) # Reproducir música de fondo en bucle
	_iniciar_juego()

func _process(delta: float):
	# Ejecutar la lógica del juego en cada frame
	
	# Si estamos muertos o en pausa, no hacemos nada más que esperar el reinicio
	if muerto: return

	# Acumulamos el tiempo para simular un "tick" de juego
	tiempo_acumulado += delta
	
	# Si no ha pasado suficiente tiempo, salimos
	if tiempo_acumulado < VELOCIDAD_JUEGO: return
	
	# ¡Tick! Es hora de moverse. Reiniciamos el acumulador
	tiempo_acumulado = 0.0
	
	# La lógica principal se ejecuta aquí, a la velocidad de VELOCIDAD_JUEGO
	_actualizar_movimiento()
	
	# Pedir a Godot que vuelva a dibujar la pantalla
	queue_redraw()

func _draw():
	# Dibujar en pantalla los elementos del juego
	# El orden importa (lo que se dibuja último, queda encima)
	_dibujar_fondo()
	_dibujar_serpiente()
	_dibujar_comida()
	_mostrar_boton_cerrar()
	if muerto: _mostrar_game_over()


func _input(event: InputEvent):
	# Comprobar la entrada de teclado, ratón o táctil
	_comprobar_controles(event)

# -------------------------
# INICIALIZACIÓN BÁSICA
# -------------------------
func _inicializar_pantalla():
	# Guardar el tamaño actual de la pantalla (ancho y alto)
	pass

# -------------------------
# AUDIO
# -------------------------
func _crear_audio_player(stream: AudioStream, bus: String, volumen = 0.0):
	# Instanciar y configurar un AudioStreamPlayer con stream, bus y volumen inicial, y devolverlo
	pass

func _inicializar_audio():
	# Construir y registrar players de música y SFX con buses/volúmenes apropiados
	pass

func _reproducir_audio(audio_player: AudioStreamPlayer):
	# Reproducir el AudioStreamPlayer
	pass

func _detener_audio(audio_player: AudioStreamPlayer):
	# Parar el AudioStreamPlayer
	pass

# -------------------------
# UI (INTERFAZ DE USUARIO)
# -------------------------
func _crear_ui():
	# Crear un Label para la puntuación
	pass

func _actualizar_etiqueta_puntuacion():
	# Muestra la puntuación actual en pantalla
	pass

# -------------------------
# BOTÓN CERRAR
# -------------------------
func _crear_boton_cerrar():
	# Definir el área clicable del botón de cierre en la esquina superior derecha
	pass

func _mostrar_boton_cerrar():
	# Dibujar la textura del botón de cierre dentro de su rectángulo clicable
	pass

# -------------------------
# LÓGICA DEL JUEGO (MOVIMIENTO)
# -------------------------
func _iniciar_juego():
	# (Re)iniciar todas las variables del juego
	pass

func _actualizar_movimiento():
	# Esta función se llama en cada "tick" (definido por VELOCIDAD_JUEGO)
	pass

# -------------------------
# SERPIENTE
# -------------------------
func _mover_serpiente(nueva_cabeza: Vector2):
	# Añadir la nueva cabeza al final del array
	pass

func _convertir_direccion_a_angulo(dir: Vector2):
	# Convertir dirección (Vector2) a ángulo en radianes (0° = derecha)
	pass

func _dibujar_textura_rotada_centrada(tex: Texture2D, centro: Vector2, angulo: float, tamano: Vector2):
	# Dibujar textura rotada alrededor de un punto central
	pass

func _dibujar_serpiente():
	pass

# -------------------------
# COMIDA
# -------------------------
func _mover_comida():
	# Mover la comida a una posición aleatoria que no esté sobre la serpiente
	pass

func _dibujar_comida():
	# Dibujar la comida en su posición
	pass

func _comprobar_comida(cabeza: Vector2):
	# Comprobar si la cabeza está en la misma casilla que la comida
	pass

# -------------------------
# CONTROLES
# -------------------------
func _comprobar_controles(event: InputEvent):
	# Comprobación del botón de cerrar
	pass

# -------------------------
# COLISIONES Y GAME OVER
# -------------------------
func _comprobar_colisiones(cabeza: Vector2):
	# Comprueba si la cabeza ha chocado con algo. Devuelve 'true' si hay colisión.
	pass

func _pausa_game_over():
	# Activar una pausa breve no bloqueante tras el Game Over esperando a un temporizador asincrónico
	pass

func _game_over():
	# Termina el juego
	pass

func _reiniciar_juego():
	# Reiniciar la escena actual para devolver todo a su estado inicial
	pass

# -------------------------
# FONDO Y CAPAS
# -------------------------
func _dibujar_fondo():
	# Dibujar la imagen de fondo que cubre toda la pantalla
	pass

func _mostrar_game_over():
	# Dibujar la textura de Game Over. Podemos modularla para un efecto.
	pass

inicializar_pantalla()

Esta función se encarga de configurar las dimensiones del juego. Primero, obtiene el tamaño actual de la ventana o viewport (el área visible del juego) y lo almacena en la variable pantalla. Luego, basándose en el tamaño de la pantalla en píxeles y la constante TAM_CUADRICULA (que define el tamaño de un «cuadrado» de nuestro tablero), calcula cuántos cuadrados caben a lo ancho (cuadricula_ancho) y a lo alto (cuadricula_alto). Esto nos permite trabajar con coordenadas de cuadrícula (ej. 0,0 o 5,10) en lugar de píxeles (ej. 0,0 o 640,1280).

func _inicializar_pantalla():
	# Guardar el tamaño actual de la pantalla (ancho y alto)
	pantalla = get_viewport_rect().size
	# Calcular cuántos "cuadrados" de la cuadrícula caben en la pantalla
	cuadricula_ancho = int(pantalla.x / TAM_CUADRICULA)
	cuadricula_alto = int(pantalla.y / TAM_CUADRICULA)

crear_audio_player()

Esta es una función «ayudante» (o helper function) diseñada para crear y configurar reproductores de sonido de forma limpia y reutilizable. Recibe el archivo de audio (stream), el canal por donde debe sonar (bus, ej: «Music» o «SFX») y un volumen opcional. Crea un nuevo nodo AudioStreamPlayer, le asigna estas propiedades, lo añade como hijo a la escena actual (usando add_child, lo cual es crucial para que funcione) y finalmente devuelve el nodo ya configurado.

func _crear_audio_player(stream: AudioStream, bus: String, volumen = 0.0):
	# Instanciar y configurar un AudioStreamPlayer con stream, bus y volumen inicial, y devolverlo
	var player = AudioStreamPlayer.new()
	player.stream = stream
	player.bus = bus
	player.volume_db = volumen
	add_child(player)
	return player

inicializar_audio()

Esta función utiliza la función ayudante _crear_audio_player (que acabamos de ver) para preparar todos los sonidos que el juego necesitará. Crea tres reproductores de audio y los asigna a las variables globales del script: musica_fondo_player (asignado al bus «Music»), sfx_comer_player (asignado al bus «SFX») y sfx_muerte_player (también en «SFX»).

func _inicializar_audio():
	# Construir y registrar players de música y SFX con buses/volúmenes apropiados
	musica_fondo_player  = _crear_audio_player(MUSICA_FONDO, "Music", -5.0)
	sfx_comer_player     = _crear_audio_player(SFX_COMER, "SFX", 0.0)
	sfx_muerte_player    = _crear_audio_player(SFX_MUERTE, "SFX")
	
	# La música de fondo se reproduce en bloque
	musica_fondo_player.finished.connect(func(): musica_fondo_player.play())

reproducir_audio()

Esta función se usa para reproducir un sonido. Recibe el audio_player que debe sonar. Primero comprueba que el player existe (if audio_player) y que tiene un archivo de sonido cargado (and audio_player.stream). Si es así, inicia la reproducción con play(). Adicionalmente, tiene un parámetro loop (bucle) que si se establece en true, conectará una señal (finished) para que, cuando el sonido termine, vuelva a empezar automáticamente.

func _reproducir_audio(audio_player: AudioStreamPlayer):
	# Reproducir el AudioStreamPlayer
	if audio_player and audio_player.stream: audio_player.play()

detener_audio()

Función simple para parar un sonido. Comprueba si el audio_player existe y si se está reproduciendo actualmente (.playing). Si ambas condiciones son ciertas, detiene la reproducción inmediatamente usando stop().

func _detener_audio(audio_player: AudioStreamPlayer):
	# Parar el AudioStreamPlayer
	if audio_player and audio_player.playing: audio_player.stop()

crear_ui()

Prepara la interfaz de usuario básica. En este caso, crea un nuevo nodo de tipo Label (etiqueta de texto) y lo asigna a la variable etiqueta_puntuacion. Establece su posición en la esquina superior izquierda (10, 10). Luego, configura programáticamente su apariencia (tamaño de fuente TAM_TEXTO y color COLOR_TEXTO) usando un objeto LabelSettings. Finalmente, añade la etiqueta a la escena con add_child().

func _crear_ui():
	# Crear un Label para la puntuación
	etiqueta_puntuacion = Label.new()
	etiqueta_puntuacion.position = Vector2(10, 10)
	
	# Configurar la fuente (programáticamente)
	var fuente_puntuacion = LabelSettings.new()
	fuente_puntuacion.font_size = TAM_TEXTO
	fuente_puntuacion.font_color = COLOR_TEXTO
	etiqueta_puntuacion.label_settings = fuente_puntuacion
	add_child(etiqueta_puntuacion)

actualizar_etiqueta_puntuacion()

Esta función se llama cada vez que la puntuación cambia (al comer una fruta o al iniciar el juego). Simplemente actualiza la propiedad text de la etiqueta_puntuacion (creada en la función anterior) para mostrar el texto «Puntuación: » seguido del valor actual de la variable puntuacion.

func _actualizar_etiqueta_puntuacion():
	# Muestra la puntuación actual en pantalla
	etiqueta_puntuacion.text = "Puntuación: %s" % puntuacion

crear_boton_cerrar()

Esta función no crea un nodo de botón, sino que define el área donde el botón de cerrar será «clicable». Crea un Rect2 (un rectángulo) usando las coordenadas de la pantalla (pantalla.x) y el tamaño del botón (TAM_BOTON_CERRAR) para posicionarlo en la esquina superior derecha, con un pequeño margen de 10 píxeles. Esta área se almacenará en la variable boton_cerrar y se usará después en _input para detectar clics.

func _crear_boton_cerrar():
	# Definir el área clicable del botón de cierre en la esquina superior derecha
	boton_cerrar = Rect2(pantalla.x - TAM_BOTON_CERRAR - 10, 10, TAM_BOTON_CERRAR, TAM_BOTON_CERRAR)

mostrar_boton_cerrar()

Esta función se llama dentro de _draw(). Se encarga de dibujar la textura del botón (TEX_BOTON_CERRAR) en la pantalla, usando exactamente el rectángulo (boton_cerrar) que definimos en la función anterior. También le aplica un tinte (un color gris semitransparente) para que no sea tan brillante.

func _mostrar_boton_cerrar():
	# Dibujar la textura del botón de cierre dentro de su rectángulo clicable
	draw_texture_rect(TEX_BOTON_CERRAR, boton_cerrar, false, Color(0.8, 0.8, 0.8, 1.0))

iniciar_juego()

Esta es la función clave para empezar (o reiniciar) una partida. Restablece todas las variables de estado a sus valores iniciales: vacía la serpiente (serpiente.clear()), pone la puntuación a 0, actualiza la etiqueta, quita el estado muerto, resetea la direccion a Vector2.RIGHT y reinicia el tiempo_acumulado. Después, crea la serpiente inicial añadiendo tres segmentos (cola, cuerpo y cabeza) al array serpiente en posiciones de cuadrícula fijas. Finalmente, llama a _mover_comida() para colocar la primera manzana.

func _iniciar_juego():
	# (Re)iniciar todas las variables del juego
	serpiente.clear()
	puntuacion = 0
	_actualizar_etiqueta_puntuacion()
	muerto = false
	crecer_serpiente = false
	direccion = Vector2.RIGHT
	proxima_direccion = Vector2.RIGHT
	tiempo_acumulado = 0.0

	# Crear serpiente inicial (en posiciones de cuadrícula)
	serpiente.push_back(Vector2(3, 5)) # Cola
	serpiente.push_back(Vector2(4, 5)) # Cuerpo
	serpiente.push_back(Vector2(5, 5)) # Cabeza

	_mover_comida()

actualizar_movimiento()

Este es el corazón de la lógica del juego, se ejecuta en cada «tick» (controlado por VELOCIDAD_JUEGO en _process). Primero, actualiza la direccion actual con la proxima_direccion (que guardó el input). Segundo, calcula cuál será la nueva_cabeza sumando la dirección a la posición de la cabeza actual. Tercero, comprueba si esa nueva_cabeza choca con algo (_comprobar_colisiones); si choca, llama a _game_over() y se detiene. Cuarto, comprueba si come (_comprobar_comida). Quinto y último, llama a _mover_serpiente() para efectuar el movimiento.

func _actualizar_movimiento():
	# Esta función se llama en cada "tick" (definido por VELOCIDAD_JUEGO)
	
	# 1. Actualizar dirección
	direccion = proxima_direccion
	
	# 2. Calcular nueva posición de la cabeza
	var cabeza_actual = serpiente.back() # .back() es el último elemento
	var nueva_cabeza = cabeza_actual + direccion
	
	# 3. Comprobar colisiones
	if _comprobar_colisiones(nueva_cabeza):
		_game_over()
		return # Detener movimiento si morimos
	
	# 4. Comprobar si comemos
	_comprobar_comida(nueva_cabeza)
	
	# 5. Mover serpiente
	_mover_serpiente(nueva_cabeza)

mover_serpiente()

Esta función actualiza el array serpiente para simular el movimiento. Siempre añade la nueva_cabeza al final del array (push_back). Luego, comprueba la variable crecer_serpiente. Si es false (no hemos comido), elimina el primer segmento del array (pop_front), que es la cola. Esto hace que la serpiente mantenga su tamaño pero avance. Si crecer_serpiente es true (hemos comido), no elimina la cola, haciendo que la serpiente crezca un segmento, y resetea el flag a false.

func _mover_serpiente(nueva_cabeza: Vector2):
	# Añadir la nueva cabeza al final del array
	serpiente.push_back(nueva_cabeza)

	# Si no hemos comido (no crecemos), borramos el primer segmento (la cola)
	if not crecer_serpiente:
		serpiente.pop_front() # .pop_front() elimina el primer elemento
	else:
		# Si crecimos, reseteamos el flag y no borramos la cola
		crecer_serpiente = false

convertir_direccion_a_angulo()

Esta función recibe una dirección representada como un vector 2D (como Vector2.UP, Vector2.RIGHT, etc.) y devuelve el ángulo correspondiente en radianes, asumiendo que la orientación por defecto apunta hacia la derecha (orientación estándar en muchos motores 2D). Usa una estructura match para evaluar la dirección de entrada: si es UP, devuelve -90 grados convertidos a radianes (lo que apunta hacia arriba); si es DOWN, devuelve 90 grados (hacia abajo); si es LEFT, devuelve 180 grados (hacia la izquierda); y para cualquier otro caso —especialmente Vector2.RIGHT—, devuelve 0.0, ya que esa es la orientación base. Esta función es útil para rotar texturas (como la cabeza de la serpiente) de forma que siempre miren en la dirección en la que se están moviendo.

func _convertir_direccion_a_angulo(dir: Vector2):
	# Convertir dirección (Vector2) a ángulo en radianes (0° = derecha)
	match dir:
		Vector2.UP:    return deg_to_rad(-90)
		Vector2.DOWN:  return deg_to_rad(90)
		Vector2.LEFT:  return deg_to_rad(180)
		_:             return 0.0  # Vector2.RIGHT

dibujar_textura_rotada_centrada()

Esta función se encarga de dibujar una textura (tex) en la pantalla rotada alrededor de su propio centro, en una posición específica (centro). Para lograrlo, primero aplica una transformación de dibujo con draw_set_transform(centro, angulo, Vector2(1, 1)), que desplaza el origen del sistema de coordenadas al punto centro y lo rota según el ángulo dado (en radianes). Luego, dibuja la textura usando draw_texture_rect, pero con un rectángulo cuyo origen está en -tamano / 2.0, lo que centra la textura respecto al nuevo origen (el punto centro). Finalmente, restablece la transformación de dibujo a la identidad con draw_set_transform(Vector2.ZERO, 0.0, Vector2(1, 1)) para que los siguientes dibujos no se vean afectados por esta rotación ni traslación. Es una forma común y eficaz de dibujar sprites rotados centrados en Godot usando el sistema de dibujo personalizado (_draw).

func _dibujar_textura_rotada_centrada(tex: Texture2D, centro: Vector2, angulo: float, tamano: Vector2):
	# Dibujar textura rotada alrededor de un punto central
	draw_set_transform(centro, angulo, Vector2(1, 1))
	draw_texture_rect(tex, Rect2(-tamano / 2.0, tamano), false)
	draw_set_transform(Vector2.ZERO, 0.0, Vector2(1, 1))

dibujar_serpiente()

Esta función se encarga de renderizar visualmente todos los segmentos de la serpiente en la pantalla, diferenciando entre cabeza, cuerpo y cola, y orientando cada parte según la dirección en la que apunta. Primero verifica que la serpiente no esté vacía; si lo está, no hace nada. Luego, recorre cada segmento (almacenado como coordenadas en una cuadrícula) y convierte su posición a píxeles. Calcula el centro de la celda para dibujar la textura centrada. Dependiendo de la posición del segmento en el array (i), asigna la textura correspondiente: la cabeza (último elemento) se orienta según la dirección actual de movimiento (direccion); la cola (primer elemento) se orienta según la dirección desde la cola hacia el siguiente segmento (serpiente[1] - serpiente[0]); y los segmentos intermedios del cuerpo se orientan según la dirección entre el segmento actual y el siguiente (serpiente[i + 1] - serpiente[i]). En todos los casos, usa _dibujar_textura_rotada_centrada() para dibujar la textura rotada correctamente alrededor de su centro, logrando una serpiente visualmente coherente y alineada con su trayectoria.

func _dibujar_serpiente():
	# Dibujar todos los segmentos de la serpiente (cabeza, cuerpo, cola) en la pantalla
	if serpiente.is_empty(): return

	for i in range(serpiente.size()):
		var posicion_celda = serpiente[i]
		var posicion_pixel = posicion_celda * TAM_CUADRICULA
		var centro = posicion_pixel + Vector2(TAM_CUADRICULA, TAM_CUADRICULA) / 2.0
		var textura: Texture2D
		var angulo_rotacion = 0.0

		if i == serpiente.size() - 1:  # Cabeza
			textura = TEX_CABEZA_SERPIENTE
			angulo_rotacion = _convertir_direccion_a_angulo(direccion)
			_dibujar_textura_rotada_centrada(textura, centro, angulo_rotacion, Vector2(TAM_CABEZA, TAM_CABEZA))
		elif i == 0:  # Cola
			textura = TEX_COLA_SERPIENTE
			var dir_cola = serpiente[1] - serpiente[0]
			angulo_rotacion = _convertir_direccion_a_angulo(dir_cola)
			_dibujar_textura_rotada_centrada(textura, centro, angulo_rotacion, Vector2(TAM_CUADRICULA, TAM_CUADRICULA))
		else:  # Cuerpo
			textura = TEX_CUERPO_SERPIENTE
			var dir_cuerpo = serpiente[i + 1] - serpiente[i]
			angulo_rotacion = _convertir_direccion_a_angulo(dir_cuerpo)
			_dibujar_textura_rotada_centrada(textura, centro, angulo_rotacion, Vector2(TAM_CUADRICULA, TAM_CUADRICULA))

mover_comida()

Esta función coloca la comida en una posición aleatoria dentro de la cuadrícula del juego, asegurándose de que no aparezca encima de ningún segmento de la serpiente. Para ello, entra en un bucle infinito (while true) que genera repetidamente coordenadas aleatorias (nueva_pos) usando randi_range, dentro de los límites horizontales (cuadricula_ancho) y verticales (cuadricula_alto). Cada vez que genera una posición, verifica si esa coordenada no está ya ocupada por la serpiente (con nueva_pos not in serpiente). En cuanto encuentra una ubicación libre, sale del bucle con break y asigna esa posición a la variable global comida. Este enfoque garantiza que la comida siempre aparezca en un lugar accesible.

func _mover_comida():
	# Mover la comida a una posición aleatoria que no esté sobre la serpiente
	var nueva_pos: Vector2
	
	# Generar posiciones aleatorias hasta encontrar una que no esté ocupada por la serpiente
	while true:
		nueva_pos = Vector2(
			randi_range(0, cuadricula_ancho - 1),
			randi_range(0, cuadricula_alto - 1)
		)
		if nueva_pos not in serpiente: break  # Salir del bucle cuando la posición sea válida
	
	# Asignar la posición válida a la comida
	comida = nueva_pos

dibujar_comida()

Esta función se encarga de renderizar la comida en la pantalla de forma centrada dentro de su celda de la cuadrícula. Primero calcula el centro exacto de la celda donde se encuentra la comida: multiplica las coordenadas de la cuadrícula (comida) por el tamaño de cada celda (TAM_CUADRICULA) para obtener la esquina superior izquierda, y luego suma la mitad del tamaño de la celda (TAM_CUADRICULA / 2) en ambas direcciones (usando Vector2.ONE) para llegar al centro. Luego, llama a la función auxiliar _dibujar_textura_rotada_centrada() pasando la textura de la comida (TEX_COMIDA), ese punto central, un ángulo de rotación de 0.0 (ya que la comida no necesita girar) y su tamaño personalizado (TAM_COMIDA). Esto asegura que la comida se dibuje perfectamente centrada en su celda, independientemente del tamaño de la textura o de la cuadrícula, manteniendo una apariencia limpia y alineada con el resto del juego.

func _dibujar_comida():
	# Dibujar la comida en su posición, centrada y con su tamaño personalizado
	
	# 1. Calcular el centro de la celda de la cuadrícula
	var centro_celda = (comida * TAM_CUADRICULA) + (Vector2.ONE * TAM_CUADRICULA / 2.0)
	
	# 2. Llamar a nuestra función auxiliar para dibujar la comida
	_dibujar_textura_rotada_centrada(
		TEX_COMIDA,
		centro_celda,
		0.0, # Ángulo (0.0 para la comida)
		Vector2(TAM_COMIDA, TAM_COMIDA) # Tamaño
	)

comprobar_comida()

Esta función, llamada en _actualizar_movimiento, comprueba si la cabeza de la serpiente ha aterrizado en la misma casilla que la comida. Si cabeza == comida, activa el flag crecer_serpiente a true (para que _mover_serpiente la haga crecer), incrementa la puntuacion, actualiza la etiqueta de texto, llama a _mover_comida() para buscar una nueva posición, y reproduce el sonido de comer (sfx_comer_player).

func _comprobar_comida(cabeza: Vector2):
	# Comprobar si la cabeza está en la misma casilla que la comida
	if cabeza == comida:
		crecer_serpiente = true # Marcar para crecer en el próximo movimiento
		puntuacion += 1
		_actualizar_etiqueta_puntuacion()
		_mover_comida() # Mover la comida a un nuevo sitio
		_reproducir_audio(sfx_comer_player) # Reproducir SFX de comer

comprobar_controles()

Esta función gestiona toda la entrada del jugador de forma ordenada y segura. Primero, verifica si el usuario ha hecho clic (con ratón o pantalla táctil) dentro del área del botón de cerrar; si es así, finaliza la aplicación de forma segura usando get_tree().quit.call_deferred(). Luego, si el juego ya terminó (muerto == true), únicamente permite reiniciar al pulsar la acción "ui_accept" (como Enter o Espacio), ignorando cualquier otro input. Finalmente, cuando el juego está en marcha, traduce las entradas de dirección (ui_up, ui_down, etc.) a vectores (Vector2.UP, DOWN, etc.), pero evita giros completos sobre sí misma (por ejemplo, de abajo a arriba) comparando la dirección propuesta con la opuesta a la actual (-direccion). Solo si la nueva dirección es válida (distinta de la opuesta), se asigna a proxima_direccion, lo que garantiza un movimiento fluido y evita que la serpiente se suicide al retroceder directamente sobre su propio cuerpo.

func _comprobar_controles(event: InputEvent):
	# Comprobación del botón de cerrar
	if (event is InputEventScreenTouch or event is InputEventMouseButton) and event.pressed:
		if boton_cerrar.has_point(event.position):
			if get_tree(): get_tree().quit.call_deferred() # Cerrar de forma segura
			return

	# Si el juego ha terminado, solo escuchamos 'Enter' para reiniciar
	if muerto:
		# 'ui_accept' es 'Enter' o 'Espacio'
		if event.is_action_pressed("ui_accept"): _reiniciar_juego()
		return

	# Si el juego está activo, leemos las flechas de dirección
	var direccion_opuesta = -direccion
	var nueva_dir = null

	if   event.is_action_pressed("ui_up"):    nueva_dir = Vector2.UP
	elif event.is_action_pressed("ui_down"):  nueva_dir = Vector2.DOWN
	elif event.is_action_pressed("ui_left"):  nueva_dir = Vector2.LEFT
	elif event.is_action_pressed("ui_right"): nueva_dir = Vector2.RIGHT
	
	# Evitar que la serpiente se dé la vuelta sobre sí misma	
	if nueva_dir and nueva_dir != direccion_opuesta: proxima_direccion = nueva_dir

comprobar_colisiones()

Esta función recibe la posición de la cabeza y devuelve true si hay colisión, o false si es seguro. Comprueba dos cosas: 1) Si la cabeza está fuera de los límites de la cuadrícula (ej. cabeza.x < 0). 2) Recorre el array serpiente (excepto el último segmento, que es la cabeza actual) y comprueba si la cabeza es igual a la posición de alguno de esos segmentos. Si cualquiera de estas comprobaciones es positiva, devuelve true.

func _comprobar_colisiones(cabeza: Vector2):
	# Comprueba si la cabeza ha chocado con algo. Devuelve 'true' si hay colisión.

	# Colisión con bordes
	if cabeza.x < 0 or cabeza.x >= cuadricula_ancho or \
	   cabeza.y < 0 or cabeza.y >= cuadricula_alto:
		return true

	# Colisión con el cuerpo (excluyendo la cabeza)
	return cabeza in serpiente.slice(0, -1)

pausa_game_over()

Esta es una función asíncrona (usa await) que sirve para crear una pequeña pausa después de morir. Al ser llamada, crea un temporizador (create_timer) con la duración PAUSA_GAME_OVER (1 segundo) y «espera» (await) a que ese temporizador emita la señal timeout. Esto evita que el jugador pueda reiniciar el juego instantáneamente al morir, dándole un segundo para ver qué pasó.

func _pausa_game_over():
	# Activar una pausa breve no bloqueante tras el Game Over esperando a un temporizador asincrónico
	await get_tree().create_timer(PAUSA_GAME_OVER).timeout

game_over()

Esta función se llama cuando _comprobar_colisiones detecta un choque. Establece el estado muerto a true, lo que detiene el bucle principal en _process. Detiene la música de fondo y reproduce el sonido de muerte. Llama a queue_redraw() para forzar un último dibujado (que mostrará la pantalla de Game Over) y finalmente llama a _pausa_game_over() para iniciar la pausa antes de poder reiniciar.

func _game_over():
	# Termina el juego
	muerto = true
	_detener_audio(musica_fondo_player)
	_reproducir_audio(sfx_muerte_player)
	queue_redraw() # Forzar un último dibujado para mostrar la pantalla de Game Over
	_pausa_game_over() # Añadir una breve pausa ant

reiniciar_juego()

La forma más sencilla y robusta de reiniciar el juego. Simplemente le pide al árbol de escenas (get_tree()) que vuelva a cargar la escena actual (reload_current_scene()). Esto restablece todas las variables y nodos a su estado inicial, como si el juego se acabara de abrir.

func _reiniciar_juego():
	# Reiniciar la escena actual para devolver todo a su estado inicial
	get_tree().reload_current_scene()

dibujar_fondo()

Función de dibujo llamada en _draw(). Dibuja la textura TEX_FONDO ocupando toda la pantalla (desde Vector2.ZERO hasta pantalla). Le aplica un tinte gris semitransparente para oscurecerla ligeramente y que la serpiente y la comida destaquen más.

func _dibujar_fondo():
	# Dibujar la imagen de fondo que cubre toda la pantalla
	draw_texture_rect(TEX_FONDO, Rect2(Vector2.ZERO, pantalla), false, Color(0.5, 0.5, 0.5))

mostrar_game_over()

Esta función de dibujo solo se activa (gracias a la comprobación if muerto: en _draw()) cuando el juego ha terminado. Dibuja la textura TEX_GAME_VER ocupando toda la pantalla, también con un tinte semitransparente.

func _mostrar_game_over():
	# Dibujar la textura de Game Over. Podemos modularla para un efecto.
	draw_texture_rect(TEX_GAME_OVER, Rect2(Vector2.ZERO, pantalla), false, Color(0.75, 0.75, 0.75, 0.75))

El resultado