Pasos a seguir
El esqueleto del juego
extends Node2D
# ----------------------------------------
# SERPIENTE (SNAKE) v1 GODOT 4.x
# ----------------------------------------
#
# - Usa _process y _draw para la lógica y el dibujado (sin Timer).
# - No usa imágenes, solo 'draw_rect' para los gráficos.
# - La UI (puntuación y Game Over) se crea como nodos Label.
#
# 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.
#
# -------------------------
# CONSTANTES DE CONFIGURACIÓN
# -------------------------
const TAM_CUADRICULA = 20 # Tamaño de cada "cuadrado" de la cuadrícula en píxeles
const VELOCIDAD_JUEGO = 0.1 # Segundos entre cada movimiento (más bajo = más rápido)
# Colores (en lugar de texturas)
const COLOR_FONDO = Color(0.1, 0.1, 0.1)
const COLOR_SERPIENTE = Color(0.2, 1, 0.2)
const COLOR_COMIDA = Color(1, 0, 0)
const COLOR_TEXTO = Color(1, 1, 1, 0.8)
const COLOR_TEXTO_GAME_OVER = Color(1, 0.8, 0.8)
# -------------------------
# 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 # Para bufferizar el input
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 etiqueta_game_over: Label
# -------------------------
# CICLO DE VIDA PRINCIPAL
# -------------------------
func _ready():
# Ejecutar al iniciar la escena
randomize()
_inicializar_pantalla()
_crear_ui()
_iniciar_juego()
func _process(delta: float):
# Ejecutar la lógica del juego en cada frame
# Si estamos muertos, 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_comida()
_dibujar_serpiente()
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 y calcular la cuadrícula
pass
# -------------------------
# UI (INTERFAZ DE USUARIO)
# -------------------------
func _crear_ui():
# Crear y configurar los nodos Label para la UI
pass
func _actualizar_etiqueta_puntuacion():
# Actualizar el texto del Label de puntuación
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"
pass
# -------------------------
# SERPIENTE
# -------------------------
func _mover_serpiente(nueva_cabeza: Vector2):
# Añadir nueva cabeza y quitar la cola (si no crecemos)
pass
func _dibujar_serpiente():
# Dibujar cada segmento de la serpiente
pass
# -------------------------
# COMIDA
# -------------------------
func _mover_comida():
# Mover la comida a una posición aleatoria válida
pass
func _dibujar_comida():
# Dibujar la comida en su posición
pass
func _comprobar_comida(cabeza: Vector2):
# Comprobar si la cabeza ha comido la comida
pass
# -------------------------
# CONTROLES
# -------------------------
func _comprobar_controles(event: InputEvent):
# Leer el input del jugador para cambiar de dirección o reiniciar
pass
# -------------------------
# COLISIONES Y GAME OVER
# -------------------------
func _comprobar_colisiones(cabeza: Vector2):
# Comprobar si la cabeza choca con los bordes o consigo misma
pass # Reemplazar con 'return false' por ahora
func _game_over():
# Termina el juego y muestra la pantalla de Game Over
pass
# -------------------------
# FONDO
# -------------------------
func _dibujar_fondo():
# Dibujar el rectángulo del fondo
pass
inicializar_pantalla()
Esta función se llama una sola vez al principio, desde _ready, porque solo necesitamos calcular estos valores una vez. Su trabajo es «medir» la pantalla. Primero, get_viewport_rect().size nos da el tamaño en píxeles de la ventana del juego (por ejemplo, 1024×600). Luego, dividimos ese ancho y alto por nuestra TAM_CUADRICULA (que es 20). Si la pantalla mide 1024 píxeles de ancho, 1024 / 20 = 51.2. Al usar int(), nos quedamos con la parte entera (51). Esto nos dice que nuestra cuadrícula de juego tiene 51 celdas de ancho. Guardamos estos valores en cuadricula_ancho y cuadricula_alto para que más tarde, en _comprobar_colisiones, podamos saber si la serpiente se ha chocado contra el «borde».
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_ui()
Esta función también se llama desde _ready porque queremos que la Interfaz de Usuario (UI) exista desde el principio. En lugar de añadir los nodos Label arrastrándolos en el editor de escenas de Godot, los creamos «programáticamente» (por código) usando Label.new(). Esto mantiene nuestra escena principal más limpia y nos da control total. Para cada etiqueta, creamos una instancia, le damos propiedades (como la position o el text), y luego configuramos su estilo (fuente, color, tamaño) creando un recurso LabelSettings. Lo más importante es que, después de crearlos, debemos «engancharlos» a nuestra escena principal usando add_child(), de lo contrario, existirían en la memoria pero no se verían. Fíjate que la etiqueta etiqueta_game_over se crea, se centra, y al final se oculta con hide(); está lista y esperando, pero no la mostraremos hasta que el jugador pierda.
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 = 24 fuente_puntuacion.font_color = COLOR_TEXTO etiqueta_puntuacion.label_settings = fuente_puntuacion add_child(etiqueta_puntuacion) # ¡Importante añadirlo a la escena! # Crear un Label para el Game Over etiqueta_game_over = Label.new() etiqueta_game_over.text = "GAME OVER\nPulsa 'Enter' para reiniciar" etiqueta_game_over.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER # Configurar la fuente var fuente_game_over = LabelSettings.new() fuente_game_over.font_size = 32 fuente_game_over.font_color = COLOR_TEXTO_GAME_OVER fuente_game_over.outline_size = 4 fuente_game_over.outline_color = Color(0,0,0) etiqueta_game_over.label_settings = fuente_game_over # Centrarla etiqueta_game_over.size.x = pantalla.x etiqueta_game_over.position = Vector2(0, pantalla.y / 2 - 50) etiqueta_game_over.hide() # Ocultarla al inicio add_child(etiqueta_game_over) # ¡Añadirlo también!
actualizar_etiqueta_puntuacion()
Es una función «ayudante» (o helper) muy simple. Podríamos haber escrito etiqueta_puntuacion.text = ... en dos sitios distintos (en _iniciar_juego y en _comprobar_comida), pero es una buena práctica de programación crear una función para tareas que se repiten (principio «Don’t Repeat Yourself» o DRY). Su única tarea es actualizar la propiedad text de nuestra etiqueta_puntuacion. La línea "...Puntuación: %s" % puntuacion usa un formato de string: el %s actúa como un «marcador de posición» (placeholder) que es reemplazado por el valor de la variable puntuacion. Godot convertirá automáticamente el número puntuacion a un string de texto.
func _actualizar_etiqueta_puntuacion(): etiqueta_puntuacion.text = "Puntuación: %s" % puntuacion
iniciar_juego()
Esta es la función clave de (re)inicio. La llamamos en _ready para empezar la primera partida, pero también la llamaremos cada vez que el jugador pierda y pulse ‘Enter’. Por eso, tiene que ser un «reseteo» total. «Limpia» todas las variables de estado: serpiente.clear() vacía el Array de la serpiente anterior; puntuacion vuelve a 0 (e informamos a la UI con _actualizar_etiqueta_puntuacion()); muerto se pone en false para que _process vuelva a funcionar; y se resetea la dirección. Luego, «construimos» la serpiente inicial: usamos push_back() para añadir tres Vector2 (posiciones de cuadrícula) al array. El orden es importante: primero añadimos la cola (3,5), luego el cuerpo (4,5) y finalmente la cabeza (5,5). Al final del array (serpiente.back()) siempre estará la cabeza. Por último, llamamos a _mover_comida() para que ponga la primera manzana en el tablero.
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 etiqueta_game_over.hide() tiempo_acumulado = 0.0 # Crear serpiente inicial (en posiciones de cuadrícula) # La cabeza será el último elemento (5,5) 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! Esta función se llama en cada «tick» (es decir, cada VELOCIDAD_JUEGO segundos, gracias a nuestro contador en _process). Orquesta la secuencia completa de lo que debe pasar en un solo «paso» de la serpiente. El orden es muy importante: 1) Leemos la proxima_direccion (que el jugador pulsó) y la convertimos en la direccion actual. 2) Calculamos dónde estará la cabeza en el siguiente fotograma (nueva_cabeza). 3) ¡Paramos! Antes de movernos, comprobamos si esa nueva_cabeza choca con algo (_comprobar_colisiones). Si choca, llamamos a _game_over() y usamos return para salir de la función inmediatamente, cancelando el movimiento. 4) Si no chocamos, comprobamos si la nueva_cabeza está sobre la comida (_comprobar_comida). 5) Finalmente, y solo si hemos superado las comprobaciones, le decimos a la serpiente que se mueva a esa nueva_cabeza (_mover_serpiente).
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 se encarga de la mecánica de «oruga» de la serpiente. Es un truco muy ingenioso que usa un Array (una lista). La serpiente siempre se mueve añadiendo una nueva cabeza en la posición calculada: serpiente.push_back(nueva_cabeza). Esto añade un elemento al final del array. Ahora mismo, la serpiente tiene un segmento de más. Entonces, comprobamos la variable crecer_serpiente. Si es false (no hemos comido en este tick), tenemos que mantener el tamaño original, así que eliminamos el primer segmento del array (la cola) usando serpiente.pop_front(). Si crecer_serpiente es true (porque acabamos de comer), ¡simplemente no llamamos a pop_front()! El resultado es que la serpiente tiene un segmento más, y hemos crecido. Reseteamos la variable a false para que en el próximo tick, si no comemos, volvamos a movernos con normalidad.
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
dibujar_serpiente()
Esta función se llama automáticamente cada vez que Godot ejecuta _draw (lo cual forzamos en _process con queue_redraw()). Su trabajo es dibujar la serpiente, sin importar dónde esté o cuán larga sea. Lo hace recorriendo cada elemento (cada Vector2 de posición) en nuestro array serpiente con un bucle for segmento in serpiente:. Para cada uno de esos segmentos, tiene que convertir su posición de cuadrícula (ej: (5,5)) a píxeles en pantalla (ej: (100,100)). Esto lo hace con una simple multiplicación: pos_pixel = segmento * TAM_CUADRICULA. Finalmente, usa el comando draw_rect para dibujar un rectángulo de COLOR_SERPIENTE en esa posición de píxeles.
func _dibujar_serpiente(): # Dibujar cada segmento de la serpiente for segmento in serpiente: # Convertir la posición de cuadrícula (ej: 5,5) a píxeles (ej: 100,100) var pos_pixel = segmento * TAM_CUADRICULA var rect = Rect2(pos_pixel, Vector2(TAM_CUADRICULA, TAM_CUADRICULA)) draw_rect(rect, COLOR_SERPIENTE)
mover_comida()
Esta función tiene que encontrar un nuevo lugar para la manzana. No podemos simplemente ponerla en un sitio aleatorio, porque podría aparecer debajo de la serpiente, ¡haciendo imposible comerla! Por eso usamos un bucle while not pos_valida. Es un bucle que dice: «Repítete mientras no encuentres una posición válida». Dentro del bucle, primero somos optimistas (pos_valida = true). Luego, generamos una nueva_pos aleatoria en la cuadrícula (usando randi_range). Entonces, comprobamos esa nueva_pos contra todos los segmentos de la serpiente. Si encontramos una coincidencia (if segmento == nueva_pos), ponemos pos_valida = false y rompemos el bucle for. El bucle while ve que pos_valida es falsa y vuelve a empezar, buscando otra posición. Solo cuando el bucle for termina sin encontrar coincidencias, pos_valida se mantiene true y el while por fin termina. Asignamos esa nueva_pos 100% segura a la variable comida.
func _mover_comida(): # Mover la comida a una posición aleatoria que no esté sobre la serpiente var pos_valida = false var nueva_pos: Vector2 while not pos_valida: # Calcular una posición aleatoria en la cuadrícula nueva_pos = Vector2( randi_range(0, cuadricula_ancho - 1), randi_range(0, cuadricula_alto - 1) ) # Comprobar si esa posición está sobre la serpiente pos_valida = true # Asumir que es válida... for segmento in serpiente: if segmento == nueva_pos: pos_valida = false # ...ops, no lo era. Repetir el 'while'. break # Si salimos del 'while', 'nueva_pos' es válida comida = nueva_pos
dibujar_comida()
Se llama dentro de la función principal _draw. Es muy similar a _dibujar_serpiente, pero mucho más simple porque solo hay una comida. No necesita un bucle for. Simplemente coge la posición de cuadrícula actual de la variable comida (que _mover_comida nos aseguró que es válida), la convierte a píxeles en pantalla (multiplicando por TAM_CUADRICULA), y dibuja un solo rectángulo de COLOR_COMIDA en esa posición usando draw_rect.
func _dibujar_comida(): # Dibujar la comida en su posición var pos_pixel = comida * TAM_CUADRICULA var rect = Rect2(pos_pixel, Vector2(TAM_CUADRICULA, TAM_CUADRICULA)) draw_rect(rect, COLOR_COMIDA)
comprobar_comida()
Se llama desde _actualizar_movimiento en cada tick. Recibe la nueva_cabeza (dónde va a estar la serpiente) como argumento. Su trabajo es una simple comprobación: if cabeza == comida:. Si la posición de la cabeza es exactamente la misma que la de la comida, ¡hemos comido! Esto dispara tres acciones: 1) Activamos el flag crecer_serpiente = true. _mover_serpiente verá esto en un momento y sabrá que no debe borrar la cola. 2) Aumentamos la puntuacion y llamamos a _actualizar_etiqueta_puntuacion para que el jugador vea su nuevo punto. 3) Inmediatamente llamamos a _mover_comida() para que busque un nuevo sitio para la siguiente manzana, antes de que volvamos a dibujar.
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
comprobar_controles()
Esta función se llama automáticamente en _input cada vez que Godot detecta una entrada (como pulsar una tecla). A diferencia de _process, que se ejecuta constantemente, _input solo se ejecuta cuando algo pasa. Primero, comprueba si estamos muerto. Si es así, ignora todas las flechas y solo escucha ui_accept (Enter/Espacio) para reiniciar el juego. Si estamos vivos, escucha las flechas. La parte más importante es que no cambiamos direccion directamente, sino proxima_direccion. Esto es un «buffer» de entrada. ¿Por qué? Primero, evita que el jugador pulse dos teclas tan rápido dentro de un mismo tick que confunda al juego. Segundo, nos permite la lógica anti-suicidio: if direccion != Vector2.DOWN:. Esto comprueba que, si el jugador pulsa «arriba», nosotros no estemos ya yendo «abajo». Esto evita que la serpiente se choque consigo misma instantáneamente. La direccion real solo se actualiza en _actualizar_movimiento, al ritmo del «tick» del juego.
func _comprobar_controles(event: InputEvent):
# Si el juego ha terminado, solo escuchamos 'Enter' para reiniciar
if muerto:
if event.is_action_pressed("ui_accept"): # 'ui_accept' es 'Enter' o 'Espacio'
_iniciar_juego()
return
# Si el juego está activo, leemos las flechas de dirección
if event.is_action_pressed("ui_up"):
# Evitar que la serpiente se dé la vuelta sobre sí misma
if direccion != Vector2.DOWN:
proxima_direccion = Vector2.UP
elif event.is_action_pressed("ui_down"):
if direccion != Vector2.UP:
proxima_direccion = Vector2.DOWN
elif event.is_action_pressed("ui_left"):
if direccion != Vector2.RIGHT:
proxima_direccion = Vector2.LEFT
elif event.is_action_pressed("ui_right"):
if direccion != Vector2.LEFT:
proxima_direccion = Vector2.RIGHT
comprobar_colisiones()
Esta función crucial decide si morimos o no. Fíjate en -> bool: esto significa que la función está obligada a devolver (return) un valor booleano (true o false). Se llama desde _actualizar_movimiento antes de mover la serpiente. Comprueba dos tipos de colisión: 1) Colisión con los bordes: Comprueba si la cabeza se ha salido de los límites de la cuadrícula. Es decir, si su x es menor que 0 (borde izquierdo) o mayor o igual que cuadricula_ancho (borde derecho), y lo mismo para y (arriba y abajo). 2) Colisión consigo misma: Recorre cada segmento en el array serpiente y comprueba si la nueva_cabeza está en la misma posición que cualquier otro segmento. Si cualquiera de estas dos condiciones es cierta, la función devuelve true (¡colisión!). Si se comprueban todos los bordes y todos los segmentos y no hay colisión, la función devuelve false (seguro).
func _comprobar_colisiones(cabeza: Vector2): # Comprueba si la cabeza ha chocado con algo. Devuelve 'true' si hay colisión. # 1. Colisión con los bordes de la pantalla (en coordenadas de cuadrícula) if cabeza.x < 0 or cabeza.x >= cuadricula_ancho or \ cabeza.y < 0 or cabeza.y >= cuadricula_alto: return true # Hay colisión # 2. Colisión consigo misma (chocar con cualquier segmento) for segmento in serpiente: if cabeza == segmento: return true # Hay colisión # Si no hemos chocado con nada return false
game_over()
Se llama desde _actualizar_movimiento tan pronto como _comprobar_colisiones devuelve true. Esta función simplemente activa el flag muerto = true. Al principio de _process, tenemos una línea que dice if muerto: return. Así que, al poner muerto = true, estamos deteniendo toda la lógica de movimiento y el juego se «congela» eficazmente. Segundo, llama a show() en la etiqueta de «Game Over» que creamos y ocultamos al inicio, mostrando el mensaje de reinicio al jugador.
func _game_over(): # Termina el juego muerto = true etiqueta_game_over.show() queue_redraw() # Forzar un último dibujado (aunque _process esté parado)
dibujar_fondo()
La última función de dibujado, y la más simple. Es muy importante que sea la primera función que llamamos dentro de _draw. El dibujado en Godot funciona como un pintor: lo que dibujas primero, queda debajo. Por lo tanto, dibujamos el fondo primero, luego la comida, y finalmente la serpiente, para que la serpiente aparezca «encima» de la comida y del fondo. La función simplemente usa draw_rect para dibujar un rectángulo gigante del COLOR_FONDO que ocupa toda la pantalla, desde (0,0) (Vector2.ZERO) hasta el tamaño de la pantalla.
func _dibujar_fondo(): # Dibujar un rectángulo grande del color de fondo que cubra toda la pantalla draw_rect(Rect2(Vector2.ZERO, pantalla), COLOR_FONDO)
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: