Your browser does not support the canvas tag.

previous        Show / Hide Source    Download        next
/* //////////////////////
 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;
  }
}