Hoy vamos a ver el desarrollo del videojuego snake en Arduino usando una LCD de 16 x 2.
Lo que haremos será dibujar una serpiente por la pantalla y permitir que se mueva, creciendo en su tamaño, comiendo y refrescando la LCD con las nuevas posiciones.
Es decir, un juego snake completo pero usando un Arduino y una LCD como hardware, nada de computadoras.
Aunque parece simple la verdad es que fue algo difícil.
Antes de comenzar el post dejo el vídeo de demostración. Estoy usando un Arduino Mega, pero supongo que en el UNO debe funcionar muy bien. También uso una LCD (obviamente) con un módulo I2C.
Quiero recalcar que es la primera parte; el primer avance, y no la versión final, pero la parte que hace posible al juego ya está terminada.
Lo hice por diversión, ya que anteriormente había querido hacerlo pero me ganó el estrés o lo que sea que haya sido; pero ahora, con más experiencia intenté hacerlo de nuevo y avancé bastante.
Además, quería procrastinar al máximo para evitar hacer mi tarea, así que limpié mi viejo Arduino MEGA, conecté todo y comencé a programar.
La LCD que tengo es de 16 x 2 y permite usar 32 pixeles si lo vemos de ese modo. Pero, ¿Alguien imagina una serpiente que ocupe toda la pantalla y que sea así de gigante?
Aquí la LCD traída de mi post sobre LCD y Arduino:
Entonces una serpiente de ese tamaño no se vería nada bien; no habría espacio. Así que me pregunté…
¿Y si ocupo un pixel pequeño? ya que si nos fijamos, cada cuadro tiene 40 cuadros más pequeños dentro del mismo:
De este modo la serpiente tendría más espacio, ya que suponiendo que ocupara toda la pantalla se tendría el total de 1280 pixeles, con una altura de 16 y una anchura de 80.
A partir de este momento llamaremos pixel a los cuadros pequeños (los que en total son 1280).
Pero la LCD no es un lienzo en donde puedas pintar cada pixel, ya que dibuja en un cuadro grande. La buena noticia es que se puede crear una forma personalizada definiendo un arreglo de tipo byte de longitud 8 en donde cada elemento representa un número binario indicando la distribución de la fila.
Por ejemplo, lo siguiente dibujará una sonrisa:
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
byte smiley[8] = {
B00000,
B10001,
B00000,
B00000,
B10001,
B01110,
B00000,
};
void setup() {
lcd.createChar(0, smiley);
lcd.begin(16, 2);
lcd.write(byte(0));
}
void loop() {}
Un 1 es un pixel encendido, un 0 es un pixel apagado. Eso abre un montón de posibilidades ya que podríamos dibujar a la serpiente en cualquier lugar, incluso dibujar su cuerpo por partes.
Aunque tenemos un byte en la forma B00100
en realidad podría ser convertido a decimal por su posición, terminando en que B00100
es 4
. Entonces podemos definir el dato (o la fila de la forma) como un byte o un decimal.
Si no entiendes bien, mira la conversión de números binarios a decimal.
El juego de snake en Arduino cuenta con una matriz que es el tablero de juego. Al inicio la llené manualmente:
int prueba[16][20] = {
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 1, 1, 1,/* */1, 1, 1, 1, 1, /* */1, 1, 1, 1, 1,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 1,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 1,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 1,/* */1, 1, 1, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 1, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 1, 0, 0,},
/***************************************************************************/ {0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 1, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 1, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 1,/* */1, 1, 1, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 1,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 1, 1, 1, /* */1, 1, 1, 1, 1,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 1, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 1, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 1, 1, 1, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
};
Pero como cada cuadro grande tiene 8 x 5 cuadros tuve que leer en porciones de ese tamaño:
byte figura[8];
int numeroFigura = 0;
for (int cuadritoX = 0; cuadritoX < 4; cuadritoX++) {
for (int cuadritoY = 0; cuadritoY < 2; cuadritoY++) {
// Magia aquí
}
}
Por cada cuadro gigante tenía que convertir cada fila en un byte y lo colocaba dentro del arreglo que formaría mi carácter personalizado más tarde:
for (int x = 0; x < 8; x++) {
int numero = 0;
int indice = cuadritoY == 0 ? x : (x + 8);
int inicio = cuadritoX * 5;
// Quién te conoce math.pow
if (prueba[indice][inicio + 0] == 1)numero += 16;
if (prueba[indice][inicio + 1] == 1)numero += 8;
if (prueba[indice][inicio + 2] == 1)numero += 4;
if (prueba[indice][inicio + 3] == 1)numero += 2;
if (prueba[indice][inicio + 4] == 1)numero += 1;
///
figura[x] = numero;
}
Después de llenar el arreglo, lo almacenaba en la LCD y creaba un char, aumentando además un contador que llevaba el registro de qué número de figura iba:
pantalla.createChar(numeroFigura, figura);
pantalla.setCursor(cuadritoX, cuadritoY); // X, Y
pantalla.write(byte(numeroFigura));
numeroFigura++;
El código completo que dibuja la matriz, convirtiendo los ceros y unos en bytes, además de guardar y crear la figura es el siguiente:
byte figura[8];
int numeroFigura = 0;
for (int cuadritoX = 0; cuadritoX < 4; cuadritoX++) {
for (int cuadritoY = 0; cuadritoY < 2; cuadritoY++) {
for (int x = 0; x < 8; x++) {
int numero = 0;
int indice = cuadritoY == 0 ? x : (x + 8);
int inicio = cuadritoX * 5;
// Quién te conoce math.pow
if (prueba[indice][inicio + 0] == 1)numero += 16;
if (prueba[indice][inicio + 1] == 1)numero += 8;
if (prueba[indice][inicio + 2] == 1)numero += 4;
if (prueba[indice][inicio + 3] == 1)numero += 2;
if (prueba[indice][inicio + 4] == 1)numero += 1;
///
figura[x] = numero;
}
pantalla.createChar(numeroFigura, figura);
pantalla.setCursor(cuadritoX, cuadritoY); // X, Y
pantalla.write(byte(numeroFigura));
numeroFigura++;
}
}
Ese es el problema más complejo al que me enfrenté, porque aunque el código es pequeño, lo complejo fue llegar a la solución ya que las formas personalizadas se crean y dibujan en tiempo de ejecución.
Ahora que muestro este método también debo mostrar el que limpia la matriz:
void limpiarMatriz() {
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 20; x++) {
prueba[y][x] = 0;
}
}
}
Si has estado poniendo atención y siguiendo el código, te darás cuenta de que no se está usando toda la pantalla; y eso es porque solo podemos crear 8 figuras personalizadas (estoy limitado por la tecnología de mi tiempo.jpg)
Así que esa es la razón por la que no se ocupa toda la pantalla y solo 8 cuadros.
Pero bueno, con la matriz que se veía así (he marcado los unos para mostrar cómo se verá la serpiente):
Se dibujaba lo siguiente:
Cuando llegué a este punto supe que lo había logrado y que era totalmente posible hacer el juego. Digamos que había terminado la rutina de programación para dibujar el escenario; lo demás era implementar la funcionalidad del juego.
Ahora que había terminado la parte del dibujo en la matriz lo demás fue implementar la serpiente. Me inspiré del snake que hice con JavaScript, así que solo tenía que portarlo a C++, pues el Luis del pasado ya se había quebrado la cabeza programando al mismo.
Comenzamos definiendo a la serpiente con una clase, y un arreglo que iba a tener los pedazos de la serpiente:
class PedazoSerpiente {
public:
int x, y;
PedazoSerpiente(int a, int b) {
x = a; y = b;
}
PedazoSerpiente() {
}
};
PedazoSerpiente serpiente[MAXIMA_LONGITUD_SERPIENTE];
El arreglo es de tamaño definido, el mismo tiene el valor de todos los pixeles que puede usar la serpiente. También necesité una variable para llevar el conteo de cuánto medía la serpiente (es decir, la longitud del arreglo era la capacidad, pero esta variable era la longitud)
La dirección de la serpiente es importante; la he definido como 4 enteros usando #define. La función que cambia la dirección y valida las cosas es la siguiente:
#define DIRECCION_DERECHA 0
#define DIRECCION_IZQUIERDA 1
#define DIRECCION_ARRIBA 2
#define DIRECCION_ABAJO 3
int direccion = DIRECCION_DERECHA;
void cambiarDireccion(int nuevaDireccion) {
if (
nuevaDireccion != DIRECCION_DERECHA
&& nuevaDireccion != DIRECCION_IZQUIERDA
&& nuevaDireccion != DIRECCION_ARRIBA
&& nuevaDireccion != DIRECCION_ABAJO
) {
return;
}
if (
(nuevaDireccion == DIRECCION_DERECHA || nuevaDireccion == DIRECCION_IZQUIERDA)
&& (direccion == DIRECCION_DERECHA || direccion == DIRECCION_IZQUIERDA)
) return;
if (
(nuevaDireccion == DIRECCION_ARRIBA || nuevaDireccion == DIRECCION_ABAJO)
&& (direccion == DIRECCION_ARRIBA || direccion == DIRECCION_ABAJO)
) return;
direccion = nuevaDireccion;
}
Por defecto la dirección es la derecha, así que la serpiente avanza hacia la derecha.
Cuando la serpiente come, se hace más grande. Veamos la función que agrega un pedazo junto con algunas constantes para validar los límites:
#define ALTURA_TABLERO 16
#define ANCHURA_TABLERO 20
#define MAXIMA_LONGITUD_SERPIENTE (ALTURA_TABLERO * ANCHURA_TABLERO)
void agregarPedazo(int x, int y) {
if (longitudSerpiente >= MAXIMA_LONGITUD_SERPIENTE) return;
if (x + 1 >= ANCHURA_TABLERO || x < 0)return;
if (y + 1 >= ALTURA_TABLERO || y < 0)return;
serpiente[longitudSerpiente] = PedazoSerpiente(x, y);
longitudSerpiente++;
}
Ya tenemos dirección y tamaño de la serpiente, pero todavía no la estamos moviendo; es decir, no estamos actualizando sus coordenadas en cada paso. El método que lo hace es:
void moverSerpiente() {
for (int i = longitudSerpiente - 1; i >= 1; i--) {
serpiente[i].x = serpiente[i - 1].x;
serpiente[i].y = serpiente[i - 1].y;
}
switch (direccion) {
case DIRECCION_DERECHA:
if (serpiente[0].x + 1 >= ANCHURA_TABLERO)serpiente[0].x = 0;
else serpiente[0].x++;
break;
case DIRECCION_IZQUIERDA:
if (serpiente[0].x <= 0)serpiente[0].x = ANCHURA_TABLERO - 1;
else serpiente[0].x--;
break;
case DIRECCION_ARRIBA:
if (serpiente[0].y <= 0)serpiente[0].y = ALTURA_TABLERO - 1;
else serpiente[0].y--;
break;
case DIRECCION_ABAJO:
if (serpiente[0].y + 1 >= ALTURA_TABLERO)serpiente[0].y = 0;
else serpiente[0].y++;
break;
}
}
Valida la dirección y si se toca un borde lo convierte en un portal que mueve la serpiente al borde contrario.
La serpiente en sí ya está preparada para ser pintada en cualquier lugar, y la matriz del juego ya está siendo dibujada en la LCD; lo que falta aquí es agregar la serpiente dentro de la matriz y el método que lo hace es:
void colocarSerpienteEnMatriz() {
for (int i = 0; i < longitudSerpiente; i++) {
int x = serpiente[i].y,
y = serpiente[i].x;
prueba[x][y] = 1;
}
}
Como gran parte de la pantalla quedará desocupada se me ocurrió dibujar el puntaje ahí. Por el momento es un número estático, sin embargo es fácil de convertir en uno dinámico:
void dibujarPuntaje() {
pantalla.setCursor(6, 0);
pantalla.print("SCORE");
pantalla.setCursor(6, 1);
pantalla.print("666");
}
Como no he colocado los métodos con los que se movería la serpiente decidí generar un número aleatorio para moverla. Primero alimenté al generador:
randomSeed(analogRead(0));
Después generé números aleatorios:
cambiarDireccion(random(0, 15));
Aunque la dirección solo va del 0 al 3 quise darle más aleatoridad generando números en un rango más amplio, para que no se moviera en cada paso y se viera más natural.
El código completo queda así:
/*
Programado por Luis Cabrera Benito
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
*/#include <LiquidCrystal_I2C.h>
#define ANCHURA_LCD 16
#define ALTURA_LCD 2
#define DIRECCION_LCD 0x3F // Si no sabes la dirección, visita https://parzibyte.me/blog/2018/02/02/obtener-direccion-modulo-i2c-lcd-arduino/
#define ALTURA_TABLERO 16
#define ANCHURA_TABLERO 20
#define MAXIMA_LONGITUD_SERPIENTE (ALTURA_TABLERO * ANCHURA_TABLERO)
#define DIRECCION_DERECHA 0
#define DIRECCION_IZQUIERDA 1
#define DIRECCION_ARRIBA 2
#define DIRECCION_ABAJO 3
LiquidCrystal_I2C pantalla(DIRECCION_LCD, ANCHURA_LCD, ALTURA_LCD);
int prueba[ALTURA_TABLERO][ANCHURA_TABLERO] = {
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
/***************************************************************************/ {0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
{0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0, /* */0, 0, 0, 0, 0,/* */0, 0, 0, 0, 0,},
};
class PedazoSerpiente {
public:
int x, y;
PedazoSerpiente(int a, int b) {
x = a; y = b;
}
PedazoSerpiente() {
}
};
PedazoSerpiente serpiente[MAXIMA_LONGITUD_SERPIENTE];
int longitudSerpiente = 0;
int direccion = DIRECCION_DERECHA;
void cambiarDireccion(int nuevaDireccion) {
if (
nuevaDireccion != DIRECCION_DERECHA
&& nuevaDireccion != DIRECCION_IZQUIERDA
&& nuevaDireccion != DIRECCION_ARRIBA
&& nuevaDireccion != DIRECCION_ABAJO
) {
return;
}
if (
(nuevaDireccion == DIRECCION_DERECHA || nuevaDireccion == DIRECCION_IZQUIERDA)
&& (direccion == DIRECCION_DERECHA || direccion == DIRECCION_IZQUIERDA)
) return;
if (
(nuevaDireccion == DIRECCION_ARRIBA || nuevaDireccion == DIRECCION_ABAJO)
&& (direccion == DIRECCION_ARRIBA || direccion == DIRECCION_ABAJO)
) return;
direccion = nuevaDireccion;
}
void agregarPedazo(int x, int y) {
if (longitudSerpiente >= MAXIMA_LONGITUD_SERPIENTE) return;
if (x + 1 >= ANCHURA_TABLERO || x < 0)return;
if (y + 1 >= ALTURA_TABLERO || y < 0)return;
serpiente[longitudSerpiente] = PedazoSerpiente(x, y);
longitudSerpiente++;
}
void moverSerpiente() {
for (int i = longitudSerpiente - 1; i >= 1; i--) {
serpiente[i].x = serpiente[i - 1].x;
serpiente[i].y = serpiente[i - 1].y;
}
switch (direccion) {
case DIRECCION_DERECHA:
if (serpiente[0].x + 1 >= ANCHURA_TABLERO)serpiente[0].x = 0;
else serpiente[0].x++;
break;
case DIRECCION_IZQUIERDA:
if (serpiente[0].x <= 0)serpiente[0].x = ANCHURA_TABLERO - 1;
else serpiente[0].x--;
break;
case DIRECCION_ARRIBA:
if (serpiente[0].y <= 0)serpiente[0].y = ALTURA_TABLERO - 1;
else serpiente[0].y--;
break;
case DIRECCION_ABAJO:
if (serpiente[0].y + 1 >= ALTURA_TABLERO)serpiente[0].y = 0;
else serpiente[0].y++;
break;
}
}
void colocarSerpienteEnMatriz() {
for (int i = 0; i < longitudSerpiente; i++) {
int x = serpiente[i].y,
y = serpiente[i].x;
prueba[x][y] = 1;
}
}
void setup() {
randomSeed(analogRead(0));
pantalla.init();
pantalla.backlight();
for (int i = 0; i < 20; i++) {
agregarPedazo(5, i);
}
}
void dibujarPuntaje() {
pantalla.setCursor(6, 0);
pantalla.print("SCORE");
pantalla.setCursor(6, 1);
pantalla.print("666");
}
void dibujarMatriz() {
pantalla.clear();
byte figura[8];
int numeroFigura = 0;
for (int cuadritoX = 0; cuadritoX < 4; cuadritoX++) {
for (int cuadritoY = 0; cuadritoY < 2; cuadritoY++) {
for (int x = 0; x < 8; x++) {
int numero = 0;
int indice = cuadritoY == 0 ? x : (x + 8);
int inicio = cuadritoX * 5;
// Quién te conoce math.pow
if (prueba[indice][inicio + 0] == 1)numero += 16;
if (prueba[indice][inicio + 1] == 1)numero += 8;
if (prueba[indice][inicio + 2] == 1)numero += 4;
if (prueba[indice][inicio + 3] == 1)numero += 2;
if (prueba[indice][inicio + 4] == 1)numero += 1;
///
figura[x] = numero;
}
pantalla.createChar(numeroFigura, figura);
pantalla.setCursor(cuadritoX, cuadritoY); // X, Y
pantalla.write(byte(numeroFigura));
numeroFigura++;
}
}
}
void limpiarMatriz() {
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 20; x++) {
prueba[y][x] = 0;
}
}
}
void loop() {
//pantalla.noBlink();
limpiarMatriz();
cambiarDireccion(random(0, 15));
moverSerpiente();
colocarSerpienteEnMatriz();
dibujarMatriz();
dibujarPuntaje();
// pantalla.blink();
delay(50);
}
Puedes verlo en mi perfil de GitHub. También tengo la demostración en mi canal de YouTube.
Como puedes ver, el juego no ha sido terminado; falta colocar la comida y mover a la serpiente con un dispositivo como botones o un joystick; cosa que haré en un futuro.
La razón por la que pausé el desarrollo es que no pude conectar los botones porque están en mal estado, pero tan pronto compre nuevos componentes terminaré la serpiente.
Actualización: ¡el juego está terminado! míralo aquí.
Te invito a seguirme para que, cuando publique la segunda parte seas el primero en enterarte.
También dejo enlaces para aprender más sobre Arduino, ver el juego de snake en JavaScript o ver el juego de la batalla naval en Arduino.
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Esta web usa cookies.