Week 3: Object Oriented Game Ballz

Overview:

This weeks task was to apply modularity concepts for object oriented programming through the of classes in either a game or work of art. I decided to try my hand at making knockoff of a (what I though was simple) IOS game Ballz. In this game the player shoots off a series of balls that bounce off tiles until they are destroyed. The tiles must not reach the bottom of the screen otherwise the player loses.

Programming: 

To accomplish the object oriented process, I used two custom objects, the ball and the block, and then a central main runner with several assisting methods.

My first task was to simulate an IPhone screen. I did this by maximizing the height or width, and constraining the screen to a 13 by 6 aspect ratio. By only printing in this rectangle. I had to define new variables as the native height and width values would not be applicable to my project. I also needed a game board area within the simulated IPhone screen which had to be just as wide as the phone, but smaller in height as a perfect fit for 7 x 9 tiles. With this area defined, I made a method that would reprint my new phone shaped background.

I then had to determine stages of the game. There are two main stages. First the player aims the ball, and can see where the shot will go based on a preview of balls on the screen, and then the game progresses to where the balls are actually moving and bouncing off the tiles.

The first stage was much more simple. I used polar coordinates determined based on a starting location of the ball at the bottom of the screen, and the players clicked mouse to create a line of balls coming out of the source. I could then save the angle this created and use it for the next stage of the game.

The next stage was far more complicated. I had to manage several balls, blocks, angles, and values all at the same time. For each ball I had to tell if it was hitting a block, and if so where it would go next. I had to determine the interaction each ball had on a collision, as well as other game mechanics such as the ability to get more balls.

This took some time but using equations to calculate which edge of a box a ball had hit, I was able to simulate the ball bouncing off of a box.

Results:

Here is a link to a google drive with the executables as well as the code

https://drive.google.com/drive/folders/1jCDt0v6tsLXJFusUTlFkGKHPYW1y-drF?usp=sharing

Here is the code:

Main Class

//Inspired by IOS game Ballz
//Created by Cole Beasley for Intro to Interactive Media
//7-8/2/21

import java.util.Iterator;  // Import the class of Iterator

//Designed for IPhone X screen res of 13:6 ratio, will be full screnned contained by height 

//To do
/*
Initalize board, set squares to be 7 sqaures across, 7 down, plus empty row on top and bottom (9 vertical rows total)
 Initialize ball with size proportional 1/6th of square size
 Have a difficulty value
 
 Every round:
 Include one "new ball" square
 Random number of squares 1-7 show up with random values, higher values as rounds go on, color of box based on value of box
 
 On shot line up
 Draw line of darker balls to show where shot will go from mouse pull back
 */

//Global Variables
int phoneWidth;
int phoneHeight;

float gameBoardOriginX;
float gameBoardOriginY;
float gameBoardHeight;
float gameBoardWidth;

int timer = 0;
int savedTime = 0;

float gridSize;  //Square dimensions without padding
float blockPadding = 0.1; //What percent of a block should be left to padding around it, 0 will be no padding and blocks touch, 1 will be 100% padding and no block visible
float blockSize;  //Square dimensions minus padding
float ballSize;

//2D array containing current block elements
Block blocks[][] = new Block[9][7];
ArrayList<Ball> balls = new ArrayList<Ball>();

//Game control variables
boolean newRound = true;  //Should the squares advance
boolean gameOver = false;  //Did they lose
boolean activeRound = false;  //Is a round running is is someone aiming
boolean firstBallDone = false;  //Has a ball landed to set the starting pos for next round
float ballStartPosX;  //Where the launch will come from 
float initTheta;  //The initial theta that will be used as a reference
float ballSpeed = 10;  //how fast the ball is going (going faster increases hit errors)
int ballTimer = 100;  //How frequently the balls are released, 1 = 1ms)
boolean firstBall = true;  //Controller as to if the first ball has landed yet to set ballStartPosX

//Mouse control variables
boolean mouseDown = false;
int mouseOriginX;
int mouseOriginY;

//Game score
int gameScore = 1;
int highScore = 1;
int reserveBalls = 1;

//Ball Colors
color ballColor = color(200);
color ballPreviewColor = color(150);

void setup() {
  fullScreen();

  //Set "phone" screen to 13x6 aspect ratio (IPhone X)
  if (height / 13 > width / 6) {
    phoneWidth = width;
    phoneHeight = int((width / 6) * 13);
  } else {
    phoneHeight = height;
    phoneWidth = int((height / 13) * 6);
  }
  //Set basic variables defining game area width height and pos
  gameBoardWidth = phoneWidth;
  gameBoardHeight = (phoneWidth / 7) * 9.1666;
  gameBoardOriginX = (width/2) - (gameBoardWidth/2);
  gameBoardOriginY = (height/2) - (gameBoardHeight/2);
  //Set block sizes
  gridSize = gameBoardWidth / 7;
  blockSize = gridSize - (gridSize * blockPadding);
  ballSize = gridSize/6;
  ballStartPosX = width/2;
}


void draw() {
  //Draw the game board background
  gameBackground();

  //Check if it is a new round
  if (newRound) {
    advanceRound();
    newRound = false;
  }

  drawSquares();

  //Detect if game is over
  if (gameOver()) {
    noLoop();
  }

  //Print out game score, highscore
  textAlign(CENTER, BOTTOM);
  textSize(30);
  fill(255);
  text("Score " + gameScore, width/2, gameBoardOriginY);
  textAlign(LEFT, BOTTOM);
  textSize(20);
  text("Highscore: " + highScore, gameBoardOriginX, gameBoardOriginY);

  //If round is not yet running
  if (!activeRound) {
    //Draw ball at start pos
    fill(ballColor);
    noStroke();
    ellipse(ballStartPosX, gameBoardOriginY + gameBoardHeight - ballSize/2, ballSize, ballSize);

    //If mouse is down and in the board and below original click, draw preview balls
    if (mouseDown && mouseY > mouseOriginY && mouseInBounds()) {
      float initX = ballStartPosX;
      float initY = gameBoardOriginY + gameBoardHeight - ballSize/2;
      float w = mouseX - mouseOriginX;
      float h = mouseY - mouseOriginY;
      float theta = atan(h/w);
      //Adjust if in other quad
      if (theta > 0) {
        theta += PI;
      }
      initTheta = theta;
      float r = 50; //Distance between preview balls
      float x = r * cos(theta);
      float y = r * sin(theta);
      fill(ballPreviewColor);
      while ((initX + x - (ballSize/2) > gameBoardOriginX && initX + x + (ballSize/2) < gameBoardOriginX + gameBoardWidth) && (initY + y - (ballSize/2) > gameBoardOriginY && initY + y + (ballSize/2) < gameBoardOriginY + gameBoardHeight)) {
        pushMatrix();
        translate(initX, initY);
        ellipse(x, y, ballSize, ballSize);
        x = r * cos(theta);
        y = r * sin(theta);

        //Adjust conditions in statement
        initX += x;
        initY += y;

        popMatrix();
      }
    }
  }
  //Round is running
  else {
    //Spawn in more balls if there are some still and the timer has passed half a second
    timer = millis() - savedTime;
    if (reserveBalls > 0 && timer >= ballTimer) {
      savedTime = millis();  //Reset timer for future ball spawns
      balls.add(new Ball(ballColor, ballStartPosX, gameBoardOriginY + gameBoardHeight - ballSize/2, initTheta, ballSize));
      reserveBalls--;
    }
    //Draw balls and advance their pos
    Iterator<Ball> ballIterator = balls.iterator();
    while (ballIterator.hasNext()) {
      Ball ball = ballIterator.next(); // must be called before you can call i.remove()

      //Move the ball
      Block hitBlock = ball.moveBall(ballSpeed, blocks, gameBoardOriginX, gameBoardOriginX + gameBoardWidth, gameBoardOriginY, gameBoardOriginY + gameBoardHeight, gridSize);

      //See if a block was hit
      if (hitBlock != null) {
        //See if it is a ball increase block
        if (hitBlock.freeBall) {
          gameScore++;
          blocks[hitBlock.gridY(ball.y, gridSize, gameBoardOriginY)][hitBlock.gridX(ball.x, gridSize, gameBoardOriginX)] = null;
        } else {
          hitBlock.decrease();
          //See if block was destroyed
          if (hitBlock.strength <= 0) {
            blocks[hitBlock.gridY(ball.y, gridSize, gameBoardOriginY)][hitBlock.gridX(ball.x, gridSize, gameBoardOriginX)] = null;
          }
        }
      }
      //Check to see if any balls should be removed 
      if (ball.y > gameBoardOriginY + gameBoardHeight) {
        //If it is the first ball to be removed, set the next rounds start position to its x pos
        if (firstBall) {
          firstBall = false;
          ballStartPosX = ball.x;
        }
        ballIterator.remove();
      } else {
        ball.drawBall();
      }
    }
    //Check to see if the round ended
    if (balls.size() == 0 && reserveBalls == 0) {
      newRound = true;
      activeRound = false;
      reserveBalls = gameScore;
      if (gameScore > highScore) {
        highScore = gameScore;
      }
    }
  }
}

//Acts like background function, but draws for aspect ratio of phone
void gameBackground() {
  //translate to center of screen
  pushMatrix();
  translate(width/2, height/2);

  //draw main game background dark gray
  rectMode(CENTER);
  noStroke();
  fill(30);
  rect(0, 0, phoneWidth, phoneHeight);

  //draw board area
  fill(20);
  //Game board width is defined as the max width, gameboard ratio is 9.166/7 height to width
  rect(0, 0, gameBoardWidth, gameBoardHeight);
  popMatrix();
}


//When called advances the round, moves all the blocks down a row and creates a new row in the block matrix
void advanceRound() {
  //First move each square down a row
  for (int i = 8; i > 0; i--) {
    for (int j = 0; j < 7; j++) {
      blocks[i][j] =  blocks[i-1][j];
    }
  }
  //Clear first row
  for (int i = 0; i < 7; i++) {
    blocks[1][i] = null;
  }
  //Generate new row
  for (int i = 0; i < 7; i++) {
    //random chance for new block to be added
    if (random(1) > 0.5) {
      //Get random strength valuse roughly twice current score
      int strength = int(random(0.8, 2) * gameScore) + 1;
      blocks[1][i] = new Block(strength);
    }
  }
  //Set the one block which gives you a ball
  Block ballBlock = new Block();
  ballBlock.freeBall = true;
  blocks[1][int(random(0, 7))] = ballBlock;

  firstBall = true;
}

//Draw the squares
void drawSquares() {
  for (int i = 0; i < 9; i++) {
    for (int j = 0; j < 7; j++) {
      if (blocks[i][j] != null) {
        blocks[i][j].drawBlock(gameBoardOriginX + (gridSize/2) + (j*gridSize), gameBoardOriginY + (gridSize/2) + (i*gridSize), blockSize);
      }
    }
  }
}

//See if game is over
boolean gameOver() {
  for (int i = 0; i < 7; i++) {
    if (blocks[8][i] != null) {
      //Print game over
      textAlign(CENTER, CENTER);
      textSize(50);
      fill(255);
      text("Game Over =(", width/2, height/2);
      return true;
    }
  }
  return false;
}

//Mouse processing
void mousePressed() {
  //Make sure not in round and not yet counted as clicked
  if (!activeRound && !mouseDown) {
    mouseDown = true;
    mouseOriginX = mouseX;
    mouseOriginY = mouseY;
  }
}
void mouseReleased() {
  //Make sure not in round
  if (!activeRound) {
    mouseDown = false;
    //Start round if mouse pos Y is below origin y
    if (mouseY > mouseOriginY) {
      activeRound = true;
      savedTime = millis() - ballTimer;
    }
    mouseOriginX = 0;
    mouseOriginY = 0;
  }
}

//Check to see if mouse is on board
boolean mouseInBounds() {
  if ((mouseX > gameBoardOriginX && mouseX < gameBoardOriginX + gameBoardWidth) && (mouseY > 0 && mouseY < height)) {
    return true;
  }
  return false;
}

Ball Class

class Ball {

  boolean directionY = true; //true = down
  boolean directionX = true; //true = right
  float incrementX = 0;
  float incrementY = 0;
  float theta;
  float ballSize;
  float x = 0;
  float y = 0;
  color colorValue;

  Ball(color colorVal, float x, float y, float theta, float ballSize) {
    colorValue = colorVal;
    this.x = x;
    this.y = y;
    this.theta = theta;
    this.ballSize = ballSize;
  }

  void drawBall() {
    ellipseMode(CENTER);
    fill(colorValue);
    noStroke();
    ellipse(x, y, ballSize, ballSize);
  }

  void moveBall(float speed) {
    incrementX = speed * cos(theta);
    incrementY = speed * sin(theta);
    x += incrementX;
    y += incrementY;
  }

  Block moveBall(float speed, Block blocks[][], float minX, float maxX, float minY, float maxY, float gridSize) {
    incrementX = speed * cos(theta);
    incrementY = speed * sin(theta);
    //If ball has bounced into a vertical wall
    if (checkTopBound(incrementY, minY)) {
      y = minY + ballSize/2;
      incrementY = 0;
      theta *= -1; //Adjust theta
    }
    //Check horizontal walls
    if (checkHorizontalLeftBound(incrementX, minX)) {
      x = minX + ballSize/2;
      incrementX = 0;
      //Adjust theta
      if (theta >= 0)
        theta = PI - theta;
      else
        theta = -PI - theta;
    }
    if (checkHorizontalRightBound(incrementX, maxX)) {
      x = maxX - ballSize/2;
      incrementX = 0;
      //Adjust theta
      if (theta >= 0)
        theta = PI - theta;
      else
        theta = -PI - theta;
    }

    //Check for a box collision
    Block hitBlock = checkBoxCollision(blocks, minX, minY, gridSize);
    
    x += incrementX;
    y += incrementY;
    
    if(hitBlock != null){
      return hitBlock;
    }
    return null;
  }

  //Check vertical bounds of game area
  boolean checkTopBound(float incrementY, float minY) {
    //Check if gone off top
    if ((y + incrementY - ballSize/2) < minY) {
      return true;
    }
    return false;
  }
  //Check walls of game area
  boolean checkHorizontalRightBound(float incrementX, float maxX) {
    //Check for too far right
    if ((x + incrementX + ballSize/2) > maxX) {
      return true;
    }
    return false;
  }
  boolean checkHorizontalLeftBound(float incrementX, float minX) {
    //Check if gone off left
    if ((x + incrementX - ballSize/2) < minX) {
      return true;
    }
    return false;
  }

  //Check to see if a box has been hit, return box that was hit if hit
  Block checkBoxCollision(Block blocks[][], float minX, float minY, float gridSize) {
    //First get which grid the ball is in and which grid it will be in
    int initGridX = int((x - minX)/gridSize);
    int initGridY = int((y - minY)/gridSize);
    int newGridX = int(((x + incrementX) - minX)/gridSize);
    int newGridY = int(((y + incrementY) - minY)/gridSize);

    //Check to see if ball has gone off the bottom
    if (initGridY == 9) {
      initGridY = 8;
    }
    if (newGridY == 9) {
      newGridY = 8;
    }
    //Check to see if moving to a different grid
    if (initGridX != newGridX || initGridY != newGridY) {
      //Check to see if new grid has a block
      if (blocks[newGridY][newGridX] != null) {
        //Check to see which edge has been hit first
        //Edge values
        float leftEdge = minX + (newGridX * gridSize);
        float rightEdge = minX + ((newGridX+1) * gridSize);
        float topEdge = minY + (newGridY * gridSize);
        float bottomEdge = minY + ((newGridY+1) * gridSize);
        //Which edges have been hit
        boolean hitLeft = false;
        boolean hitRight = false;
        boolean hitTop = false;
        boolean hitBottom = false;

        //Check if vertical slope first
        if ((x+incrementX)-x != 0) {
          //Slope of line between oringal point and collision point
          float m = ((y+incrementY)-y)/((x+incrementX)-x);

          //Check which two edges have been hit
          //Top edge
          if (((topEdge-y)/m)+x > leftEdge && ((topEdge-y)/m)+x < rightEdge) {
            hitTop = true;
          }
          //Bottom Edge
          if (((bottomEdge-y)/m)+x > leftEdge && ((bottomEdge-y)/m)+x < rightEdge) {
            hitBottom = true;
          }
          //Left edge
          if (m*(leftEdge-x)+y < bottomEdge && m*(leftEdge-x)+y > topEdge) {
            hitLeft = true;
          }
          //Right edge
          if (m*(rightEdge-x)+y < bottomEdge && m*(rightEdge-x)+y > topEdge) {
            hitRight = true;
          }

          //Calculate which of the two is closest
          if (hitRight && hitLeft) {
            //println("1");
            if (dist(x, y, leftEdge, y) < dist(x, y, rightEdge, y)) {
              hitRight = false;
            } else {
              hitLeft = false;
            }
          } else if (hitRight && hitTop) {
            //println("2");
            if (dist(x, y, rightEdge, y) < dist(x, y, x, topEdge)) {
              hitTop = false;
            } else {
              hitRight = false;
            }
          } else if (hitRight && hitBottom) {
            //println("3");
            if (dist(x, y, rightEdge, y) < dist(x, y, x, bottomEdge)) {
              hitBottom = false;
            } else {
              hitRight = false;
            }
          } else if (hitLeft && hitTop) {
            //println("4");
            if (dist(x, y, leftEdge, y) < dist(x, y, x, topEdge)) {
              hitTop = false;
            } else {
              hitLeft = false;
            }
          } else if (hitLeft && hitBottom) {
            //println("5");
            if (dist(x, y, leftEdge, y) < dist(x, y, x, bottomEdge)) {
              hitBottom = false;
            } else {
              hitLeft = false;
            }
          } else if (hitTop && hitBottom) {
            //println("6");
            if (dist(x, y, x, bottomEdge) < dist(x, y, x, topEdge)) {
              hitTop = false;
            } else {
              hitBottom = false;
            }
          }
        }
        //Vertical slope
        else {
          //println("7");
          if (dist(x, y, x, bottomEdge) < dist(x, y, x, topEdge)) {
            hitBottom = true;
          } else {
            hitTop = true;
          }
        }

        //Determine if the block being hit is actually the free ball block
        if(!blocks[newGridY][newGridX].freeBall){
          //Transform theta if hitting vertal side
          if (hitLeft || hitRight) {
            //Adjust theta
            if (theta >= 0)
              theta = PI - theta;
            else
              theta = -PI - theta;
          }
          //Transform theta if hitting horizontal side
          else {
            theta *= -1; //Adjust theta
          }
        }
      }
      return blocks[newGridY][newGridX];
    }
    return null;
  }
}

 

Block Class

class Block {
  color colorValue;
  int strength;
  color colorArray[] = {color(#efb92b), color(#c0c038), color(#82b54a), color(#c6654c), color(#dd3a4d), color(#df1375), color(#1f76ba), color(#17998c)};
  boolean freeBall = false;  //Is this a block that when you hit it you get a ball?

  Block() {
    colorValue = color(int(random(0, 255)), int(random(0, 255)), int(random(0, 255)));
    strength = 10;
  }  

  //Block constructor that takes in a strength value and associates a color based on this
  Block(int strenth) {
    this.strength = strenth;
    colorValue = getColor(strength);
  }

  //Draw the block, parameters are: x position, y position: height of side
  void drawBlock(float x, float y, float sideLength) {
    if (!freeBall) {
      pushMatrix();
      translate(x, y);
      rectMode(CENTER);
      noStroke();
      fill(colorValue);
      rect(0, 0, sideLength, sideLength);
      fill(0);
      textAlign(CENTER);
      textSize(20);
      text(strength, 0, 10);
      popMatrix();
    } else {
      pushMatrix();
      translate(x, y);
      ellipseMode(CENTER);
      noStroke();
      fill(255);
      ellipse(0, 0, sideLength/4, sideLength/4);
      fill(0);
      ellipse(0, 0, sideLength/6, sideLength/6);
      popMatrix();
    }
  }

  //Decrease value and color, called when a ball hits it
  void decrease() {
    strength--;
    colorValue = getColor(strength);
  }

  //Get its grid x value, typically 0-6
  int gridX(float x, float grid, float minX) {
    return int((x - minX)/grid);
  }

  //Get its grid x value, typically 0-8
  int gridY(float y, float grid, float minY) {
    return int((y - minY)/grid);
  }

  //Computes the color of block based on its strength
  color getColor(int val) {
    //Yellow
    if (val <= 5){
      return colorArray[0];
    }
    //Olive
    else if(val > 5 && val <= 10){
      return colorArray[1];
    }
    //Green
    else if(val > 10 && val <= 15){
      return colorArray[2];
    }
    //Orange
    else if(val > 15 && val <= 20){
      return colorArray[3];
    }
    //Red
    else if(val > 20 && val <= 30){
      return colorArray[4];
    }
    //Pink
    else if(val > 30 && val <= 50){
      return colorArray[5];
    }
    //Blue
    else if(val > 50 && val <= 100){
      return colorArray[6];
    }
    //Teal
    else{
      return colorArray[7];
    }
  }
}

 

Some screenshots from the game

 

Problems:

There are several aspects of the game I am not quite happy with. For starters, the way Processing deals with movement is by stepping a shape a certain number of pixels. This is problematic as if a step is too large, it could skip a series of pixels where other shapes lie and as a result a collision is not detected. This is observed in my game when a ball hits a corner of a box, it often behaves as if the box is not there. This Ould be fixed with more rigorous calculations, but would also make the game more resource dependent as every grid would have to be checked for every ball which could quickly get out of hand.

I would also like to implement the ability to restart the round, but I ran out of time. In theory this would be easy as variables would just have to be reset and the game board cleared.

Leave a Reply