**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.