Arduino 2P Shooting Game

8-bit fun for two players

This will show you how to make your own two player Arduino based computer game which pits two players against each other in a 24-shot show-down.

The project builds on techniques learned from building the Wank-o-Meter, utilising the same 128×64 pixel OLED screen.

What you will need:

  • An Arduino and Arduino IDE to program.
  • 128×64 OLED (SSD1306)
  • 2 sets of five button keypads with analog outputs.
  • General hook up wires (m-m & m-f)
  • Time to squander to pointless tasks

What you will learn:

As always, not much. However, the code uses:

  • Arrays
  • Switch…Case program control
  • Do…While loops
  • More graphic controls for OLED

Hook up guide:

In short; wire the screen to the I2C pins of your Arduino (A4 for the SDA & A5 for the SCL) and connect the output of P1 keypad to A0 and P2 keypad to A1.

Before you Start:

Ensure that the I2C address of your OLED screen is the same as the main sketch. An I2C scanner can help with this. Typically for the SSD1306 the addres is 0x3C.

The outputs of the keypads will vary, so before uploading the main sketch you’ll need to determine the values of the keys.

To map the key values, connect the output of the keypad to A0 input on the arduino. Upload the sketch to the right and open the serial monitor. Take note of the values displayed when you press the various keys – these will be used in the main sketch later.

void setup() {

  Serial.begin(9600);
  pinMode(A0, INPUT);
  
}

void loop() {

  int i = (analogRead(A0)/10);
  Serial.println(i);
  delay(100);

}

With the values from the above you can use them next to the switch…case statements within the gamePlay() function.

Code:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128      // OLED display width, in pixels
#define SCREEN_HEIGHT 64      // OLED display height, in pixels
#define OLED_RESET     -1     // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C   // Check with i2c scanner

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

const int keypadP1 = A0;   
const int keypadP2 = A1; 

int xP1 = 6;
int yP1 = 41;
int xP2 = 121;
int yP2 = 41;

long timer = 0;
const int refreshRate = 25;    //50ms refresh rate (20hz) (approx 1.1s)
const int debounceTime = 25;
const int totalShots = 24;

long shotTimeP1 = 0;
long shotTimeP2 = 0;

int scoreP1 = 0;
int scoreP2 = 0;
int xShotP1[24];
int yShotP1[24];
int xShotP2[24];
int yShotP2[24];
int ammoP1;
int ammoP2;
int shotsP1 = 0;
int shotsP2 = 0;

bool outOfAmmoP1 = false;
bool outOfAmmoP2 = false;
bool newGameP1 = false;
bool newGameP2 = false;

//*************************************************************
void setup() {
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(WHITE);
  display.clearDisplay();
  display.display();
}
//*************************************************************
void loop() {
  playerReady();
  initialisePlaces();  
  countdown();
  gamePlay();
  endGame();
  
  delay(2000);
}
//*************************************************************
void playerReady() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(10,15);
  display.print("NEW GAME?");
  display.setTextSize(1);
  display.setCursor(26,30);
  display.print("Press any key");
  display.setCursor(31,41);
  display.print("to continue");
  
  int P1n = (analogRead(keypadP1) / 10);
  int P2n = (analogRead(keypadP2) / 10);

  if (P1n < 100) {      //if any key is pressed then flag raised
    newGameP1 = true;
  }

  if (newGameP1 == true) {    //display status if flag is raised
   display.setCursor(8,52);
   display.print("P1 READY");
   display.drawTriangle(0,52,4,54,0,56,WHITE);
    
  }

  if (P2n < 100) {        //if any key is pressed then flag raised
    newGameP2 = true;
  }

  if (newGameP2 == true) {      //display status if flag is raised
    display.setCursor(73,52);
    display.print("P2 READY");
    display.drawTriangle(127,52,123,54,127,56,WHITE);    
  }

  display.display();
  delay(100);

  if (newGameP1 && newGameP2 == true) {   //if both players want new game then reset all global variables
    xP1 = 6;
    yP1 = 41;
    xP2 = 121;
    yP2 = 41;
    timer = millis();
    shotTimeP1 = 0;
    shotTimeP2 = 0;
    shotsP1 = 0;
    shotsP2 = 0;
    outOfAmmoP1 = false;
    outOfAmmoP2 = false;
    newGameP1 = false;
    newGameP2 = false;
    scoreP1 = 0;
    scoreP2 = 0;
    delay(500);
  } else {
    playerReady();                        //loop back to see if players want to play again
  }
  
}
//**************************************************
void countdown() {
  display.clearDisplay();
  display.display();
  display.setTextSize(4);
  for (int cd = 3; cd > 0; cd--){
    display.setCursor(53,16);
    display.print(cd);
    display.display();
    delay(1000);
    display.fillRect(50,10,100,53,BLACK);
  }
  display.setTextSize(2);
}
//************************************************************
void gamePlay() {

  long currentTime = millis();

  do {
    display.clearDisplay();

    drawBackground();
    displayScore();

    int P1 = (analogRead(keypadP1) / 10);
    int P2 = (analogRead(keypadP2) / 10);

    
  
    switch (P1) {
     case 14:    // up
       if (yP1 > 21) {
            if (millis() > shotTimeP1 + debounceTime) {
                shotTimeP1 = millis();
                yP1 -= 5; 
              }
           }      
       break;
       
      case 30:    //down
        if (yP1 < 58) {
            if (millis() > shotTimeP1 + debounceTime) {
                shotTimeP1 = millis();
                yP1 += 5;
              }
           }   
        break;

      case 0:      //left
        if (xP1 >= 11) {
          if (millis() > shotTimeP1 + debounceTime) {
            shotTimeP1 = millis();
            xP1 -= 5;
          }
        }
        break;

        case 49:    //right
          if (xP1 <= 116) {
             if (millis() > shotTimeP1 + debounceTime) {
                shotTimeP1 = millis();
                xP1 += 5;
             }
          }
          break;
          
      case 78:    //action
        if (millis() < shotTimeP1 + 50) {  
          break;
        }
        if (shotsP1 == totalShots) {    //ammo check
          break;
        }
        shotTimeP1 = millis();
        yShotP1[shotsP1] = yP1;   //set co ordinates of new shot
        xShotP1[shotsP1] = xP1 + 5;    
        shotsP1++;
        break;
      default:    //no button
        break;     
    }

    collisionCheck();

    switch (P2) {
      case 14:    // up
        if (yP2 > 21) {
            if (millis() > shotTimeP2 + debounceTime) {     
                shotTimeP2 = millis();
                yP2 -= 5; 
            }
        }
        break;
      case 30:    //down
        if (yP2 < 58) {
            if (millis() > shotTimeP2 + debounceTime) {
                shotTimeP2 = millis();
                yP2 += 5;
            }
        }
        break;

      case 0:      //left
        if (xP2 >= 11) {
          if (millis() > shotTimeP2 + debounceTime) {
            shotTimeP1 = millis();
            xP2 -= 5;
          }
        }
        break;

        case 49:    //right
          if (xP2 <= 116) {
             if (millis() > shotTimeP2 + debounceTime) {
                shotTimeP2 = millis();
                xP2 += 5;
             }
          }
          break;
        
      case 78:    //action
      
        if (millis() < shotTimeP2 + 50){
          break;
        }
        if (shotsP2 == totalShots) {      //ammo check
          break;
        }
        shotTimeP2 = millis();
        yShotP2[shotsP2] = yP2;   //set co ordinates of new shot
        xShotP2[shotsP2] = xP2 - 5;
        shotsP2++;
        break;
      default:    //no button
        break;     
    }  

    collisionCheck();

    checkHitP1(shotsP2);    //pass number of shots taken
    checkHitP2(shotsP1);    //pass number of shots taken
    currentTime = millis();

    if (currentTime > (timer + refreshRate)){     //advance shots if timer is above refresh rate
      timer = millis();

      for (int p1a = 0; p1a < shotsP1; p1a++){    //p1 shots go forwards
        xShotP1[p1a] += 5;
      }

      for (int p2a = 0; p2a < shotsP2; p2a++){    //p2 shots go backwards
        xShotP2[p2a] -= 5;
      }
    } 

    plotPlayers();
    plotShots();
    display.display();
    
  } while (outOfAmmoP1 == false || outOfAmmoP2 == false);

  display.fillRect(51,0,12,18,BLACK);
  display.fillRect(64,0,12,18,BLACK);
  displayScore();
  display.display();
  
}
//**************************************************
void initialisePlaces() {
  for (int iP1 = 0; iP1 < 8; iP1++) {   //top row
    int kiP1 = iP1 * 6;
    xShotP1[iP1] = (2 + kiP1);
    yShotP1[iP1] = 2;
  }

  for (int iP1 = 8; iP1 < 16; iP1++) {    //middle row
    int kiP1 = (iP1 - 8) * 6;
    xShotP1[iP1] = (2 + kiP1);
    yShotP1[iP1] = 8;
  }

  for (int iP1 = 16; iP1 < 24; iP1++) {   //bottom row
    int kiP1 = (iP1 - 16) * 6;
    xShotP1[iP1] = (2 + kiP1);
    yShotP1[iP1] = 14;
  }

  for (int iP2 = 0; iP2 < 8; iP2++) {   //top row
    int kiP2 = iP2 * 6;
    xShotP2[iP2] = (81 + kiP2);
    yShotP2[iP2] = 2;
  }

  for (int iP2 = 8; iP2 < 16; iP2++) {    //middle row
    int kiP2 = (iP2 - 8) * 6;
    xShotP2[iP2] = (81 + kiP2);
    yShotP2[iP2] = 8;
  }

  for (int iP2 = 16; iP2 < 24; iP2++) {    //top row
    int kiP2 = (iP2 - 16) * 6;
    xShotP2[iP2] = (81 + kiP2);
    yShotP2[iP2] = 14;
  }    
}
//*******************************************************
void drawBackground() {
  display.drawLine(0,18,127,18,WHITE);    //long horizontal
  display.drawLine(63,0,63,18,WHITE);     //centre vertical
  display.drawLine(50,0,50,18,WHITE);     //left vertical
  display.drawLine(76,0,76,18,WHITE);     //right vertical  
  display.fillRect(0,19,3,44,WHITE);      //left dead area
  display.fillRect(125,19,3,44,WHITE);      //right dead area 
}
//**********************************************************
void plotPlayers() {
  display.drawTriangle(xP1-2,yP1-2,xP1+2,yP1,xP1-2,yP1+2,WHITE);
  display.drawTriangle(xP2+2,yP2-2,xP2-2,yP2,xP2+2,yP2+2,WHITE);
}
//**********************************************************
void plotShots() {
  for (int pS1 = 0; pS1 < totalShots; pS1++) { 
    display.drawCircle(xShotP1[pS1],yShotP1[pS1],2,WHITE);
  }

  if (xShotP1[totalShots-1] > 127) {    //if last shot off screen
    outOfAmmoP1 = true; 
  }

  for (int pS2 = 0; pS2 < totalShots; pS2++) {
    display.drawCircle(xShotP2[pS2],yShotP2[pS2],2,WHITE);
  }

  if (xShotP2[totalShots-1] < 0) {      //if last shot off screen
    outOfAmmoP2 = true;
  }
}
//**********************************************************
void checkHitP1(int firedP2) {         
  for (int chP1 = 0; chP1 < firedP2; chP1++) {    //only iterate through shots that have been fired   
    if (xShotP2[chP1] == xP1) {       //checks x coordinate against player position 
      if (yShotP2[chP1] == yP1) {       //checks y coordinate against player position
          display.invertDisplay(true);    //flash screen
          delay(50);
          display.invertDisplay(false);
          scoreP2++;
          xShotP2[chP1] = 0;
      }
    }
  }
}
//***********************************************************
void checkHitP2(int firedP1) {     //only iterate through shots that have been fired
  for (int chP2 = 0; chP2 < firedP1; chP2++) { 
    if (xShotP1[chP2] == xP2) {       //checks x coordinate against player position    
      if (yShotP1[chP2] == yP2) {       //checks y coordinate against player position
          display.invertDisplay(true);     //flash screen
          delay(50);
          display.invertDisplay(false);
          scoreP1++;
          xShotP1[chP2] = 127;
      }
    }
  }
}
//**********************************************************
void endGame() {
  display.setTextSize(2);
  display.setCursor(10,20);
  display.print("GAME OVER");
  display.setCursor(22,40);
  
  if (scoreP1 > scoreP2){
    display.print("P1 WINS");
  } else if (scoreP1 < scoreP2) {
    display.print("P2 WINS");
  } else if (scoreP1 == scoreP2){
    display.setCursor(40,40);
    display.print("DRAW");
  }

  display.display();
  delay(5000);
}
//***********************************************************
void displayScore() {
  display.setTextSize(2);

  if (scoreP1 > 9 || scoreP2 > 9){    //reduce text size if score goes to double digits
    display.setTextSize(1);
  }

  if (scoreP1 < 0 || scoreP2 < 0){    //reduce text size if score goes in to minus numbers
    display.setTextSize(1);
  }

  display.setCursor(52,2);
  display.print(scoreP1);
  display.setCursor(65,2);
  display.print(scoreP2); 
}
//***********************************************************
void collisionCheck() {         
      if ((xP1 == xP2) && (yP1 == yP2)) {   //collision check
          display.invertDisplay(true);    //flash screen
          delay(50);
          display.invertDisplay(false);
          //scoreP1--;                   //uncomment to enable penalty points for collisions
          //scoreP2--;
          xP1 = 6;
          xP2 = 121;
          delay(100); 
    }
}
//***********************************************************

Enjoy.

If you have any questions or spot any bugs then submit them via the Contact page.

Footnote: Ducks.

If you’re reading this then you’re in a very obscure corner of the internet. Welcome.

Ducks always look calm and composed as they serenely float down the river. What you cannot see is them frantically flapping away under the surface of the water.

Just like a duck, what I’ve shown above is how to build a working game. What you haven’t seen is hundreds of ways not to do it and all the strange bugs that have been ironed out along the way.

In the interests of openness, I’ve included some scans below from my notepad to show that the process is far from calm and collected.

In this page you can see:

  • Initial screen layout & design.
  • Failed attempts at using a heart symbol for a life-count.
  • Raw figures from the keypad.
  • Snippets of code for creating various elements and how they are mapped.

The above is a custom 128×64 grid made from 2mm squares to enable accurate mapping of the various game entities.

The graph paper was downloaded for free from: Free Online Graph Paper / Asymmetric and Specialty Grid Paper PDFs (incompetech.com)