Search code examples
processing

Processing: How to fix timer display and how to display time score at end of game?


I am working on an ascending order puzzle that records the time it takes the user to complete the puzzle and once the puzzle is completed it takes the user to a game over screen that shows the user their time. My issue is that the time that is being displayed is overlapping and not updating as it should be. I am also unsure how to save the time that the user took to complete the puzzle.

final int NUM_SQUARES = 4;
int[][] board = new int[NUM_SQUARES][NUM_SQUARES];
int sqSide;
Timer startTimer;

void setup(){
  size(500, 500);
  setupGame();
  sqSide = width/NUM_SQUARES;
}

void setupGame(){
  sqSide = width/NUM_SQUARES;
  startTimer = new Timer(0);
  //populate the board
    //generate random number
    //check if we have it already inside the array
    //if we have, then go on to generate another random number
    //if we do not have it, then we can store inside array
  for(int row=0; row<NUM_SQUARES; row++){
    for(int col=0; col<NUM_SQUARES; col++){
      int randVal;
      do{
        randVal = int(random(1, NUM_SQUARES*NUM_SQUARES+1) );
      }while( searchFor(randVal) );
      board[row][col] = randVal;
    }
  }
  
  //visual representation of the board
  for(int row=0; row<NUM_SQUARES; row++){
    for(int col=0; col<NUM_SQUARES; col++){
      fill(random(0,255), random(0,255), random(0,255));
      rect(col*sqSide, row*sqSide, sqSide, sqSide);
      fill(0);
      textSize(30);
      text(board[row][col], (col+0.5)*sqSide, (row+0.5)*sqSide);
    }
  }
}

class Timer{
  float Time;
  
  Timer(float set){
    Time = set;
  }
  float getTime(){
    return(Time);
  }
  void setTime(float set){
     Time = set;
  }
  void countUP(){
    Time += 1/frameRate;
  }
}


boolean searchFor(int itemToBeSearched){
  for(int i=0; i<NUM_SQUARES; i++){
    for(int j=0; j<NUM_SQUARES; j++){
      if(itemToBeSearched == board[i][j]){
        return true;
      }
    }
  }
  return false;
}

void draw(){
  startTimer.countUP();
  fill(0);
  text(startTimer.getTime(), 20,20);
}

int clickedRow, clickedCol, releasedRow, releasedCol;

void mousePressed(){
  clickedRow = int(mouseY/sqSide);
  clickedCol = int(mouseX/sqSide);
}
void mouseReleased(){
  releasedRow = int(mouseY/sqSide);
  releasedCol = int(mouseX/sqSide);
  //swap
  int buffer = board[clickedRow][clickedCol];
  board[clickedRow][clickedCol] = board[releasedRow][releasedCol];
  board[releasedRow][releasedCol] = buffer;
  
  //visual representation - finish up
  //show what is inside board[clickedRow][clikedCol]
  //then show what is inside board[releasedRow][releasedCol]
  //where the child pressed
  
  fill(random(0,255), random(0,255), random(0,255));
  rect(clickedCol*sqSide, clickedRow*sqSide, sqSide, sqSide);
  fill(0);
  text(board[clickedRow][clickedCol],(clickedCol+0.5)*sqSide, (clickedRow+0.5)*sqSide) ;
  
  //where the child released
  fill(random(0,255), random(0,255), random(0,255));
  rect(releasedCol*sqSide, releasedRow*sqSide, sqSide, sqSide);
  fill(0);
  text(board[releasedRow][releasedCol],(releasedCol+0.5)*sqSide, (releasedRow+0.5)*sqSide);
  
  
  if(gameOver()==true){ //calling function gameOver
    background(255);
    String s = "Congratulations!"; 
    String d = "Click to start again!";
    fill(0);
    text(s, 125, 225);
    text(d, 125, 250);
    if(mousePressed == true){
      setupGame();
    }
  }
}

//definition of gameOver
boolean gameOver(){
  int counter=1;
  for(int row=0; row<NUM_SQUARES; row++){
    for(int col=0; col<NUM_SQUARES; col++){
      if(board[row][col] !=counter){
        return false;
      }
      counter++;
    }
  }
  return true;
}

Solution

  • There's plenty of good work there, especially when it comes to managing the board / swapping elements / etc. It takes focus to keep it in check.

    It also looks like there's a bit of confusion regarding drawing. You can draw elements once and in draw(); render overlapping elements on top as you are doing now, however you need to pay attention to what needs to be rendered once or multiple times.

    The timer text is leaving trails because the background is never cleared, but the board is only drawn once. You will want to redraw the background/board to fix the text, but some bits of code will need to move or need to be handled differently.

    For example:

    fill(random(0,255), random(0,255), random(0,255));
    

    If you call that in draw() the colours will change continuously you don't want.

    You could however generate the colours once in setup() then re-use them in draw() without resetting the values.

    There seems to be also some confusion with mouse events and when or in which state should resets happen.

    Here's a re-organised version of your code taking into account the notes above:

    final int NUM_SQUARES = 4;
    int[][] board       = new int[NUM_SQUARES][NUM_SQUARES];
    // also remember the colours set once in setup() to re-use in draw()
    int[][] boardColors = new int[NUM_SQUARES][NUM_SQUARES];
    int sqSide;
    Timer startTimer;
    
    int clickedRow, clickedCol, releasedRow, releasedCol;
    // this will store the result of gameOver() once so it can be re-used
    boolean isGameOver;
    
    void setup(){
      size(500, 500);
      setupGame();
    }
    
    void setupGame(){
      sqSide = width/NUM_SQUARES;
      startTimer = new Timer(0);
      clearBoard();
      //populate the board
      //generate random number
      //check if we have it already inside the array
      //if we have, then go on to generate another random number
      //if we do not have it, then we can store inside array
      shuffleBoard();
    }
    
    void clearBoard(){
      for(int row = 0; row < NUM_SQUARES; row++){
        for(int col = 0; col < NUM_SQUARES; col++){
          board[row][col] = 0;
        }
      }
    }
    
    void shuffleBoard(){
      for(int row = 0; row < NUM_SQUARES; row++){
        for(int col = 0; col < NUM_SQUARES; col++){
          int randVal;
          do{
            randVal = int(random(1, NUM_SQUARES*NUM_SQUARES+1) );
          }while( searchFor(randVal) );
          board[row][col] = randVal;
          boardColors[row][col] = color(random(0,255), random(0,255), random(0,255));
        }
      }
    }
    
    void drawBoard(){
      //visual representation of the board
      for(int row = 0; row < NUM_SQUARES; row++){
        for(int col = 0; col < NUM_SQUARES; col++){
          fill(boardColors[row][col]);
          rect(col * sqSide, row * sqSide, sqSide, sqSide);
          fill(255);
          textSize(30);
          text(board[row][col], (col + 0.5) * sqSide, (row + 0.5) * sqSide);
        }
      }
    }
    
    boolean searchFor(int itemToBeSearched){
      for(int i = 0; i < NUM_SQUARES; i++){
        for(int j = 0; j < NUM_SQUARES; j++){
          if(itemToBeSearched == board[i][j]){
            return true;
          }
        }
      }
      return false;
    }
    
    void draw(){
      background(255);
      // draw based on state
      if(gameOver()){
        // render game over state
        String s = "Congratulations!"; 
        String d = "Click to start again!";
        fill(0);
        text(s, 125, 225);
        text(d, 125, 250);
        // reset game only if clicking in game over state 
        if(mousePressed == true){
          setupGame();
        }
      }else{
        // render game state
        startTimer.countUp();
        // re-render board 
        drawBoard();
        // render text on top
        fill(0);
        text(startTimer.getTime(), 20, 30);
      }
    }
    
    
    
    void mousePressed(){
      clickedRow = int(mouseY/sqSide);
      clickedCol = int(mouseX/sqSide);
    }
    
    void mouseReleased(){
      releasedRow = int(mouseY/sqSide);
      releasedCol = int(mouseX/sqSide);
      //swap
      int buffer = board[clickedRow][clickedCol];
      board[clickedRow][clickedCol] = board[releasedRow][releasedCol];
      board[releasedRow][releasedCol] = buffer;
    }
    
    //definition of gameOver
    boolean gameOver(){
      int counter=1;
      for(int row=0; row<NUM_SQUARES; row++){
        for(int col=0; col<NUM_SQUARES; col++){
          if(board[row][col] !=counter){
            return false;
          }
          counter++;
        }
      }
      return true;
    }
    
    class Timer{
      float time;
      
      Timer(float set){
        time = set;
      }
      float getTime(){
        return(time);
      }
      void setTime(float set){
         time = set;
      }
      void countUp(){
        time += 1 / frameRate;
      }
    }
    

    It might make it easier to organise a basic finite state machine(FSM). It sounds more complicated than it is: the basic idea is to isolate functionality per state and tightly handle transitions between states which allow you to reset/update data accordingly. For example a game play state will only have to deal with gameplay related data until the state completes. Similarly the game over state only deals with it's data until it completes (when a user clicks). There are many ways you can write this.

    Here's a suggestion organised around how a Processing sketch runs:

    • setup() is where things get set up once at the start
    • draw() is where things get updated/rendered
    • mouse / key / etc. events handle user input

    You're already using a class (Timer) this will be very similar:

    // the two states
    StateDisplay playState;
    StateDisplay gameOverState;
    // a reference to the current state
    StateDisplay currentState;
    
    void setup(){
      size(500, 500);
      textAlign(CENTER);
      // setup each state
      playState = new PlayState();
      gameOverState = new GameOverState();
      // set the reference to the 1st state
      currentState = playState;
    }
    
    // forward events to the current state: doesn't matter if it's play or game over, they're can all handle it
    void draw(){
      currentState.draw();
    }
    
    void mousePressed(){
      currentState.mousePressed();
    }
    
    void mouseReleased(){
      currentState.mouseReleased();
    }
    
    // global function to be called by each state as it exits
    // this does the state switch: reset state data as required
    void onStateExit(StateDisplay exitingState){
      // game play state to game over state
      if(exitingState == playState){
        // cast each state to access specialised variables
        // in this case store the last timer value to display on the game over screen
        ((GameOverState)gameOverState).timerValue = ((PlayState)playState).startTimer.getTimeFormatted();
        // set the state
        currentState = gameOverState;
      }
      // game over state to game play state
      if(exitingState == gameOverState){
        // reset game
        playState.setup();
        // set the state
        currentState = playState;
      }
    }
    
    class Timer{
      float time;
      
      Timer(float set){
        time = set;
      }
      
      float getTime(){
        return time;
      }
      
      String getTimeFormatted(){
        return nfc(time, 2);
      }
      
      void setTime(float set){
         time = set;
      }
      
      void countUp(){
        time += 1 / frameRate;
      }
    }
    
    // "parent" state class that all other states will extend
    // and inherit functionality from
    class StateDisplay{
      
      StateDisplay(){
        // as soon as the constructor is called, set up this state
        this.setup();
      }
      
      void setup(){
        // to be overriden by subclass
      }
      
      void draw(){  
        // to be overriden by subclass 
      }
        
      
      void mousePressed(){
        // to be overriden by subclass
      }
      
      void mouseReleased(){
        // to be overriden by subclass
      }
    
    }
    
    class PlayState extends StateDisplay{
      
      final int NUM_SQUARES = 4;
      int[][] board;
      int[][] boardColors;
      int sqSide;
      Timer startTimer;
      
      int clickedRow, clickedCol, releasedRow, releasedCol;
      
      void setup(){
        setupGame();
      }
      
      void setupGame(){
        this.board       = new int[NUM_SQUARES][NUM_SQUARES];
        this.boardColors = new int[NUM_SQUARES][NUM_SQUARES];
        
        sqSide = width/NUM_SQUARES;
        startTimer = new Timer(0);
        //populate the board
        //generate random number
        //check if we have it already inside the array
        //if we have, then go on to generate another random number
        //if we do not have it, then we can store inside array
        for(int row = 0; row < NUM_SQUARES; row++){
          for(int col = 0; col < NUM_SQUARES; col++){
            int randVal;
            do{
              randVal = int(random(1, NUM_SQUARES*NUM_SQUARES+1) );
            }while( searchFor(randVal) );
            board[row][col] = randVal;
            boardColors[row][col] = color(random(0,255), random(0,255), random(0,255));
          }
        }
      }
      
      
      boolean searchFor(int itemToBeSearched){
        for(int i = 0; i < NUM_SQUARES; i++){
          for(int j = 0; j < NUM_SQUARES; j++){
            if(itemToBeSearched == board[i][j]){
              return true;
            }
          }
        }
        return false;
      }
      
      //definition of gameOver
      boolean gameOver(){
        int counter=1;
        for(int row=0; row<NUM_SQUARES; row++){
          for(int col=0; col<NUM_SQUARES; col++){
            if(board[row][col] !=counter){
              return false;
            }
            counter++;
          }
        }
        return true;
      }
    
    
      
      void drawBoard(){
        //visual representation of the board
        for(int row = 0; row < NUM_SQUARES; row++){
          for(int col = 0; col < NUM_SQUARES; col++){
            fill(boardColors[row][col]);
            rect(col * sqSide, row*sqSide, sqSide, sqSide);
            fill(255);
            textSize(30);
            text(board[row][col], (col+0.5) * sqSide, (row + 0.5) * sqSide);
          }
        }
      }
      
      void draw(){
        // clear screen
        background(255);
        // redraw board
        drawBoard();
        // optional: drag and drop visual cue
        if(mousePressed){
          fill(255, 192);
          text(board[clickedRow][clickedCol], mouseX, mouseY);
        }
        
        startTimer.countUp();
        fill(255);
        text(startTimer.getTimeFormatted()+"s", width * 0.5, 30);
      }
      
      void mousePressed(){
        clickedRow = int(mouseY/sqSide);
        clickedCol = int(mouseX/sqSide);
      }
      
      void mouseReleased(){
        releasedRow = int(mouseY/sqSide);
        releasedCol = int(mouseX/sqSide);
        //swap
        int buffer = board[clickedRow][clickedCol];
        board[clickedRow][clickedCol] = board[releasedRow][releasedCol];
        board[releasedRow][releasedCol] = buffer;
        
        if(gameOver()){
          onStateExit(this);
        }
      }
      
    }
    
    class GameOverState extends StateDisplay{
      // time taken to solve
      String timerValue = "";
    
      void draw(){
        String s = "Congratulations!\n" +
                   "Solved in " + timerValue + "s\n" + 
                   "Click to start again!";
        background(255);
        fill(0);
        text(s, width * 0.5, 225);
      }
      
      void mouseReleased(){
        onStateExit(this);
      }
      
    }
    

    The code may look verbose but essentially most of the work that's doing is grouping functionality per state: as if you're running mini-sketches and swapping between them.

    The things that may be new are:

    • extending a class: the class that extends (subclass) will inherit properties/methods from the parent class (superclass)
    • polymorphism: relying on what subclasses have in common with the superclass to treat them as if they are the same (though internally the implementation for each is different). (e.g. all states have draw(), but each state renders something else)
    • casting: in this particular case allowing subclass to behave as itself (with all it's extra properties/methods), not just what's shared from the superclass. More specifically in the code above the timer data is passed from game play state to the game over state to display.

    Even though it's a bit more work to set up this way it may be worth doing to keep states isolated and easily add more states using the same logic (e.g. a high scores stable, a start / game menu screen / etc.). With each state as a min-sketch you can also keep it organised with tabs, saving time having scroll up and down all the time:

    tab management using the Processing IDE