For this project, I created a juggling game and named it Morbid Juggler. The only rule is to keep juggling the (eye)balls for as long as possible. I myself don’t really know how to juggle, so I had to think a little bit about how to create the same stressful experience with code, and the p5.js project embedded below is the culmination of that.
The player adds an (eye)ball to the screen by pressing shift
on the keyboard, and the new (eye)ball is added at the cursor’s current position. The (eye)ball travels just as a juggler’s ball would in real life, with gravity acting on it, in a parabolic trajectory. The game goes on if the player can catch (click and hold) an eye(ball) before it leaves the screen, and bring it over to the side and release it again. When an (eye)ball is rereleased, it restarts its motion. The player can add as many (eye)balls as they can handle by pressing shift
, but there is a counter in the corner that shows how many (eye)balls they have dropped. The game goes on forever: there’s no winning, because in the Morbid Juggler the player is already dead, and they can keep juggling forever. When they run out of (eye)balls, they can just hit shift
again.
Note: when the drawing first loads, the canvas needs to be woken up with a click for it to be able to detect key presses.
Behind the scenes, the (eye)balls are objects of the Ball
class. When the user hits shift
, a new Ball
object is instantiated. I think this is where I had the most fun, figuring out how to make the objects move like a real object in free fall. If we launch an object at an angle in a vacuum chamber (no drag), the projectile will have a constant horizontal velocity. Vertically, on the other hand, the object will accelerate due to gravity, and its vertical distance (y) would be a function of time elapsed since launch, which also dictates its horizontal distance (x).
Equipped with this very basic understanding of projectile motion, I save the initial time when an (eye)ball is created, and measure the time that has passed since. This allows me to use values of elapsedTime
as a set of x values, which, when plugged into a quadratic equation, give a parabola. Changing the coefficients in the equation allows me to modify the shape of the parabola and thus the trajectory of the (eye)ball, such as how wide the arc created by its motion is. I played around with the coefficients and decided to use (0.4x)(2-x)
, which works nicely for a canvas of this size.
So I use elapsedTime
to move the object horizontally: x=x+elapsedTime/3
, and the y values come from the quadratic equation. But I also used the map
function to normalize the values, which gives me a nice set of y values from 1 to 5. Both these choices ensure that the simulation runs at a speed appropriate for a playable game.
When the object is created, the time elapsed is zero, so our x value is also zero. As timeElapsed
goes on increasing, the y values trace a parabola. The user can click and drag an (eye)ball, and upon release the object’s contactTime
property is reset so that timeElapsed
is zeroed. This is why the object restarts its parabolic motion.
The drag and drop effect was a little tricky to achieve. Since I’m updating the position of each object using a for loop, and I was trying to update the selected object’s position within the loop, my initial code kept selecting the wrong object for dragging. Although it took me a while to arrive at this, a simple solution was to use a helper function to detect when a particular object is clicked, and then sync its position with mouseX and mouseY in the draw function.
// detect when a user is dragging a ball
function mousePressed() {
for (let i = 0; i < balls.length; i++) {
let d = dist(mouseX, mouseY, balls[i].x, balls[i].y);
if (d < 20) {
draggingBall = i;
offsetX = balls[i].x - mouseX;
offsetY = balls[i].y - mouseY;
// break oout of the for loop immediately, otherwise a different ball might be selected
break;
}
}
}
For some time, I was trying to import an .obj model into my project so the ball would be 3D. The game would look nicer, but it was starting to get more complicated, and I kept getting errors from the p5 script. i settled for simply rotating the (eye)balls as they move, which is a much simpler workaround but produces a similar visual effect.