For my final project I made a two-player running race game.
The concept is fairly straightforward: 100 meters hurdles, the speed of the avatar on the screen is controlled by the player’s pulse, the hurdles are to be avoided by jumping on pressure sensing mats, and the goal is to cross the finish line first. The pulse sensors I used were easy to implement as they came with pre-written Arduino and Processing code. Similarly quick and hassle-free was the creation and implementation of DIY cardboard pressure switches. I spent quite some time nesting functionality in order to enable seamless switching between different modes (i.e. run mode, restart mode etc.), but I eventually managed to write code that accommodated different scenarios.
At the IM show, the visitors’ vigorous jumps soon resulted in cardboard pressure switches getting squashed together. Thereafter, the switches were unable to distinguish between two different states (i.e. person standing on the mat versus nobody standing on the mat) and were thus useless. Luckily, the solution to the problem was not overly complex: I simply had to replace the broken switches with something else. I connected two pushbuttons to Arduino and the visitors of the show were able to ‘jump’ once again.
Here’s my code processing code:
import processing.serial.*; //serial port object Serial myPort; Serial myPortPulse2; //new instance of the racetrack Background racetrack; Background racetrack2; //new instance of the player Player myPlayer; Player myPlayer2; PImage stadium1; // pulse sensor variables PFont font; int Sensor; // HOLDS PULSE SENSOR DATA FROM ARDUINO int pushSensor; int pushSensor2; int IBI; // HOLDS TIME BETWEN HEARTBEATS FROM ARDUINO //BPM int BPM; // HOLDS HEART RATE VALUE FROM ARDUINO int BPM2; int[] RawY; // HOLDS HEARTBEAT WAVEFORM DATA BEFORE SCALING int[] ScaledY; // USED TO POSITION SCALED HEARTBEAT WAVEFORM int[] rate; // USED TO POSITION BPM DATA WAVEFORM float zoom; // USED WHEN SCALING PULSE WAVEFORM TO PULSE WINDOW float offset; // USED WHEN SCALING PULSE WAVEFORM TO PULSE WINDOW color eggshell = color(255, 253, 248); int heart = 0; // This variable times the heart image 'pulse' on screen // THESE VARIABLES DETERMINE THE SIZE OF THE DATA WINDOWS int PulseWindowWidth = 490; int PulseWindowHeight = 512; int BPMWindowWidth = 180; int BPMWindowHeight = 340; boolean beat = false; // set when a heart beat is detected, then cleared when the BPM graph is advanced //other variables color crowd = color(5,0,56); color field = color(10,105,36); //time variables float startTimeHit; float startTimeHit2; float startTime; boolean startMeasuring = false; boolean startMeasuringHit = false; boolean startMeasuringHit2 = false; boolean runMe = true; boolean restartMode = false; boolean play = false; boolean runMode = false; boolean aPressed = false; boolean dPressed = false; boolean jumpStart = false; boolean jumpStart2 = false; boolean competition = false; float finishLineTime1 = 0; float finishLineTime2 = 0; boolean disqualified = false; boolean disqualified2 = false; float disqualificationLimit = 50000; float disqualificationLimit2 = 50000; float jumpStartMillis; float jumpKeepMillis; float jumpStartMillis2; float jumpKeepMillis2; float difference = 0; float difference2 = 0; //finishLine boolean displayWinner = false; float player1Time; float player2Time; boolean finishLineCheck = false; /*----------------------------------------------SETUP-----------------------*/ void setup(){ frameRate(100); textAlign(CENTER); // GO FIND THE ARDUINO printArray(Serial.list()); String portName = Serial.list()[0]; String portName2 = Serial.list()[1]; //PORT1 myPort = new Serial(this, portName, 115200); myPort.clear(); // flush buffer myPort.bufferUntil('\n'); // set buffer full flag on receipt of carriage return //PORT2 myPortPulse2 = new Serial(this, portName2, 115200); myPortPulse2.clear(); myPortPulse2.bufferUntil('\n'); //BACKGROUND size(1200,600); background(crowd); noStroke(); racetrack = new Background(200,100,0); racetrack2 = new Background(500,100,300); //PLAYERS myPlayer = new Player(100, 60, height*2/6, 149); myPlayer2 = new Player(100, 60, 500, 449); stadium1 = loadImage("stadium.jpg"); } void keyPressed(){ if(key == 'a' || key == 'A'){ aPressed = true; //println("A pressed is true: " + aPressed); } else if(key == 'l' || key == 'L'){ dPressed = true; //println("Dpressed is true: " + dPressed); } else if(key == 'r' || key == 'R'){ //println("Restart key is pressed"); restartMode = true; } } /*-----------------------------------------DRAW ---------------------------------------------------*/ void draw(){ /*---------------------------------------RESTART MODE-------------------------------------*/ if(restartMode){ startMeasuring = false; startMeasuringHit = false; startMeasuringHit2 = false; runMe = true; play = false; runMode = false; aPressed = false; dPressed = false; jumpStart = false; jumpStart2 = false; disqualified = false; disqualified2 = false; racetrack.restartMode(); racetrack2.restartMode(); competition = false; displayWinner = false; finishLineCheck = false; finishLineTime1 = 0; finishLineTime2 = 0; jumpStartMillis = 0; jumpKeepMillis = 0; jumpStartMillis2 = 0; jumpKeepMillis2 = 0; difference = 0; difference2 = 0; restartMode = false; } //MAP BPM float moveHurdle = map(BPM, 50, 200, 6, 13); float moveHurdle2 = map(BPM2, 50, 200, 6, 13); /*---------CHECK IF BOTH PLAYERS ARE READY TO PLAY--------*/ if(runMe){checkGameMode(); } if(!play){ background(0); fill(255); textSize(60); text("100 METRE HURDLES",width/2, height/2); textSize(40); text("PLAYER 1", 400,400); text("Press A", 400, 450); text("PLAYER 2", 800, 400); text("Press L", 800, 450); } //player1 HIT functionality if(startMeasuringHit){ float ms = millis()- startTimeHit; if(ms < 2000){ //println(ms); moveHurdle = 4; } else{ moveHurdle = map(BPM, 50, 200, 6, 13); } } //player2 HIT functionality if(startMeasuringHit2){ float ms = millis()- startTimeHit2; if(ms < 2000){ //println(ms); moveHurdle2 = 4; } else{ moveHurdle2 = map(BPM2, 50, 200, 6, 13); } } /*-----------------PLAY MODE-------------*/ if(play){ background(crowd); fill(255); textSize(40); text("Player1",1100,50); text("Player2",1100,350); fill(0); //the ads 1 fill(200); rect(0,170, width,30); //the ads 2 rect(0,470, width,30); //racetracks racetrack.render(); racetrack2.render(); //players myPlayer2.render(); myPlayer2.update(); myPlayer.render(); myPlayer.update(); /*--------------------------------------RUN MODE----------------------------------*/ if(runMode){ competition = true; racetrack.update(moveHurdle); racetrack2.update(moveHurdle2); racetrack.checkBounds(); racetrack2.checkBounds(); racetrack.finishLine(); racetrack2.finishLine(); //player1COLLISION DETECTION if(myPlayer.xPos() > racetrack.hurdleXposLeft() && myPlayer.xPos() < racetrack.hurdleXposLeft() + 30){ if(myPlayer.yPos() > 130){ textSize(50); text("HIT", 200, 200); startTimeHit = millis(); startMeasuringHit = true; } } //player2COLLISION DETECTION if(myPlayer2.xPos() > racetrack2.hurdleXposLeft() && myPlayer2.xPos() < racetrack2.hurdleXposLeft() + 30){ if(myPlayer2.yPos() > 430){ textSize(50); text("HIT", 200, 500); startTimeHit2 = millis(); startMeasuringHit2 = true; } } //print sensorVals /* textSize(15); text("pushSensor" + pushSensor, 400, 200); text("pushSensor2" + pushSensor2, 500, 200);// tell them what you are text(BPM + " BPM",200,200); text(BPM2 + " BPM",50,200);*/ /*------------------jump functionality------*/ if(competition){ sensorJump(); } } if(disqualified){ fill(0); rect(0,0,width,height); fill(255); textSize(80); text("Player1 is disqualified!",width/2,height/2); text("Player2 wins!",width/2,400); } if(disqualified2){ fill(0); rect(0,0,width,height); fill(255); textSize(80); text("Player2 is disqualified!",width/2,height/2); text("Player1 wins!",width/2,400); } } if(startMeasuring){ float ms = millis()- startTime; //println(ms); // print time on the screen if(ms > 200 && ms < 1000){ //println("yes"); textSize(100); text("READY", 600, 300); } if(ms > 1300 && ms < 2000){ text("SET", 600, 300); } if(ms > 2300){ if(ms < 3000){ textSize(100); text("GO", 600, 300); } runMode = true; } } //finish line finishLineTime1 = racetrack.finishTimeCheck(); finishLineTime2 = racetrack2.finishTimeCheck(); if(finishLineTime1 > 0 && finishLineTime2 > 0 && displayWinner){ if(finishLineTime1 < finishLineTime2){ textSize(100); text(finishLineTime1,100, 100); text(finishLineTime2,500, 100); text("Player1 won!",width/2,height/2); } else if(finishLineTime2 < finishLineTime1){ textSize(100); text(finishLineTime1,100, 100); text(finishLineTime2,500, 100); text("Player2 won!",width/2,height/2); } else if(finishLineTime2 == finishLineTime1){ textSize(100); text("Both players won",width/2,height/2); } } } /*----------END OF DRAW------------------*/ void sensorJump(){ if(pushSensor == 1){ myPlayer.startJump(); if(!jumpStart){ jumpStartMillis = millis(); //println("This is the start of the jump: " + jumpStartMillis); } jumpStart = true; jumpKeepMillis = millis(); difference = jumpKeepMillis - jumpStartMillis; if(difference > disqualificationLimit){ disqualified = true; } } else if(pushSensor == 0){ myPlayer.endJump(); jumpStart = false; } //println("Jump start is " + jumpStart); if(pushSensor2 == 1){ myPlayer2.startJump(); if(!jumpStart2){ jumpStartMillis2 = millis(); //println("This is the start of the jump: " + jumpStartMillis); } jumpStart2 = true; jumpKeepMillis2 = millis(); difference2 = jumpKeepMillis2 - jumpStartMillis2; if(difference2 > disqualificationLimit2){ disqualified2 = true; } }else if(pushSensor2 == 0){ myPlayer2.endJump(); jumpStart2 = false; } } void checkGameMode(){ if(dPressed && aPressed){ runMe = false; startMeasuring = true; play = true; startTime = millis(); } }
Here’s code for the ‘Background’ class:
class Background{ //declare variables float offsetHurdle = 70; color orange = color(232,133,12); float moveLeft = 0; float hurdleXposLeft; int hurdleCounter = 0; int hurdleLimit = 8; int finishLineOffset = 33; int finishStringInit; int trackTop; int trackBottom; int hurdleLeftPart; int hurdleRightPart; int hurdleBottom; int hurdleTop; int hurdleScreenOffset; int finishLinePosX; int finishLineBottom; boolean finishLinePosCheck = false; float finishTime = 0; Background(int trackTop_, int trackBottom_, int hurdleScreenOffset_){ trackTop = trackTop_; trackBottom = trackBottom_; hurdleLeftPart = width - 60; hurdleRightPart = width - 30; hurdleScreenOffset = hurdleScreenOffset_; hurdleBottom = (height/2) + hurdleScreenOffset; hurdleTop = (height*2/6) + hurdleScreenOffset; finishLinePosX = width + 100; finishLineBottom = trackTop_; finishStringInit = 30 + hurdleScreenOffset_; } void render(){ //the track fill(orange); rect(0,trackTop, width,trackBottom); stroke(255); strokeWeight(5); /*-----------------------------------------------THE HURDLES------------------------------------------------------*/ /*line(width/3 + moveLeft,height*2/(3*splitScreen),width/3 - 30 + moveLeft,height/splitScreen);//baseline line(width/3 + moveLeft,height*2/(3*splitScreen), width/3 + moveLeft, height*2/(3*splitScreen) - offsetHurdle);//rightLeg line(width/3 - 30 + moveLeft,height/splitScreen, width/3 - 30 + moveLeft, height/splitScreen - offsetHurdle);//leftLeg line(width/3 - 30 + moveLeft, height/splitScreen - offsetHurdle, width/3 + moveLeft, height*2/(3*splitScreen) - offsetHurdle);//upperPart*/ //baseline line(hurdleRightPart + moveLeft,hurdleTop,hurdleLeftPart + moveLeft,hurdleBottom); //rightLeg line(hurdleRightPart + moveLeft,hurdleTop, hurdleRightPart + moveLeft, hurdleTop - offsetHurdle); //leftLeg line(hurdleLeftPart + moveLeft,hurdleBottom, hurdleLeftPart + moveLeft, hurdleBottom - offsetHurdle); //upperPart line(hurdleLeftPart + moveLeft, hurdleBottom - offsetHurdle, hurdleRightPart + moveLeft, hurdleTop - offsetHurdle); } void update(float moveLeft_){ moveLeft -= moveLeft_; hurdleXposLeft = hurdleLeftPart + moveLeft; } void checkBounds(){ //check when the right leg goes out of the frame if(hurdleCounter <= hurdleLimit){ if((hurdleLeftPart + moveLeft + 10) < 0 ){ moveLeft = 50; hurdleCounter++; } } } void finishLine(){ if(hurdleCounter > hurdleLimit){ strokeWeight(10); line(finishLinePosX + moveLeft,hurdleScreenOffset, finishLinePosX + moveLeft,finishLineBottom); fill(255,255,255); rect(finishLinePosX - 50 + moveLeft, hurdleScreenOffset, 50, 200); fill(0); textSize(30); text("F", width + 75 + moveLeft, finishStringInit); text("I", width + 75 + moveLeft, finishStringInit + finishLineOffset); text("N", width + 75 + moveLeft, finishStringInit + 2*finishLineOffset); text("I", width + 75 + moveLeft, finishStringInit + 3*finishLineOffset); text("S", width + 75 + moveLeft, finishStringInit + 4*finishLineOffset); text("H", width + 75 + moveLeft, finishStringInit + 5*finishLineOffset); line(width + 100 + moveLeft,height*2/3,width + 70 + moveLeft,height); strokeWeight(2); textSize(8); if(finishLinePosX + 100 + moveLeft < 0){ runMode = false; displayWinner = true; competition = false; if(!finishLinePosCheck){ //finishLineCheck = true; finishTime = millis(); finishTimeCheck(); } finishLinePosCheck = true; } } } public float hurdleXposLeft(){ return hurdleXposLeft; } public float finishTimeCheck(){ return finishTime; } void restartMode(){ hurdleCounter = 0; moveLeft = 0; finishLinePosCheck = false; finishTime = 0; } }
… and for the ‘Player’ class:
class Player{ //declare variables float playerHeight; float playerWidth; //track dimensions float trackHeight = height/3; float trackTop; float baseOffset = 50; int counter = 0; float playerXpos; float playerYbase; //jump parameters int jumpPlayer = 0; boolean onGround = false; float playerYpos; float trackMiddle; float velocityY = 0.0; float gravity = 0.5; //constructor - define variables, setup for the class Player(float playerHeight_, float playerWidth_, int trackTop_, float trackMiddle_){ playerHeight = playerHeight_; playerWidth = playerWidth_; trackTop = trackTop_; playerYpos = trackTop_ - playerHeight_/2 - jumpPlayer; trackMiddle = trackMiddle_; playerXpos = baseOffset + playerWidth/2; } void render(){ //the player // at the beginning of the screen noStroke(); fill(0); rect(baseOffset,playerYpos, playerWidth, playerHeight); } void update(){ velocityY += gravity; playerYpos += velocityY; playerYbase = playerYpos + 100; if(playerYpos > trackMiddle){ playerYpos = trackMiddle + 1; velocityY = 0.0; onGround = true; } } void startJump(){ if(onGround){ velocityY = -14.0; onGround = false; } } void endJump(){ if(velocityY < - 6.0){ velocityY = - 6.0; } } public float yPos(){ return playerYbase; } public float xPos(){ return playerXpos; } }
I also used the code from this link to get pulse sensor values from Arduino to Processing.
Making a game was challenging, but immensely rewarding.