arduino

Snake con Arduino usando LCD y Joystick – Completado

Estoy contento de anunciar que al fin he terminado el videojuego que comencé hace casi un mes. Se trata del juego de la serpiente, viborita, snake o como lo conozcas; pero en Arduino usando una LCD.

Antes que nada te invito a ver la primera parte en donde todavía no terminaba el proyecto, el cual pausé porque no tenía los componentes necesarios, pero recientemente llegó mi pedido y pude terminar el proyecto.

Características de snake en Arduino

  • Lleva contador de puntaje
  • La serpiente puede ser movida con cualquier entrada, en este caso es un Joystick pero puede ser un botón, Bluetooth, etcétera
  • Opción para colocar comida en el tablero, de manera aleatoria
  • Si la serpiente come, se hace más larga
  • La pantalla no parpadea

Lecturas recomendadas

Vamos a usar cosas simples, pero si eres un principiante te recomiendo leer los enlaces que dejo a continuación. Para empezar, vamos a generar números aleatorios con Arduino; esto servirá para colocar la comida de manera aleatoria.

En segundo lugar vamos a usar un Joystick conectado al Arduino.

Circuito

Es como combinar el circuito de LCD I2C con Arduino y Arduino con Joystick. Aquí está:

Snake (serpiente) en Arduino con LCD y Joystick

Hay que notar que no he conectado el botón del joystick, pues por ahora no tiene ningún uso.

Nota: he usado un Arduino MEGA pero en un UNO debería funcionar como un encanto.

Comida de la serpiente

Como lo dije, una de las mejoras o actualizaciones es que se coloca comida de manera aleatoria en el escenario.

Para dibujar la comida tenemos dos funciones; una de ella calcula las coordenadas y la otra la coloca sobre el escenario:

// Calcula coordenadas aleatorias para colocar la comida
void randomizarComida() {
  comidaX = random(0, ANCHURA_TABLERO);
  comidaY = random(0, ALTURA_TABLERO);
}
// Coloca la comida en el escenrio
void acomodarComida() {
  escenario[comidaY][comidaX] = 1;
}

Colisiones y puntaje

Le damos seguimiento a la posición de la comida y calculamos si la serpiente colisiona con la misma, es decir, sabemos si la serpiente come, valga la redundancia, comida (podemos imaginar que la serpiente es una anaconda y la comida es un ratón).

De igual modo ahora el puntaje se aumenta de manera correcta.

// Saber en dónde está la comida para, más tarde, saber si la serpiente colisiona
int comidaX, comidaY;
// El puntaje. Se desborda cuando llega a 32767, creo
int puntaje = 0;

bool colisionaConComida() {
  return serpiente[0].x == comidaX && serpiente[0].y == comidaY;
}

void loop() {
  // ...
  if (colisionaConComida()) {
    puntaje++;
    randomizarComida();
    agregarPedazo(0, 0);// De hecho la posición del pedazo no importa al momento de agregar
  }
  // ...
}

Dirección con el joystick

También tenemos una función que lee un joystick y devuelve la dirección hacia la que se debe dirigir la serpiente:

// Leer del joystick
int obtenerDireccion() {

  int valorX = analogRead(pinX),
      valorY = analogRead(pinY);
  if (valorX > 1000) {
    return DIRECCION_ARRIBA;
  } else if (valorX < 200) {
    return DIRECCION_ABAJO;
  }

  if (valorY > 1000) {
    return DIRECCION_DERECHA;
  } else if (valorY < 200) {
    return DIRECCION_IZQUIERDA;
  }
  // Regresamos algo inválido, pues no se cambió,
  // y dependemos de la función cambiarDireccion
  return -1;
}

Evitar parpadeo de la pantalla

Me di cuenta de que no era necesario invocar al método clear de la pantalla, pues en cada momento se estaba dibujando el nuevo valor; eso redujo que la pantalla parpadee y sea desagradable a la vista.

Snake sobre Arduino – terminado

Finalmente dejaré la prueba de que todo funciona. Primero, el código:

/*
    Programado por Luis Cabrera Benito
    14 de febrero de 2020
   ____          _____               _ _           _
  |  _ \        |  __ \             (_) |         | |
  | |_) |_   _  | |__) |_ _ _ __ _____| |__  _   _| |_ ___
  |  _ <| | | | |  ___/ _` | '__|_  / | '_ \| | | | __/ _ \
  | |_) | |_| | | |  | (_| | |   / /| | |_) | |_| | ||  __/
  |____/ \__, | |_|   \__,_|_|  /___|_|_.__/ \__, |\__\___|
         __/ |                               __/ |
        |___/                               |___/
    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

// Para leer el Joystick
const int pinX = 0,
          pinY = 1;


LiquidCrystal_I2C pantalla(DIRECCION_LCD, ANCHURA_LCD, ALTURA_LCD);
int escenario[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;
// Saber en dónde está la comida para, más tarde, saber si la serpiente colisiona
int comidaX, comidaY;
// El puntaje. Se desborda cuando llega a 32767, creo
int puntaje = 0;


// Leer del joystick
int obtenerDireccion() {

  int valorX = analogRead(pinX),
      valorY = analogRead(pinY);
  if (valorX > 1000) {
    return DIRECCION_ARRIBA;
  } else if (valorX < 200) {
    return DIRECCION_ABAJO;
  }

  if (valorY > 1000) {
    return DIRECCION_DERECHA;
  } else if (valorY < 200) {
    return DIRECCION_IZQUIERDA;
  }
  // Regresamos algo inválido, pues no se cambió,
  // y dependemos de la función cambiarDireccion
  return -1;
}

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;
    escenario[x][y] = 1;
  }
}

// Calcula coordenadas aleatorias para colocar la comida
void randomizarComida() {
  comidaX = random(0, ANCHURA_TABLERO);
  comidaY = random(0, ALTURA_TABLERO);
}
// Coloca la comid en el escenrio
void acomodarComida() {
  escenario[comidaY][comidaX] = 1;
}



void setup() {
  randomSeed(analogRead(0));
  pantalla.init();
  pantalla.backlight();
  for (int i = 0; i < 3; i++) {
    agregarPedazo(5, i);
  }

  randomizarComida();

  pantalla.setCursor(0, 0);
  pantalla.print("Snake");
  pantalla.setCursor(0, 1);
  pantalla.print("By Parzibyte");
  delay(700);
  pantalla.clear();
}

void dibujarPuntaje() {
  pantalla.setCursor(6, 0);
  pantalla.print("SCORE");
  pantalla.setCursor(6, 1);
  pantalla.print(puntaje);
}

void dibujarMatriz() {
  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 (escenario[indice][inicio + 0] == 1)
          numero += 16;
        if (escenario[indice][inicio + 1] == 1)
          numero += 8;
        if (escenario[indice][inicio + 2] == 1)
          numero += 4;
        if (escenario[indice][inicio + 3] == 1)
          numero += 2;
        if (escenario[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++) {
      escenario[y][x] = 0;
    }
  }
}

bool colisionaConComida() {
  return serpiente[0].x == comidaX && serpiente[0].y == comidaY;
}

void loop() {
  limpiarMatriz();
  cambiarDireccion(obtenerDireccion());
  moverSerpiente();
  colocarSerpienteEnMatriz();
  acomodarComida();
  dibujarMatriz();
  if (colisionaConComida()) {
    puntaje++;
    randomizarComida();
    agregarPedazo(0, 0);// De hecho la posición del pedazo no importa al momento de agregar
  }

  dibujarPuntaje();
  delay(10);// Mientras menor sea, más rápido va la serpiente, pero menor sensibilidad tiene el joystick
}

Aquí la demostración de mí jugando:

Parzibyte jugando snake en Arduino usando un joystick

Lo tengo de igual modo en vídeo:

Por cierto, mi mayor puntaje creo que fue 169:

Mayor puntaje obtenido en juego de la serpiente con arduino y LCD

PD: lo sé, mi pantalla está oxidada, le tendré más cuidado.

Conclusión

Dejo el enlace a mi repositorio de GitHub en donde actualizaré el código si lo hago algún día; pero por ahora estoy muy satisfecho con el resultado. No olvides leer la primera parte, pues explico detalles importantes.

De hecho nadie solicitó este juego y no fue un requisito de ningún modo, simplemente fue un capricho. Lo intenté hacer hace algún largo tiempo y no pude, pero al intentarlo de nuevo, lo logré y quedó estupendo.

Si quieres puedes leer más sobre Arduino o Electrónica en mi blog.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

  • Buenas tardes, queria consultar que libreria estan utilizando ya que liquidCrystal.h de arduino no me toma el programa

  • hay un error, no sé a que se refiera, ya pude la dirección LCD, y las librerías y me dice esto:

    es un texto más grande pero se va repitiendo esto varias veces

    In file included from C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.49.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/Arduino.h:32:0,
    from sketch\Proyecto.ino.cpp:1:
    C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.49.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/binary.h:58:0: note: this is the location of the previous definition
    #define B00000100 4

    C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.49.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/binary.h:58:19: error: expected ',' or '...' before numeric constant
    #define B00000100 4
    ^
    C:\Users\Pedro\OneDrive\Documentos\Arduino\libraries\LiquidCrystal_I2C/LiquidCrystal_I2C.h:51:12: note: in expansion of macro 'B00000100'
    #define En B00000100 // Enable bit
    ^~~~~~~~~

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

2 semanas hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 semanas hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 semanas hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 semanas hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 semanas hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 semanas hace

Esta web usa cookies.