Si tuviste un Nokia 3310, sabes de lo que estamos hablando. El Snake no es solo un juego; es el padre de los videojuegos móviles. Y hoy, nosotros vamos a programarlo desde cero.
A diferencia de otros proyectos donde el reto es el montaje eléctrico, aquí el desafío es el software. ¿Cómo hacemos para que una fila de luces se “mueva” como una serpiente? ¿Cómo recordamos dónde está cada parte de su cuerpo? ¿Cómo detectamos si se ha mordido la cola?
No vamos a usar una pantalla LCD, sino una Matriz de LEDs 8x8 estilo retro.
Este proyecto es ideal para entender el manejo de Arrays (Arreglos) y la diferencia entre la Lógica del Juego (lo que pasa en la memoria) y el Renderizado (lo que se ve en las luces).
Anatomía de una serpiente digital
El error de principiante es pensar en la pantalla como una cuadrícula de píxeles grid[8][8] e intentar mover los puntos iluminados. Eso es una pesadilla de programación.
La forma correcta es abstraerse. Nuestra serpiente no son luces, es una lista de coordenadas. Usaremos dos Arrays:
cuerpoX[]: Guarda las coordenadas X de cada segmento.cuerpoY[]: Guarda las coordenadas Y de cada segmento.
El índice 0 será siempre la Cabeza.
El índice 1 será el cuello.
El último índice será la Cola.
Algoritmo de movimiento
Para mover la serpiente, no movemos todos los puntos a la vez. Aplicamos la lógica de “arrastre”:
Empezamos por la cola. La cola se mueve a donde estaba el segmento anterior.
Repetimos hasta llegar al cuello.
Finalmente, movemos la Cabeza una casilla en la dirección que diga el Joystick.
Componentes
- [1x|blue] Arduino Uno | O Nano
- [1x|green] Módulo Matriz LED 8x8 con MAX7219 | Pantalla retro {driver integrado}
- [1x|orange] Joystick Analógico (KY-023) | Control de dirección
- [Varios|gray] Cables Dupont | Conexiones
Usar el chip MAX7219 nos ahorra mucho trabajo. Si intentas controlar 64 LEDs directamente con Arduino, te quedarás sin pines y sin paciencia. Este chip lo hace todo con solo 3 cables.
Esquema de conexión
Tenemos dos periféricos: la entrada (Joystick) y la salida (Pantalla).
Matriz LED 8x8
La matriz con MAX7219 usa tres señales de control más alimentación.
[ { “from”: “Matriz VCC”, “to”: “5V Arduino”, “color”: “red” }, { “from”: “Matriz GND”, “to”: “GND Arduino”, “color”: “black” }, { “from”: “Matriz DIN”, “to”: “Pin 12”, “color”: “green”, “note”: “Datos para LedControl” }, { “from”: “Matriz CS”, “to”: “Pin 10”, “color”: “blue”, “note”: “Chip Select” }, { “from”: “Matriz CLK”, “to”: “Pin 11”, “color”: “purple”, “note”: “Reloj para LedControl” } ]
Joystick analógico
El joystick entrega dos señales analógicas, una para cada eje. El pulsador integrado no lo usaremos en esta versión.
[ { “from”: “Joystick VCC”, “to”: “5V Arduino”, “color”: “red” }, { “from”: “Joystick GND”, “to”: “GND Arduino”, “color”: “black” }, { “from”: “Joystick VRx”, “to”: “Pin A0”, “color”: “orange” }, { “from”: “Joystick VRy”, “to”: “Pin A1”, “color”: “orange” } ]
El código
Necesitaremos la librería LedControl.h para hablar con el chip MAX7219. Podéis instalarla desde el gestor de librerías.
El código es un poco más largo de lo habitual porque incluye la lógica del juego. Lo hemos dividido en funciones para que sea legible.
Variables globales y configuración
Definimos los arrays con un tamaño máximo (ej. 64 segmentos, que sería pantalla llena).
#include <LedControl.h>
// Pines de la Matriz: DIN=12, CLK=11, CS=10
LedControl lc = LedControl(12, 11, 10, 1);
// Pines del Joystick
const int pinJoyX = A0;
const int pinJoyY = A1;
// --- VARIABLES DEL JUEGO ---
const int MAX_LONGITUD = 64;
int cuerpoX[MAX_LONGITUD];
int cuerpoY[MAX_LONGITUD];
int largoSerpiente = 3; // Empezamos con tamaño 3
int direccion = 1; // 0:Arr, 1:Der, 2:Abj, 3:Izq
// Comida
int comidaX, comidaY;
int colaAnteriorX, colaAnteriorY;
// Timing (Velocidad del juego)
unsigned long tiempoAnterior = 0;
int velocidad = 300; // Milisegundos por movimiento (menor = más rápido)
void setup() {
// Inicializar Matriz
lc.shutdown(0, false); // Despertar
lc.setIntensity(0, 4); // Brillo (0-15)
lc.clearDisplay(0);
// Posición inicial (centro)
cuerpoX[0]=4; cuerpoY[0]=4; // Cabeza
cuerpoX[1]=3; cuerpoY[1]=4;
cuerpoX[2]=2; cuerpoY[2]=4;
randomSeed(analogRead(A5)); // Semilla aleatoria
generarComida();
}
Bucle principal
En videojuegos, el loop suele tener tres fases: Leer Inputs -> Actualizar Lógica -> Dibujar (Render).
void loop() {
// 1. LEER INPUTS (Joystick)
leerJoystick();
// 2. ACTUALIZAR LÓGICA (Solo cada 'velocidad' ms)
if (millis() - tiempoAnterior >= velocidad) {
tiempoAnterior = millis();
moverSerpiente();
verificarColisiones();
// 3. DIBUJAR
dibujarJuego();
}
}
Lectura del joystick
Aquí hay un truco importante: Evitar el suicidio. Si la serpiente va hacia la DERECHA, no podemos permitir que el usuario pulse IZQUIERDA, porque chocaría contra su propio cuello instantáneamente.
void leerJoystick() {
int x = analogRead(pinJoyX);
int y = analogRead(pinJoyY);
// Umbrales para detectar dirección
// 0:Arr, 1:Der, 2:Abj, 3:Izq
// Nota: La orientación depende de cómo coloques el joystick.
// Ajusta estos 'ifs' según tu montaje.
if (y < 200 && direccion != 2) direccion = 0; // Arriba (salvo que vayamos abajo)
if (y > 800 && direccion != 0) direccion = 2; // Abajo
if (x > 800 && direccion != 3) direccion = 1; // Derecha
if (x < 200 && direccion != 1) direccion = 3; // Izquierda
}
Lógica de movimiento
Aquí aplicamos el algoritmo de desplazamiento del array.
void moverSerpiente() {
// Guardamos la cola por si la serpiente come en este movimiento
colaAnteriorX = cuerpoX[largoSerpiente - 1];
colaAnteriorY = cuerpoY[largoSerpiente - 1];
// Cada segmento ocupa la posición anterior del que tenía delante
for (int i = largoSerpiente - 1; i > 0; i--) {
cuerpoX[i] = cuerpoX[i - 1];
cuerpoY[i] = cuerpoY[i - 1];
}
// Movemos la cabeza
if (direccion == 0) cuerpoY[0]--;
if (direccion == 1) cuerpoX[0]++;
if (direccion == 2) cuerpoY[0]++;
if (direccion == 3) cuerpoX[0]--;
}
Colisiones y comida
Comprobamos si hemos muerto o si hemos comido.
void verificarColisiones() {
// 1. Choque con PAREDES
if (cuerpoX[0] < 0 || cuerpoX[0] > 7 || cuerpoY[0] < 0 || cuerpoY[0] > 7) {
gameOver();
return;
}
// 2. Choque con UNO MISMO
for (int i = 1; i < largoSerpiente; i++) {
if (cuerpoX[0] == cuerpoX[i] && cuerpoY[0] == cuerpoY[i]) {
gameOver();
return;
}
}
// 3. COMER
if (cuerpoX[0] == comidaX && cuerpoY[0] == comidaY) {
if (largoSerpiente < MAX_LONGITUD) {
cuerpoX[largoSerpiente] = colaAnteriorX;
cuerpoY[largoSerpiente] = colaAnteriorY;
largoSerpiente++;
}
// Aumentamos velocidad (dificultad)
if (velocidad > 100) velocidad -= 10;
if (largoSerpiente < MAX_LONGITUD) generarComida();
}
}
void generarComida() {
bool ocupada;
do {
comidaX = random(0, 8);
comidaY = random(0, 8);
ocupada = false;
for (int i = 0; i < largoSerpiente; i++) {
if (cuerpoX[i] == comidaX && cuerpoY[i] == comidaY) {
ocupada = true;
break;
}
}
} while (ocupada);
}
void gameOver() {
// Parpadeo de pantalla
for(int i=0; i<3; i++){
lc.clearDisplay(0);
delay(200);
dibujarJuego();
delay(200);
}
// Reiniciar juego
largoSerpiente = 3;
cuerpoX[0]=4; cuerpoY[0]=4;
cuerpoX[1]=3; cuerpoY[1]=4;
cuerpoX[2]=2; cuerpoY[2]=4;
direccion = 1;
velocidad = 300;
generarComida();
}
Renderizado
Finalmente, volcamos el estado de la memoria a los LEDs.
void dibujarJuego() {
lc.clearDisplay(0);
// Dibujar Comida
lc.setLed(0, comidaY, comidaX, true); // (addr, row, col, state)
// Dibujar Serpiente
for (int i = 0; i < largoSerpiente; i++) {
lc.setLed(0, cuerpoY[i], cuerpoX[i], true);
}
}
Fíjate que lc.setLed suele pedir (fila, columna). Dependiendo de cómo hayas soldado tu matriz, es posible que tus X e Y estén intercambiados. Si la serpiente se mueve raro (arriba es izquierda), simplemente intercambia los parámetros en esta función.
Posibles mejoras
Puedes añadir sonido al recoger comida, mostrar una animación de victoria al ocupar las 64 casillas o conectar dos matrices para crear un tablero de 16x8.