En este artículo, desarrollaremos un juego Arduino Starship que se mostrará en una pantalla LCD de 16x2. El juego será controlado por un joystick y por una computadora a través del Serial Monitor. Además, almacenaremos una puntuación alta en EEPROM y la actualizaremos cuando se rompa el récord.
El código del proyecto está publicado en mi GitHub
El proyecto es interesante y abre oportunidades para aprender cosas nuevas:
En el juego, el jugador controla la nave estelar. Delante de la nave espacial voladora habrá algunos enemigos. Como habrá una colisión de la nave estelar con uno de los enemigos, el juego habrá terminado. Starship tiene una bala para disparar pero con un límite. El límite para la bala es solo una bala a la vez. Significa, que mientras podemos ver la bala en la pantalla, no se pueden disparar más balas, debemos esperar hasta que choque con uno de los enemigos o desaparezca de la pantalla.
Vídeo del juego:
https://www.youtube.com/watch?v=HW6j_PRgFx4
Primero, repasaremos cada elemento individualmente. Luego, descubriremos cómo interactuará todo esto para que el juego funcione. Si sabe cómo funciona un elemento, puede pasar a la siguiente sección.
Comencemos con la pantalla LCD. En el proyecto, he estado usando la popular pantalla LCD 16x2 que se puede encontrar en casi todos los kits de Arduino. En mi caso, la pantalla viene con un adaptador LCD I2C y la conexión será GND-GND, VCC-5V, SDA-A4 y SCL-A5.
Como siempre, antes que nada, necesitamos incluir bibliotecas:
#include <Wire.h> #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x3F, 16, 2);
En la función LiquidCrystal_I2C lcd(0x3F, 16, 2) definimos la dirección de nuestro adaptador LCD I2C. Y sí, significa que podemos conectar muchos elementos I2C a Arduino. La dirección por defecto es 0x3F o 0x27. Los siguientes dos elementos son el tamaño de nuestra pantalla.
Así es como iniciamos y mostramos el texto:
void setup() { lcd.begin(); lcd.backlight(); lcd.clear(); lcd.setCursor(0,0); lcd.print("Hello World!"); lcd.setCursor(0,1); lcd.print("Chingiz"); }
lcd.begin() - inicia el lcd. lcd.backlight() - enciende la luz de fondo de la pantalla LCD. lcd.clear() - borra la pantalla lcd.setCursor(0,0) - coloca el cursor en la posición escrita. Tenga en cuenta que el primer dígito es el eje X y el segundo dígito es el eje Y. lcd.print("Hello World!") - imprime el texto escrito en la pantalla LCD.
Cada dígito de la pantalla consta de 5x8 píxeles. Para crear un personaje personalizado como una nave espacial, debemos definirlo e iniciarlo:
byte c1[8]={B00000,B01010,B00000,B00000,B10001,B01110,B00000,B00000}; //Smile-1 byte c2[8]={B10000,B10100,B01110,B10101,B01110,B10100,B10000,B00000}; //Starship-2 //In setup: lcd.createChar(0 , c1); //Creating custom characters in CG-RAM lcd.createChar(1 , c2); //Creating custom characters in CG-RAM
Como puede ver, la creación de caracteres personalizados se realiza utilizando un byte con una longitud de cinco que representa una línea y tenemos ocho de ellos para tener un dígito personalizado.
Encontre un
Y así es como mostramos nuestros caracteres personalizados:
lcd.print(char(0)); lcd.print(char(1));
El joystick tiene un botón, eje X e Y. El botón funciona como de costumbre. Los ejes X e Y se pueden considerar como un potenciómetro, que proporciona datos entre 0 y 1023. Un valor predeterminado es la mitad de eso. Usaremos solo el eje X para controlar la nave estelar. He conectado el SW al pin 2 y el eje X a A1.
Aquí está la iniciación del joystick:
// Arduino pin numbers const int SW_pin = 2; // digital pin connected to switch output const int X_pin = 1; // analog pin connected to X output //In setup: //joystick initiation pinMode(SW_pin, INPUT); digitalWrite(SW_pin, HIGH); //default value is 1
Lectura de los datos del Joystick y detección de comandos:
//In loop: //Joystick input to commands: if(digitalRead(SW_pin)==LOW){ //Fire bullet detected } if(analogRead(X_pin)>612){ //Go up command detected } if(analogRead(X_pin)<412){ //Go down command detected }
Incluyamos todas las bibliotecas e iniciemos todas las variables necesarias para el juego:
#include <Wire.h> #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x3F, 16, 2); #include <EEPROM.h> byte c1[8]={B10000,B10100,B01110,B10101,B01110,B10100,B10000,B00000}; //Starship byte c2[8]={B00100,B01000,B01010,B10100,B01010,B01000,B00100,B00000}; //Enemy byte c3[8]={B00000,B00000,B00000,B00110,B00110,B00000,B00000,B00000}; //Bullet String lcd_array[2][16] = {{"}"," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "}, {" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "}}; /* } - Starship > - Bullet < - Enemy */ const unsigned int MAX_MESSAGE_LENGTH = 12; int starship_possiton = 0; bool game_is_in_progress = false; unsigned long game_score = 0; unsigned long game_start = 0; bool bullet_is_in_progress = false; int bullet_possiton[2]; unsigned long bullet_last_move = 0; unsigned long bullet_speed = 100; bool enemies_array[5] = {false,false,false,false,false};//{false,true,true,true,true};// long randNumber; int enemies_possiton[5][2] = {{-1,-1},{-1,-1},{-1,-1},{-1,-1},{-1,-1}}; unsigned long enemies_last_move[5] = {0,0,0,0,0}; unsigned long enemies_overall_last_move = 0; unsigned long enemies_speed = 200; char message[MAX_MESSAGE_LENGTH] = ""; //w - UP, s - Down, f - Fire /* Commands: Up Down Fire */ // Arduino pin numbers const int SW_pin = 2; // digital pin connected to switch output const int X_pin = 1; // analog pin connected to X output
En la configuración, iniciaremos el monitor Serial, LCD, Joystick y estableceremos una pantalla de inicio del juego. Aquí hemos utilizado algunas de las variables iniciadas anteriormente.
void setup(){ Serial.begin(9600); lcd.begin(); //Creating custom characters in CG-RAM lcd.createChar(1 , c1); lcd.createChar(2 , c2); lcd.createChar(3 , c3); //initiate random randomSeed(analogRead(0)); //joystick initiation pinMode(SW_pin, INPUT); digitalWrite(SW_pin, HIGH); //default value is 1 //Starter screen of the game lcd.backlight(); lcd.clear(); lcd.setCursor(0,0); lcd.print(" Starship game"); lcd.setCursor(0,1); lcd.print(char(1)); lcd.print(" Press any key to start"); }
En el bucle, escucharemos el monitor serie para obtener un comando para subir (w), bajar (s) o disparar (f):
while (Serial.available() > 0){ static unsigned int message_pos = 0; //Read the next available byte in the serial receive buffer char inByte = Serial.read(); //Message coming in (check not terminating character) and guard for over message size if ( inByte != '\n' && (message_pos < MAX_MESSAGE_LENGTH - 1) ){ //Add the incoming byte to our message message[message_pos] = inByte; message_pos++; }else{//Full message received... //Add null character to string message[message_pos] = '\0'; //Print the message (or do other things) Serial.print("[["); Serial.print(message); Serial.println("]]"); print_array_to_serial(); //Reset for the next message message_pos = 0; } }
En cuanto se presione una de las teclas se iniciará el juego:
//Start game if (game_is_in_progress==false && (message[0] == 'w' || message[0] == 's' || message[0] == 'f')){ game_is_in_progress = true; game_start = millis(); }
Necesitamos actualizar la posición de la nave estelar a medida que obtenemos el comando Arriba o Abajo. A medida que obtengamos el comando de disparo, debemos asegurarnos de que la bala no esté ya en curso y, después de eso, se iniciará con la posición X 1 y la posición Y como la posición actual de la nave estelar.
//Processing input if(message[0] == 'w'){ // Up command starship_possiton = 0; }else if(message[0] == 's'){ // Down command starship_possiton = 1; }else if(message[0] == 'f' && bullet_is_in_progress == false){ //Fire command bullet_possiton[0] = starship_possiton; bullet_possiton[1] = 1; bullet_is_in_progress = true; bullet_last_move = millis(); }
Verificaremos si la suma de bullet_last_move y bullet_speed es igual o menor que millis(). Por eso, si desea que la bala sea más rápida, la variable bullet_speed debe reducirse. Moveremos la viñeta hasta el final de la pantalla y como su posición sobrepasará el tamaño de la pantalla, la viñeta se reiniciará.
if(bullet_is_in_progress && bullet_last_move+bullet_speed <= millis()){ if(bullet_possiton[1] != 15){ Serial.println("moving bullet"); bullet_last_move = millis(); bullet_possiton[1] = bullet_possiton[1]+1; }else if(bullet_possiton[1] == 15){ bullet_possiton[1] = -1; bullet_is_in_progress = false; } }
Tendremos un máximo de 5 enemigos a la vez. Como antes, debemos verificar si tenemos un enemigo inactivo para activarlo. Además, para tener un poco de espacio entre los enemigos, esperaremos el triple de la velocidad de los enemigos del último movimiento general de los enemigos. Generaremos un valor aleatorio de 0 a 6. Si el valor es cero o uno, el enemigo se iniciará con la posición Y correspondiente y la última celda (15) en la posición X.
//Enemies initiation if((enemies_array[0]==false || enemies_array[1]==false || enemies_array[2]==false || enemies_array[3]==false || enemies_array[4]==false) && enemies_overall_last_move+enemies_speed*3 <= millis() ){ // print a random number from 0 to 6 randNumber = random(0, 6); // Serial.print("randNumber: "); Serial.println(randNumber); if(randNumber==0 || randNumber==1){ // Serial.print("Enemies initiation: "); Serial.println(randNumber); for(int i=0; i<5; i++){ if(enemies_array[i]==false){ lcd_array[randNumber][15]="<"; enemies_array[i]=true; enemies_possiton[i][0] = randNumber; enemies_possiton[i][1] = 15; enemies_last_move[i] = millis(); enemies_overall_last_move = millis(); break; } } } }
Mover enemigos es bastante similar a mover la bala pero en dirección contraria:
//moving enemies for(int i=0; i<5; i++){ if(enemies_array[i]==true && enemies_last_move[i]+enemies_speed <= millis()){ enemies_possiton[i][1] = enemies_possiton[i][1] - 1; enemies_last_move[i] = millis(); } //if enemy passed through starship if(enemies_possiton[i][1]==-1){ enemies_array[i]=false; } }
Actualice lcd_array y verifique los enamoramientos.
Insertaremos nuestros elementos de juego en el lcd_array. Por defecto, todas las celdas estarán en blanco. Luego, dibujaremos la nave espacial, la bala y los enemigos. En el arreglo los elementos tienen los siguientes símbolos:
for(int i=0;i<2;i++){ for(int j=0;j<16;j++) if(game_is_in_progress){ lcd_array[i][j] = " ";//by default all cells are blank //drawing starship if(starship_possiton==i && j==0){ lcd_array[i][j] = "}"; } //drawing bullet if(bullet_is_in_progress == true && bullet_possiton[0] == i && bullet_possiton[1] == j){ lcd_array[i][j] = ">"; } //drawing enemies for(int k=0; k<5; k++){ if(enemies_array[k]==true && enemies_possiton[k][0] == i && enemies_possiton[k][1] == j){ lcd_array[i][j]="<"; } } } } }
A continuación, comprobaremos si hay flechazos:
for(int k=0; k<5; k++){ if(bullet_is_in_progress == true && bullet_possiton[0] == i && bullet_possiton[1] == j && ((enemies_array[k]==true && enemies_possiton[k][0] == i && enemies_possiton[k][1] == j) || (enemies_array[k]==true && enemies_possiton[k][0] == i && enemies_possiton[k][1] == j-1) ) ){ Serial.println("bullet enemy crush"); enemies_array[k] = false; enemies_possiton[k][0] = -1; enemies_possiton[k][1] = -1; bullet_is_in_progress = false; bullet_possiton[0] = -1; bullet_possiton[1] = -1; lcd_array[i][j]=" "; } } //starship enemy crush if(j==0 && starship_possiton==i){ for(int k=0; k<5; k++){ if(enemies_array[k]==true && enemies_possiton[k][0] == i && enemies_possiton[k][1] == j){ Serial.println("starship enemy crush"); //Game Over. Your score. High Score game_score = millis() - game_start; //need to reset all game values starship_possiton = 0; game_is_in_progress = false; bullet_is_in_progress = false; for(int z=0; z<5; z++){ enemies_array[z] = false; enemies_possiton[z][0] = -1; enemies_possiton[z][1] = -1; } enemies_speed = 200; message[MAX_MESSAGE_LENGTH] = ""; //w - UP, s - Down, f - Fire break; } } }
En el aplastamiento del enemigo bala, si verificamos solo si la bala y el enemigo están en la misma posición, podemos enfrentar un problema cuando el enemigo y la bala cambian su posición al mismo tiempo. Esto parecerá que el enemigo atravesó la bala:
https://www.youtube.com/watch?v=E5Qm3N1_h5o
Esto no será siempre, pero sí muy a menudo. Para evitarlo, también debemos comprobar si el enemigo se encuentra detrás de la bala. No interfiere con el juego de ninguna manera y resuelve perfectamente nuestro problema.
Después de que la nave estelar y el enemigo se aplasten, obtendremos la puntuación del juego restando los milis de inicio del juego de los milis actuales. Además, las variables del juego se restablecerán.
Después de la actualización de la matriz LCD, imprimiremos la matriz en la pantalla LCD. Los símbolos de enemigo, bala y enemigo serán reemplazados por nuestros personajes personalizados:
//Printing game to lcd for(int i=0;i<2;i++){ lcd.setCursor(0,i); for(int j=0;j<16;j++){ if(lcd_array[i][j] == "}"){ lcd.print(char(1)); }else if(lcd_array[i][j] == "<"){ lcd.print(char(2)); }else if(lcd_array[i][j] == ">"){ lcd.print(char(3)); }else{ lcd.print(lcd_array[i][j]); } } }
Después de aplastar al enemigo y la nave estelar, mostraremos la puntuación más alta (récord) y la puntuación del juego. Si el puntaje del juego es más que un puntaje alto, se actualizará. La próxima vez que se muestre la nueva puntuación más alta, incluso después de que Arduino se apague:
if(game_score!=0){ EEPROM.get(0, game_start); Serial.print("High score: "); Serial.println(game_start); Serial.print("Score: "); Serial.println(game_score); //Game over screen lcd.clear(); lcd.setCursor(0,0); lcd.print("Record: "); lcd.print(game_start); lcd.setCursor(0,1); lcd.print("Score: "); lcd.print(game_score); if(game_score > game_start){ EEPROM.put(0, game_score); } game_score = 0;//reset game score for next game }
Al final del bucle, tendremos un breve retraso y un comando de reinicio:
delay(50); message[0] = ' '; //reset command
La impresión del lcd_array al monitor serie se ha separado para funcionar y se puede mostrar por solicitud o constantemente:
void print_array_to_serial(){ //Printing game to Serial Monitor: Serial.println("lcd_array:"); for(int i=0;i<2;i++){ for(int j=0;j<16;j++){ Serial.print(lcd_array[i][j]); } Serial.println(""); } }
Y el control del juego por el joystick se agrega de esta manera fácil:
if(digitalRead(SW_pin)==LOW){ message[0] = 'f'; } if(analogRead(X_pin)>612){ message[0] = 'w'; } if(analogRead(X_pin)<412){ message[0] = 's'; }
Este juego te ayudará a practicar la creación de un juego en un nivel básico y además activará tu imaginación para crear juegos más complejos.
Para los fanáticos de Arduino y el desarrollo de juegos, esta es una buena base para pulir sus habilidades.
Lo mejor después de tanto trabajo es jugar el juego que tú mismo desarrollaste.
Espero que el proyecto te haya resultado interesante. A través de este proyecto, hemos aprendido y utilizado en la práctica la pantalla LCD y el joystick.
Aquí hay algunas ideas de mejora para el proyecto, que usted puede implementar: