/* ////////////////////// 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; } }