/* //////////////////////
Curling Practice
Luke Archbell 991404559
Interactive Toy
PROG14998
////////////////////// */
PVector[] rockPos = new PVector[8]; //An array that keeps track of my curling rock positions.
PVector[] rockSpeed = new PVector[8]; //An array that keeps track of my curling rock speeds.
float friction = 0.99; //Friction multiplies with speed to slow the rocks down.
int currentRock; //The current rock I am throwing. Doubles as the amount of rocks in play.
int stateMode; //The state of the program. I use 0 for default, 1 for aiming a rock, and 2 for determining a rock's power.
int powerLevel; //The power of the throw. I
int rockRadius = 12; //The radius of the curling rock. Used to help with collisions.
boolean powerUp; //A boolean that I use to determine if the power bar is moving up or down.
void setup() {
size(200, 600); //Tall and thin for the curling rink shape.
background(240); //Near white background for the ice.
//Initialize the rock positions.
for (int i=0; i<rockPos.length; i++) {
rockPos[i] = new PVector(0, 0);
}
//Initialize the rock speeds.
for (int i=0; i<rockSpeed.length; i++) {
rockSpeed[i] = new PVector(0, 0);
}
}
void draw() {
DrawIce(); //Draw the ice first.
//Check for collisions between rocks. Then move the rocks and draw them.
for (int i=0; i<=currentRock; i++) {
for (int j=0; j<currentRock; j++) {
if (j == i)
break;
if (rockCollision(i, j)) {
CheckCollision(i, j);
}
}
MoveRock(i);
DrawRock(rockPos[i]);
}
//Switch the program state. 0 Aims, and 1 is for Power. Collision happens above this switch, so I can leave 2 empty.
switch(stateMode) {
case 0:
AimRock();
break;
case 1:
Power();
break;
case 2:
//An empty case to allow for Movement and collision.
break;
default:
break;
}
//Draw the rock icons to show how many shots are left.
DrawRocksLeft();
}
//draw the ice and the curling circles.
void DrawIce() {
background(252, 252, 255);
noStroke();
fill(255);
//Shiny lines on the ice.
quad(100, 0, 110, 0, 0, 220, 0, 200);
quad(200, 0, 210, 0, 0, 420, 0, 400);
quad(300, 0, 310, 0, 0, 620, 0, 600);
quad(400, 0, 410, 0, 0, 820, 0, 800);
//THe curling targets.
ellipseMode(CENTER);
fill(0, 0, 255);
ellipse(100, 100, 200, 200);
fill(255);
ellipse(100, 100, 150, 150);
fill(255, 0, 0);
ellipse(100, 100, 100, 100);
fill(255);
ellipse(100, 100, 50, 50);
//The hog line. I use this line as the bottom collision to prevent shot rocks
//from colliding with rocks that are currently aiming or choosing power.
stroke(200, 0, 0);
line(0, 540, width, 540);
}
//Move the mouse to choose where to put the curling rock.
void AimRock() {
rockPos[currentRock].x = mouseX;
rockPos[currentRock].y = height-20;
}
//Update how the rocks move.
void MoveRock(int num) {
//Multiply speed by friction to slow down the rocks.
rockSpeed[num].x = rockSpeed[num].x * friction;
//Then change position based on speed.
rockPos[num].x += rockSpeed[num].x;
rockSpeed[num].y = rockSpeed[num].y * friction;
rockPos[num].y += (rockSpeed[num].y * friction);
//Prevent the rocks from going off the sides of the screen.
if (rockPos[num].x > width-rockRadius) {
rockPos[num].x = width-rockRadius;
rockSpeed[num].x *= -1;
} else if (rockPos[num].x < rockRadius) {
rockPos[num].x = rockRadius;
rockSpeed[num].x *= -1;
}
//Prevent the rocks from going off the top of the screen or through the hog line.
if (rockPos[num].y > 540-rockRadius && rockSpeed[num].y > 0) {
rockPos[num].y = 540-rockRadius;
rockSpeed[num].y *= -1;
} else if (rockPos[num].y < rockRadius) {
rockPos[num].y = rockRadius;
rockSpeed[num].y *= -1;
}
}
//This function checks if two rocks are close enough to be colliding and returns a boolean.
//If two rocks are colliding, I can call the CheckCollision function on them to handle bouncing.
boolean rockCollision(int r1, int r2) {
if (dist(rockPos[r1].x, rockPos[r1].y, rockPos[r2].x, rockPos[r2].y) < 2*rockRadius) {
return true;
} else {
return false;
}
}
//Have a power bar show up on the screen to indicate throwing speed.
void Power() {
//powerUp controls whether the bar is increasing or decreasing.
//Maxes at 200. Mins at 10 to prevent a throw of 0 speed.
if (powerUp)
{
powerLevel += 10;
if (powerLevel > 200) {
powerLevel = 200;
powerUp = false;
}
} else {
powerLevel -= 10;
if (powerLevel < 10) {
powerLevel = 10;
powerUp = true;
}
}
//Draw the power bar.
noStroke();
fill(255, 0, 0);
rect(180, 500-powerLevel, 15, powerLevel);
stroke(1);
noFill();
rect(180, 300, 15, 200);
}
//Draw the curling rock at the specified position.
void DrawRock(PVector pos) {
noStroke();
fill(150);
ellipseMode(CENTER);
ellipse(pos.x, pos.y, rockRadius*2, rockRadius*2);
fill(200, 0, 0);
ellipse(pos.x, pos.y, rockRadius*2-5, rockRadius*2-5);
}
//Draw the icons that indicate how many rocks are remaining.
void DrawRocksLeft() {
int rocksLeft = 8-currentRock;
//If stateMode is 2, we're watching the current rock bounce around, but haven't cued the next rock.
//currentRock won't change yet, but we need to subtract one rock from the icons.
if (stateMode == 2)
rocksLeft--;
//Draw the icons.
for (int i=0; i<rocksLeft; i++) {
noStroke();
fill(255, 0, 0);
rect(10+10*i, 555, 8, 5);
}
}
//When the mouse is clicked, change the program state and set up variables.
void mouseClicked() {
//If we click in aim state, change to power state. Initialize the power variables too.
if (stateMode == 0) {
stateMode = 1;
powerLevel = 10;
powerUp = true;
}
//If we click in power state, change to watch state.
//Set the speed of the rock based on the power bar size.
else if (stateMode == 1) {
rockSpeed[currentRock].y = -powerLevel/15;
stateMode = 2;
}
//If we click in watch state, change back to aim and cue up a new rock.
else if (stateMode == 2) {
stateMode = 0;
currentRock++;
//If currentRock is 8, we have used all the rocks and need to reset.
//The program will through an out of bounds exception if we try to reference rockPos[8],
//so we need to set it back to 7, and then reset all rock positions and speeds.
if (currentRock == 8) {
currentRock = 7;
for (int i =0; i<currentRock; i++) {
rockPos[i].x = 0;
rockPos[i].y = 0;
rockSpeed[i].x = 0;
rockSpeed[i].y = 0;
}
//Then we can set the currentRock to 0 to remove all rocks but one.
currentRock = 0;
}
//We set the rock position to the corret spot here to prevent a single frame where the rock appears in the top corner
//(since their positions are initialized to 0, 0).
rockPos[currentRock].x = mouseX;
rockPos[currentRock].y = height-20;
}
}
//This function has been modified from the Processing.org examples. URL:
//https://processing.org/examples/circlecollision.html
//
//I get the rare error where two rocks will sort of phase into each other and vibrate around together,
//but 99% of the time, it just works.
void CheckCollision(int r1, int r2) {
// get distances between the balls components
PVector bVect = PVector.sub(rockPos[r2], rockPos[r1]);
// calculate magnitude of the vector separating the balls
float bVectMag = bVect.mag();
if (bVectMag < 2*rockRadius) {
// get angle of bVect
float theta = bVect.heading();
// precalculate trig values
float sine = sin(theta);
float cosine = cos(theta);
/* bTemp will hold rotated ball positions. You
just need to worry about bTemp[1] position*/
PVector[] bTemp = {
new PVector(), new PVector()
};
/* this ball's position is relative to the other
so you can use the vector between them (bVect) as the
reference point in the rotation expressions.
bTemp[0].position.x and bTemp[0].position.y will initialize
automatically to 0.0, which is what you want
since b[1] will rotate around b[0] */
bTemp[1].x = cosine * bVect.x + sine * bVect.y;
bTemp[1].y = cosine * bVect.y - sine * bVect.x;
// rotate Temporary velocities
PVector[] vTemp = {
new PVector(), new PVector()
};
vTemp[0].x = cosine * rockSpeed[r1].x + sine * rockSpeed[r1].y;
vTemp[0].y = cosine * rockSpeed[r1].y - sine * rockSpeed[r1].x;
vTemp[1].x = cosine * rockSpeed[r2].x + sine * rockSpeed[r2].y;
vTemp[1].y = cosine * rockSpeed[r2].y - sine * rockSpeed[r2].x;
/* Now that velocities are rotated, you can use 1D
conservation of momentum equations to calculate
the final velocity along the x-axis. */
PVector[] vFinal = {
new PVector(), new PVector()
};
// final rotated velocity for b[0]
vFinal[0].x = vTemp[1].x;
vFinal[0].y = vTemp[0].y;
// final rotated velocity for b[0]
vFinal[1].x = vTemp[0].x;
vFinal[1].y = vTemp[1].y;
// hack to avoid clumping
bTemp[0].x += vFinal[0].x;
bTemp[1].x += vFinal[1].x;
/* Rotate ball positions and velocities back
Reverse signs in trig expressions to rotate
in the opposite direction */
// rotate balls
PVector[] bFinal = {
new PVector(), new PVector()
};
bFinal[0].x = cosine * bTemp[0].x - sine * bTemp[0].y;
bFinal[0].y = cosine * bTemp[0].y + sine * bTemp[0].x;
bFinal[1].x = cosine * bTemp[1].x - sine * bTemp[1].y;
bFinal[1].y = cosine * bTemp[1].y + sine * bTemp[1].x;
// update balls to screen position
rockPos[r2].x = rockPos[r1].x + bFinal[1].x;
rockPos[r2].y = rockPos[r1].y + bFinal[1].y;
rockPos[r1].add(bFinal[0]);
// update velocities
rockSpeed[r1].x = cosine * vFinal[0].x - sine * vFinal[0].y;
rockSpeed[r1].y = cosine * vFinal[0].y + sine * vFinal[0].x;
rockSpeed[r2].x = cosine * vFinal[1].x - sine * vFinal[1].y;
rockSpeed[r2].y = cosine * vFinal[1].y + sine * vFinal[1].x;
}
}