How to add a "Play Again" button

Viewed 415

Link to code for reference: https://editor.p5js.org/tpant963/sketches/wQy1zfKBW

Hello! So I'm using p5.js to make a Java OOP concepts review game where a question comes up on the screen with bubbles floating around that have word associations on them. If a bubble turns green when you click on it, it means that you've clicked the correct answer/association. If it turns red, then you've clicked the incorrect one.

I wanted to make my game so that if all the correct answers are clicked (meaning all the possible bubbles that can turn green have been clicked), then a "Play Again" button shows up and if you click on it, then the game restarts and you can play again.

How can I do this? This is a sample of the try that got me the closest to my desired final outcome aesthetically, but it's not able to return back to the main menu and actually restart the game when I click "Play again" Also, I can still click the "incorrect" bubbles during the game and have the same outcome, which I don't want.

let menu = 0;

function draw() {
    if (menu == 1) {
    background(bubblepopperG)
    fill(0)
    textSize(25)
    text(currentQuestion.question, 25, 300);
    
    //Allow the bubbles to move and be displayed on the screen.
    bubbles.forEach(bubble => {
      bubble.move();
      bubble.display();
    })
    
    if (answerCount > currentQuestion.correct.length){
      background(bubblepopperG)
      textSize(30)
      text('YOU GOT ALL THE RIGHT BUBBLES!', 25, height / 2)
    
      //Add button to return to main menu.
      fill(255, 0, 255);
      rect(180, 400, 250, 75);
      stroke(100);
      strokeWeight(3);
      textSize(26);
      text('PLAY AGAIN', 230, 450);
      }  
  }

Then I tried the same thing but tried assigning a mousePressed() function to the "Play Again" button at the end of the game. This was the code:

if (answerCount > currentQuestion.correct.length){
      menu = 4;
      background(bubblepopperG)
      textSize(30)
      text('YOU GOT ALL THE RIGHT BUBBLES!', 25, height / 2)
    
      //Add button to return to main menu.
      fill(255, 0, 255);
      rect(180, 400, 250, 75);
      stroke(100);
      strokeWeight(3);
      textSize(26);
      text('PLAY AGAIN', 230, 450);
      }  
  }
  
  function mousePressed(){
        if (menu == 4) {
    if (mouseX < 430 && mouseX > 180) {
      if (mouseY < 475 && mouseY > 400) {
        menu = 0
      }
    }
  }
  }

However, this one doesn't even allow the post-game screen to come up at all. It just takes me straight back to the game menu I made, but I'm unable to click on anything when I get there.

Thanks

1 Answers

So close!

Short answer: you already have a condition to check if a button has been pressed within a bounding box and a condition to check if the play again menu should be displayed (e.g. if(answerCount > currentQuestion.correct.length)).

Remember what state / menu you're in and use the right condition.

Mimicking you're code, you'd have something like:

if(menu == 1){
    if (mouseX < 430 && mouseX > 180) {
      if (mouseY < 475 && mouseY > 400) {
        // reset answer count
        answerCount = 0;
        // return to start menu
        menu = 0;
      }
    }
  }

Update: Based on your comments you need to count the number of correct bubbles (one way might be filtering the CorrectBubble instances and counting them (e.g. bubbles.filter(bubble => bubble instanceof CorrectBubble)) and that total number of correct bubbles with the number of correct bubbles that have been clicked (again filtering can help: e.g. correctBubbles.filter(bubble => bubble.col === bubble.clickedColor).length))

clicked() could update an internal boolean property which would make the code more readable (e.g. adding this.isClicked = true; in clicked() would make the cliecked bubbles filter read as correctBubbles.filter(bubble => bubble.isClicked). There's also a minor collision detection bug, you probably meant something like if (d < this.r * 0.5) (and perhaps renaming this.r to this.diameter to reflect how it's used). Even better, a reset() method could be added to the Bubble class to reset both the boolean flag and the colour:

reset(){
    this.isClicked = false;
    this.col = '#000000';
  }

Bubbles are nicely encapsulated into classes. You could do the same for buttons which minimise repetition.

Here's a modified version of your main sketch with a basic Button class encapsulating the display and clicking within a bounding rectangle functionality:

let menu = 0 //Variable to show the game main menu.
let bubblepopper; //A variable for the image
let bubblepopperM; //A variable for the menu background image
let bubblepopperG; //A variable for the game background image
const bubbles = []; //Array to store the bubbles.
let currentQuestion = null;
const questions = [
  {
    question: 'What are the 4 types of access modifiers?',
    correct: ['public', 'private', 'protected', 'default'],
    incorrect: ['method', 'class', 'protection', 'published', 'nested', 'abstract'],
  },
  {
    question: 'Words that go with encapsulation:',
    correct: ['class', 'method', 'variable','access modifiers'],
    incorrect: ['initial', 'encapsulate', 'complex', 'automatic', 'primitive', 'interface'],
  },
  {
    question: 'Things you can put in a UML OBJECT diagram:',
    correct: ['classes', 'variables', 'components', 'methods'],
    incorrect: ['packages', 'Java Project', 'client code', 'no booleans', 'no objects', 'concatenation'],
  },
  {
    question: 'Abstract classes...',
    correct: ['declare abstract', 'extend subclass', 'no instantiation', 'static & non-static'],
    incorrect: ['interface', 'only static', 'slow', 'only public access', 'default methods', 'multiple inheritance'],
  },
  {
    question: 'OOP important words:',
    correct: ['encapsulation', 'inheritance', 'polymorphism', 'abstraction'],
    incorrect: ['capsuling', 'aggravation', 'adhesion', 'dissociation', 'array', 'submethod'],
  },
  {
    question: 'Which of the following words go together?',
    correct: ['private class', 'getter', 'setter', 'protect data'],
    incorrect: ['public class', 'char', 'float', 'array', 'settings', 'extends class'],
  },
];

class Button{
  
  constructor(x, y, width, height, label, textSize, fillColor){
    
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    
    this.label = label;
    this.textSize = textSize;
    this.fillColor = fillColor;
  }
  
  draw(){
    push();
    textAlign(CENTER);
    fill(this.fillColor);
    rect(this.x, this.y, this.width, this.height);
    stroke(100);
    strokeWeight(3);
    textSize(this.textSize);
    fill(255);
    text(this.label, this.x + (this.width * 0.5), this.y + (this.height * 0.75));
    pop();
  }
  
  isPressed(){
    return ((mouseX >= this.x && mouseX <= this.x + this.width) && 
            (mouseY >= this.y && mouseY <= this.y + this.height)); 
  }
  
}

let playButton;
let exitButton;
let instructionsButton;

function preload() {
  bubblepopper = loadImage('Bubble Popper Logo.png');
  bubblepopperM = loadImage('Bubble Popper Menu Background.jpg');
  bubblepopperG = loadImage('Bubble Popper Game Background.jpg');
}

function setup() {
  currentQuestion = questions[Math.floor(Math.random() * questions.length)];
  createCanvas(600, 600);
  for(let i = 0; i < currentQuestion.correct.length; i++) {
    bubbles.push(new CorrectBubble(random(-3, 3), random(-3, 3), currentQuestion.correct[i], random(width), random(height), random(80,100)));
  } //The new bubbles will be added to the bubbles array.
  
  
  for(let i = 0; i < currentQuestion.incorrect.length; i++) {
    bubbles.push(new IncorrectBubble(random(-3, 3), random(-3, 3), currentQuestion.incorrect[i], random(width), random(height), random(80,100)));
  } //The new bubbles will be added to the bubbles array.
  
  playButton = new Button(50, 100, 200, 75, 'PLAY', 50, color(64,224,208));
  exitButton = new Button(50, 400, 200, 75, 'EXIT', 50, color(255, 0, 255));
  instructionsButton = new Button(50, 250, 200, 75, 'INSTRUCTIONS', 26, color(64,224,208));
  playAgainButton = new Button(180, 400, 250, 75, 'PLAY AGAIN', 26, color(255, 0, 255));
  returnToMenuButton = new Button(180, 400, 250, 75, 'RETURN TO MENU', 26, color(255, 0, 255));
}

function mousePressed() {
  //For each bubble in the array, click it and allow it to change to it's correct colour.
  bubbles.forEach(bubble => bubble.clicked(mouseX, mouseY));
}

function draw() {
  //Set up the main menu and the buttons in it.
  background(bubblepopperM);
  playButton.draw();
  exitButton.draw();
  instructionsButton.draw();
  image(bubblepopper, 275, 150, 300, 300); //Add the Bubble Popper logo.

  if (menu == 1) {
    background(bubblepopperG)
    fill(0)
    textSize(25)
    text(currentQuestion.question, 25, 300);
    
    //Allow the bubbles to move and be displayed on the screen.
    bubbles.forEach(bubble => {
      bubble.move();
      bubble.display();
    })
    
    const correctBubbles = getCorrectBubbles();
    if (countClickedBubbles(correctBubbles) === correctBubbles.length){
      background(bubblepopperG)
      textSize(30)
      text('YOU GOT ALL THE RIGHT BUBBLES!', 25, height / 2)
      //Add button to return to main menu.
      playAgainButton.draw();
    }  
  }
  
  //When clicked this will show the user instructions on how to play the game.
  if (menu == 2) {
    background(bubblepopperM)
    fill(255);
    stroke(100);
    strokeWeight(3);
    textSize(20)
    text('1. A question will be displayed to do with object-oriented', 50, 150)
    text('programming (OOP) concepts.', 80, 175)
    text('2. Click on the bubbles with the correct word associations', 50, 225)
    text('to the question. Correct answers will turn green,', 80, 250)
    text(' incorrect answers will turn red when clicked on.', 80, 275)
    text('3. The round is over when you select all the correct bubbles', 50, 325)
    
    returnToMenuButton.draw();
  } 
  
  //Will exit out of the program.
  if (menu == 3) {
    background(bubblepopperM);
    fill(255);
    textSize(50);
    text('THANKS FOR PLAYING!', 25, height / 2);
    
    playAgainButton.draw();
  }  
}


function getCorrectBubbles(){
  return bubbles.filter(bubble => bubble instanceof CorrectBubble);
}

function countClickedBubbles(bubbles){
  return bubbles.filter(bubble => bubble.col === bubble.clickedColor).length;
}

//Determine the mouse coordinates so that the user can click on the buttons.
function mouseClicked() {
  
  if (menu == 0) {
    if(playButton.isPressed()){
      menu = 1;
    }
    if(instructionsButton.isPressed()){
      menu = 2;
    }
    if(exitButton.isPressed()){
      console.log('exit');
      menu = 3;
    }
  }
  
  if(menu == 1){
    if (playAgainButton.isPressed()) {
      // return to start menu
      menu = 0;
    }
    
  }
  
  //Allow the user to go back to the main menu if they click instructions.
  if (menu == 2) {
    if(returnToMenuButton.isPressed()){
      menu = 0;
    }
  }
  
  //Allow the user to go back to the main menu if they click exit.
  if (menu == 3) {
    if(playAgainButton.isPressed()){
      menu = 0;
    }
  }
}

Additionally, the menu based functionality could also be encapsulated into classes. Here is variant of the above code with a basic state machine illustrating polymorphism:

let bubblepopper; //A variable for the image
let bubblepopperM; //A variable for the menu background image
let bubblepopperG; //A variable for the game background image
const bubbles = []; //Array to store the bubbles.
let currentQuestion = null;
const questions = [
  {
    question: 'What are the 4 types of access modifiers?',
    correct: ['public', 'private', 'protected', 'default'],
    incorrect: ['method', 'class', 'protection', 'published', 'nested', 'abstract'],
  },
  {
    question: 'Words that go with encapsulation:',
    correct: ['class', 'method', 'variable','access modifiers'],
    incorrect: ['initial', 'encapsulate', 'complex', 'automatic', 'primitive', 'interface'],
  },
  {
    question: 'Things you can put in a UML OBJECT diagram:',
    correct: ['classes', 'variables', 'components', 'methods'],
    incorrect: ['packages', 'Java Project', 'client code', 'no booleans', 'no objects', 'concatenation'],
  },
  {
    question: 'Abstract classes...',
    correct: ['declare abstract', 'extend subclass', 'no instantiation', 'static & non-static'],
    incorrect: ['interface', 'only static', 'slow', 'only public access', 'default methods', 'multiple inheritance'],
  },
  {
    question: 'OOP important words:',
    correct: ['encapsulation', 'inheritance', 'polymorphism', 'abstraction'],
    incorrect: ['capsuling', 'aggravation', 'adhesion', 'dissociation', 'array', 'submethod'],
  },
  {
    question: 'Which of the following words go together?',
    correct: ['private class', 'getter', 'setter', 'protect data'],
    incorrect: ['public class', 'char', 'float', 'array', 'settings', 'extends class'],
  },
];

class Button{
  
  constructor(x, y, width, height, label, textSize, fillColor){
    
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    
    this.label = label;
    this.textSize = textSize;
    this.fillColor = fillColor;
  }
  
  draw(){
    push();
    textAlign(CENTER);
    fill(this.fillColor);
    rect(this.x, this.y, this.width, this.height);
    stroke(100);
    strokeWeight(3);
    textSize(this.textSize);
    fill(255);
    text(this.label, this.x + (this.width * 0.5), this.y + (this.height * 0.75));
    pop();
  }
  
  isPressed(){
    return ((mouseX >= this.x && mouseX <= this.x + this.width) && 
            (mouseY >= this.y && mouseY <= this.y + this.height)); 
  }
  
}

class State{
  constructor(){
    
  }
  mouseClicked(){}
  draw(){}
}

class MainMenuState extends State{
  
  draw(){
    background(bubblepopperM);
    playButton.draw();
    exitButton.draw();
    instructionsButton.draw();
    image(bubblepopper, 275, 150, 300, 300); //Add the Bubble Popper logo.  
  }
  
  mouseClicked(){
    if(playButton.isPressed()){
      currentState = playState;
    }
    if(instructionsButton.isPressed()){
      currentState = instructionsState;
    }
    if(exitButton.isPressed()){
      currentState = exitState;
    }

  }
}

class PlayState extends State{
  draw(){
    background(bubblepopperG)
    fill(0)
    textSize(25)
    text(currentQuestion.question, 25, 300);
    
    //Allow the bubbles to move and be displayed on the screen.
    bubbles.forEach(bubble => {
      bubble.move();
      bubble.display();
    })
  }
  mouseClicked(){
    //For each bubble in the array, click it and allow it to change to it's correct colour.
    bubbles.forEach(bubble => bubble.clicked(mouseX, mouseY));
    // check if all correct bubbles have been clicked
    const correctBubbles = getCorrectBubbles();
    if (countClickedBubbles(correctBubbles) === correctBubbles.length){
      currentState = winState;
    }
  }
}

class WinState extends State{
  draw(){
    background(bubblepopperG);
    fill(255);
    textSize(30);
    text('YOU GOT ALL THE RIGHT BUBBLES!', 25, height / 2);
    //Add button to return to main menu.
    playAgainButton.draw();
  }
  mouseClicked(){
    if(playAgainButton.isPressed()){
      // reset clicked answers first
      bubbles.forEach(bubble => bubble.reset());
      // then change state
      currentState = menuState;
    }
  }
}

class InstructionsState extends State{
  draw(){
    background(bubblepopperM)
    fill(255);
    stroke(100);
    strokeWeight(3);
    textSize(20);
    text('1. A question will be displayed to do with object-oriented', 50, 150);
    text('programming (OOP) concepts.', 80, 175);
    text('2. Click on the bubbles with the correct word associations', 50, 225);
    text('to the question. Correct answers will turn green,', 80, 250);
    text(' incorrect answers will turn red when clicked on.', 80, 275);
    text('3. The round is over when you select all the correct bubbles', 50, 325);
    
    returnToMenuButton.draw();
  }
  mouseClicked(){
    if(returnToMenuButton.isPressed()){
      currentState = menuState;
    } 
  }
}

class ExitState extends State{
  draw(){
    background(bubblepopperM);
    fill(255);
    textSize(50);
    text('THANKS FOR PLAYING!', 25, height / 2);
    
    playAgainButton.draw();
  }
  mouseClicked(){
    if(returnToMenuButton.isPressed()){
      currentState = menuState;
    } 
  }
}

let playButton;
let exitButton;
let instructionsButton;

let menuState = new MainMenuState();
let playState = new PlayState();
let winState  = new WinState();
let instructionsState = new InstructionsState();
let exitState = new ExitState();

let currentState = menuState;

function preload() {
  bubblepopper = loadImage('Bubble Popper Logo.png');
  bubblepopperM = loadImage('Bubble Popper Menu Background.jpg');
  bubblepopperG = loadImage('Bubble Popper Game Background.jpg');
}

function setup() {
  currentQuestion = questions[Math.floor(Math.random() * questions.length)];
  createCanvas(600, 600);
  for(let i = 0; i < currentQuestion.correct.length; i++) {
    bubbles.push(new CorrectBubble(random(-3, 3), random(-3, 3), currentQuestion.correct[i], random(width), random(height), random(80,100)));
  } //The new bubbles will be added to the bubbles array.
  
  
  for(let i = 0; i < currentQuestion.incorrect.length; i++) {
    bubbles.push(new IncorrectBubble(random(-3, 3), random(-3, 3), currentQuestion.incorrect[i], random(width), random(height), random(80,100)));
  } //The new bubbles will be added to the bubbles array.
  
  playButton = new Button(50, 100, 200, 75, 'PLAY', 50, color(64,224,208));
  exitButton = new Button(50, 400, 200, 75, 'EXIT', 50, color(255, 0, 255));
  instructionsButton = new Button(50, 250, 200, 75, 'INSTRUCTIONS', 26, color(64,224,208));
  playAgainButton = new Button(180, 400, 250, 75, 'PLAY AGAIN', 26, color(255, 0, 255));
  returnToMenuButton = new Button(180, 400, 250, 75, 'RETURN TO MENU', 26, color(255, 0, 255));
}

function draw() {
  currentState.draw();
}

function mouseClicked() {
  currentState.mouseClicked();
}

function getCorrectBubbles(){
  return bubbles.filter(bubble => bubble instanceof CorrectBubble);
}

function countClickedBubbles(bubbles){
  return bubbles.filter(bubble => bubble.isClicked).length;
}

Of course the Button class could live in button.js and the state clasess in states.js for example: this would greatly reduce the code in the main sketch.

Related