Project overview:
The three of us collectively decided to make something similar to an arcade machine as it was a very exciting part of our childhoods. Therefore, the project is a collection of three games that are designed to be played using an Arduino controller. The games include Flappy Bird, a popular mobile game in which the player controls a bird and navigates through obstacles; Racing Game, in which the player controls a race car and avoids colliding with the randomly generated cars as the race car overtakes them; and Space Invaders, a classic arcade game in which the player controls a spaceship and fights against invading aliens.
Arduino to P5js communication:
int button = 2; int pot = A0; int button1 = 4; int button2 = 7; int button3 = 8; int lastPotValue; int lastbutton; long previousmill = 0; long timebutton = 500; void setup() { // put your setup code here, to run once: Serial.begin(9600); pinMode(button, INPUT_PULLUP); pinMode(button1, INPUT_PULLUP); pinMode(button2, INPUT_PULLUP); pinMode(button3, INPUT_PULLUP); pinMode(pot, INPUT); } int getpot(){ int potValue = analogRead(pot)/255 ; int temp; if(potValue == 2 || potValue == 1){ temp = 1; }else if(potValue == 3 || potValue == 4){ temp = 2; }else{ temp = 0; } return temp; } void loop() { int potValue = getpot(); int buttonState = !digitalRead(button); long currentmill = millis(); int buttonstate1 = !digitalRead(button1); int buttonstate2 = !digitalRead(button2); int buttonstate3 = !digitalRead(button3); int game = 0; if(buttonstate1 == 1){ game = 1; } if(buttonstate2 == 1){ game = 2; } if(buttonstate3 == 1){ game = 3; } Serial.println(String(buttonState) + "," + String(potValue) + "," + String(game)); if(buttonState == 1 && currentmill - previousmill >= timebutton){ previousmill = currentmill; lastbutton = buttonState; } }
- The user connects an Arduino board to their computer using a USB cable.
- The user writes and uploads a sketch (see above for the code) to the Arduino board that defines the behavior of the board and the data that it will send to the computer.
- The user opens a P5.js sketch in their web browser and includes the p5.webserial.js library in their code.
- The user adds event listeners to their P5.js sketch that will be called when the user connects or disconnects from the Arduino board, when the Arduino board is ready to be connected to, when there is an error communicating with the Arduino board, or when data is received from the Arduino board.
- The user calls the getPorts() method of the p5.WebSerial object to check for any available Arduino boards. If an Arduino board is available, the portavailable event listener is called, which can be used to open a connection to the Arduino board.
- Once the connection to the Arduino board is established, the user can send data to the Arduino board using the send() method of the p5.WebSerial object. The user can also receive data from the Arduino board using the data event listener, which is called whenever data is received from the Arduino board.
- The user can use the received data from the Arduino board to control the behavior and appearance of their P5.js sketch. The user can also send data from the P5.js sketch to the Arduino board to control the behavior of the Arduino board.
- When the user is finished using the Arduino board, they can close the connection to the board using the close() method of the p5.WebSerial object.
Flappy Bird:
Code:
function restart(){ menu = 0; bird = new Bird(WIDTH / 2, HEIGHT / 2, 30); pipes = new Pipes(60, 200, 130); SCORE = 0; SCROLL_SPEED = 4; lives = 5; } function getRndInteger(min, max) { // https://www.w3schools.com/js/js_random.asp return Math.floor(Math.random() * (max - min)) + min; } function StartGame(){ background(bg); fill("#7cfc00"); rect(0, HEIGHT - GROUND_HEIGHT, WIDTH, HEIGHT); bird.draw(); bird.update(); bird.checkDeath(pipes); pipes.update(); pipes.drawPipes(); fill(255); textSize(60); textAlign(CENTER); text(SCORE, WIDTH / 9, HEIGHT / 7); textSize(30); text("lives: "+lives,WIDTH - WIDTH / 4, HEIGHT / 9); textAlign(CORNER); } class Bird { constructor(x, y, size) { this.x = x; this.y = y; this.size = size; this.vely = 0; } draw() { //fill("#eaff00"); //circle(this.x+112.5, this.y-112.5, this.size); image(b,this.x-this.size, this.y-this.size,this.size, this.size); //image("cow.jpg",this.x, this,y); } update() { this.y += this.vely; this.vely = lerp(this.vely, GRAVITY, 0.05); this.y = Math.max(this.size / 2, Math.min(this.y, HEIGHT - GROUND_HEIGHT - this.size / 2)); } flap() { this.vely = -JUMP_HEIGHT; jump.play(); } checkDeath(pipes) { for (var pipe of pipes.pipes_list) { if (this.x + this.size / 2 > pipe.x && pipe.height && this.x - this.size / 2 < pipe.x + pipes.width) { if (this.y - this.size / 2 <= pipe.height || this.y + this.size / 2 >= pipe.height + pipes.gap) { // window.location.reload(); lives--; oof.play(); if(lives === 0){ // losing1.play(); // restart(); // break; window.location.reload(); } pipes.pipes_list.splice(pipes.pipes_list.indexOf(pipe),1); pipes.retq(); } } if (this.x - this.size / 2 > pipe.x + pipes.width && pipe.scored == false) { SCORE += 1; pipe.scored = true; } } } } class Pipes { constructor(width, frequency, gap) { this.width = width; this.frequency = frequency; this.gap = gap; this.pipes_list = [ { x: 500, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false }, { x: 500 + this.width + this.frequency, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false } ]; } update() { for (var pipe of this.pipes_list) { pipe.x -= SCROLL_SPEED; if (pipe.x + this.width <= 0) { pipe.x = WIDTH; pipe.height = getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap - this.gap); pipe.scored = false; } } } retq(){ this.pipes_list = [ { x: 500, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false }, { x: 500 + this.width + this.frequency, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false } ]; } drawPipes() { fill((0),(150),(0)); for (var pipe of this.pipes_list) { rect(pipe.x, 0, this.width, pipe.height); rect(pipe.x, HEIGHT - GROUND_HEIGHT, this.width, -HEIGHT + pipe.height + GROUND_HEIGHT + this.gap); } } }
The getRndInteger() function is a helper function that returns a random integer between two given values. This function is used to randomly generate the heights of the pipes in the game. The Bird and Pipes classes define the objects that appear in the game. The Bird class has a draw() method that is used to draw the bird on the screen, an update() method that is used to update the bird’s position and velocity, a flap() method that causes the bird to jump upwards, and a checkDeath() method that checks if the bird has collided with any of the pipes and ends the game if necessary. The Pipes class has an update() method that updates the positions of the pipes and a drawPipes() method that draws the pipes on the screen. Overall, the code defines a simple game in which the player controls a bird and must avoid colliding with pipes by jumping over them. The game keeps track of the player’s score and ends if the bird hits a pipe.
Racing Game:
The generateCars() function is used to randomly generate cars that appear on the screen and the displayCars() function is used to draw the cars on the screen. The displayScore() function is used to display the player’s current score on the screen. The potentiometer returns three readings: 0,1, and 2 based on the positioning. Based on the number being returned by the potentiometer – we handle the car movement.
Space Invaders:
// both games =================================== function score(){ textSize(32); fill(250); text("Score: "+currentScore,20,50); } function increaseD(){ if(currentScore === 1 + prevScore){ difficulty += 0.5; prevScore = currentScore; // console.log(difficulty); } return random(1,5)+difficulty; } //space invadors ====================================== function startPage(){ textSize(27); fill(250); text("Space invador",27,250); textSize(15); text("press enter to start",52,290); } function removeRocks(){ rocks.splice(0,rocks.length); rocksctr = 0; } function displaybullets(){ for(let i = 0; i < bullets.length; i++){ bullets[i].display(); if(bullets[i].y < 0){ bullets.splice(i,1); numBullets--; } } // console.log(numBullets); } function generaterocks(){ let rand = int(random(0, 100)); let rand2 = int(random(0, 100)); if(rand % 7 == 0){ if(rand2 % 3 == 0){ if(rand2 % 2 == 0 && rand % 2 == 0){ rocks[rocksctr] = new boulders(); rocks[rocksctr].display(); // console.log(rocksctr); rocksctr++; } } } } function displayrocks(){ for(let i = 0; i < rocks.length; i++){ rocks[i].display(); // console.log(">",rocks.length); let temp = false; for(let j = 0; j < bullets.length; j++){ if(bullets[j].didcollide(rocks[i])){ temp = true; bullets.splice(i,1); numBullets--; } } if(mainship.didcollide(rocks[i])){ rocks.splice(i,1); rocksctr--; gamestatus = "end"; bomb.play(); losing1.play(); }else if(rocks[i].y > height || temp){ rocks.splice(i,1); rocksctr--; } } } var timechecker = 0.5; function makebullet(x,y){ // console.log(x); bullets[numBullets] = new bulletClass(x,y); m0 = millis(); //time when laser created if(timechecker>0.3){ bullets[numBullets].display();} numBullets++; m = millis(); timechecker = m - m0; } //tried to disable continous shooting maybe look into it later // end space invadors ================================ // start racing car game functions===================== function startPage2(){ textSize(27); fill(250); text("Car racing",63,255); textSize(15); text("press enter to start",52,290); } function generateCars(){ let rand = int(random(0, 100)); let rand2 = int(random(0, 100)); if(rand % 7 == 0 && carrs.length < 4){ if(rand2 % 3 == 0){ if(rand2 % 2 == 0 && rand % 2 == 0){ carrs[carsctr] = new cars(); carrs[carsctr].display(); // console.log(carsctr); carsctr++; } } } } function displayCars(){ for(let i = 0; i < carrs.length; i++){ carrs[i].display(); // console.log(">",carrs.length); let temp = false; if(maincar.didcollide(carrs[i])){ checklanes(0,carrs[i]); carrs.splice(i,1); carsctr--; currentScore = 0; // bomb.play(); gamestatus = "end"; losing2.play(); // gamestatus = "end"; // bomb.play(); }else if(carrs[i].y > height || temp){ checklanes(0,carrs[i]); carrs.splice(i,1); carsctr--; currentScore++; cargoing.play(); } } } function checklanes(x,other){ if(x === 1){ if(lanes2[other.temp] === 1){ other.temp = int(random(0,4)); other.x = lanes[other.temp]; checklanes(1,other); }else{ lanes2[other.temp] = 1; } }else if(x === 0){ lanes2[other.temp] = 0; } } function removeCars(){ carrs.splice(0,carrs.length); carsctr = 0; }
Initialization:
we initialized a lot of variables that would be used by the serial and the three games. the pre load function was also used to prepare the necessary pictures and sounds as well as fonts. the we set up what was necessary in the set up function.
let whichgame = 0; let whichgameprev = 0; // space invadors let mainship; let bullets = []; let numBullets = 0; let arrRocks; let rocks = [] let lasersound; let rocksctr = 0; let difficulty = 0; // both games let currentScore = 0; //both games let prevScore = 0; //both games let gamestatus = "start"; // both games let rate = 0; // both games let widthh = 400; //racing game let arrCars; let carrs = []; let carsctr = 0; let lanes = [8,88,168,248,328]; let lanes2 = [0,0,0,0,0]; //flappy bird var menu = 0; var SCROLL_SPEED = 4; var SCORE = 0; let oof; let bruh; let music; var bird ; var pipes; var lives = 5; const GRAVITY = 8.81; const JUMP_HEIGHT = 6.0; const GROUND_HEIGHT = 20; const WIDTH = 600; const HEIGHT = 550; //-------------arduino---------------- // let x=0; // var c; let values = []; // variable to hold an instance of the p5.webserial library: const serial = new p5.WebSerial(); // HTML button object: let portButton; let inData; // for incoming serial data let outByte = 0; function preload() { main = loadImage('mainpicture.png'); //space invadar img = loadImage('Hs4QN2.gif'); startScreen = loadImage('startscreen.gif'); ship = loadImage('Untitled-1.png'); bullet = loadImage('bullet.png'); soundFormats('mp3', 'ogg'); lasersound = loadSound('lasersound.mp3'); bomb = loadSound('explo.mp3'); rock1 = loadImage('rock1.png'); rock2 = loadImage('rock2.png'); rock3 = loadImage('rock3.png'); gameoverpng = loadImage('gameover.png'); mainFont = loadFont('PressStart2P-vaV7.ttf'); alternateFont = loadFont('metal lord.otf'); losing2 = loadSound('losing2.wav'); arcade = loadSound('arcade.mp3'); sp = loadSound('space.wav'); //racing car game; imgg = loadImage('maincar.png'); car1 = loadImage('car1.png'); car2 = loadImage('car2.png'); car3 = loadImage('car3.png'); backgroundd = loadImage('background.png'); backgrounddd = loadImage('final.gif'); cargoing = loadSound('mixkit-fast-car-drive-by-1538.wav'); startscreen = loadImage('startscreen.png'); done = loadImage('gameovercar.png'); losing1 = loadSound('losing1.wav'); carpassing = loadSound('carpassing.wav'); extraedge = loadImage('extraedge.png'); //flappy bird music = loadSound("bgmusic.mp3"); bg = loadImage('bg11.png'); home = loadImage('homescreem.png'); b = loadImage('bird.png'); jump = loadSound('flap-1.mp3'); oof = loadSound('oof.mp3'); } function setup() { createCanvas(600, 550); arcade.play(); if (!navigator.serial) { alert("WebSerial is not supported in this browser. Try Chrome or MS Edge."); } // if serial is available, add connect/disconnect listeners: navigator.serial.addEventListener("connect", portConnect); navigator.serial.addEventListener("disconnect", portDisconnect); // check for any ports that are available: serial.getPorts(); // if there's no port chosen, choose one: serial.on("noport", makePortButton); // open whatever port is available: serial.on("portavailable", openPort); // handle serial errors: serial.on("requesterror", portError); // handle any incoming serial data: serial.on("data", serialEvent); serial.on("close", makePortButton); //space invadors mainship = new spaceship(); arrRocks = [rock1,rock2,rock3] ; textFont(mainFont); //racing maincar = new main_car(); arrCars = [car1,car2,car3] ; //flappy bird bird = new Bird(WIDTH / 2, HEIGHT / 2, 30); pipes = new Pipes(60, 200, 130); }
The draw function: this is where all the functions get called:
The draw()
function starts by clearing the background of the canvas with background(0)
, then it checks the value of whichgame
and renders the appropriate game. The code uses several other functions, such as controls()
, score()
, and startPage()
, to handle game mechanics and display game elements.
In the first if
statement, the code checks if whichgame
is equal to 0, and if so, it displays two images: main
and extraedge
and this is like the default screen. In the second if
statement, the code checks if whichgame
is equal to 1 and, if so, it displays the game for whichgame 1 which is space invadors. This game has several possible states (running, end, start) and the code uses if
statements to handle each state. The third and fourth if
statements do the same thing for games 2 and 3, car race and flappy bird respectively.
function draw() { //console.clear(); var y=int(values[0]); //button value 1 or 0 var x = int(values[1]); //potentiometer reading 0,1,2 var prewhichgame= int(values[2]); if(prewhichgame != whichgame && prewhichgame != 0 &&!isNaN(prewhichgame)){ whichgame = prewhichgame; } values.splice(0); background(0); if(whichgame != whichgameprev){ gamestatus = "start"; whichgameprev = whichgame; currentScore = 0; difficulty = 0; prevScore = 0; restart(); } if(whichgame == 0){ image(main,0,0); image(extraedge,400,0); }if(whichgame == 1){ arcade.pause(); if(gamestatus == "running"){ image(img, 0, 0); controls(x,y); mainship.display(); displaybullets(); generaterocks(); displayrocks(); score(); }else if(gamestatus == "end"){ image(gameoverpng, 0, 0); removeRocks(); currentScore = 0; difficulty = 0; prevScore = 0; controls2(x,y); }else if(gamestatus == "start"){ controls2(x,y); background(startScreen); startPage(); } image(extraedge,400,0); }else if(whichgame == 2){ arcade.pause(); if(gamestatus == "running"){ image(backgrounddd,0,0); maincar.display(); controls(x,y); generateCars(); displayCars(); score(); }else if(gamestatus == "end"){ image(done, 0, 0); controls2(x,y); removeCars(); currentScore = 0; difficulty = 0; prevScore = 0; }else if(gamestatus == "start"){ controls2(x,y); background(startScreen); startPage2(); // background(startscreen); } image(extraedge,400,0); }else if(whichgame == 3){ arcade.pause(); if(menu==0){ controls2(x,y); background(home); textSize(25); textFont() }else if(menu==1){ StartGame(); controls(x,y); } } }
the classes:
The boulders
class represents the falling boulders, and the bulletClass
class represents bullets that the player can shoot to destroy the boulders. The spaceship
class represents the player’s spaceship, and the cars
class represents the cars that the player must avoid. The main_car
class is a subclass of spaceship
, and it appears to have the same functionality as the spaceship
class.
The boulders
class has a display()
function that is used to draw the boulder on the screen, a move()
function that is used to update the boulder’s position, and a width()
function that is used to determine the width of the boulder. The bulletClass
class has a display()
function that is used to draw the bullet on the screen, a move()
function that is used to update the bullet’s position, and a didcollide(other)
function that is used to check if the bullet has collided with another object.
The spaceship
and main_car
classes have a display()
function that is used to draw the spaceship or car on the screen, a move()
function that is used to update the spaceship or car’s position, and a didcollide(other)
function that is used to check if the spaceship or car has collided with another object. The cars
class has the same functions as the boulders
class, but it is used to represent cars rather than boulders.
// space invador classes start class boulders{ constructor(){ this.x = random(0,widthh-50); this.y = -20; this.rocktype = int(random(0, 3)); this.rateFall = increaseD(); } display(){ image(arrRocks[this.rocktype],this.x,this.y); this.move(); } move(){ this.y += this.rateFall; } width(){ if(this.rocktype == 0){ return 71; }else if(this.rocktype == 1){ return 48; }else if(this.rocktype == 2){ return 91; } } } class bulletClass{ constructor(x,y){ this.x = x; this.y = y; lasersound.play(); this.collision = false; } display(){ image(bullet,this.x,this.y); this.move(); } move(){ this.y -= 7; } didcollide(other){ if ( (this.x <= (other.x + other.width())) && (this.x >= other.x)) { if ((this.y <= (other.y + other.width())) && (this.y >= other.y)){ // print("Collision"); currentScore++; return true; } } } } class spaceship{ constructor(){ this.x = 200; this.y = 450; this.display(); } display(){ imageMode(CENTER); image(ship,this.x,this.y); this.move(); this.checkboundries(); imageMode(CORNER); } move(){ this.x += rate; } checkboundries(){ if(this.x > widthh){ this.x = 0; }else if(this.x < 0){ this.x = widthh; } } didcollide(other){ if ( (this.x <= (other.x + other.width())) && (this.x >= other.x)) { if ((this.y <= (other.y + other.width())) && (this.y >= other.y)){ // print("Collision"); return true; } } } } //start racing car classes: ================================ class cars{ constructor(){ this.temp = int(random(0,5)); this.x = lanes[this.temp]; this.y = -20; this.cartype = int(random(0, 3)); this.rateFall = increaseD(); //checklanes(1,this); commenting this cuz you've used recursion its causing stack overflow theres no base case here } display(){ image(arrCars[this.cartype],this.x,this.y); this.move(); } move(){ this.y += this.rateFall; } width(){ return 70; } } class main_car{ constructor(){ this.x = 200; this.y = 450; this.display(); } display(){ imageMode(CENTER); image(imgg,this.x,this.y); this.move(); this.checkboundries(); imageMode(CORNER); } move(){ this.x += rate; } checkboundries(){ if(this.x > widthh){ this.x = 0; }else if(this.x < 0){ this.x = widthh; } } didcollide(other){ if ( (this.x <= (other.x + other.width())) && (this.x >= other.x)) { if ((this.y <= (90 + other.y + other.width())) && (this.y >= other.y)){ // print("Collision"); return true; } } } } //end racing car classes========================= //flappy bird classes================================== class Bird { constructor(x, y, size) { this.x = x; this.y = y; this.size = size; this.vely = 0; } draw() { //fill("#eaff00"); //circle(this.x+112.5, this.y-112.5, this.size); image(b,this.x-this.size, this.y-this.size,this.size, this.size); //image("cow.jpg",this.x, this,y); } update() { this.y += this.vely; this.vely = lerp(this.vely, GRAVITY, 0.05); this.y = Math.max(this.size / 2, Math.min(this.y, HEIGHT - GROUND_HEIGHT - this.size / 2)); } flap() { this.vely = -JUMP_HEIGHT; jump.play(); } checkDeath(pipes) { for (var pipe of pipes.pipes_list) { if (this.x + this.size / 2 > pipe.x && pipe.height && this.x - this.size / 2 < pipe.x + pipes.width) { if (this.y - this.size / 2 <= pipe.height || this.y + this.size / 2 >= pipe.height + pipes.gap) { // window.location.reload(); lives--; oof.play(); if(lives === 0){ // losing1.play(); // restart(); // break; window.location.reload(); } pipes.pipes_list.splice(pipes.pipes_list.indexOf(pipe),1); pipes.retq(); } } if (this.x - this.size / 2 > pipe.x + pipes.width && pipe.scored == false) { SCORE += 1; pipe.scored = true; } } } } class Pipes { constructor(width, frequency, gap) { this.width = width; this.frequency = frequency; this.gap = gap; this.pipes_list = [ { x: 500, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false }, { x: 500 + this.width + this.frequency, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false } ]; } update() { for (var pipe of this.pipes_list) { pipe.x -= SCROLL_SPEED; if (pipe.x + this.width <= 0) { pipe.x = WIDTH; pipe.height = getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap - this.gap); pipe.scored = false; } } } retq(){ this.pipes_list = [ { x: 500, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false }, { x: 500 + this.width + this.frequency, height: getRndInteger(this.gap, HEIGHT - GROUND_HEIGHT - this.gap), scored: false } ]; } drawPipes() { fill((0),(150),(0)); for (var pipe of this.pipes_list) { rect(pipe.x, 0, this.width, pipe.height); rect(pipe.x, HEIGHT - GROUND_HEIGHT, this.width, -HEIGHT + pipe.height + GROUND_HEIGHT + this.gap); } } } // end flappy bird classes =============================
Game Controls
Flappy Bird: Use the button on the arduino or the UP key to jump.
Racing Game: Use the potentiometer or left and right keys to control the car’s steering.
Space Invaders: Use the potentiometers or left and right keys to control the spaceship’s movement and button or UP key to fire lasers.
user testing:
Conclusion
This project demonstrates how to create and play games using p5.js and Arduino. The project includes three games that can be controlled using potentiometers and push buttons, and can be easily extended to include additional games and custom controller designs. We’re particularly proud of the aesthetics of the games – we were able to recreate what we initally had in mind. Furthermore, we had a lot of bugs which wouldn’t let the games run smoothly. We figured out how to implement the games smoothly by making changes in our algorithms and by handling specific types of errors which were mostly given by the arduino board. However, there is no proper restart function for flappy bird – if you lose, the canvas simply starts from scratch.
Work contributed: Car racing game, Space invaders, while helping in integrating the games together, and using my graphic design skills in making 1962 more user friendly. Also helped making the circuit necessary to send data.