paint-brush
Cómo construir un juego Arduino Starship controlado por joystick y computadorapor@chingiz
18,669 lecturas
18,669 lecturas

Cómo construir un juego Arduino Starship controlado por joystick y computadora

por Chingiz Nazar2022/04/01
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

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. En el juego, el jugador controla la nave estelar voladora y una puntuación alta se almacena en la EEPROM. Trataremos cada elemento por separado y cómo funcionarán todos juntos. El proyecto es interesante y abre oportunidades para aprender cosas nuevas: cómo crear y mostrar un personaje personalizado en la pantalla LCD. Cómo leer y escribir datos en EEPROM (memoria no volátil)

Company Mentioned

Mention Thumbnail
featured image - Cómo construir un juego Arduino Starship controlado por joystick y computadora
Chingiz Nazar HackerNoon profile picture

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 Página del proyecto 'Arduino_Starship_Game' . En el artículo, trataremos cada elemento por separado y cómo funcionarán todos juntos.


El proyecto es interesante y abre oportunidades para aprender cosas nuevas:


  • ¿Cómo funciona una pantalla LCD?
  • Cómo crear y mostrar un carácter personalizado en la pantalla LCD
  • Cómo leer datos del monitor serie
  • ¿Cómo funciona un joystick?
  • Cómo leer y escribir datos en EEPROM (memoria no volátil)


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.

el proceso del juego

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.

pantalla LCD

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.

Código

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.

Caracteres personalizados

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 sitio eso será útil con la creación de personajes personalizados LCD. Aquí puedes dibujar tu personaje personalizado y el código se generará automáticamente para él:


Creación de personajes personalizados LCD


Y así es como mostramos nuestros caracteres personalizados:

 lcd.print(char(0)); lcd.print(char(1)); 


Hello World con personajes personalizados

Palanca de mando

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  }

desarrollo de juegos

Iniciación

Incluyamos todas las bibliotecas e iniciemos todas las variables necesarias para el juego:

  • Traté de nombrar cada variable para que quedara claro para qué sirve.
  • Tres personajes personalizados: nave estelar, enemigo y bala.
  • Lcd array 2x16, que se ha utilizado para depurar fácilmente el juego.
  • game_score y game_start se utilizan para obtener la puntuación del juego.
  • y tenemos algunas variables relacionadas con la bala y los enemigos.


 #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

Configuración

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"); } 


Pantalla de inicio

Círculo

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();    }


Moviendo la bala

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;      }    }


Iniciación de enemigos

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:

  • } - nave estelar
    • bala
  • < - enemigo
 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:

  • bala enemigo aplastar
  • aplastamiento del enemigo de la nave estelar
          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    } 


Pantalla de fin de juego


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';  }

Conclusión

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:


  • Vive
  • Niveles de dificultad del juego
  • Una complicación del juego a medida que el jugador avanza
  • Crea un jefe enemigo que tendrá la capacidad de enviar enemigos y disparar balas.
  • Posibilidad de ingresar el nombre o el apodo si se ha superado la puntuación más alta. Que también se ingresará en la EEPROM.