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.



