Main Concept
For the final project, I created a California themed car game. The physical hardware consists of a box with an arduino installed inside along with an ultrasonic sensor. There is a long road panel in front of it where the player places their NYU card keys and slides it along the road lanes to mimic the movement of a car on the road. The ultrasonic sensor detects the motion of the card along the lanes and causes the actual car in p5.js to move as well. On the screen, I decided to design the UI on a californian theme so I went with pictures and elements which depicted that concept. When the game begins, there are cars approaching the player from each lane and the player needs to move their key on the physical road to change the lane of the car on screen. With time the car speed increases and becomes more difficult. As it is a high score based game, every incoming car that is avoided is counted as one point so the ending page shows the high score and individual player score as well. As an indication of when the game has started, the green LED lights up on the box labeled “gas station” and similarly when the car crashes, red LED lights up or yellow goes off when the player is reading instructions, almost to imitate real life traffic lights.
P5.js
In the p5.js code, I have created a game using different classes. There is the player class which deals with the movement of the player by receiving values from arduino and then mapping it according to the canvas and player position. Then there is a class for the incoming cars, which deals with the downward motion of the cars and is also passed as an argument to the player class to check if the x or y coordinates of the player coincide with those of the incoming cars, in which case the game ends as the cars crash. I created an array for the incoming cars so that they are added at a certain frameRate count and are removed from the array as soon as they go out of the canvas.
//variable declarations let h = 600; let w = 400; let count = 0; let pos = [55, 165, 270]; let charsize = 80 ; //defines size of player let charheight = 100 let player; //variable for the player class object let newcarssize = 100; let newcarswidth = 80; let newcars = []; let score=0; let highscore=0; let bonusarr = []; //array to store bonus objects let caught = false; let bonuscaught = []; // let scorebool = false; let newchar = false; //boolean variable to help in creating new character when game starts again let foodnum = 0; //to iterate over food array let speedincr = 1; //for the vertical motion of newcarss let carimg = 0; let carspeeds = 2; let r1=0; let r2 =-600; let r3 = 0; let food = []; //array for food images let gamestate = 'start'; //gamestate for changes in display framewidth = 20; let a = 150; let b = 300; let c= 450; let d = 550; //image variables var gif_loadImg, gif_createImg; let pixelfont; let startimg; let startbutton; let instructbutton; let instructpage; let endimg; let road1, road2, road3; let car1, car2, car3, car4, car5; let cars = []; let endsound, crash, backsound; let tree; //arduino variables let rVal = 0; //preload function for all the images and sound function preload() { startimg = loadImage('2.png'); startbutton = loadImage('startbutton.png'); instructpage = loadImage('3.png'); instructbutton = loadImage('instruct.png'); endimg = loadImage('1.png'); road1 = loadImage('road.png'); road2 = loadImage('road.png'); road3 = loadImage('road.png'); car1 = loadImage('car1.png'); car2 = loadImage('car2.png'); car3 = loadImage('car3.png'); car4 = loadImage('car4.png'); car5 = loadImage('car5.png'); tree = loadImage('tree.png'); endsound = loadSound('endsound.mp3'); crash = loadSound('crash.mp3'); backsound = loadSound('background.mp3'); } //==================================================== //class for the player object class Character{ constructor(){ this.x = 5+ width/2-(charsize/2); this.y = height - ((charsize)); this.speed = 4; //speed for the movement of player horizontally } display(){ image(car2, this.x, this.y-10, charsize, charheight ); } moveleft(){ //if left key is pressed, causes player to move left this.x -= this.speed; if (this.x < 0) { this.x = 0; //so player does not go beyond the canvas } } moveright(){ //if right key is pressed, causes player to move left this.x+=this.speed; if (this.x+charsize > width) { this.x = width-charsize; //so player does not go beyond the canvas } } update(newcars){ if(rVal < 7){ this.x = 55; } else if(rVal>=7 && rVal<14 ){ this.x = 165; } else if(rVal >= 14){ this.x = 270; } if(this.x>=newcars.x && this.x <= newcars.x+(newcars.pwidth) && this.y>=newcars.y && this.y<=newcars.y+ newcars.pheight){ count++; console.log("Loss:" , count); crash.play(); gamestate = 'over'; } else if(this.x+charsize >=newcars.x && this.x+charsize <= newcars.x+(newcars.pwidth) && this.y>=newcars.y && this.y<=newcars.y+ newcars.pheight){ count++; console.log("Loss:" , count); crash.play(); gamestate = 'over'; } else if(this.x>=newcars.x && this.x <= newcars.x+(newcars.pwidth) && this.y+charheight>=newcars.y && this.y+charheight<=newcars.y+newcars.pheight){ count++; crash.play(); console.log("Loss:" , count); gamestate = 'over'; } } } //==================================================== //class to create newcarss class incars{ constructor(carimg, carspeeds){ this.x = random(pos); this.y = 0; this.pwidth = newcarswidth; this.pheight = newcarssize; this.speed = carspeeds; this.num = carimg } displaynewcars(){ image(cars[this.num], this.x, this.y , this.pwidth, this.pheight); } //causes newcars to move upwards update(){ this.y += this.speed; } } function setup() { createCanvas(w,h); background(220); player = new Character(); cars = [car1, car2, car3, car4, car5]; } function draw() { if(frameCount == 1){ backsound.play(); } background(220); if (!serialActive) { text("Press Space Bar to select Serial Port", 20, 30); } else { } if(gamestate == 'start'){ //if game is at start page then display homepage startpage(); } //if gamestate is of 'game' then plays the game through thegame() function else if(gamestate == 'game'){ image(road1, 0,r1, width, height, 0, 0, road1.width, road1.height); //homepage image displayed image(road1, 0,r2, width, height, 0, 0, road1.width, road1.height); r1+=2; r2+=2; if(r1 >= height){ r1 = r2- height ; } if(r2>= height){ r2 = r1-height ; } thegame(); //plays game through this function a++; b++; c++; d++; image(tree, -75,a, 150, 150, 0, 0, tree.width, tree.height); image(tree, -75,b, 150, 150, 0, 0, tree.width, tree.height); image(tree, width-60,c, 150, 150, 0, 0, tree.width, tree.height); image(tree, width-60,d, 150, 150, 0, 0, tree.width, tree.height); if(a > height+150){ a = -150; } else if(b> height+150){ b = -150; } else if(c> height+150){ c = -150; } else if(d> height+150){ d = -150; } } else if(gamestate == 'over'){ //if gamestate is over then end page displayed image(endimg, 0, 0, width, height, 0, 0, endimg.width, endimg.height); endpage(); } else if(gamestate == 'instructions'){ //displays instructions instructionspage(); } } //======================== function thegame(){ //checks if game is started again and creates a new player to play the game again if(newchar == true){ player = new Character(); newchar = false; } //for keys to move the player if(keyIsDown(LEFT_ARROW)){ player.moveleft(); } else if (keyIsDown(RIGHT_ARROW)){ player.moveright(); } //for new newcars and elements to be created and added to their arrays if(frameCount%150 == 0){ newcars.push(new incars(carimg, carspeeds)); score++; console.log(score); carimg++; if(carimg>4){ carimg = 0; } } //FOR LOOP for(let i = 0 ; i<newcars.length ; i++){ //shows newcarss and updates their positions by decreasing y coordinated to move them up newcars[i].displaynewcars(); newcars[i].update(); //when newcars and food goes beyond the roof, they are removed from their arrays if (newcars[i].y > height+400 ){ newcars.splice(i, 1); // bonusarr.splice(i, 1); // bonuscaught.splice(i,1); } //Player's position checked against newcars and ladder's position player.update(newcars[i]); newcars[i].update();//moves it up } player.display(); //END of FOR } //============================== function startpage(){ //function to display the images of starting page image(startimg, 0, 0, width, height, 0, 0, startimg.width, startimg.height); imageMode(CENTER); image(startbutton, width/2, height/2, map(startbutton.width, 0, startbutton.width, 0, width/4), map(startbutton.height, 0, startbutton.height, 0, height/6)); imageMode(CORNER); //checks if mouse pressed on the button and directs to instructions if(mouseX < width/2 + (width/(width/100)/2) && mouseX > width/2 - (width/(width/100)/2) && mouseY > height/2 - (height/(height/100)/2) && mouseY < height/2 + (height/(height/100)/2) && mouseIsPressed){ gamestate = 'instructions'; // success.play(); } } function endpage(){ //function to display score and highscore and 'game over' page for(let i = 0 ; i<newcars.length ; i++){ newcars.pop(); bonusarr.pop(); bonuscaught.pop(); } newchar = true; if(score>highscore){ //updates the highscore with each previous score highscore = score; } textAlign(CENTER); textFont('Times New Roman'); fill("#FAF8F4"); textSize(0.05*width); text('Score: '+score , width/2, (height/2)-58); text('High Score: '+highscore , width/2, ((height/2))-40); if(keyIsDown(ENTER)){ //when ENTER is pressed, gamestate changed so that game starts again gamestate = 'start'; score= 0 ; } } function instructionspage(){ //displays instruction images image(instructpage, 0, 0, width, height, 0, 0, instructpage.width, instructpage.height); image(instructbutton, (2.3*width)/4, (3.3*height)/3.7, map(instructbutton.width, 0, instructbutton.width, 0, 150), map(instructbutton.height, 0, instructbutton.height, 0, 50)); //checks if mouse clicked on the button and continues to game state of playing the game if(mouseX>(2.3*width)/4 && mouseX < width && mouseY > (3.3*height)/3.7 && mouseY < height && mouseIsPressed){ gamestate = 'game'; //success.play(); } } //==================== //ARDUINO function keyPressed() { if (key == " ") { // important to have in order to start the serial connection!! setUpSerial(); } } function mouseIsPressed() { readSerial(); } // This function will be called by the web-serial library // with each new line of data. The serial library reads // the data until the newline and then gives it to us through // this callback function function readSerial(data) { //////////////////////////////////// //READ FROM ARDUINO HERE //////////////////////////////////// if (data != null) { rVal = data; } //============= let sendToArduino=0; if(gamestate == 'game'){ sendToArduino = 1; } else if(gamestate == 'instructions' || gamestate == 'start'){ sendToArduino = 2; } else if(gamestate == 'over'){ sendToArduino = 3; } sendToArduino = sendToArduino+"\n"; writeSerial(sendToArduino); }
Arduino
In arduino, the main component is the ultrasonic sensor and then the LEDs. I have set the trig and echo pins as well as the LED pins initially. The connection between arduino and p5.js is in sending data from the ultrasonic sensor when the card is moved in front of it. The data is read by p5.js and is then used to control the x-coordinate of the player car. The p5.js sends the data regarding the game states which controls the LED pins. The data is sent as an integer, each indicating the game state, and is then used to control the respective LEDs. I soldered the LEDs so I could fix it on top of the box.
I was facing an issue with consistent readings from the ultrasonic sensor. As it sends out signals in a more spread out manner and reads very minor changes, the car was glitching because slightest tilt of the card was also causing the ultrasonic sensor to change values. I had to add a bit more delay to make sure the card movement has stabilized before the sensor reads its distance.
const int trigPin = 9; const int echoPin = 10; const int ledgreen = 2; const int ledyellow= 4; const int ledred = 7; // defines variables long duration; int distance; void setup() { pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output pinMode(echoPin, INPUT); // Sets the echoPin as an Input Serial.begin(9600); // Starts the serial communication pinMode(ledgreen, OUTPUT); pinMode(ledyellow, OUTPUT); pinMode(ledred, OUTPUT); } void loop() { // Clears the trigPin digitalWrite(trigPin, LOW); delayMicroseconds(2); // Sets the trigPin on HIGH state for 10 micro seconds digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); // Reads the echoPin, returns the sound wave travel time in microseconds duration = pulseIn(echoPin, HIGH); // Calculating the distance distance = duration * 0.034 / 2; // Prints the distance on the Serial Monitor //Serial.print("Distance: "); Serial.println(distance); //Serial.write(distance); int brightness = Serial.parseInt(); if(Serial.read() == '\n') { //Serial.println(brightness); if(brightness == 1){ digitalWrite(ledgreen, HIGH); digitalWrite(ledyellow, LOW); digitalWrite(ledred, LOW); } else if(brightness == 2){ digitalWrite(ledyellow, HIGH); digitalWrite(ledgreen, LOW); digitalWrite(ledred, LOW); } else if(brightness == 3){ digitalWrite(ledred, HIGH); digitalWrite(ledgreen, LOW); digitalWrite(ledyellow, LOW); } } delay(300); //analogWrite(ledgreen, brightness); }
Future improvements
I feel like there is a lot that could still be improved. I think the first thing would be to add vertical motion to the car, I was unable to implement it because the ultrasonic sensor detects horizontal distance changes only so it was a bit difficult to manage both the axes. Another factor would be to change the sensor to infrared which would solve the problem of inconsistent readings because infrared sensors send signals in a direct line while ultrasonic sensors send them spread out so the accuracy of readings would be improved. I would also like to add button to the Arduino such as buttons to turn the music on or off and also for the port selection, which I was unable to implement as well.