I was quite excited for this week’s project as it was about making a game and thought of the ideas that I could pursue to make a game that would be fun to play. I was initially thinking of something where the user has a specific time limit to book the gym slot and if he is unable to do so, he loses the slot. But then I realized that the game does not implement the concept of classes that well and hence won’t be that relevant to this week’s topic. Therefore, I thought of making a shooter game in which you battle the world’s number one enemy, the Corona virus!
Difficulties:
Although this game was really fun to make, I did run into a bunch of really annoying problems. The first major problem I ran into was to make bullets that would appear and shoot on my command. In my first attempt of doing so, I had created something in which when I would press the command to shoot, the bullet would just extend indefinitely and not work as a bullet does. After this was resolved, I faced another challenge of aligning the movement of the bullet with that of the player. Initially, when I would move the player, the bullet would not move accordingly and hence I needed to figure a way around that.
The next major challenge that I faced was to actually find a game mechanic that would allow the bullet to kill the virus when it touched it. I was struggling to find a clean and simple solution to this problem as I was going with the incorrect approach of emphasizing the fact that whether a bullet has gone through many viruses or not and making checks for that. This logic obviously did not work as it was actually the viruses that needed to be checked and seen if the bullet passed through them or not. Once I changed my logic and coded accordingly, the function began to work.
Resolution:
In order to properly implement the bullet mechanics, I decided to nest the bullet class in the player class. This allowed to easily link the data of the player with that of the bullet and made it easy to correlate their positions. Previously, I had experimented with the bullet class inheriting from the player class but realized that the player and bullet do not have a Is-a relationship but have a Has-a relationship and therefore nesting would work best for this problem (I could have used composition but decided to go with nesting).
The way I tackled the problem of killing the virus with the bullet was by creating a function that would check if the bullet is in the vicinity of the virus and then running it to check for every virus and every bullet.
Ideation:
My ideation for this task primarily involved the following:
Game Concept Idea
Game Play Idea
Game Concept:
For the concept, as I had mentioned before, I was inclined towards making a game for the gym slots but this idea of a shooting game, dawned upon me when I was siting in a cafe and just being frustrated over the fact that my mask was irritating me.
Game Play:
As I started building this game, I knew that I would be building a top down game wherein the virus appears to be falling down the screen and the player is positioned in the end of the screen and can only move in the horizontal direction while his bullets travel vertically up and can interact with the virus to kill them. I believe that for the time being the top down approach works well but I would like to experiment with different view points of playing the game. I tried looking at the first person perspective but apparently that required P3D rendering. However, this could be something I could try out for my midterm project.
Notes:
While this game solves the purpose of this week’s assignment, I am hopeful that I can develop this much better than this and probably turn it into my midterm project. This version is therefore a prototype that will be worked upon as I learn more about processing in the following weeks.
Concepts Learned:
This project taught me how to nest classes and how to better link one class to the next.
It taught me to scout for game art and then effectively apply them in my own projects using the PImage, img and images() functions in Processing.
This project taught me how to work with the ArrayList function.
Project Video:
Project Code:
Main draw() Class
ArrayList<Virus> viruses = new ArrayList<Virus>(); //gives the list of viruses
Player player; //player object
boolean shoot = false; // ensures that the player only shoots when commanded
ArrayList<Player.Bullet> bullets = new ArrayList<Player.Bullet>(); //bullet list
void setup()
{
size(1280, 960);
imageMode(CENTER);
viruses.add(new Virus(random(width*0.25, width*0.75), 0, int(random(1, 3)))); //initialising the list of viruses to spawn in between the roads of the game
player = new Player();
}
void draw()
{
PImage bg;
bg = loadImage("background-1.png");
bg.resize(int(width),int(height));
background(bg); //background is the top down view of a road
Player_Mechanics();
Virus_Mechanics();
}
void Player_Mechanics()
{
player.drawPlayer(); //draws the Player on Screen
for(int i =0; i< bullets.size(); i++)
{
bullets.get(i).draw_Bullet();
bullets.get(i).shoot_Bullet(); //if bullets are present, draws them and shoots them
if(bullets.get(i).bullet_posY_1 < 0 )
bullets.remove(i); //removes the bullet once they are out of frame
}
}
void Virus_Mechanics()
{
for (int i = 0; i < viruses.size(); i++)
{
viruses.get(i).drawVirus();
viruses.get(i).moveVirus(); //draws and moves virus on screen
if(viruses.get(i).posY > height + viruses.get(i).size)
viruses.remove(i); //removes the virus once they are out of frame
if( bullets.size() > 0 && viruses.size() > 0 )
{
for(int j=0; j<bullets.size(); j++)
{
if( bullets.size() > 0 && viruses.size() > 0 )
{
if(viruses.get(i).vicinty_check(bullets.get(j).bullet_posX,bullets.get(j).bullet_posY_2))
{
viruses.remove(i); //kills the virus and removes it once bullet touches it
}
}
}
}
}
}
void keyPressed()
{
if (key == ' ')
{
viruses.add(new Virus(random(width*0.25, width*0.75), 0, int(random(1, 3)))); //spawns a virus
}
if (key == 'a')
{
player.moveLeft(); //player goes left
}
if (key == 'd')
{
player.moveRight(); //player goes right
}
if (key == 'f')
{
bullets.add(player.new Bullet(0)); //shoots bullets
}
}
Player Class
class Player {
public float posX = width/2;
public float posY = height-100;
PImage player;
int movement=20;
int size =200;
Player()
{
player = loadImage("player_rifle.png");
imageMode(CENTER);
}
void drawPlayer()
{
player.resize(size, 0);
pushMatrix();
translate(posX, posY);
rotate(radians(270));
image(player, 0, 0);
popMatrix(); //resizes and rotates the image of the player on screen so that it is appropriate for the game
}
void moveLeft()
{
if (posX > size/2)
posX -= movement; //player moves left
}
void moveRight()
{
if (posX < width-size/2)
posX += movement; //player moves right
}
class Bullet { //nested bullet class in player class
float bullet_posX = posX + size*0.15;
float bullet_posY_1 = posY - size/2;
float bullet_size = size * 0.1;
float bullet_posY_2 = posY - size/2 - bullet_size;
int bullet_speed = 20;
Bullet(float y)
{
bullet_posY_1 -= y;
bullet_posY_2 -= y; //assigns intial position to bullet
}
void draw_Bullet()
{
stroke(195,153,83);
strokeWeight(6);
line(bullet_posX, bullet_posY_1, bullet_posX, bullet_posY_2); //draws a line for a bullet
}
void shoot_Bullet()
{
bullet_posY_1 -= bullet_speed;
bullet_posY_2 -= bullet_speed; //moves bullet
}
}
}
Virus Class
class Virus {
float posX;
float posY;
int speed = 2;
int size;
PImage corona;
Virus(float x, float y, int s)
{
posX = x;
posY = y;
size = s*100;
corona = loadImage("corona.png");
imageMode(CENTER);
}
void drawVirus()
{
corona.resize(size, 0);
image(corona, posX, posY); //resizes and draws the virus
}
void moveVirus()
{
posY += speed; //moves the virus
}
boolean vicinty_check(float x, float y)
{
if ((x < posX+size/2 && x > posX-size/2) && (y < posY+size/2 && y > posY-size/2)) //checks if anything exists in the vicintiy of the virus's body
return true;
else
return false;
}
}
This time around I was feeling very inspired to improve on my previous week’s forest animation work. Although it did work, it didn’t work as it was intended with a background. Only after the class, I realized that I should have added for class and put all the curve inside the for loop and give a variable that would constantly instantiate on itself.
Laser show animation inspired by Art NOW. It’s mesmerizing to look at a laser show, just like fire you could stare at it endlessly. I tried to recreate the laser show by using multiple rectangles that will rotate from the center and clash between each other from different angles that would give the abstract laser-like feel.
This time, however, I tried to implement waves using sin as it reminds me of soundwaves, rotating them and doing it for rectangles would have created a unique look.
The difficulty was in aligning to the center, from the very first tries it would only be rotating over the top left corner until I remembered about the translate function to place all the rotations to happen right in the center of the screen.
Randomizing the angle for the rotate function creates a new pattern each and every time and that’s the beauty of generative art. strokeWeight(map(laser, -1, 1, 0.5,weight )); makes the lines fade out and look smooth. By putting mouseX now it is also interactive to speed up and have control over the rotating rectangles. The only backlash is it’s really difficult to control speed, I specifically don’t get why is it fast in the beginning.
I wanted to create an easy game to see how I can use all functions, arrays, and OOP we learned during the class. The first thing that came to my mind was the game “Breakout” (1976) by Steve Wozniak.
Since Valentine’s day is coming, I decided to connect my game with it, and breaking hearts was the most logical thing for me. So my game is the little arrow is destroying hearts on the pink circles. Cute and relevant!
Process
My plan looked like this: create circles as a class > make them randomly move > add a player with an arrow as a class > detect collisions between circles and an arrow > add score > work with the design of the game.
Let me explain in detail every step I had with problems which have arisen and solutions I’ve found.
Classes and OOP
I have 3 classes in total: “Object” which is hearts player breaks, “Player,” and “Bullets” which is an arrow (the first idea was to use bullets, and I called all variables based on that). I used “getter” and “setter” functions to be able to use and manipulate class variables outside of the class.
Random movement
Circled I created in the Object class should move randomly, and making them do so was the first problem I faced making this game. When I’ve just changed the X and Y coordinates, they all moved in the same direction, even though they were allocated randomly on the canvas. The next thing I used was the direction variable, but I couldn’t find a way to randomly change its value between 1 and -1. To make this movement possible, I modified this code. I included variables such as speed, direction, position for both X and Y coordinates, and angle with the offset of the object. Using angle and its sine with cosine functions allowed me to do a random movement.
Collisions and Score
The next problem I had was making bullets “collide” with the objects aka circles. To do it I used an ArrayList of Object class objects (read about them here and here), so I used it for a loop every time I wanted to use a function with an array list.
for (int i=0; i<objects.size(); i++) {
objects.get(i).RunProgram();
}
Then, after the program detects collision (with if statement), it removes the object which collided and adds 1 point to the score variable.
for (int i=0; i<objects.size(); i++) {
Object o = objects.get(i);
if (dist(b.getBulletX(), b.getBulletY(), o.getObjectPosX(), o.getObjectPosY()) <= b.getBulletRadius() + o.getObjectRadius()) {
objects.remove(i);
score++;
}
}
Design
As I said before, I decided to make this game in Valentine’s day theme, so I just added an image of the heart to the circles I had before and painted them different shades of pink. I also added a score in the upper left corner to make the interface look more friendly.
In this simple game, the player controls a spacecraft whose mission is to collect StarBits, however, they must avoid colliding with meteors or they will risk loosing all the progress they have made. Score is displayed on the upper left corner. When the player collides with a meteor, the spaceship becomes red for a while and the score keeps decreasing until they get out of the way of the meteor.
Idea
I got inspiration from one of Daniel Shiffman’s Coding Challenges titled Starfield and from Super Mario Galaxy in which Mario collects this star shaped things called Starbits.
Process
After watching the video and playing a little of Mario Galaxy with a friend, I decided to write down the classes and functions that I thought I would need for this assignment. I did this mainly because last assignment was very chaotic for me in terms of knowing what I needed to do, so I decided to write how I was thinking of making the code rather than just go at it.
This is what I ended up with:
I decided to star with the background and do the star object first because I thought that after watching the video, I could do it in a breeze, after all Shiffman’s video is 13 minutes.
I took a whole day to figure out how to do just the background. Also, the field that I was making only appeared on the lower right hand corner. Like this:
So I texted on the Discord chat and Professor Shiloh and Professor Aaron came up with a solution, thank God.
Anyways, moving on.
After doing the Stars, I proceeded to make the spaceship. I did two versions of it: one controlled with the keyboard and one with the mouse. I wasn’t sure which one to leave because I felt like they had a different range of motion and deciding would rely on how the meteors and starbits.
Once the controls for the spaceship were done and the image of it was resized, I decided to make the meteors.
(Just a side note: I know that I am actually doing meteroids and not meteors, but meteors is just easier on my poor brain).
I think this was the hardest part of the process. The meteors were just not cooperating with me. When they were immobile and I just placed them at random positions, they looked beautiful, but then, when I started to make the functions to move them, it was just horrible. My idea was to do a similar mechanic to the stars and increase the meteor’s sizes as it moved through the screen. Nonetheless, when I tried to resize it, the more time I left it moving, the blurrier the image got.
Like this:
After spending almost a day in useless attempts to better this, I decided to text on the Discord group and professor Aaron recommended using the scale() function along with changing the values I used to map the size of the meteor.
It worked, but I still had problems with the fact that the resizing was not stopping but getting way past the size that I wanted it to be. The problem was that I wasn’t aware that the map function did not restrain the value from continuing to increase, so I had to learn how to use constrain(). Furthermore, I also had to learn how to use the push/popMatrix() and the push/popStyle().
For the Starbits, I decided to use the star() function from the Processing reference. I had issues figuring out how to change the position of the star itself (every time I tried, what changes was the rotation or the size, but not the position). I don’t know if it is just me, but I found that some of the explanations given on the Processing reference could use a little more information about what the values passed down in the function represent.
After figuring out how to change the position of the Starbit, it was time to do the collisions. Professor Aaron taught me how to do it in an easier way than how I was doing it. There was a problem however with the values that the keyboard was giving. I printed them out and they were way too big in comparison with the location of the canvas. I later found out it was because the function was multiplying the value of the keyboard by two. I reused the same logic for detecting collisions to collect the Starbits. I then used the collision to change the tint of the spaceship to red when it got hit and to generate a new starbit in a random location when the player collected it.
When I put it all together, I didn’t really like the movement of the spaceship with the keyboard. It was too linear for the player to be able to avoid colliding with the meteors, so I opted for the mouse controls instead.
Finally, it was time to do the score. I had to watch a tutorial for the score and learn how to use the text() and its style functions. I placed conditions to increase the score when collecting the Starbit and decrease it when colliding with a meteor. I forgot to call the functions in the main which is why when I placed the code within the assignment, it wasn’t working.
I cried of happiness when I started to see the score change in the screen.
As a final touch, I decided to make the color of the score become red when the score dropped below 0.
Code for the Main Tab
// make variables and initialize them with the sizes of the array for the Stars
int sizeStars = 1000;
//make variable of class spaceship
Spaceship spacecraft;
//make variable of class meteor
Meteor meteor;
//make variable of class Starbit
StarBit starBit;
//make array of class Star
Star[] stars = new Star[sizeStars];
//make variable of class score
Score score;
void setup()
{
//set size and background
size(1000, 700);
background(0);
//call constructor for spacecraft
spacecraft = new Spaceship();
//call constructor for meteor
meteor = new Meteor();
//call constructor for Starbit
starBit = new StarBit();
//call constructor for score
score = new Score();
//create a Star object for each element in the array
for (int i = 0; i < sizeStars; i++)
{
stars[i]= new Star();
}
}
void draw()
{
background(0);
//display the Starbit using the function
starBit.display();
//update the position of the spacecraft
spacecraft.updatePosition();
//show the spacecraft
spacecraft.show();
//update the position of the meteor
meteor.updatePosition();
//show the meteor
meteor.show();
//check if there has been a collision to decrease score
score.collided();
//check if player collected Starbits to increase score
score.collected();
// check if the spacecraft changed tint from collision
spacecraft.changeTint();
//check if player collected Starbit to create a new Starbit
starBit.createStarBit();
//display current score
score.display();
//change position of the stars in the canvas
translate(width/2, height/2);
//update the position of the stars and show them in the canvas
for (int i = 0; i < sizeStars; i++)
{
stars[i].updatePosition();
stars[i].show();
}
}
Meteor Class
class Meteor
{
//create varaible to hold image of the meteor
PImage meteor;
//create variables for the x, y and z position of the stars
float x, y, z;
//create variable to hold the direction (speed) at which the meteor will move
float xDir, yDir;
//constructor
Meteor()
{
//load image of the meteor to the variable
meteor = loadImage("meteor.PNG");
//initialize the position of the meteor at random location
x = random(width);
//y can be left at 0 because the image size is 0 at the beginning
y = 0;
//assign random speed in the y direction
yDir = random(7, 20);
/*assign random speed in the x position depending on whether the value of x is
less than the width*/
if (x < width/2) {
xDir = random(7, 20);
} else {
xDir = random(-20, -7);
}
//testing at the center
//x = width/2;
//y = height/2;
//assign random value to z
z = random(width);
}
//function to reset the values of Meteor
void reset()
{
x = random(width);
y = 0;
z = width;
yDir = random(7, 20);
if (x < width/2) {
xDir = random(7, 20);
} else {
xDir = random(-20, -7);
}
}
//function to update the position of the stars as they move
void updatePosition()
{
//add speed to x and y
x += xDir;
y += yDir;
//changing the z position
z = z - 10;
// re-set the positions
if (z < 0)
{
reset();
}
}
//show the meteor in the canvas
void show()
{
//make variable that increases the size of the meteor as it travels through the screen
float size = map(z, 0, width, 0.8, 0.02);
//limit how bit the size can get
size = constrain(size, 0.02, 0.8);
//show the image
pushMatrix();
//translate to position of meteor so that it scales at the center of the image
translate(x, y);
//scale the meteor to its current size
scale(size);
//show the meteor
//center the image
pushStyle();
imageMode(CENTER);
image(meteor, 0, 0);
popStyle();
popMatrix();
}
}
Spaceship Class
class Spaceship
{
//create variable to hold image of the Spaceship
PImage spaceship;
//create variables for the x, y position of the spacecraft
int x, y;
int speed = 5;
boolean collided, collected;
//constructor
Spaceship()
{
//initialize the x and y position at the center
x = width/2;
y = height/2;
//load image of the meteor to the variable
spaceship = loadImage("SpaceShip.PNG");
}
//function to update the position of the spacecraft by tracking the mouse position
void updatePosition()
{
//assign the position of the mouse to the x and y variables of the spaceship
x = mouseX;
y = mouseY;
}
//show the spaceship in the canvas
void show()
{
//place the cursors at the center of the image
imageMode(CENTER);
//draw the spacecraft where the mouse is located
spaceship.resize(200, 200);
image(spaceship, x, y );
}
//returns true if the player had "collected" the starbit or false if they haven't
boolean checkCollect()
{
//make variable to calculate distance between the spaceship and the Starbit
float distance = dist(mouseX, mouseY, starBit.xPos, starBit.yPos);
//radius of space that the starBit occupies
float radius = 30;
/*if distance between the starbit and the spaceship is smaller than the radius,
then the player has collected the starbit*/
if (distance < radius)
{
return true;
}
//else, player hasn't collected the starbit
else
{
return false;
}
}
//returns true if the player has "collided" with a meteor or false if they haven't
boolean checkCollide()
{
//make variable to calculate distance between the spaceship and the meteor
float distance = dist(mouseX, mouseY, meteor.x, meteor.y);
//radius of space that the meteor occupies
float radius = 100;
/*if distance between the meteor and the spaceship is smaller than the radius,
then the player has collided with the meteor*/
if (distance < radius)
{
return true;
}
//else, player hasn't collided with the meteor
else
{
return false;
}
}
//change tint of the spaceship to red if the collision happened
void changeTint()
{
collided = checkCollide();
//collected = checkCollect();
if (collided == true)
{
pushStyle();
tint(255, 0, 0);
image(spaceship, mouseX, mouseY);
popStyle();
}
}
}
Star Class
class Star
{
//create variables for the x, y and z position of the stars
float x, y, z;
//constructor
Star()
{
//initialize the position of the stars at random locations
x = random(-width, width);
y = random(-height, height);
z = random(width);
}
//function to update the position of the stars as they move
void updatePosition()
{
//decrease value of z
z = z - 20;
//reset the star's position
if (z < 1)
{
x = random(-width, width);
y = random(-height, height);
z = width;
}
}
//show the star in the canvas
void show()
{
fill(255);
noStroke();
//divide the x and y position by x and map it with the width and height to move the stars
float newXpos = map(x/z, 0, 1, 0, width);
float newYpos = map(y/z, 0, 1, 0, height);
//increase radius of the star as the get closer to the end of the canva
float radius = map(z, 0, width, 10, 0);
//use a circle to draw the stars
circle(newXpos, newYpos, radius);
}
}
StarBit Class
class StarBit
{
//make variables for x and y position of the Starbit
int xPos, yPos;
/*radius1 is for how big the starBit will be, radius2 is for the dents between the
points of the StarBit*/
float radius1 = 20;
float radius2 = 40;
//make variable for the number of points in the starBit
int numPoints = 5;
//make variables to hold the red, green and blue colors of the rgb
int r, g, b;
//make bolean to check if StarBit was collected
boolean collected;
//constructor
StarBit()
{
/*assign random x and y positions to the starbits (the 35 is so that they don't)
appear outside the canva*/
xPos = round(random(35, width-35));
yPos = round(random(35, height-35));
//assign random values for the rgb
r = round(random(255));
g = round(random(255));
b = round(random(255));
}
//create a new starbit
void newStarBit()
{
xPos = round(random(35, width-35));
yPos = round(random(35, height-35));
r = round(random(255));
g = round(random(255));
b = round(random(255));
}
//make shape of starbit (taken from Processing refence)
void star(float x, float y, float radius1, float radius2, int npoints)
{
float angle = TWO_PI / npoints;
float halfAngle = angle/2.0;
beginShape();
for (float a = 0; a < TWO_PI; a += angle) {
float sx = x + cos(a) * radius2;
float sy = y + sin(a) * radius2;
fill(r, g, b);
vertex(sx, sy);
sx = x + cos(a+halfAngle) * radius1;
sy = y + sin(a+halfAngle) * radius1;
vertex(sx, sy);
}
endShape(CLOSE);
}
//display the starBit
void display()
{
pushMatrix();
//make the star rotate
translate(xPos, yPos);
rotate(frameCount/ -100.0);
//draw the starbit
star(0, 0, radius1, radius2, numPoints);
popMatrix();
}
//create starBit if the current starBit had been collected
void createStarBit()
{
//asign value returned by spacecraft.checkCollect()to variable
collected = spacecraft.checkCollect();
if (collected == true)
{
newStarBit();
}
}
}
Score Class
class Score
{
//make variable to hold the score
int score;
//make variable that will be used to display the score
String s;
//variables to check for staBit collection and meteor collisions
boolean collected, collide;
//constructor
Score()
{
//initialize score to 0
score = 0;
s = "Score: " + score;
}
//increase score if Starbit has been collected
void collected()
{
//assign value returned by spacecraft.checkCollect()
collected = spacecraft.checkCollect();
//increase score if starbit was collected
if (collected == true)
{
score ++;
}
//update s with the new score
s = "Score: " + score;
}
//decrease score if there was a collision with a meteor
void collided()
{
//assign value returned by spacecraft.checkCollide();
collide = spacecraft.checkCollide();
//if collided, decrease score until player gets away from the meteor
if (collide == true)
{
score --;
}
//update s with the new score
s = "Score: " + score;
}
//display the score
void display()
{
//if score is below 0, show the text in red
if (score < 0)
{
textSize(20);
textAlign(LEFT);
fill(255, 0, 0);
text(s, 10, 20);
}
//else show them in white
else
{
textSize(20);
textAlign(LEFT);
fill(255);
text(s, 10, 20);
}
}
}
Result:
Final Thoughts
I really like how this assignment turned out, but I must admit it was very difficult. I feel like most of this project was me doing something, seen it work when tested, then seen it failed once I implemented it in the actual code and asking for help. To be honest, I feel like most of the time I was asking for help because I had no idea of what I was doing wrong or how to start debugging.
I think one of my biggest problem was keeping track of whether I was calling the functions or not. I made a lot of classes and a lot of versions of the project because I was scared of messing up what I had, yet that also made it hard because I had one place in which I was testing the score, one in which I was testing the collisions, etc, and when I placed them together, it was very easy to forget to add stuff to the main tab. I guess this is something that I will fix with practice.
Another thing that was difficult was centering the images and keeping track of the points of reference used. At some point it became very confusing to know which point of reference was been used by which object. Another thing was that I didn’t really understood the difference between push/popMatrix and push/popStyle. Now I know, thank to Prof. Aaron. This made me realize that a lot of my problems could be fixed more easily if only I knew the correct syntax.
What I think was what made me very slow in my process, however, was coming up with the logic of the code. It was very extremely difficult to visualize what I was doing and think of other ways to accomplish something when my initial idea failed. I also think that this is something that will become easier the more I practice, but man it was hard (and at times discouraging), but still, after taking a break I feel like I was able to solve many issues by myself.
I am extremely happy with this mini game and after completing it I had a lot of fun playing (I even sent it to my parents so they could play).
This is the link to the GitHub with the code. If you decide to play it, I hope you enjoy it!
For week 3, my goal is to make the game 2048 with Processing. I think this is a pretty suitable game to practice the various concepts from object-oriented programming, since it has various objects with the same attributes, and the different components of the game interact with one another regularly.
The game is essentially a 4×4 board with small square 1×1 tiles appearing on it, with each tile having a value which is 2 raised to some power (2, 4, 8, 16, etc.). The user can control in which direction they want to move the tiles using the right, left, up, or down keys on their keyboard. Whenever two tiles with the same value (and color) collide, they merge into a tile with double the value of each individual tile. After each move, a new tile of the lowest value (2) will appear randomly in an empty position. The goal of the game is to maximize the value on the tiles, that is, merge as many tiles as possible. The game will end when the user either wins (they reach a 2048 tile) or loses (there is no more empty space on the board and hence no further move is possible).
Here is a short demo of my program:
And here is a diagram illustrating the overall interface and outlining the basic classes of the game:
The code for the game can be found at the end of the post.
A note before starting on the details, as you can tell from the demo, I am not done with the game! The following parts outline my progress so far, and I intend to continue working on the game, so if you want to see how it turns out, here is my GitHub.
Breakdown of classes
Tile
Class Tile provides a common structure for all the tiles that will be constructed while the game runs. It has a number of attributes, but the most important ones include value (the value displaying on the tile, which determines the colors of the tile and the text), rowPos and colPos (the current coordinate of the tile on the board, with [0, 0] being the top left corner and [3, 3] being the bottom right corner; they get updated every time the tile is moved), moving and doneMoving (as the names reflect; used to initialize movement of various tiles). The class also has a number of functions, most of which are used in other classes to control the tiles’ interactions with one another and with other components of the game. The most important ones are display(), moveTileV() and moveTileH() (used to make a tile move vertically or horizontally).
Cell
The cells can be thought of as the underlying game board. They have the same layout and coordinates as the tiles, but as the tiles are initialized, displayed (and disappear) one by one as the game proceeds and move around the board, the cells are immobile. The most important attributes of a cell include occupied (boolean value to indicate whether or not there is a tile presently lying on top of the cell; it gets updated as the tiles move around), rowPos and colPos (similar to tiles), and queue (each cell has its own array to store tiles that are currently in its position; normally a cell’s queue will either be empty (unoccupied) or has one tile (occupied), so when the queue has two elements, there is a collision of tiles and they will merge). Cells have few functions among themselves, since most cell-related interactions are configured in the next class.
Grid
Each Grid object has an attribute called cells which is an array of Cell objects. My intention is to make Grid a kind of wrapper class that acts as a container for all the cells and an mediator between Cell class and Game class. For this reason, Grid is not exactly a unique class on its own, and you can see that most of the functions of this class actually call functions on cells. Some of the most usually used functions include vacant() and occupy() (used after moving existing tiles or creating new tiles to update the occupied status of a cell), enqueue() and dequeue() (to add a tile to the queue of a cell that it’s just moved to, or to remove a tile from the queue of a cell that it’s leaving, sometimes retrieving and removing the tile at once), peek() (to retrieve a tile without removing it from the queue).
Game
This is the encompassing class, wrapping and coordinating the interactions of all other classes. Each Game object has a Grid object, which has a Cell object itself. It also has 4 array containing Tile objects (4 queues). Another attribute is a HashMap called keyHandler, which maps the four key codes RIGHT, LEFT, UP, and DOWN with a boolean value to indicate which key has been pressed by the user to trigger the right movement of the tiles.
The two functions randTile() and specificTile() are used to initialized new tiles to add to the game.
There are four functions to calculate the next possible move of a tile, corresponding to which key the user has pressed: getRightDest(), getLeftDest(), getTopDest(), getBottomDest().
Using the coordinate retrieved from those functions, there are four other functions to move each individual tile: moveTileRight(), moveTileLeft(), moveTileUp(), moveTileDown(). These functions calls on various other functions and ensure interactions among the grid, the cells, and the tiles.
Before moving, the tiles are added to the four queues of the game one by one, in the order that they will be moved later. For example, if the user chooses to move all the tiles in the right direction, then the rightmost tiles in each row will be moved first, then the next tiles to their left will be moved, and so on. This is also the reason I choose the to use the queue data structure which enables the first in – first out flow. This phase of lining the tiles in queues in the four enqueueTiles() functions. Once all tiles have been added to queues in the correct order, they will in turn get retrieved and removed from the queue (still in the correct order, very important) and moved (using the moveTile() functions mentioned above). This moving phase is included in moveTilesFromQueue(). Moving one layer up, the two phases – adding tiles and moving tiles – are wrapped in the four moveGame() functions that are triggered by the key signal from the user.
Another important function is mergeTiles(), to be invoked when two same values tiles collide. I’m not done with this function yet – it partly works, as shown in the demo, but it still has a lot of bugs and pitfalls. This is also the starting point for further development of the game. Once two tiles collide and merge, they disappear and a new tile of twice the value will appear in their place, and functions from the Score class will be called on to update the score of the current game.
The Game class is missing some other important attributes and functions that I hope to incorporate: highestTile attribute and checkEnd() function to signal whether the game is won, lost, or in progress.
Score
This is the missing class! With the progress I’ve made, I haven’t constructed this class yet. Once made, the basics of the class will include the following: a currentScore attribute to store the score so far (the sum in value of all the currently visible tiles on the board) and is updated after each collision, a highestScore attribute storing, as the name suggests, the highest score so far of the current game section (each time the program is rerun is a new session).
Problems
Lots and lots of them! Most of the problems I encounter have to do with using the wrong logic and unable to detect unpredictable behavior. For such problems, I try to pinpoint the issues by adding println() statements to the functions, especially to conditional statements and loops, printing out various attributes of the objects (I still leave a number of them in my code in case I will need them again). Rather than trying to imagine what is going on, I find that printing the results out gives a much better and tangible view into the program and what possibly went wrong. For example, the following is what I print out when testing the getRightTest() function:
Whenever it is a logic problems (trying to figure out how to build the functions, etc.), I find taking notes by hands and drawing on pictures really help. Here is some of what I write and draw:
For the game, I also use a number of Java classes like HashMap, ArrayList, and ArrayDeque. I’m a bit lost deciding which ones to use, but other than that I find them all very helpful. Whenever I try to code some generic function, chances are there are already some built-in classes and data structures that handle exactly what I want to do. If you happen to suspect the same, I highly recommend doing a quick Google search. Most of the time I will search for something like “java queue oracle”, adding “oracle” so that it shows results from the Java platform API first.
My biggest takeaway from working on this game, though, has to do with a problem I had with my laptop. I wrote down my feelings on it and I’d like to quote from them:
Feb 6, 10:40 AM: I woke up to a blank Game class. It seemed I had accidentally deleted the temporary file that Processing had yet to save into the actual file. I was devastated. I was not prepared to write the entire class again. I hadn’t made a GitHub repo for the class at this point. Thank the Buddha I did have a backup file on Google Drive. After this I immediately went to push my code to GitHub. Ran into some other problems with command line along the way but it worked out. Now my code is on GitHub, as any code by any sane person should be.
In short, please back up your code.
Code
There are a lot of attributes and functions in my code that I define but never call on to use, which might be because I was working in some direction but then changed my mind and worked in a different direction. But since the game is not finished, I’m not deleting all of them yet in case I might need them when I continue with the program.
Tileclass
import java.util.Map;
// https://processing.org/reference/HashMap.html
class Tile {
int value;
color tileColor, textColor;
float alpha = 0;
boolean doneDisappearing = false;
boolean overlap = false;
boolean visible = true;
boolean moving = false;
boolean doneMoving = false;
int rowPos, colPos;
float yPos, xPos;
HashMap<Integer, Integer> tileColorMap = new HashMap<Integer, Integer>();
int tileSize = 100;
int tileRound = 7;
int gapSize = 12;
float xOffset = width/2 - (gapSize*2.5 + tileSize*2);
// yOffset might + extra offset to account for the score part
float yOffset = height/2 - (gapSize*2.5 + tileSize*2);
int gridNum = 4;
int tileID;
/**
* Map tile values with corresponding colors in pairs of <Value, Color>
* Note: wrapper class of color primitive type is Integer
*/
void mapColors() {
tileColorMap.put(2, color(238, 228, 218, alpha));
tileColorMap.put(4, color(237, 224, 200, alpha));
tileColorMap.put(8, color(242, 177, 121, alpha));
tileColorMap.put(16, color(245, 149, 99, alpha));
tileColorMap.put(32, color(246, 124, 96, alpha));
tileColorMap.put(64, color(246, 94, 59, alpha));
tileColorMap.put(128, color(237, 207, 115, alpha));
tileColorMap.put(256, color(237, 204, 98, alpha));
tileColorMap.put(512, color(237, 200, 80, alpha));
tileColorMap.put(1024, color(237, 197, 63, alpha));
tileColorMap.put(2048, color(237, 194, 45, alpha));
}
/**
* Constructor of a tile
* @param val value of the tile, must be of a value 2^n with 1 <= n <= 11
* row, col coordinate of the tile
* [0, 0] is top left, [3, 3] is bottom right
*/
Tile(int val, int row, int col) {
value = val;
rowPos = row;
colPos = col;
xPos = xPos(col);
yPos = yPos(row);
tileID = gridNum*row+col;
}
/**
* Display a particular tile. Necessary params are all class attributes
*/
void displayTile() {
// Alpha might get updated so need to map colors again
mapColors();
tileColor = tileColorMap.get(value);
// There are only two colors for text so no need for HashMap
if (value <= 4) {
textColor = color(119, 110, 101, alpha);
} else {
textColor = color(249, 246, 242, alpha);
}
updateAlpha();
pushStyle();
rectMode(CENTER);
noStroke();
fill(tileColor);
rect(xPos, yPos, tileSize, tileSize, tileRound);
fill(textColor);
textSize(tileSize*.4);
// tileSize*.5 is too big -- 4-digit values do not fit
textAlign(CENTER, CENTER);
text(value, xPos, yPos-textAscent()*.1);
popStyle();
}
/**
* Move tile horizontally to a new position
* @param destCol column-coordinate of destination
*/
void moveTileH(int destCol) {
float oldX = xPos(colPos);
float newX = xPos(destCol);
float stepSize = (newX-oldX)/15;
if (xPos != newX) {
xPos += stepSize;
}
// stepSize can have long decimal part
// Without rounding, xPos can offshoot newX by a fraction
if (round(xPos) == newX) {
xPos = newX;
colPos = destCol;
moving = false;
doneMoving = true;
updateID();
}
}
/**
* Move tile vertically to a new position
* @param destRow row-coordinate of destination
*/
void moveTileV(int destRow) {
float oldY = yPos(rowPos);
float newY = yPos(destRow);
float stepSize = (newY-oldY)/15;
if (yPos != newY) {
yPos += stepSize;
}
if (round(yPos) == newY) {
yPos = newY;
rowPos = destRow;
moving = false;
doneMoving = true;
updateID();
}
}
/**
* Change transparency of a tile. Used to make tile (dis)appear
*/
void updateAlpha() {
if (visible) {
if (alpha < 255) {
alpha += 17; // 17=255/15 (15, 30, 60 ...)
} else {
alpha = 255;
}
alpha = 255;
} else {
if (alpha > 0) {
alpha -= 17;
} else {
alpha = 0;
}
}
}
/**
* Update visibility of a tile to trigger its disappearing
*/
void disappear() {
visible = false;
}
/**
* Update ID of a recently moved tile
* So far not used much because it's too simple a calculation
* I forget I made a function
*/
void updateID() {
tileID = gridNum*rowPos+colPos;
}
/**
* Return the x-coordinate of a tile in pixels
* @param colPos column coordinate (0, 1, 2, 3)
* @return xPos corresponding x-coordinate in pixels
*/
float xPos(int colPos) {
return (colPos+.5)*tileSize + (colPos+1)*gapSize + xOffset;
}
/**
* Return the y-coordinate of a tile in pixels
* @param rowPos row coordinate (0, 1, 2, 3)
* @return yPos corresponding y-coordinate in pixels
*/
float yPos(int rowPos) {
return (rowPos+.5)*tileSize + (rowPos+1)*gapSize + yOffset;
}
/**
* Check if this tile and another tile has the same value
* @param tile a second tile to compare
* @return true if same value
*/
boolean sameValue(Tile tile) {
if (value == tile.value) {
return true;
} else {
return false;
}
}
/**
* Checks if this tile is at the top edge aka first row
* @return true if it is at the top edge
*/
boolean isAtTopEdge() {
if (rowPos == 0) {
return true;
} else {
return false;
}
}
/**
* Checks if this tile is at the bottom edge aka last row
* @return true if it is at the bottom edge
*/
boolean isAtBottomEdge() {
if (rowPos == 3) {
return true;
} else {
return false;
}
}
/**
* Checks if this tile is at the left edge aka first column
* @return true if it is at the left edge
*/
boolean isAtLeftEdge() {
if (colPos == 0) {
return true;
} else {
return false;
}
}
/**
* Checks if this tile is at the right edge aka last column
* @return true if it is at the right edge
*/
boolean isAtRightEdge() {
if (colPos == 3) {
return true;
} else {
return false;
}
}
/**
* Reset movement attributes of the tile
* aka not moving & hasn't started moving
*/
void resetMovement() {
moving = false;
doneMoving = false;
}
/**
* Misc code for testing purposes
*/
void test() {
println("Row: "+str(rowPos));
println("Col: "+str(colPos));
}
}
Cellclass
import java.util.ArrayDeque;
// https://docs.oracle.com/javase/9/docs/api/java/util/ArrayDeque.html
class Cell {
int gridNum = 4;
int cellID;
int rowPos, colPos;
float yPos, xPos;
color cellColor = color(205, 191, 180, 255);
boolean occupied = false;
int cellSize = 100;
int cellRound = 7;
int gapSize = 12;
float xOffset;
float yOffset;
int count = 0;
ArrayDeque<Tile> queue = new ArrayDeque();
/**
* Constructor of a cell
* @param row, col coordinate of the cell
* offset_x, offset_y offets to align the game board
*/
Cell(int row, int col, float offset_x, float offset_y) {
rowPos = row;
colPos = col;
xOffset = offset_x;
yOffset = offset_y;
xPos = xPos(col);
yPos = yPos(row);
cellID = gridNum*row+col;
}
/**
* Display the cell
* I'm warry of unpredicted behavior hence pushStyle() popStyle()
*/
void displayCell() {
pushStyle();
rectMode(CENTER);
noStroke();
fill(cellColor);
rect(xPos, yPos, cellSize, cellSize, cellRound);
popStyle();
}
/**
* Return the x-coordinate of a cell in pixels
* @param colPos column coordinate (0, 1, 2, 3)
* @return xPos corresponding x-coordinate in pixels
*/
float xPos(int colPos) {
return (colPos+.5)*cellSize + (colPos+1)*gapSize + xOffset;
}
/**
* Return the y-coordinate of a cell in pixels
* @param colPos column coordinate (0, 1, 2, 3)
* @return yPos corresponding y-coordinate in pixels
*/
float yPos(int rowPos) {
return (rowPos+.5)*cellSize + (rowPos+1)*gapSize + yOffset;
}
}
Gridclass
class Grid {
float gridSize = 460;
int gridNum = 4;
int gridRound = 7;
int cellSize = 100;
int gapSize = 12;
ArrayList<Cell> cells = new ArrayList<Cell>();
float xOffset = width/2 - (gapSize*2.5 + cellSize*2);
// yOffset might + extra offset to account for the score part
float yOffset = height/2 - (gapSize*2.5 + cellSize*2);
/**
* Constructor of a grid
*/
Grid() {
for (int i=0; i<gridNum; i++) {
for (int j=0; j<gridNum; j++) {
Cell cell = new Cell(i, j, xOffset, yOffset);
cells.add(cell);
}
}
}
/**
* Change the size of a grid
* To be honest I forgot I ever made this at all
* Might find some use for it?
*/
void setGridSize(int size) {
gridSize = size;
}
/**
* Display the background and the cells
*/
void displayGrid() {
displayBg();
for (int i=0; i<cells.size(); i++) {
Cell cell = cells.get(i);
cell.displayCell();
}
}
/**
* Display the background
* Once again I'm using pushStyle() popStyle() to be sure
*/
void displayBg() {
color bgColor = color(187, 172, 160, 255);
pushStyle();
fill(bgColor);
noStroke();
rectMode(CENTER);
rect(width/2, height/2, gridSize, gridSize, gridRound);
popStyle();
}
/**
* Check if a cell is occupied
* Though I don't think these params are ever used
* @param row, col coordinate of the cell
* @return true if occupied
*/
boolean checkIfOccupied(int row, int col) {
int id = getID(row, col);
Cell cell = cells.get(id);
return cell.occupied;
}
/**
* Check if a cell is occupied
* @param id ID of the cell
* @return true if occupied
*/
boolean checkIfOccupied(int id) {
Cell cell = cells.get(id);
return cell.occupied;
}
//void changeStatus(Tile tile) {
// int id = getID(tile.rowPos, tile.colPos);
// Cell cell = cells.get(id);
// cell.changeStatus();
//}
/**
* Change an occupied cell to unoccupied
* @param tile the tile to be moved or disappear
*/
void vacant(Tile tile) {
int id = getID(tile.rowPos, tile.colPos);
Cell cell = cells.get(id);
cell.occupied = false;
}
/**
* Change an occupied cell to unoccupied
* @param id ID of the tile to be moved away or disappear
* and/or cell to be cleared
*/
void vacant(int id) {
Cell cell = cells.get(id);
cell.occupied = false;
}
/**
* Change an unoccupied cell to occupied
* @param tile the tile to be moved here or appear
*/
void occupy(Tile tile) {
int id = getID(tile.rowPos, tile.colPos);
Cell cell = cells.get(id);
cell.occupied = true;
}
/**
* Change an unoccupied cell to occupied
* @param id ID of the tile to be moved here or appear
* and/or cell to be occupied
*/
void occupy(int id) {
Cell cell = cells.get(id);
cell.occupied = true;
}
/**
* Add to the count of the cell (how many tiles are in it)
* Cell's count attribute is actually kinda redundant.
* Only needs to check the size of the queue.
* @param tile the tile at the coordinate to be checked
*/
void addCount(Tile tile) {
int id = getID(tile.rowPos, tile.colPos);
Cell cell = cells.get(id);
cell.count += 1;
}
/**
* Minus from the count of the cell
* @param tile the tile at the coordinate to be checked
*/
void minusCount(Tile tile) {
int id = getID(tile.rowPos, tile.colPos);
Cell cell = cells.get(id);
cell.count -= 1;
}
/**
* Add a tile to (the end of) a cell's queue
* @param tile tile to be added
*/
void enqueue(Tile tile) {
int id = getID(tile.rowPos, tile.colPos);
Cell cell = cells.get(id);
cell.queue.add(tile);
}
/**
* Retrieve and remove a tile from (the head of) a cell's queue
* @param tile tile at the cell in question
* @return tile at the head of the cell's queue
*/
Tile dequeue(Tile tile) {
int id = getID(tile.rowPos, tile.colPos);
Cell cell = cells.get(id);
return cell.queue.remove();
}
/**
* Retrieve and remove a tile from (the head of) a cell's queue
* @param id coordinate of the cell in question
* @return tile at the head of the cell's queue
*/
Tile dequeue(int id) {
Cell cell = cells.get(id);
return cell.queue.remove();
}
/**
* Retrieve without removing a tile from the head of a cell's queue
* @param id coordinate of the cell in question
* @return tile at the head of the cell's queue
*/
Tile peekFirst(int id) {
Cell cell = cells.get(id);
return cell.queue.peekFirst();
}
/**
* Retrieve without removing a tile from the end of a cell's queue
* @param id coordinate of the cell in question
* @return tile at the end of the cell's queue
*/
Tile peekLast(int id) {
Cell cell = cells.get(id);
return cell.queue.peekLast();
}
/**
* Retrieve without removing a tile from the head of a cell's queue
* I've just realized it's equivalent to peekFirst()
* @param id coordinate of the cell in question
* @return tile at the head of the cell's queue
*/
Tile peek(int id) {
Cell cell = cells.get(id);
return cell.queue.peek();
}
/**
* Return the current size of a cell's queue
* aka how many tiles it currently contains
* @param id coordinate of the cell to check
* @return size of cell's queue
*/
int queueSize(int id) {
Cell cell = cells.get(id);
return cell.queue.size();
}
/**
* Check if a collision is happenning at a cell
* Can be modified to get rid of count attribute
* @param tile tile at the cell to check
* @return true if a cell currently contains >1 tiles
*/
boolean checkCollision(Tile tile) {
int id = getID(tile.rowPos, tile.colPos);
Cell cell = cells.get(id);
if (cell.count == 2) {
return true;
} else {
return false;
}
}
/**
* Check if a collision is happenning at any cell
* @return true if any cell currently contains >1 tiles
*/
boolean checkCollision() {
for (int i=0; i<cells.size(); i++) {
Cell cell = cells.get(i);
if (cell.queue.size() == 2) {
return true;
}
}
return false;
}
/**
* Return the coordinate of the cell with a collision
* Only gets revoked when there is a collision anywhere in the board
* @return i id of the first cell with a collision
*/
int whereCollision() {
for (int i=0; i<cells.size(); i++) {
Cell cell = cells.get(i);
if (cell.queue.size() == 2) {
println("Collision in: "+str(i));
return i;
}
}
return -1;
}
/**
* Return tile/cell ID corresponding to a coordinate
* Rarely used, calling it is just as long as writing the code
* @param row, col coordinate
*/
int getID(int row, int col) {
return gridNum*row+col;
}
/**
* Misc code for testing purposes
*/
void occHelper() {
for (int i=0; i<4; i++) {
for (int j=0; j<4; j++) {
if (checkIfOccupied(i, j)) {
println(i, j);
}
}
}
}
}
Gameclass
import java.util.Map;
import java.util.ArrayDeque;
class Game {
Grid grid = new Grid();
int gridNum = 4;
HashMap<Integer, Boolean> keyHandler = new HashMap<Integer, Boolean>();
int numTiles = 16;
ArrayDeque<Tile> queue0 = new ArrayDeque();
ArrayDeque<Tile> queue1 = new ArrayDeque();
ArrayDeque<Tile> queue2 = new ArrayDeque();
ArrayDeque<Tile> queue3 = new ArrayDeque();
/**
* Constructor of a game
* Reset keyHandler = no arrow key has been pressed
* Randomize a tile
*/
Game() {
keyHandler.put(LEFT, false);
keyHandler.put(RIGHT, false);
keyHandler.put(UP, false);
keyHandler.put(DOWN, false);
randTile();
}
/**
* The main proceedings of the game
*/
void displayGame() {
grid.displayGrid();
displayTiles();
// For each loop check if an arrow is pressed
// & revoke the corresponding game movement
if (keyHandler.get(RIGHT)) {
moveGameRight();
} else if (keyHandler.get(LEFT)) {
moveGameLeft();
} else if (keyHandler.get(UP)) {
moveGameUp();
} else if (keyHandler.get(DOWN)) {
moveGameDown();
}
if (allDoneMoving()) {
// This works but in the case of 2 - 2 - 2 for example it's wrong
checkAndMerge();
randTile();
}
}
/**
* Check the entire game board to detect a collision
* then merge the tiles that collided
* A bit exhaustive to check every cell but not too taxing overall
*/
void checkAndMerge() {
for (int i=0; i<numTiles; i++) {
if (grid.queueSize(i) == 2) {
mergeTiles(i);
}
}
}
/**
* Merge collided tiles
* Still has a lot of bugs e.g. 3 same tiles on a row/col
* @param id ID of the cell where the collision is
*/
void mergeTiles(int id) {
int value = grid.peek(id).value*2;
int row = grid.peek(id).rowPos;
int col = grid.peek(id).colPos;
grid.dequeue(id);
grid.dequeue(id);
specificTile(value, row, col);
}
/**
* Display all the valid tiles
* Valid tiles: visible, no colliding yet
*/
void displayTiles() {
// For testing (console printing)
int validTiles = 0;
for (int i=0; i<numTiles; i++) {
if (grid.checkIfOccupied(i)) {
Tile curTile = grid.peek(i);
validTiles += 1;
curTile.displayTile();
}
}
}
/**
* Add tiles to the four queues for moving to the right
* @param queue the queue to be added to
* row the row to consider (0, 1, 2, 3)
*/
void enqueueTilesFromRight(ArrayDeque<Tile> queue, int row) {
queue.clear();
// Rightmost tiles to be added (and later retrieved) first
// e.g. [_][2][4][_]
// -0--1--2--3-
// Tile at -2- needs to move to -3- first
// so -2- is made vacant, -3- is made occupied
// When -1- moves -2- is now free
for (int i=3; i>=0; i--) {
int id = grid.getID(row, i);
if (grid.checkIfOccupied(id)) {
Tile curTile = grid.peek(id);
queue.add(curTile);
}
}
}
/**
* Add tiles to the four queues for moving to the left
* @param queue the queue to be added to
* row the row to consider (0, 1, 2, 3)
*/
void enqueueTilesFromLeft(ArrayDeque<Tile> queue, int row) {
queue.clear();
// Leftmost tiles to be added (and later retrieved) first
// see enqueueTilesFromRight() for example, same logic
for (int i=0; i<4; i++) {
int id = grid.getID(row, i);
if (grid.checkIfOccupied(id)) {
Tile curTile = grid.peek(id);
queue.add(curTile);
}
}
}
/**
* Add tiles to the four queues for moving up
* @param queue the queue to be added to
* col the col to consider (0, 1, 2, 3)
*/
void enqueueTilesFromTop(ArrayDeque<Tile> queue, int col) {
queue.clear();
// Top tiles to be added (and later retrieved) first
// see enqueueTilesFromRight() for example, same logic
for (int i=0; i<4; i++) {
int id = grid.getID(i, col);
if (grid.checkIfOccupied(id)) {
Tile curTile = grid.peek(id);
queue.add(curTile);
}
}
}
/**
* Add tiles to the four queues for moving down
* @param queue the queue to be added to
* col the col to consider (0, 1, 2, 3)
*/
void enqueueTilesFromBottom(ArrayDeque<Tile> queue, int col) {
queue.clear();
// Bottom tiles to be added (and later retrieved) first
// see enqueueTilesFromRight() for example, same logic
for (int i=3; i>=0; i--) {
int id = grid.getID(i, col);
if (grid.checkIfOccupied(id)) {
Tile curTile = grid.peek(id);
queue.add(curTile);
}
}
}
/**
* Retrieve tiles from queues to move
* @param queue the queue to be retrieved tiles from
* dir current moving direction
*/
void moveTilesFromQueue(ArrayDeque<Tile> queue, String dir) {
// Movement of a tile is triggered when
// the previous tile has finished moving
// but the first tile in queue has no such
// anchor point to compare to, so it needs
// to be retrieved and moved on its own.
if (!queue.isEmpty()) {
boolean lastTileDone = false;
int sizeQueue = queue.size();
Tile curTile = queue.remove();
if (dir == "RIGHT") {
moveTileRight(curTile);
} else if (dir == "LEFT") {
moveTileLeft(curTile);
} else if (dir == "UP") {
moveTileUp(curTile);
} else if (dir == "DOWN") {
moveTileDown(curTile);
}
if (curTile.doneMoving) {
lastTileDone = true;
}
// Then for each remaining tiles,
// retrieve and move them
for (int i=0; i<sizeQueue-1; i++) {
if (lastTileDone == true) {
curTile = queue.remove();
if (dir == "RIGHT") {
moveTileRight(curTile);
} else if (dir == "LEFT") {
moveTileLeft(curTile);
} else if (dir == "UP") {
moveTileUp(curTile);
} else if (dir == "DOWN") {
moveTileDown(curTile);
}
if (!curTile.doneMoving) {
lastTileDone = false;
}
}
}
}
}
/**
* Move all tiles on board to the right
* After all have finished moving
* lock all keyHandler values, i.e.
* no direction is currently chosen
*/
void moveGameRight() {
enqueueTilesFromRight(queue0, 0);
enqueueTilesFromRight(queue1, 1);
enqueueTilesFromRight(queue2, 2);
enqueueTilesFromRight(queue3, 3);
moveTilesFromQueue(queue0, "RIGHT");
moveTilesFromQueue(queue1, "RIGHT");
moveTilesFromQueue(queue2, "RIGHT");
moveTilesFromQueue(queue3, "RIGHT");
lockMovement();
}
/**
* Move all tiles on board to the left
*/
void moveGameLeft() {
enqueueTilesFromLeft(queue0, 0);
enqueueTilesFromLeft(queue1, 1);
enqueueTilesFromLeft(queue2, 2);
enqueueTilesFromLeft(queue3, 3);
moveTilesFromQueue(queue0, "LEFT");
moveTilesFromQueue(queue1, "LEFT");
moveTilesFromQueue(queue2, "LEFT");
moveTilesFromQueue(queue3, "LEFT");
lockMovement();
}
/**
* Move all tiles on board up
*/
void moveGameUp() {
enqueueTilesFromTop(queue0, 0);
enqueueTilesFromTop(queue1, 1);
enqueueTilesFromTop(queue2, 2);
enqueueTilesFromTop(queue3, 3);
moveTilesFromQueue(queue0, "UP");
moveTilesFromQueue(queue1, "UP");
moveTilesFromQueue(queue2, "UP");
moveTilesFromQueue(queue3, "UP");
lockMovement();
}
/**
* Move all tiles on board down
*/
void moveGameDown() {
enqueueTilesFromBottom(queue0, 0);
enqueueTilesFromBottom(queue1, 1);
enqueueTilesFromBottom(queue2, 2);
enqueueTilesFromBottom(queue3, 3);
moveTilesFromQueue(queue0, "DOWN");
moveTilesFromQueue(queue1, "DOWN");
moveTilesFromQueue(queue2, "DOWN");
moveTilesFromQueue(queue3, "DOWN");
lockMovement();
}
/**
* Check if all the tiles have done moving
* @return false if any tile is not done
*/
boolean allDoneMoving() {
for (int i=0; i<numTiles; i++) {
if (grid.checkIfOccupied(i)) {
Tile curTile = grid.peek(i);
if (!curTile.doneMoving) {
return false;
}
}
}
return true;
}
/**
* Set all keyHandler values to false
* aka no (new) direction has been chosen.
*/
void lockMovement() {
if (allDoneMoving()) {
keyHandler.put(RIGHT, false);
keyHandler.put(LEFT, false);
keyHandler.put(UP, false);
keyHandler.put(DOWN, false);
}
}
/**
* Randomize a new tile of value 2
*/
void randTile() {
// Randomize a new coordinate
// until an empty cell is found.
int randRow = floor(random(4));
int randCol = floor(random(4));
while (grid.checkIfOccupied(randRow, randCol)) {
randRow = floor(random(4));
randCol = floor(random(4));
}
Tile tile = new Tile(2, randRow, randCol);
grid.occupy(tile);
grid.addCount(tile);
grid.enqueue(tile);
}
/**
* Add a new tile with specified value and coordinate
* @param val value to be set to tile
* row, col coordinate of tile
*/
void specificTile(int val, int row, int col) {
Tile tile = new Tile(val, row, col);
grid.occupy(tile);
grid.addCount(tile);
grid.enqueue(tile);
}
/**
* Move a tile to the rightmost valid destination
* @param tile tile to be moved
*/
void moveTileRight(Tile tile) {
tile.resetMovement();
grid.vacant(tile);
grid.minusCount(tile);
grid.dequeue(tile);
tile.moving = true;
int dest = tile.colPos;
if (tile.moving && !tile.doneMoving) {
dest = getRightDest(tile);
}
tile.moveTileH(dest);
grid.occupy(tile);
grid.addCount(tile);
grid.enqueue(tile);
}
/**
* Move a tile to the leftmost valid destination
* @param tile tile to be moved
*/
void moveTileLeft(Tile tile) {
tile.resetMovement();
grid.vacant(tile);
grid.minusCount(tile);
grid.dequeue(tile);
tile.moving = true;
int dest = tile.colPos;
if (tile.moving && !tile.doneMoving) {
dest = getLeftDest(tile);
}
tile.moveTileH(dest);
grid.occupy(tile);
grid.addCount(tile);
grid.enqueue(tile);
}
/**
* Move a tile to the top valid destination
* @param tile tile to be moved
*/
void moveTileUp(Tile tile) {
tile.resetMovement();
grid.vacant(tile);
grid.minusCount(tile);
grid.dequeue(tile);
tile.moving = true;
int dest = tile.rowPos;
if (tile.moving && !tile.doneMoving) {
dest = getTopDest(tile);
}
tile.moveTileV(dest);
grid.occupy(tile);
grid.addCount(tile);
grid.enqueue(tile);
}
/**
* Move a tile to the bottom valid destination
* @param tile tile to be moved
*/
void moveTileDown(Tile tile) {
tile.resetMovement();
grid.vacant(tile);
grid.minusCount(tile);
grid.dequeue(tile);
tile.moving = true;
int dest = tile.rowPos;
if (tile.moving && !tile.doneMoving) {
dest = getBottomDest(tile);
}
tile.moveTileV(dest);
grid.occupy(tile);
grid.addCount(tile);
grid.enqueue(tile);
}
/**
* Return coordinate of the rightmost valid destination for a tile
* @param tile tile to consider
* @return col column coordinate of destination cell
*/
int getRightDest(Tile tile) {
if (tile.isAtRightEdge()) {
// If current cell is already at right edge, don't move it
// aka destination = itself
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at right edge.");
return tile.colPos;
} else {
// If it is not at edge, consider the tile to the right
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at right edge.");
int curCol = tile.colPos + 1;
while (curCol <= 3) {
// Keep checking further (within board) until an invalid destination
//println("Checking tile at ["+str(tile.rowPos)+", "+str(curCol)+"]");
if (grid.checkIfOccupied(tile.rowPos, curCol)) {
// If the current cell has another tile, check if two tiles have the same value
// If they do they can be in the same cell (collision & merging happens)
// If they don't then move back to the last valid destination
//println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is occupied.");
int rightTileID = gridNum*tile.rowPos + curCol;
//Tile rightTile = tiles[rightTileID];
Tile rightTile = grid.peek(rightTileID);
if (tile.sameValue(rightTile)) {
//println("Tile at ["+str(rightTile.rowPos)+", "+str(rightTile.colPos)+"] has the same value.");
tile.overlap = true;
return curCol; // ADDED -1 FOR TESTING
} else {
//println("Tile at ["+str(rightTile.rowPos)+", "+str(rightTile.colPos)+"] has different value.");
return curCol-1;
}
} else {
// If the current cell is free, check further
//println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is NOT occupied.");
curCol += 1;
}
}
return curCol-1;
}
}
int getLeftDest(Tile tile) {
// See getRightDest(), same logic
if (tile.isAtLeftEdge()) {
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at left edge.");
return tile.colPos;
} else {
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at left edge.");
int curCol = tile.colPos - 1;
while (curCol >= 0) {
//println("Checking tile at ["+str(tile.rowPos)+", "+str(curCol)+"]");
if (grid.checkIfOccupied(tile.rowPos, curCol)) {
//println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is occupied.");
int leftTileID = gridNum*tile.rowPos + curCol;
//Tile leftTile = tiles[leftTileID];
Tile leftTile = grid.peek(leftTileID);
if (tile.sameValue(leftTile)) {
//println("Tile at ["+str(leftTile.rowPos)+", "+str(leftTile.colPos)+"] has the same value.");
return curCol; // ADDED +1 FOR TESTING
} else {
//println("Tile at ["+str(leftTile.rowPos)+", "+str(leftTile.colPos)+"] has different value.");
return curCol+1;
}
} else {
//println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is NOT occupied.");
curCol -= 1;
}
}
return curCol+1;
}
}
int getTopDest(Tile tile) {
// See getRightDest(), same logic
if (tile.isAtTopEdge()) {
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at top edge.");
return tile.rowPos;
} else {
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at top edge.");
int curRow = tile.rowPos - 1;
while (curRow >= 0) {
//println("Checking tile at ["+str(tile.rowPos)+", "+str(curCol)+"]");
if (grid.checkIfOccupied(curRow, tile.colPos)) {
//println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is occupied.");
int topTileID = gridNum*curRow + tile.colPos;
//Tile topTile = tiles[topTileID];
Tile topTile = grid.peek(topTileID);
if (tile.sameValue(topTile)) {
//println("Tile at ["+str(topTile.rowPos)+", "+str(topTile.colPos)+"] has the same value.");
return curRow; // ADDED +1 FOR TESTING
} else {
//println("Tile at ["+str(topTile.rowPos)+", "+str(topTile.colPos)+"] has different value.");
return curRow+1;
}
} else {
//println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is NOT occupied.");
curRow -= 1;
}
}
return curRow+1;
}
}
int getBottomDest(Tile tile) {
// See getRightDest(), same logic
//println("Moving tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"]");
if (tile.isAtBottomEdge()) {
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at bottom edge.");
//println("return"+str(tile.rowPos));
return tile.rowPos;
} else {
//println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at bottom edge.");
int curRow = tile.rowPos + 1;
while (curRow <= 3) {
//println("Checking tile at ["+str(curRow)+", "+str(tile.colPos)+"]");
if (grid.checkIfOccupied(curRow, tile.colPos)) {
//println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is occupied.");
int bottomTileID = gridNum*curRow + tile.colPos;
//Tile bottomTile = tiles[bottomTileID];
Tile bottomTile = grid.peek(bottomTileID);
if (tile.sameValue(bottomTile)) {
//println("Tile at ["+str(bottomTile.rowPos)+", "+str(bottomTile.colPos)+"] has the same value.");
//println("return"+str(curRow));
return curRow; // ADDED -1 FOR TESTING
} else {
//println("Tile at ["+str(bottomTile.rowPos)+", "+str(bottomTile.colPos)+"] has different value.");
//println("return"+str(curRow-1));
return curRow-1;
}
} else {
//println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is NOT occupied.");
curRow += 1;
}
}
//println("return"+str(curRow-1));
return curRow-1;
}
}
}
This week’s assignment was to create a game using Object-Oriented Programming. My idea for the game was inspired by one of my favorite childhood computer games in which we had to collect apples to gain points and beware of the bombs which reduced points. For this project, I wanted to recreate this game.
Object-Oriented Programming was ideal to create this game because I could create the falling objects and the basket as separate classes and then manipulate each one of those in a much easier manner by just varying a few parameters than having to deal with each object individually.
I started with first creating a falling object class in which I had apples and my first goal was to make them move as required – changing their x position each time they came again onto the screen for the next round and even increasing their speed the longer the game progresses to increase difficulty (see below). Then came the basket class and making it move according to user input.
With the basket:
Once I managed to move the apple and the basket as I desired, the next step was to check whether the user actually caught the fruit in the basket or not. Checking for this and increasing the score for it was relatively easy, however, creating a sense that the fruit was actually being captured in the basket was quite difficult. See my first attempt to check for collision:
Clearly, the score is increasing, but no user would know they caught the fruit unless they look at it. This is not user-friendly at all because you cannot concentrate not the game if you have to look at the score again and again. so, it was important that the fruits disappeared once they collided with the basket. However, the problem with this was that I was displaying the fruit image in the run() method of the class but I was checking for collision outside the class. So, if I used tint() to make the opacity of the object zero, everything on the canvas would go transparent. If I put this in the class function, the image in the run() would still draw the fruit, and the purpose of making it invisible failed.
I managed to solve this problem by first rearranging a few lines in the code so that the layering would be appropriate for what I was going to do next. I then created another class member (variable) which kept track if the object collided or not. I would only display the image in the run() method of the class if visible was true (see code below).
DETAILING
Once I got the semantics of the game in place, it was time for some diversity! I first added a scenic background to make it interface pleasing and then I decided to have different fruits; each amounting to different points. I also decided to have a stone (equivalent to the bomb in my childhood game) to make it more interesting. The game would finish when the score would go less than zero.
USER EXPERIENCE
What is a game if you do not know what to do or see how your game went? So, along with creating an illusion of fruits falling in the basket by making my fruits disappear after collision, I decided to have an initial instructions menu and a “Game Over” comment in the end with the display of the user’s highest score achieved during the game for enhanced user experience. I learned how to display text on the screen for this project!
See a video of my final game in progress:
The speed of the falling fruits increases the longer the game progresses to increase the difficulty:
CODE
My class Obj for the falling objects (the different fruits and stones):
class Obj {
float posX, posY;
float objWidth, objHeight;
float speed;
PImage objImg;
int points;
int imgCode;
boolean scored; //to not keep adding score for basket height
boolean visible; //to make it dissapear when fals in the basket
//constructor
Obj(int _imgCode, PImage _img) {
objWidth = random(30, 60);
objHeight = objWidth*780/801; //original pic ratio = 801x780
posX = random(width-objWidth)+objWidth/2;
posY = 0-objHeight/2;
speed = random(1, 3);
imgCode= _imgCode;
objImg = _img;
//points = 10;
scored = false;
visible = true;
assign();
println(speed);
}
void fallObj() {
posY += speed;
if (frameCount%300==0 && speed<25) //cap on speed so that it doesnt get too fast
{
speed*=1.2; //speed of game keeps increasing to increase difficulty
//println(frameCount);
//println(speed);
}
}
void checkEdge() {
if (posY > height+objHeight/2) {
posX = random(width-objWidth)+objWidth/2;
posY = 0-objHeight/2;
scored = false;
visible = true;
}
}
void run() {
fallObj();
if (visible == true)
image(objImg, posX, posY, objWidth, objHeight);
checkEdge();
}
void assign()
{
if (imgCode == 0)
{
points = -50;
speed++; //icrease the speed of rocks a bit
}
else if (imgCode == 1)
points = 5;
else if (imgCode == 2)
points = 15;
else if (imgCode == 3)
points = 20;
}
}
My class Basket:
class Basket
{
float posX, posY;
float bWidth, bHeight;
float speed;
PImage bImg;
//int points;
//constructor
Basket(PImage _img)
{
bWidth = 180;
bHeight = bWidth/2; //original pic ratio =
posX = width/2;
posY = height-bHeight/2;
speed=15;
bImg = _img;
}
void run()
{
image(bImg, posX, posY, bWidth, bHeight);
if (frameCount%300==0 && speed<40) //cap on speed so that it doesnt get too fast
{
speed*=1.5; //to increase speed of basket as game gets faster
}
}
void shiftR()
{
//if (posX<width-bWidth/2)
if (posX<width-bWidth/2-speed)
posX+=speed;
else
posX=width-bWidth/2;
image(bImg, posX, posY, bWidth, bHeight);
}
void shiftL()
{
if (posX>0+bWidth/2+speed)
posX-=speed;
else
posX=bWidth/2;
image(bImg, posX, posY, bWidth, bHeight);
}
}
The main draw() function:
Obj[] objects;
Basket basket;
PImage[] objImgs;
PImage basketImg;
PImage bg; //background image for game
int score = 0;
int maxScore = 0; //to display in the end
boolean started = false; //to keep track of start screen
color beige = color(225, 198, 153);
//==================================================================================================
void setup() {
size(1000, 650);
//fullScreen();
//loading the background
bg = loadImage("bg7.jpeg");
//loading the various object images - fruits, basket and stone
objImgs = new PImage[4];
objImgs[1] = loadImage("apple1.png"); //code 1 = apple = 5 points
objImgs[2] = loadImage("mango1.png"); //code 2 = mango = 15 ponits
objImgs[3] = loadImage("pear1.png"); //code 3 = pear = 20 ponts
objImgs[0] = loadImage("rock.png"); //code 0 = enemy = -50 points
basketImg = loadImage("basket4.png");
rectMode(CENTER);
imageMode(CENTER);
//creating my objects for the game
basket = new Basket(basketImg);
//falling objects
objects = new Obj[9];
for (int i=0; i<objects.length; i++)
{
int imgCode = i%objImgs.length;
objects[i] = new Obj(imgCode, objImgs[i%objImgs.length]);
}
}
//==================================================================================================
void draw()
{
if (started == false)
startScreen();
else
{
//background(bg);
image(bg, width/2, height/2, width, height);
basket.run();
for (int i = 0; i<objects.length; i++) {
objects[i].run();
}
scoring();
}
}
//==================================================================================================
void keyPressed()
{
if (started)
{
if (key == 'd' || (key == CODED && keyCode == RIGHT))
basket.shiftR();
if (key == 'a' || (key == CODED && keyCode == LEFT))
basket.shiftL();
}
if (key == ENTER)
started = true;
if (key == BACKSPACE)
endScreen();
}
//==================================================================================================
void scoring()
{
fill(255);
textSize(30);
textAlign(LEFT);
text("SCORE: " + score, 20, 40);
//check if any fruit caught in basket
for (int i=0; i<objects.length; i++)
{
if (objects[i].posX+objects[i].objWidth/2>=basket.posX-basket.bWidth/2 &&
objects[i].posX-objects[i].objWidth/2<=basket.posX+basket.bWidth/2 &&
objects[i].posY>=basket.posY-basket.bHeight/3 &&
objects[i].scored == false)
{
score+=objects[i].points;
objects[i].scored = true;
objects[i].visible = false;
}
}
if (maxScore<score)
maxScore=score;
if (score<0)
endScreen();
}
//==================================================================================================
void startScreen()
{
background(beige);
fill(255);
textAlign(CENTER);
textSize(40);
text("SHREYA'S FARMLAND", width/2, height/4);
textSize(20);
text("Collect the fruits in the basket,", width/2, height/3);
text("BEWARE of the stones!", width/2, height/3+25);
textSize(15);
text("Points gained are as follows:", width/2, height/3+80);
//textAlign(LEFT,CENTER);
text("Apple : +05", width/2, height/3+100);
text("Mango: +15", width/2, height/3+120);
text("Pear : +20", width/2, height/3+140);
text("Stone : -50", width/2, height/3+160);
textSize(15);
text("Press ENTER to start the game, press BACKSPACE to quit", width/2, height*3/4);
//text("Press BACKSPACE to quit", width/2, height*3/4);
}
//==================================================================================================
void endScreen()
{
noLoop();
//tint(255,50);
background(beige);
//noTint();
fill(255);
textAlign(CENTER);
textSize(40);
text("GAME OVER!", width/2, height/2-40);
textSize(20);
text("Your Highest Score: " + maxScore, width/2, height/2+20);
}
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
//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.
Me wearing sunglasses in the Abu Dhabi’s sun (thus yellow background!)
Process
The main purpose of the assignment is to get myself accustomed to the use of some functions in drawing shapes and lines. I found myself struggling with the arc function when drawing the bang and the shoulders.
The hair still does not look symmetrical but I am happy with it. I intended to draw untied hair at first as that’s how I usually look like, by putting 2 straight lines starting from my head to my shoulders. However, I could not find the shape I was satisfied with to fill in the hair color as well. I was wondering if we could fill in color within an area restricted by boundaries (not necessarily shapes). Eventually, I finished with a hair bun by drawing a circle which turns out quite suitable with the overall hot season and my initial motivation.
Regarding the shoulders, I attempted to use 2 separate arc functions at first to make them look smoother than a rectangle but the symmetry didn’t work well. I decided to go with one single arc instead and it worked!
This project consisted of creating a simple work of art using for() or while() loops.
Idea:
For this assignment, I had a hard time coming up with an idea so I decided to take a look at the old computer magazines provided for inspiration. I found an interesting piece of art called “Structure” by Zdenek Sykora in the Computer Graphics and Art Magazine. I really liked the composition and I thought it would accurately represent the use of for loops. While I was coming up with the composition of the artwork, I heard my friend talking about Euphoria and I saw an image. This inspired me to use the color palette represented by this television series.
Challenges:
One of the main challenges in this project was calculating the initial and final angle of the arc. I am still learning how to calculate the angles of the circle to make the arc I want to make. Another significant challenge was coming up with randomizing the colors of the square in the background. After trying to include specific colors in the program I found a website for color palettes. Through this website, I realized that I could get the colors I wanted by keeping the blue and green values in the RGB constant while changing the red value within a specific range.
Process:
For this project, I first tried to recreate several squares using for loops. Then I tried to create several arcs of the same angle and design. After accomplishing this I worked on randomizing the angles of the arcs to have different positions of arcs while maintaining the same size. Finally, I worked on the randomizing of colors for the squares and circles.
Conclusion:
Overall I think I got to practice with the for loop and how to think about two-dimensional art. Also, this project helped me work with random numbers and use RGB colors. Last assignment I mentioned that I struggled with hardcoding. However, I think for this assignment I improved on using more variables instead of constant numbers in my code. Although I haven’t been able to completely fix the habit of hardcoding, I think I have significantly improved in this area. Overall, I really liked the outcome of this project because it resembled perfectly what I wanted to make initially.
int rowofSquares = 10;
int lengthofSquares = 60;
int colorcircle;
void setup(){
size(600, 600);
frameRate(0.5);
}
void draw(){
background(238);
for(int y = 0; y < rowofSquares; y++){
for(int x = 0; x < rowofSquares; x++){
color colorsquare = color(int(random(30,180)), 0, 250);
fill(colorsquare);
noStroke();
rect(lengthofSquares * x, lengthofSquares * y, lengthofSquares, lengthofSquares);
}
}
for(int y = 0; y < rowofSquares; y++){
for(int x = 0; x < rowofSquares; x++){
float r = random(0,4);
int position = (int)r;
int colorcircle = (255*int(random(0,2)));
fill(colorcircle);
arc(lengthofSquares * x + lengthofSquares*0.5,
lengthofSquares * y + lengthofSquares*0.5, lengthofSquares,
lengthofSquares, position*HALF_PI, (position+2)*HALF_PI, CHORD);
if(position == 0){
int randpos = int(random(0,2));
if(randpos == 0){
colorcircle = (255*int(random(0,2)));
fill(colorcircle);
arc(lengthofSquares * x + lengthofSquares*0.5,
lengthofSquares * y + lengthofSquares*0.5 , lengthofSquares,
lengthofSquares, (position+2)*HALF_PI, (position+4)*HALF_PI, CHORD);
}
else{
colorcircle = (255*int(random(0,2)));
fill(colorcircle);
arc(lengthofSquares * x + lengthofSquares*0.5,
lengthofSquares * y, lengthofSquares,
lengthofSquares, (position)*HALF_PI, (position+2)*HALF_PI, CHORD);
}
}
}
}
}
My main goal -after using for/while loops- was to use the random function that is built-in processing to make an interactive artwork. I used the random function to get different hues of blue (for the ocean) and yellow (for the goldfish). I started with the circles that make the moving water up. I did not -at that point- think of it as water; I just wanted to experiment how the piece would look like with random different shades of a certain color would look like. I happen to like blues and so I started with them.
When I saw the result I immediately thought of the ocean, hence, inspiration?? To put the for loops into use I put nets as they match the theme as well. As for the fish, I thought it would be cool to have another interactive part for this. The fish’s color changes color as well (it’s not so obvious though) because it’s a shimmery goldfish.
float xValue;
float yValue;
float Rdecimal;
float Bdecimal;
int xCenterOfFish;
int yCenterOfFish;
int R;
int B;
void setup() {
size (300, 300);
background(255);
}
void draw(){
// random r values for the water
xValue = random(width);
yValue = random(height);
Rdecimal = random (100, 200);
int R = int(Rdecimal);
// circles that make the water up
fill (R, 255, 244);
noStroke();
ellipse(xValue, yValue, 50, 50);
//fish
// random b values for the water
Bdecimal = random (23, 100);
int B = int(Bdecimal);
// to draw the fish when mouse is pressed
if (mousePressed == true) {
xCenterOfFish = mouseX;
yCenterOfFish = mouseY;
noStroke();
fill(235, 188, B);
circle (xCenterOfFish, yCenterOfFish, 22);
triangle(xCenterOfFish, yCenterOfFish, xCenterOfFish+20, yCenterOfFish+10, xCenterOfFish+20, yCenterOfFish-10);
}
// net horizontally
stroke (0);
for (int i = 0; i < height; i = i+5) {
line(width, i, 0, i);
}
// net vertically
stroke (0);
for (int i = 0; i < width; i = i+4) {
line(i, height, i, 0);
}
}