Ready, set, go!

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.

Leave a Reply