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
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
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.