xxxxxxxxxx
353
// V 1.6, cleaning up code.
function setup() {
createCanvas(960, 540);
angleMode(DEGREES);
// Constants you can tweak to adjust your simulation.
spriteWidth = 80; // 80 is a good value. This is an adjustable constant also used for mating and catching distance.
spawnDelay = 90; // 90 is a good value. Delay before making a new baby breeder. 60 = 1 second.
addX = -3; // -3 is a good value. This is a global variable that is the speed for each breeder.
startHealth = 600; // 600 is a good value. Delay before catcher starves. 60 = 1 second.
breedersToStart = 25 // 25 is a good number;
catchersToStart = 2; // 2 is a good number;
// Variables that will change as the simulation progresses.
framesDelayed = 0; // This counter is used to delay actions, such as spawning new breeders.
// You *must* instantiate your objects in setup, or you can't animate them
// because draw will overwrite the initial parameters in every loop.
// Prepare bubbles.
bubbles = [];
for (let bubblesDefined = 0; bubblesDefined < 100; bubblesDefined++) {
let x = random(width);
let y = random(height);
let r = random(5, 20);
bubbles.push(new Bubble(x, y, r));
}
breeders = [];
for (let breederDefined = 0; breederDefined < breedersToStart; breederDefined++) {
let x = random(width);
let y = random(height);
breeders.push(new Clownfish(x, y));
}
catchers = [];
for (let catcherDefined = 0; catcherDefined < catchersToStart; catcherDefined++) {
let x = random(width);
let y = random(height);
catchers.push(new Piranha(x, y));
}
}
function draw() {
background("steelblue");
framesDelayed++; // This will prevent one collision from spawning multiple children.
// Update bubbles.
for (let bubblesShown = 0; bubblesShown < bubbles.length; bubblesShown++) {
bubbles[bubblesShown].update();
bubbles[bubblesShown].show();
}
// Update breeders.
for (let breederShown = 0; breederShown < breeders.length; breederShown++) {
let thisBreeder = breeders[breederShown];
thisBreeder.update();
thisBreeder.show();
// Spawn a new breeder if it collides with another,
// but only if there's been a delay since the last spawn
// to prevent the same overlap from triggering multiple children.
if (framesDelayed > spawnDelay) {
// Look through all the potential mates to see if any overlap.
for (let matesChecked = 0; matesChecked < breeders.length; matesChecked++) {
let proposedMate = breeders[matesChecked];
let isDifferentBreeder = (breederShown !== matesChecked);
if (isDifferentBreeder && isTouching(thisBreeder, proposedMate)) {
// Spawn a new breeder. Position this child randomly,
// so it doesn't overlap with its parents and accidentally trigger more children.
let x = random(width);
let y = random(height);
breeders.push(new Clownfish(x, y)); // Adding it to the array will draw it in the next draw() loop.
framesDelayed = 0; // Force the waiting period to restart after a new spawn.
break; // Breaking out of the loop after contact is more efficient.
}
}
}
}
// Update catchers.
for (let catcherShown = 0; catcherShown < catchers.length; catcherShown++) {
let thisCatcher = catchers[catcherShown];
thisCatcher.update();
thisCatcher.show();
// Remove the catcher if it starves.
if (thisCatcher.health < 0) {
catchers.splice(catcherShown);
}
// Remove a breeder if the catcher collides with it. Unlike a breeder-to-breeder collision,
// we don't need a delay because removing the breeder will prevent other overlaps.
// Look through the breeders and check for overlaps. Although we are removing an item from the array,
// in this case we don't need to run through the array backwards because we are breaking out of the loop immediately.
for (let breedersLeft = 0; breedersLeft < breeders.length; breedersLeft++) {
let proposedCatch = breeders[breedersLeft];
if (isTouching(thisCatcher, proposedCatch)) {
thisCatcher.health = startHealth;
breeders.splice(breedersLeft, 1); // First argument is offset, the second (important) is the number of items.
break; // Breaking out of the loop after contact is more efficient.
}
}
}
}
function isTouching(sprite1, sprite2) {
let spriteDistance = dist(sprite1.x, sprite1.y, sprite2.x, sprite2.y);
if (spriteDistance < spriteWidth) { // spriteWidth is a global variable you can tweak in setup().
return true;
} else {
return false;
}
}
class Bubble {
constructor(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
}
update() {
this.x = this.x + random(-1, 1);
this.y = this.y + random(-1, 1);
}
show() {
push();
translate(this.x, this.y)
noFill();
drawingContext.shadowOffsetX = 6;
drawingContext.shadowOffsetY = -6;
drawingContext.shadowBlur = 10;
drawingContext.shadowColor = 'white';
stroke("white");
strokeWeight(.3);
ellipse(0, 0, this.r * 2);
pop();
}
}
class Clownfish {
constructor(x, y, status) {
// Note to Jon: giving the fish a sequential id won't work,
// because you are removing and adding them so there's no way
// to track that consistently. You could give a random or timestamp id.
this.x = x;
this.y = y;
this.addX = addX;
this.finWag = 4;
this.isSwimmingRight = false;
this.isAlive = true;
this.status = status;
}
update() {
this.x = this.x + this.addX;
this.y = this.y;
// Reverse if it hits a wall.
let isTooFarLeft = (this.x < 0);
let isTooFarRight = (this.x > width);
if (isTooFarLeft || isTooFarRight) {
this.addX = -this.addX;
this.isSwimmingRight = !this.isSwimmingRight;
this.y = random(height);
}
// Wag its tail fin.
this.finWag = (this.finWag + 1) % 5;
}
show() {
push();
let d = +1; // Direction the fish is pointing.
if (this.isSwimmingRight) {
d = -d;
}
stroke("black");
strokeWeight(3);
translate(this.x, this.y);
fill("orangered");
// Draw tail.
if (this.isSwimmingRight) {
arc(52 * d, 0, 60, 60 * this.finWag, 135, 225);
} else {
arc(52 * d, 0, 60, 60 * this.finWag, -45, 45);
}
// Draw 1st dorsal fin.
if (this.isSwimmingRight) {
arc(-10 * d, -30, 30, 30, 140, 15);
} else {
arc(-10 * d, -30, 30, 30, 140, 15);
}
// Draw 2nd dorsal fin.
arc(34 * d, -20, 30, 30, 155, 30);
// Draw ventral fin.
arc(34 * d, 20, 30, 30, 300, 1350);
// Draw body.
noStroke();
fill("orangered");
ellipse(0 * d, 0, 100, 65);
// Draw head.
if (this.isSwimmingRight) {
arc(-34 * d, 0, 50, 42, 180, 90);
} else {
arc(-34 * d, 0, 50, 42, 90, 270);
}
// Draw stripes
fill("whitesmoke");
let stripe1 = {
qx: -12 * d,
qy: -32,
dx: 24 * d,
dy: 64,
adjustY: 0
}
let stripe2 = {
qx: -36 * d,
qy: -28,
dx: 10 * d,
dy: 56,
adjustY: 5
}
let stripe3 = {
qx: 42 * d,
qy: -6,
dx: 20 * d,
dy: 12,
adjustY: -12
}
drawStripe(stripe1);
drawStripe(stripe2);
drawStripe(stripe3);
// Draw pectoral fin.
stroke("black");
strokeWeight(3);
fill("orangered");
if (this.isSwimmingRight) {
arc(-20 * d, 25, 50, 50, 120, 170);
} else {
arc(-20 * d, 25, 50, 50, 10, 60);
}
// Draw eye.
noStroke();
fill("black");
ellipse(-44 * d, -10, 10);
fill("white");
ellipse(-42 * d, -12, 3);
pop();
}
}
function drawStripe(stripe) { // Just a utility function for drawing the Clownfish.
push();
let x1 = stripe.qx;
let y1 = stripe.qy + stripe.adjustY;
let x2 = stripe.qx + stripe.dx;
let y2 = stripe.qy;
let x3 = stripe.qx + stripe.dx;
let y3 = stripe.qy + stripe.dy;
let x4 = stripe.qx;
let y4 = stripe.qy + stripe.dy - stripe.adjustY;
quad(x1, y1, x2, y2, x3, y3, x4, y4);
noStroke();
line(x1, y1, x2, y2);
stroke("black");
line(x2, y2, x3, y3);
noStroke();
line(x3, y3, x4, y4);
stroke("black");
line(x1, y1, x4, y4);
pop();
}
class Piranha {
constructor(x, y) {
this.x = x;
this.y = y;
this.addX = -addX;
this.finWag = 4;
this.isSwimmingRight = true;
this.isAlive = true;
this.health = startHealth;
}
update() {
this.x = this.x + this.addX;
this.y = this.y;
// Reverse if it hits a wall.
let isTooFarLeft = (this.x < 0);
let isTooFarRight = (this.x > width);
if (isTooFarLeft || isTooFarRight) {
this.addX = -this.addX;
this.isSwimmingRight = !this.isSwimmingRight;
this.y = random(height);
}
// Wag its tail fin.
this.finWag = (this.finWag + 1) % 5;
// Reduce its health until it eats.
this.health--;
}
show() {
push();
translate(this.x, this.y);
let d = +1; // Value for moving left. This helps re-orient the sprite after it changes direction.
if (this.isSwimmingRight) {
d = -d;
}
noStroke();
fill("rebeccapurple")
// Draw tail.
if (this.isSwimmingRight) {
arc(34 * d, 0, 60, 60 * this.finWag, 135, 225);
} else {
arc(34 * d, 0, 60, 60 * this.finWag, -45, 45);
}
// Draw dorsal fin. Points are counterclockwise from lower right?
quad(35 * d, -15, 45 * d, -25, 15 * d, -45, -10 * d, -25);
// Draw ventral fin.
quad(10 * d, 10, 45 * d, 10, 45 * d, 15, 25 * d, 40);
fill("white");
// Draw upper tooth.
triangle(-47 * d, -5, -47 * d, 0, -43 * d, -10);
// Draw lower tooth.
triangle(-45 * d, 15, -45 * d, 0, -41 * d, 10);
// Draw body.
noStroke();
let saturationAsPercent = 100 * (this.health) / startHealth;
fill("hsla(260," + saturationAsPercent + "%, 70%, 1)");
if (this.isSwimmingRight) {
arc(0 * d, 0, 80, 65, 30, 330, CHORD);
} else {
arc(0 * d, 0, 80, 65, 210, 150, CHORD);
}
// Draw upper jaw.
if (this.isSwimmingRight) {
arc(-30 * d, 0, 38, 42, -110, 340, PIE);
} else {
arc(-30 * d, 0, 38, 42, 200, 270, PIE);
}
// Draw lower jaw.
quad(-27 * d, 5, -15 * d, 25, -45 * d, 15, -46 * d, 12);
// Draw pectoral fin.
fill("orange");
if (this.isSwimmingRight) {
arc(-20 * d, 25, 50, 50, 155, 185);
}
// Draw spots.
fill("slateblue")
ellipse(-10 * d, -18, 13, 11);
ellipse(6 * d, -17, 10, 8);
ellipse(20 * d, -12, 8, 7);
// Draw eye.
fill("red");
ellipse(-30 * d, -10, 13, 10);
fill("rebeccapurple");
ellipse(-30 * d, -10, 7);
// Draw gill.
noFill();
stroke("slateblue");
strokeWeight(3);
if (this.isSwimmingRight) {
arc(-18 * d, 6, 10, 18, 290, 80);
} else {
arc(-18 * d, 6, 10, 18, -30, 30);
}
pop();
}
}