How to Build an Arduino Starship Game Controlled by Joystick and Computer

Written by chingiz | Published 2022/04/01
Tech Story Tags: future-of-gaming | megafansesports | game-development | arduino | arduino-tutorial | arduino-basics | lcd | hackernoon-top-story

TLDRIn this article, we will develop an Arduino Starship game which will be displayed on an LCD display 16x2. The game will be controlled by a joystick and by a computer through the Serial Monitor. In the game, the player controls the flying starship and a high score is stored in EEPROM. We will deal with each element separately and how it will all work together. The project is interesting and opens up opportunities to learn new: how to make and display a custom character on the LCD display. How to read and write data to EEPRom (non-volatile memory)via the TL;DR App

In this article, we will develop an Arduino Starship game which will be displayed on LCD display 16x2. The game will be controlled by a joystick and by a computer through the Serial Monitor. In addition, we will store a high score in EEPROM and update them when the record is broken.

The project code is published on my GitHub ‘Arduino_Starship_Game’ project page. In the article, we will deal with each element separately and how it will all work together.

The project is interesting and opens up opportunities to learn new:

  • How does an LCD display work
  • How to make and display a custom character on the LCD display
  • How to read data from the Serial Monitor
  • How does a joystick work
  • How to read and write data to EEPROM (non-volatile memory)

In the game, the player controls the starship. In front of the flying starship will be some enemies. As there will be a collision of the starship with one of the enemies, the game will be over. Starship has a bullet to fire but with a limit. The limit for the bullet is only one bullet at a time. It means, that while we can see the bullet on the screen, no more bullets can be fired, we should wait until it collides with one of the enemies or disappears from the screen.

Video of the gameplay:

https://www.youtube.com/watch?v=HW6j_PRgFx4

First, we will go through each element individually. Then we will figure out how all this will interact to make the game run. If you know how an element works, you can skip to the next section.

LCD display

Let's start with the LCD display. In the project, I have been using the popular LCD Display 16x2 which can be found almost in every Arduino kit. In my case the display comes with an I2C LCD Adapter and the connection will be GND-GND, VCC-5V, SDA-A4, and SCL-A5.

Code

As always, first of all, we need to include libraries:

#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x3F, 16, 2);

In the LiquidCrystal_I2C lcd(0x3F, 16, 2) function we define the address of our I2C LCD Adapter. And Yes, it means, we can connect many I2C elements to Arduino. The address by default is 0x3F or 0x27. The next two elements are the size of our display.

Here is how we initiate and display text:

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() - initiates the lcd. lcd.backlight() - turns on LCD backlight. lcd.clear() -  clears the display lcd.setCursor(0,0) - sets cursor to the written position. Please note that the first digit is X axis and the second digit is Y axis. lcd.print("Hello World!") - prints the written text to the LCD.

Custom characters

Each display digit consists of 5x8 pixels. To create a custom character as a spaceship, we need to define and initiate it:

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

As you can see the custom character creation is done by using a byte with a length of five which stands for one line and we have eight of them to have one custom digit.

I found a site that will be helpful with LCD custom character creation. Here you can draw your custom character and the code will be automatically generated for it:

And here is how we display our custom character(s):

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

Joystick

The joystick has a button, X and Y-axis. The button works as usual. The X and Y axis can be thought of as a potentiometer, which provides data between 0 and 1023. A default value is half of that. We will use only the X-axis to control the starship. I have connected the SW to pin 2 and X-axis to A1.

Here is the initiation of the 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

Reading the Joystick data and detecting commands:

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

Game development

Initiation

Let’s include all libraries and initiate all required variables for the game:

  • I tried to name each variable so that it was clear what it is for.
  • Three custom characters: starship, enemy, and bullet.
  • Lcd array 2x16, which has been used to easily debug the game.
  • game_score and game_start are used to get the score of the game.
  • and we have some variables related to the bullet and enemies.

#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

Setup

In the setup, we will initiate the Serial monitor, LCD, Joystick and set a starter game screen. Here we have used some of earlier initiated variables.

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

Loop

In the loop we will listen to the serial monitor to get a command to go up (w), down (s), or fire (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;
  }
}

As one of the keys will be pressed the game will be started:

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

We need to update the position of the starship as we get the Up or Down command. As we get fire command, we need to make sure if the bullet is not already in progress and after that, it will be initiated with X position 1 and Y position as the current starship position.

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

Moving the bullet

We will check if the add-up of bullet_last_move and bullet_speed is equal to or less than millis(). Because of that, if you want to make the bullet faster the bullet_speed variable needs to be decreased. We will move the bullet till the end of the screen and as its position will go over the screen size, the bullet will be reset.

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

Enemies initiation

We will have a maximum of 5 enemies at a time. As earlier, we need to check if we have an inactive enemy to activate it. Also, to have a bit of space between enemies, we will wait for triple of enemies speed from enemies overall last move. We will generate a random value from 0 to 6. If the value is zero or one, the enemy will be initiated with the corresponding Y position and the last cell (15) in the X position.

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

Moving enemies is pretty similar to moving the bullet but in the reverse direction:

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

Update lcd_array and check crushes.

We will insert our game elements to the lcd_array. By default, all cells will be blank. Then, we will draw the starship, bullet, and enemies. In the array the elements have the following symbols:

  • } - starship
    • bullet
  • < - enemy
    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]="<";
            }
          }
        }
      }
    }

Next one, we will check for crushes:

  • bullet enemy crush
  • starship enemy crush
          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;
              } 
            }
          }

In the bullet enemy crush, if we check only if the bullet and enemy are in the same position, we can face an issue when the enemy and bullet change their position at the same time. This will look like the enemy passed through the bullet:

https://www.youtube.com/watch?v=E5Qm3N1_h5o

This will be not every time but quite often. To prevent it, we also need to check if the enemy is located behind the bullet. It does not interfere with the game in any way and perfectly solves our problem.

After the starship and the enemy crush, we will get the game score by subtracting the game start millis from the current millis. Also, game variables will be reset.

After the lcd array update, we will print the array to the LCD. The enemy, bullet, and enemy symbols will be replaced by our custom characters:

//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]);
        }   
      }
    }

After the crush of the enemy and starship, we will display the high score (record) and the score of the game. If the game score is more than a high score, it will be updated. Next time the new high score will be displayed, even after the Arduino power off:

    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
    }

At the end of the loop we will have a short delay and reset command:

  delay(50);
  message[0] = ' '; //reset command

The printing of the lcd_array to the serial monitor has been separated to function and can be displayed by request or constantly:

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

And the game control by the joystick is added in this easy way:

  if(digitalRead(SW_pin)==LOW){
    message[0] = 'f';
  }
  if(analogRead(X_pin)>612){
    message[0] = 'w';
  }
  if(analogRead(X_pin)<412){
    message[0] = 's';
  }

Conclusion

This game will help you practice creating a game at a basic level and you will further turn on your imagination to create more complex games.

For fans of Arduino and game development, this is a good base to polish your skills.

The best thing after such a lot of work is to play the game that was developed by you.

Hope the project was interesting for you. Through this project, we have learned about and used in practice the LCD display and the joystick.

Here are some enhancement ideas for the project, which can be implemented by you:

  • Lives
  • Game difficulty levels
  • A complication of the game as the player progresses
  • Make a Boss enemy which will have the ability to send enemies and fire bullets.
  • Ability to enter name or nick if the high score has been broken. Which will also be entered into the EEPROM.


Written by chingiz | Love Data, Robotics and IoT
Published by HackerNoon on 2022/04/01