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)