Final Project: Whack-A-Note

Concept:

My project is a whack-a-mole game with a rhythm game element where users have to hit the mole at a specific time when a mole’s shadow(a node) aligns with the mole. Users, while the song is playing, must hit the mole whenever the shadow of it

Implementation:

My hardware part of the project is basically a giant system of three switches. Each wire of the circuit connected to its respective digital inputs are attached to an tin foil and left as an uncompleted circuit. The circuit is only closed when the hammer, connected to power, hits the tin foil and completes the circuit, sending a HIGH signal through the digital input. In the UI, the p5 reads a music score and randomly generates nodes related to the notes and rhythm and creates an array. Each node in that array will move towards the designated hit zone, and if the node is aligned with the hit zone, then users can connect the physical circuit to send the signal that the mole was whacked. If the correct mole was whacked, then the user will win points, but if mole was not whacked or the incorrect mole was whacked, then users will lose points. Each song has its specific array of nodes, so the users can achieve a certain max score, and if the user loses more than -1000 points, then the game will be over.

Codes:

-p5js

let musicOne;
let musicTwo;
let musicThree;
let noteImg;
let bg;
let title;
let gameOverSign;
let moleSize = 100;
let shadowImg = [];
let moleImg;
let musicDelay = [4.8, 3.3, 5];
let gameOver = false;
let musicArr = [];
let mole = [];
let colorMole = ["red", "yellow","blue"];
let hit = false;
let score = 0;
let noteArr = [];
let shadowsArr = []
let hitArr = [0, 0, 0];
let firstrun = true;
let startGame = false;
let choice;


function preload(){
  bg = loadImage('bg.png');
  moleImg = loadImage("download.png");
  shadowImg[0] = loadImage("shadowRed.png");
  shadowImg[1] = loadImage("shadowYellow.png");
  shadowImg[2] = loadImage("shadowBlue.png"); 
  shadowImg[3] = loadImage("download.png");
  noteImg = loadImage("note.png");
  gameOverSign = loadImage("gameOver.png")
  title = loadImage("title.png")
  
  musicOne = loadSound("starWars.mp3");
  musicTwo = loadSound("zelda.mp3");
  musicThree = loadSound("canon.mp3");
  
  musicArr.push(musicOne);
  musicArr.push(musicTwo);
  musicArr.push(musicThree);
}

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont("Comic Sans MS");
  textSize(20);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

function draw() {
  //background image of the game
  //choose a song to play
  background(bg);
  if(!startGame){
    image(title, (windowWidth/2) - windowWidth/16 * 7, (windowHeight/4) - windowHeight/8, windowWidth/8 * 7, windowHeight/3 * 2);
    let stringOne = "Star Wars Theme";
    let stringTwo = "Zelda Theme";
    let stringThree = "Canon";
    fill("white");
    text(stringOne, (windowWidth/4) - (textWidth(stringOne)/2), (windowHeight/18) * 11);
    text(stringTwo, (windowWidth/2) - (textWidth(stringTwo)/2), (windowHeight/18) * 11);
    text(stringThree, (windowWidth/4) * 3 - (textWidth(stringThree)/2), (windowHeight/18) * 11);
    
    image(noteImg, (windowWidth/4) - 55, (windowHeight/10) * 7, 100, 100)
    image(noteImg, (windowWidth/2) - 55, (windowHeight/10) * 7, 100, 100)
    image(noteImg, (windowWidth/4) * 3 - 55, (windowHeight/10) * 7, 100, 100)
  }
  else{
    displayGame();
  }
}

function displayGame(){
  if (!serialActive) {
    text("Press Z to select Serial Port", 20, 30);
  }
  else{
    if(!musicArr[choice].isPlaying() && firstRun){
      musicArr[choice].play(musicDelay[choice]);
      firstRun = false;
    }
    
    if(shadowsArr.length > 0 && score > -1000){
      if(gameOver){
        gameOver = false;
      }
      for(let i = 0; i < 3; i++){
        mole[i].display();
      }
      
      strokeWeight(5);
      
      //setLineDash([5, 10, 30, 10]);
      //line(100, 0, 100, windowHeight);
      text("Score: " + score, windowWidth/5 * 4, windowHeight/40 * 3);
      for(let i = 0; i < shadowsArr.length; i++){
        shadowsArr[i].display();
      }
      //console.log(shadowsArr.length)
    }
    else{
      fill("white");
      musicArr[choice].stop();
      firstRun = false;
      let scoreText = "Score: " + score;
      image(gameOverSign, windowWidth/2 - windowWidth/16 * 5, windowHeight/4 - windowWidth/16 * 2, windowWidth/8 * 5, windowWidth/8 * 3);
      let longText = "Press R to Play Again!";
      //text(shortText, 200 - textWidth(shortText)/2, 100);
      text(longText, windowWidth/2 - textWidth(longText)/2, windowHeight/8 * 7)
      text(scoreText, windowWidth/2 - textWidth(scoreText)/2, windowHeight/4 * 3);
      for(let i = 0; i < shadowsArr.length; i++){
        shadowsArr.splice(i,1);
      }
      gameOver = true;
    }
  }
  
  
  killShadow();

}

function keyPressed(){
  if (keyCode == 90) {
    setUpSerial();
  }
  if (keyCode == 82){
    if(gameOver == true){
      startGame = false;
      score = 0;
    }
  }
  if (keyCode == 70) {
    toggleFullscreen();
  }
}

function readSerial(data) {
  if (data != null) {
    let fromArduino = split(trim(data), ",");
    if (fromArduino.length == 3) {
      for(let i = 0; i < 3; i++){
        hitArr[i] = int(fromArduino[i]);
      }
    }
  }
}


function killShadow(){
  for(let i = 0; i < shadowsArr.length; i++){
    if(shadowsArr[i].x < 40){
      shadowsArr.splice(i, 1);
      score -= 100; 
    }
  }
}

class shadow{
  constructor(x, y){
    this.x = x;
    this.y = (windowHeight/4) * y;
    this.id = y;
    this.hitVal = false;
  }
  
  display(){
    fill("white");
    if(this.x > 50){
      image(shadowImg[this.id - 1], this.x - moleSize/2, this.y - moleSize/2, moleSize, moleSize);
      if(this.x <100){
        this.hitVal = true;
        image(shadowImg[3], this.x - moleSize/2, this.y - moleSize/2, moleSize, moleSize);
      }
      
    }
    this.x -= 2;
  }
}

class Mole{
  constructor(y){
    this.x = 50;
    this.y = (windowHeight/4) * y;
    this.id = y;
  }
  
  search(){
    if(shadowsArr[0].hitVal == true && shadowsArr[0].id == this.id)
      return true;
    else
      return false;
  }
  
  checkHit(){
    if(hitArr[this.id - 1] == 1){
      if(this.search()){
        score += 100;
        hitArr[this.id - 1] = 0;
        shadowsArr.splice(0, 1);
      }
      else{
        score -= 100;
        hitArr[this.id - 1] = 0;
      }
    }
  }
  
  display(){
    if(!gameOver){
      //image(moleImg, this.x - moleSize/2, this.y - moleSize/2, moleSize, moleSize);
      setLineDash[0];
      fill("black")
      ellipse(this.x, this.y + 20, 70, 40)
    }
    this.checkHit();
  }
}

function mouseClicked(){
  if(!startGame){
    for(let i = 1; i < 4; i++){
      mole.push(new Mole(i));
    }
    if(mouseY > (windowHeight/10) * 7 && mouseY < (windowHeight/10) * 7 + 100){
      if(mouseX > (windowWidth/4) - 55 && mouseX < (windowWidth/4) + 55){
        choice = 0;
        loadNotes(choice);
        startGame = true;
      }
      else if(mouseX > (windowWidth/2) - 55 && mouseX < (windowWidth/2) + 55){
        choice = 1;
        loadNotes(choice);
        startGame = true;
      }
      else if(mouseX > (windowWidth/4) * 3 - 55 && mouseX < (windowWidth/4) * 3 + 55){
        choice = 2;
        loadNotes(choice);
        startGame = true;
      }
    }
  }
}

function loadNotes(x){
  let xCo = windowWidth; 
  firstRun = true;
  let datas = [];
  let notes = [];
  let rhythm = [];
  if(x == 0){
    datas = data1;
  }
  if(x == 1){
    datas = data2;
  }
  if(x == 2){
    datas = data3;
  }
  for(let i = 0; i < datas.length; i++){
    if(i % 2 == 0){
      let temp = 0;
      for(let j = 0; j < datas[i].length; j++){
        temp += datas[i].charCodeAt(j);
      }
      temp = (temp % 3) + 1;
      notes.push(temp);
    }
    else{
      if(datas[i] > 0){
        rhythm.push(datas[i] * 20);
      }
      else{
        rhythm.push(datas[i] * 15);
      }
    }
  }
  
  for(let i = 0; i < notes.length; i++){
    shadowsArr.push(new shadow(xCo, notes[i]));
    xCo += rhythm[i];
  }
}

function toggleFullscreen() {
  let fs = fullscreen(); // Get the current state
  fullscreen(!fs); // Flip it!
}

function setLineDash(list) {
  drawingContext.setLineDash(list);
}

 

-arduino

int lastButton1State = LOW;
int lastButton2State = LOW;
int lastButton3State = LOW;

int buttonState1;
int buttonState2;
int buttonState3;

unsigned long lastDebounceTime1 = 0;
unsigned long lastDebounceTime2 = 0;
unsigned long lastDebounceTime3 = 0;
unsigned long debounceDelay = 20;


void setup() {
  Serial.begin(57600);
  pinMode(10, INPUT);
  pinMode(9, INPUT);
  pinMode(8, INPUT);
}

void loop() {

  int readingOne = digitalRead(10);
  int readingTwo = digitalRead(9);
  int readingThree = digitalRead(8);

  // check to see if you just pressed the button
  // (i.e. the input went from LOW to HIGH), and you've waited long enough
  // since the last press to ignore any noise:

  // If the switch changed, due to noise or pressing:
  if (readingOne != lastButton1State) {
    // reset the debouncing timer
    lastDebounceTime1 = millis();
  }
  if (readingTwo != lastButton2State) {
    // reset the debouncing timer
    lastDebounceTime2 = millis();
  }
  if (readingThree != lastButton3State) {
    // reset the debouncing timer
    lastDebounceTime3 = millis();
  }

  if ((millis() - lastDebounceTime1) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    // if the button state has changed:
    if (readingOne != buttonState1) {
      buttonState1 = readingOne;

      // only toggle the LED if the new button state is HIGH
      if (buttonState1 == HIGH) {
        Serial.print(readingOne);
        Serial.print(",");
        Serial.print(readingTwo);
        Serial.print(",");
        Serial.println(readingThree);
      }
    }
    
  }

  if ((millis() - lastDebounceTime2) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    // if the button state has changed:
    if (readingTwo != buttonState2) {
      buttonState2 = readingTwo;

      // only toggle the LED if the new button state is HIGH
      if (buttonState2 == HIGH) {
        Serial.print(readingOne);
        Serial.print(",");
        Serial.print(readingTwo);
        Serial.print(",");
        Serial.println(readingThree);
      }
    }
    
  }

  if ((millis() - lastDebounceTime3) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    // if the button state has changed:
    if (readingThree != buttonState3) {
      buttonState3 = readingThree;

      // only toggle the LED if the new button state is HIGH
      if (buttonState3 == HIGH) {
        Serial.print(readingOne);
        Serial.print(",");
        Serial.print(readingTwo);
        Serial.print(",");
        Serial.println(readingThree);
      }
    }
    
  }
  lastButton1State = readingOne;
  lastButton2State = readingTwo;
  lastButton3State = readingThree;
}

 

Communication:

Whenever the user whacks a mole, the arduino, through serial communication, sends an array with three elements. In that array, the element with the array position that correlates to the mole number that was hit will be 1 and the rest that wasn’t hit will be 0. P5, constantly reading for this data, will take the data and update the targetHit array which will contain exactly that same data from arduino. When a shadow note reaches the designated hit zone, the mole instance will automatically check for the targetHit array and if the targetHit array matches the the id of the shadow mole, then the user will score a point. If the targetHit array does not match the id, then the user will lose a point. If the targetHit array was not updated at all, then p5 will recognize that the user didn’t hit any moles and user will lose a point. The communication from p5 to arduino was not necessary for my project, so there is none.

Aspects I am proud of:

Overall, I generally like the idea that physically completing a circuit with a non-traditional switch to make a serial communication since this was an activity we started the arduino chapter with. Also for some unexplainable reason, I really like the idea of whacking something to make something work. I am also very proud of my object orienting programming of the shadow node instance and mole instance interacting with each other to check whether the shadow node is within the designated hit zone and confirming whether the arduino’s sent information matches with the shadow id. That part took some time coming up with the mechanism that worked, but once I did, throughout the project, the mechanism worked like a charm. Lastly, I really like the arrangement of the nodes correlating to the music since I specifically used an algorithm that took the music score and translated the notes to either 1, 2, or 3 by changing the note to its ASCII code and remainder dividing it by 4. Then, I used the length of that note to decide the distance between it and its next node to match the rhythm of the node and the music, and last but not least, I manually tweaked some of the nodes and rhythms so that it specifically matched to some parts of the song. To be honest, this manual peaking took a huge portion of my time spent in this project.

Improvements:

Originally, I wanted to add more to this project by having more hittable nodes, adding another hitting element to use gloves instead of hammers to make the  game more dynamic and interesting, but unfortunately, the technical difficulties of having two hitting objects made it almost impossible to implement, and adding more hittable objects required too much time and resources. The technical difficulty was that the methodology of adding another hitting object was to add another power connected circuit differentiated by different level of resistance, which would then require analog reading instead of digital reading, and this didn’t work out with my existing switch debouncing mechanism and therefore I had to give up on this improvement. However, given more time and information, I would like to add this improvement which would add a whole new spectrum of dynamicness of the game.

Leave a Reply