Related
I'm trying to create a ball hop game where the ball has to jump from one platform to another. However, I'm struggling with having the ball land on the platform and I think it might be because the platform itself is moving rather than the camera/background. Does anyone know a way I could move the camera, or a solution to the ball landing on the platform even if the platform is moving?
This is my code now.
let gravity = 1.5;
class Ball{
constructor(){
this.x = 0;
this.y= 200;
this.d = 30;
// constant velocity of the ball
this.v = 0.001;
this.xg = 0;
this.yg = 0;
}
draw() {
// stroke(0, 255, 255);
ellipse(this.x, this.y, this.d);
}
update() {
this.draw();
this.y += this.yg;
// ensures that ball doesnt go past screen
if (this.y + this.d + this.yg <= windowHeight) {
this.yg += gravity;
}
}
}
class Platform{
constructor({x,y}){
this.x = x;
this.y= y;
this.w = 30;
this.h = 30;
this.s = 1;
}
draw(){
push();
rectMode(CENTER);
rotateX(1.45);
rect(this.x,this.y,this.w,this.h);
pop();
}
move(){
this.y = this.y + this.s
this.s = this.s + 0.0001
}
}
let ball;
let plat;
let scrollOffset = 0;
function setup(){
createCanvas(710, 400, WEBGL);
ball = new Ball();
plat = [
(new Platform({x:0,y:200})),
(new Platform({x:50,y:100})),
(new Platform({x:0,y:0})),
(new Platform({x:-50,y:-100})),
(new Platform({x:0,y:-200})),
];
}
function draw(){
background(255);
ball.update();
plat.forEach((plat) => {
plat.draw();
plat.move();
});
plat.forEach((plat) => {
if (
ball.y + ball.d <= plat.y &&
ball.y + ball.d + ball.yg >= plat.y &&
ball.x + ball.d >= plat.x &&
ball.x <= plat.x + plat.w
) {
ball.yg = 0;
}
});
}
function keyPressed() {
if (keyCode === UP_ARROW) {
ball.yg -= 20;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js"></script>
Hi guys i'm very new to p5/js and not quite good at OOP programming.
I want to drop a ball by changing its speed once the mouse is clicked.
After trying to write and edit the code many times, the p5 editor didn't show any error massages but there aren't anything shown up on the display. So, I need a hand and some advices to fix this kind of problem.
Thank you in advance (:
let ball1;
let circleX = 50;
let circleY = 50;
let xspeed = 0; // Speed of the shape
let yspeed = 0; // Speed of the shape
let xdirection = 1; // Left or Right
let ydirection = 1; // Top to Bottom
let rad =50;
function setup() {
createCanvas(400, 400);
ball1 = new Ball();
}
function draw() {
background(220);
ball1.x =20;
ball1.y = 50;
ball1.c = color(25,0,100)
ball1.body();
ball1.move();
ball1.bounce();
}
class Ball {
constructor(){
this.x = width/2;
this.y = height;
this.w = 30;
this.h = 30;
this.c = color(0,255,0);
this.xspeed = 0;
this.yspeed = 0;
}
body(){
noStroke();
fill(this.c);
ellipse(this.x, this.y, this.w, this.h);
}
move() {
//this.xpos = width / 2;
//this.ypos = height / 2;
this.x = this.x + this.xspeed * this.xdirection;
this.y = this.y + this.yspeed * this.ydirection;
}
bounce() {
if (this.x > width - rad || this.x < rad) {
this.xdirection *= -1;
}
if (this.y > height - rad || this.y < rad) {
this.ydirection *= -1;
}
}
if(mouseIsPressed) { // if the mouse is pressed
//set the new speed;
this.fill(0, 0, 0);
this.xspeed =1.5;
this.yspeed=1.5;
}
}
You've got some good beginnings to your code. I modified it just a bit to assist toward OOP programming. See the code at the end of my answer for the full source.
Solution
The main question you are asking involves setting the ball's speed when the mouse button is pressed. For that, you should declare a mousePressed() function in your code as such (borrowing from your if block):
function mousePressed() {
// if the mouse is pressed
// set the new speed;
ball.xspeed = 1.5;
ball.yspeed = 1.5;
}
That should set the speed of the ball and that should solve your problem.
Other thoughts
Some other steps I took to adjust your code to more working conditions include:
I created a reset() function to be used in the setup() function and potentially able to reset the animation whenever you would like.
I changed the rad variable declaration to const rad = 50 since it doesn't change
I changed the Ball constructor to include initial (x, y) coordinates when creating the ball (could be helpful if creating multiple balls)
If you want to create an arbitrary number of balls, create an array and use array.push(new Ball(x, y)) for each ball and loop through the array to move()/bounce()/body() each ball.
I'm assuming rad is meant to be the radius of the balls, so I set the this.w and this.h to rad * 2 since with and height would equate to the diameter.
let ball;
const rad = 50;
// let circleX = 50;
// let circleY = 50;
//let xspeed = 0; // Speed of the shape
//let yspeed = 0; // Speed of the shape
function setup() {
createCanvas(400, 400);
reset();
}
function draw() {
background(220);
ball.body();
ball.move();
ball.bounce();
}
function mousePressed() {
// if the mouse is pressed
//set the new speed;
ball.xspeed = 1.5;
ball.yspeed = 1.5;
}
function reset() {
ball = new Ball(width / 2, height / 2);
}
class Ball {
constructor(x, y) {
this.x = x;
this.y = y;
this.w = rad * 2;
this.h = rad * 2;
this.c = color(25, 0, 100);
this.xspeed = 0;
this.yspeed = 0;
this.xdirection = 1;
this.ydirection = 1;
}
body() {
noStroke();
fill(this.c);
ellipse(this.x, this.y, this.w, this.h);
}
move() {
this.x = this.x + this.xspeed * this.xdirection;
this.y = this.y + this.yspeed * this.ydirection;
}
bounce() {
if (this.x > width - rad || this.x < rad) {
this.xdirection *= -1;
}
if (this.y > height - rad || this.y < rad) {
this.ydirection *= -1;
}
}
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.min.js"></script>
Numbers before code are line numbers, they might be wrong, but should be close enough...
You need to move the 64 if(mouseIsPressed){} into a function like 47 move().
Also you haven't declared and initialized 39 this.xdirection
in the constructor(){}.
I think 67 this.fill(0, 0, 0) should be fill(0).
Maybe 24 ball1.body(); should be after the other methods (24 -> 26 line).
I think the if() statements in bounce(){} are wrong, since they'd need to have AND or && instead of OR or ||.
Also you might want to make the if(mouseIsPressed) into function mousePressed(){} mouseIsPressed is a variable that is true if mouse button is down; mousePressed is a function that gets called once when user presses the mouse button.
function mousePressed(){
ball1.xSpeed = 1.5
ball1.ySpeed = 1.5
}
There might very well be other things i haven't noticed.
I am trying to write aa small physics demo using Javascript. I have multiple balls that bounce off each other just fine, but things go wrong when I try to add gravity.
I am trying to conserve the momentum once they hit, but when I add constant gravity to each one, the physics start to break down.
Here is what I have in terms of code:
class Ball {
constructor ({
x,
y,
vx,
vy,
radius,
color = 'red',
}) {
this.x = x
this.y = y
this.vx = vx
this.vy = vy
this.radius = radius
this.color = color
this.mass = 1
}
render (ctx) {
ctx.save()
ctx.fillStyle = this.color
ctx.strokeStyle = this.color
ctx.translate(this.x, this.y)
ctx.strokeRect(-this.radius, -this.radius, this.radius * 2, this.radius * 2)
ctx.beginPath()
ctx.arc(0, 0, this.radius, Math.PI * 2, false)
ctx.closePath()
ctx.fill()
ctx.restore()
return this
}
getBounds () {
return {
x: this.x - this.radius,
y: this.y - this.radius,
width: this.radius * 2,
height: this.radius * 2
}
}
}
const intersects = (rectA, rectB) => {
return !(rectA.x + rectA.width < rectB.x ||
rectB.x + rectB.width < rectA.x ||
rectA.y + rectA.height < rectB.y ||
rectB.y + rectB.height < rectA.y)
}
const checkWall = (ball) => {
const bounceFactor = 0.5
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius
ball.vx *= -bounceFactor
}
if (ball.x - ball.radius < 0) {
ball.x = ball.radius
ball.vx *= -bounceFactor
}
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius
ball.vy *= -1
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius
ball.vy *= -bounceFactor
}
}
const rotate = (x, y, sin, cos, reverse) => {
return {
x: reverse ? x * cos + y * sin : x * cos - y * sin,
y: reverse ? y * cos - x * sin : y * cos + x * sin
}
}
const checkCollision = (ball0, ball1, dt) => {
const dx = ball1.x - ball0.x
const dy = ball1.y - ball0.y
const dist = Math.sqrt(dx * dx + dy * dy)
const minDist = ball0.radius + ball1.radius
if (dist < minDist) {
//calculate angle, sine, and cosine
const angle = Math.atan2(dy, dx)
const sin = Math.sin(angle)
const cos = Math.cos(angle)
//rotate ball0's position
const pos0 = {x: 0, y: 0}
//rotate ball1's position
const pos1 = rotate(dx, dy, sin, cos, true)
//rotate ball0's velocity
const vel0 = rotate(ball0.vx, ball0.vy, sin, cos, true)
//rotate ball1's velocity
const vel1 = rotate(ball1.vx, ball1.vy, sin, cos, true)
//collision reaction
const vxTotal = (vel0.x - vel1.x)
vel0.x = ((ball0.mass - ball1.mass) * vel0.x + 2 * ball1.mass * vel1.x) /
(ball0.mass + ball1.mass)
vel1.x = vxTotal + vel0.x
const absV = Math.abs(vel0.x) + Math.abs(vel1.x)
const overlap = (ball0.radius + ball1.radius) - Math.abs(pos0.x - pos1.x)
pos0.x += vel0.x / absV * overlap
pos1.x += vel1.x / absV * overlap
//rotate positions back
const pos0F = rotate(pos0.x, pos0.y, sin, cos, false)
const pos1F = rotate(pos1.x, pos1.y, sin, cos, false)
//adjust positions to actual screen positions
ball1.x = ball0.x + pos1F.x
ball1.y = ball0.y + pos1F.y
ball0.x = ball0.x + pos0F.x
ball0.y = ball0.y + pos0F.y
//rotate velocities back
const vel0F = rotate(vel0.x, vel0.y, sin, cos, false)
const vel1F = rotate(vel1.x, vel1.y, sin, cos, false)
ball0.vx = vel0F.x
ball0.vy = vel0F.y
ball1.vx = vel1F.x
ball1.vy = vel1F.y
}
}
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let oldTime = 0
canvas.width = innerWidth
canvas.height = innerHeight
document.body.appendChild(canvas)
const log = document.getElementById('log')
const balls = new Array(36).fill(null).map(_ => new Ball({
x: Math.random() * innerWidth,
y: Math.random() * innerHeight,
vx: (Math.random() * 2 - 1) * 5,
vy: (Math.random() * 2 - 1) * 5,
radius: 20,
}))
requestAnimationFrame(updateFrame)
function updateFrame (ts) {
const dt = ts - oldTime
oldTime = ts
ctx.clearRect(0, 0, innerWidth, innerHeight)
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// ADD GRAVITY HERE
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// CHECK FOR COLLISIONS HERE
checkCollision(ball0, ball1, dt)
}
}
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
// const dist = ball2.x - ball1.x
// if (Math.abs(dist) < ball1.radius + ball2.radius) {
// const vxTotal = ball1.vx - ball2.vx
// ball1.vx = ((ball1.mass - ball2.mass) * ball1.vx + 2 * ball2.mass * ball2.vx) / (ball1.mass + ball2.mass)
// ball2.vx = vxTotal + ball1.vx
// ball1.x += ball1.vx
// ball2.x += ball2.vx
// }
// ball.vy += 0.5
// ball.x += ball.vx
// ball.y += ball.vy
//
// ball.render(ctx)
requestAnimationFrame(updateFrame)
}
* { margin: 0; padding: 0; }
As you can see, I have checkCollision helper method, which calculates the kinetic energy and new velocities of a ball once it has collided with another ball. My update loop looks like this:
// add velocities to balls position
// check if its hitting any wall and bounce it back
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// Add constant gravity to the vertical velocity
// When balls stack up on each other at the bottom, the gravity is still applied and my
// "checkCollision" method freaks out and the physics start to explode
ball.vy += 0.8
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// Check collisions between two balls
checkCollision(ball0, ball1, dt)
}
}
// Finally render the ball on-screen
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
How do I calculate aa gravity, while preventing the physics from exploding when the balls start stacking on top of each other?
It seems that the gravity force is colliding with the "checkCollision" method. The checkCollision method tries to move them back in place, but the constant gravity overwrites it and continues pulling them down.
EDIT: After some reading I understand some Verlet integration is in order, but am having difficulties with wrapping my head around it.
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// This line needs to be converted to verlet motion?
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
Balls do not overlap
There is a fundamental flaw in the collision testing due to the fact that the collisions are calculated only when 2 balls overlap. In the real world this never happens.
The result of "collide on overlap" when many balls are interacting, will result in behavior that does not conserve the total energy of the system.
Resolve by order of collision
You can resolve collisions such that balls never overlap however the amount of processing is indeterminate per frame, growing exponentially as the density of balls increases.
The approach is to locate the first collision between balls in the time between frames. Resolve that collision and then with the new position of that collision find the next collision closest in time forward from the last. Do that until there are no pending collisions for that frame. (There is more to it than that) The result is that the simulation will never be in the impossible state where balls overlap.
Check out my Pool simulator on CodePen that uses this method to simulate pool balls. The balls can have any speed and always resolve correctly.
Verlet integration.
However you can reduce the noise using the overlapping collisions by using verlet integration which will keep the total energy of the balls at a more stable level.
To do that we introduce 2 new properties of the ball, px, py that hold the previous position of the ball.
Each frame we calculate the balls velocity as the difference between the current position and the new position. That velocity is used to do the calculations for the frame.
When a ball changes direction (hits wall or another ball) we also need to change the balls previous position to match where it would have been on the new trajectory.
Use constant time steps.
Using time steps based on time since last frame will also introduce noise and should not be used in the overlap collision method.
Reduce time, increase iteration
To further combat the noise you need to slow the overall speed of the balls to reduce the amount they overlay and thus more closely behave as if they collided at the point ballA.radius + ballB.radius apart. Also you should test every ball against every other ball, not just ball against balls above it in the balls array.
To keep the animation speed up you solve the ball V ball V wall collisions a few times per frame. The example does 5. The best value depends on the total energy of the balls, the level of noise that is acceptable, and the CPU power of the device its running on.
Accuracy matters
Your collision function is also way out there. I had a quick look and it did not look right. I added an alternative in the example.
When a ball hits a wall it does so at some time between the frames. You must move the ball away from the wall by the correct distance. Not doing so is like simulating a ball that sticks to a wall a tiny bit each time it hits, further diverging from what really happens.
Example
This is a rewrite of your original code. Click canvas to add some energy.
const ctx = canvas.getContext("2d");
const BOUNCE = 0.75;
const resolveSteps = 5;
var oldTime = 0;
const $setOf = (count, fn = (i) => i) => {var a = [], i = 0; while (i < count) { a.push(fn(i++)) } return a };
const $rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const $randP = (min = 1, max = min + (min = 0), p = 2) => Math.random() ** p * (max - min) + min;
var W = canvas.width, H = canvas.height;
const BALL_COUNT = 80;
const BALL_RADIUS = 15, BALL_MIN_RADIUS = 6;
const GRAV = 0.5 / resolveSteps;
requestAnimationFrame(updateFrame);
canvas.addEventListener("click", () => {
balls.forEach(b => {
b.px = b.x + (Math.random() * 18 - 9);
b.py = b.y + (Math.random() * -18);
})
});
class Ball {
constructor({x, y, vx, vy, radius}) {
this.x = x;
this.y = y;
this.px = x - vx;
this.py = y - vy;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.mass = radius * radius * Math.PI * (4 / 3); // use sphere volume as mass
}
render(ctx) {
ctx.moveTo(this.x + this.radius, this.y);
ctx.arc(this.x, this.y, this.radius, Math.PI * 2, false);
}
move() {
this.vx = this.x - this.px;
this.vy = this.y - this.py;
this.vy += GRAV;
this.px = this.x;
this.py = this.y;
this.x += this.vx;
this.y += this.vy;
this.checkWall();
}
checkWall() {
const ball = this;
const top = ball.radius;
const left = ball.radius;
const bottom = H - ball.radius;
const right = W - ball.radius;
if (ball.x > right) {
const away = (ball.x - right) * BOUNCE;
ball.x = right - away;
ball.vx = -Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
} else if (ball.x < left) {
const away = (ball.x - left) * BOUNCE;
ball.x = left + away;
ball.vx = Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
}
if (ball.y > bottom) {
const away = (ball.y - bottom) * BOUNCE;
ball.y = bottom - away;
ball.vy = -Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
} else if (ball.y < top) {
const away = (ball.y - top) * BOUNCE;
ball.y = top + away;
ball.vy = Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
}
}
collisions() {
var b, dx, dy, nx, ny, cpx, cpy, p, d, i = 0;
var {x, y, vx, vy, px, py, radius: r, mass: m} = this;
while (i < balls.length) {
b = balls[i++];
if (this !== b) {
const rr = r + b.radius;
if (x + rr > b.x && x < b.x + rr && y + rr > b.y && y < b.y + rr) {
dx = x - b.x;
dy = y - b.y;
d = (dx * dx + dy * dy) ** 0.5;
if (d < rr) {
nx = (b.x - x) / d;
ny = (b.y - y) / d;
p = 2 * (vx * nx + vy * ny - b.vx * nx - b.vy * ny) / (m + b.mass);
cpx = (x * b.radius + b.x * r) / rr;
cpy = (y * b.radius + b.y * r) / rr;
x = cpx + r * (x - b.x) / d;
y = cpy + r * (y - b.y) / d;
b.x = cpx + b.radius * (b.x - x) / d;
b.y = cpy + b.radius * (b.y - y) / d;
px = x - (vx -= p * b.mass * nx);
py = y - (vy -= p * b.mass * ny);
b.px = b.x - (b.vx += p * m * nx);
b.py = b.y - (b.vy += p * m * ny);
}
}
}
}
this.x = x;
this.y = y;
this.px = px;
this.py = py;
this.vx = vx;
this.vy = vy;
this.checkWall();
}
}
const balls = (() => {
return $setOf(BALL_COUNT, () => new Ball({
x: $rand(BALL_RADIUS, W - BALL_RADIUS),
y: $rand(BALL_RADIUS, H - BALL_RADIUS),
vx: $rand(-2, 2),
vy: $rand(-2, 2),
radius: $randP(BALL_MIN_RADIUS, BALL_RADIUS, 4),
}));
})();
function updateFrame(ts) {
var i = 0, j = resolveSteps;
ctx.clearRect(0, 0, W, H);
while (i < balls.length) { balls[i++].move() }
while (j--) {
i = 0;
while (i < balls.length) { balls[i++].collisions(balls) }
}
ctx.fillStyle = "#0F0";
ctx.beginPath();
i = 0;
while (i < balls.length) { balls[i++].render(ctx) }
ctx.fill();
requestAnimationFrame(updateFrame)
}
<canvas id="canvas" width="400" height="180" style="border:1px solid black;"></canvas>
<div style="position: absolute; top: 10px; left: 10px;">Click to stir</div>
Well, I am making a tank game. In this very basic question(hoping it'll be a basic answer) all I wanna know how to do is, pointing my tank towards my mouse. The code in my Tank class is:
class Tank {
constructor(x, y, size) {
this.startX = x;
this.startY = y;
this.size = size;
this.speed = 2;
this.rotation = 0;
this.pos = this.getPos();
this.exp = 5;
this.level = 1;
this.expRequired = 100;
this.smallerChosen = 30;
this.vel = createVector(0, 0);
this.accelaration = 1;
}
getPos() {
return createVector(this.startX, this.startY);
}
drawTurret() {
let x = this.pos.x + this.size / 4;
let y = this.pos.y + this.size / 4;
fill(0, 255, 0);
rect(x, y, this.size * 1.5, this.size * 1.5, this.size / 4);
fill(0, 255, 0)
rect(x + this.size / 2 + this.size / 10, y - this.size, this.size / 4, this.size);
}
drawBase() {
fill(0, 220, 0)
rect(this.pos.x, this.pos.y, this.size * 2, this.size * 2)
}
show() {
push();
rotate(this.rotation);
this.drawBase();
this.drawTurret();
rotate(-this.rotation);
this.size = mainTankSize + (this.exp / 3) * this.level / (this.smallerChosen / 10);
pop()
}
move() {
var newvel = createVector(mouseX - width / 2, mouseY - height / 2);
newvel.setMag(this.speed);
this.vel.lerp(newvel, this.accelaration / 30)
this.pos.add(this.vel);
}
increaseExp(exp) {
this.exp += exp;
let color;
if (exp >= this.expRequired / 5) {
color = 255, 255, 0
}
else {
color = 255
}
let s = "+" + exp;
}
eats(obj) {
let d = p5.Vector.dist(this.pos, obj.pos);
let area = PI * obj.r * obj.r;
let exp = sqrt(area / PI);
if (d - this.size <= this.size - obj.r - 50) {
if (this.pos.y - obj.pos.y < 0) {
return false;
}
this.increaseExp(exp)
return true;
}
else if (d - this.size <= this.size + obj.r && this.pos.y - obj.pos.y < 0) {
if (this.pos.x - obj.pos.x > 0) {
return false
}
this.increaseExp(exp)
return true;
}
return false;
}
}
That is it! And I have done literally everything as you can see up there but when I rotate the tank change the this.rotation variable according to mouseX, it does rotate, it surely does but... It ruins everything, like, in another file I've written alot of code which aligns the tank to the center and changes the view using the translate() function and zooms out using the scale() function. Everything was and is working perfectly but if I even hard-code and change the this.rotation variable, everything stops working all over again!
So, what I need is, some really really smart dude shows up and tells me or shares some code to change the this.rotation variable without ruining the entire website.
Thanks in advance - BSK Animations
You need to use the functions push() and pop(), use push() right before you rotate and pop() after you draw.
p5 reference
Try setting the angle to:
atan2(mouseY - this.pos.y, mouseX - this.x);
Summary:
In attempt to make a simple collision detection system between a moving rect and a falling circle, I would like to make it more realistic.
Main question:
-The main thing I would like to solve is detecting when the circle Object is hitting the corner of the rect and in return having the circle bounce based off of that angle.
The Code:
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle();
}
function draw() {
background(75);
obstacle.display();
obstacle.update();
for (var i = 0; i < balls.length; i++) {
balls[i].display();
if (!RectCircleColliding(balls[i], obstacle)){
balls[i].update();
balls[i].edges();
}
//console.log(RectCircleColliding(balls[i], obstacle));
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY));
}
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
}
this.edges = function() {
if (this.y >= height - this.r) {
this.y = height - this.r;
this.velocity = this.velocity * -1;
this.gravity = this.gravity * 1.1;
}
}
}
function Obstacle() {
this.x = width - width;
this.y = height / 2;
this.w = 200;
this.h = 25;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
function RectCircleColliding(Ball, Obstacle) {
// define obstacle borders
var oRight = Obstacle.x + Obstacle.w;
var oLeft = Obstacle.x;
var oTop = Obstacle.y;
var oBottom = Obstacle.y + Obstacle.h;
//compare ball's position (acounting for radius) with the obstacle's border
if (Ball.x + Ball.r > oLeft) {
if (Ball.x - Ball.r < oRight) {
if (Ball.y + Ball.r > oTop) {
if (Ball.y - Ball.r < oBottom) {
let oldY = Ball.y;
Ball.y = oTop - Ball.r;
Ball.velocity = Ball.velocity * -1;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return (true);
}
}
}
}
return false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>
Expected output:
I would like to see the falling circles bounce off the rectangle with respect to where they are hitting on the rectangle.
If the circles hit the corners they should bounce differently versus hitting dead center.
Prerequisite
The ball's velocity must be a vector (XY components), not just a single number.
1. Determine if the circle might hit a side or a corner
Obtain the components of the vector from the center of the rectangle to the circle, and check it against the rectangle's dimensions:
// Useful temporary variables for later use
var hx = 0.5 * obstacle.w;
var hy = 0.5 * obstacle.h;
var rx = obstacle.x + hx;
var ry = obstacle.y + hy;
// displacement vector
var dx = ball.x - rx;
var dy = ball.y - ry;
// signs
var sx = dx < -hx ? -1 : (dx > hx ? 1 : 0);
var sy = dy < -hy ? -1 : (dy > hy ? 1 : 0);
If both sx, sy are non-zero, the ball might hit a corner, otherwise it might hit a side.
2. Determine whether the circle collides
Multiply each sign by the respective half-dimension:
// displacement vector from the nearest point on the rectangle
var tx = sx * (Math.abs(dx) - hx);
var ty = sy * (Math.abs(dy) - hy);
// distance from p to the center of the circle
var dc = Math.hypot(tx, ty);
if (dc <= ball.r) {
/* they do collide */
}
3. Determine the collision normal vector
(tx, ty) are the components of the normal vector, but only if the ball's center is outside the rectangle:
// epsilon to account for numerical imprecision
const EPSILON = 1e-6;
var nx = 0, ny = 0, nl = 0;
if (sx == 0 && sy == 0) { // center is inside
nx = dx > 0 ? 1 : -1;
ny = dy > 0 ? 1 : -1;
nl = Math.hypot(nx, ny);
} else { // outside
nx = tx;
ny = ty;
nl = dc;
}
nx /= nl;
ny /= nl;
4. Resolve any "penetration"
(No immature jokes please)
This ensures that the ball will never penetrate into the surface of the rectangle, which improves the visual quality of the collisions:
ball.x += (ball.r - dc) * nx;
ball.y += (ball.r - dc) * ny;
5. Resolve the collision
If the circle is travelling in the direction of the normal, don't resolve the collision as the ball might stick to the surface:
// dot-product of velocity with normal
var dv = ball.vx * nx + ball.vy * ny;
if (dv >= 0.0) {
/* exit and don't do anything else */
}
// reflect the ball's velocity in direction of the normal
ball.vx -= 2.0 * dv * nx;
ball.vy -= 2.0 * dv * ny;
Working JS snippet
const GRAVITY = 250.0;
function Ball(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
this.vx = 0;
this.vy = 0;
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.collidePage = function(b) {
if (this.vy > 0 && this.y + this.r >= b) {
this.y = b - this.r;
this.vy = -this.vy;
}
}
this.updatePosition = function(dt) {
this.x += this.vx * dt;
this.y += this.vy * dt;
}
this.updateVelocity = function(dt) {
this.vy += GRAVITY * dt;
}
}
function Obstacle(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle(0, height / 2, 200, 25);
}
const DT = 0.05;
function draw() {
background(75);
obstacle.update();
obstacle.display();
for (var i = 0; i < balls.length; i++) {
balls[i].updatePosition(DT);
balls[i].collidePage(height);
ResolveRectCircleCollision(balls[i], obstacle);
balls[i].updateVelocity(DT);
balls[i].display();
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY, 15));
}
const EPSILON = 1e-6;
function ResolveRectCircleCollision(ball, obstacle) {
var hx = 0.5 * obstacle.w, hy = 0.5 * obstacle.h;
var rx = obstacle.x + hx, ry = obstacle.y + hy;
var dx = ball.x - rx, dy = ball.y - ry;
var sx = dx < -hx ? -1 : (dx > hx ? 1 : 0);
var sy = dy < -hy ? -1 : (dy > hy ? 1 : 0);
var tx = sx * (Math.abs(dx) - hx);
var ty = sy * (Math.abs(dy) - hy);
var dc = Math.hypot(tx, ty);
if (dc > ball.r)
return false;
var nx = 0, ny = 0, nl = 0;
if (sx == 0 && sy == 0) {
nx = dx > 0 ? 1 : -1; ny = dy > 0 ? 1 : -1;
nl = Math.hypot(nx, ny);
} else {
nx = tx; ny = ty;
nl = dc;
}
nx /= nl; ny /= nl;
ball.x += (ball.r - dc) * nx; ball.y += (ball.r - dc) * ny;
var dv = ball.vx * nx + ball.vy * ny;
if (dv >= 0.0)
return false;
ball.vx -= 2.0 * dv * nx; ball.vy -= 2.0 * dv * ny;
return true;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
I'll try to present a solution, which keeps as may from the original code as possible. The solution intends to be a evolution of the code presented in the question.
Add a sideward movement (selv.sideV) to the Ball object, which is initialized by 0:
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.sideV = 0
// ...
}
Move the ball to the side in update, by the sideward movement and reduce the sideward movment:
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
this.x += this.sideV;
this.sideV *= 0.98;
}
Create function for an intersection test of 2 boxes:
function IsectRectRect(l1, r1, t1, b1, l2, r2, t2, b2) {
return l1 < r2 && l2 < r1 && t1 < b2 && t2 < b1;
}
And a function which can calculate the refection vector R of an incident vector V to a the normal vector of surface N (the reflection like a billiard ball):
function reflect( V, N ) {
R = V.copy().sub(N.copy().mult(2.0 * V.dot(N)));
return R;
}
When the Ball collides with the Obstacle, then you've to handle 3 situations.
The Ball fully hits the top of the Obstacle: IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB)
The Ball hits the left edge of the Obstacle: IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB)
The Ball hits the right edge of the Obstacle: IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB)
In each case the normal vector for the reflection has to be calculated. This is the vector from either the top or the edge of the Obstacle to the center of the Ball.
Use the function reflect to bounce the Ball on the Obstacle:
function RectCircleColliding(Ball, Obstacle) {
let oL = Obstacle.x;
let oR = Obstacle.x + Obstacle.w;
let oT = Obstacle.y;
let oB = Obstacle.y + Obstacle.h;
let bL = Ball.x - Ball.r;
let bR = Ball.x + Ball.r;
let bT = Ball.y - Ball.r;
let bB = Ball.y + Ball.r;
let isect = false;
let hitDir = createVector(0, 1);
if ( IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB) ) {
isect = true;
} else if ( IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oL, oT))
isect = hitDir.mag() < Ball.r;
} else if ( IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oR, oT))
isect = hitDir.mag() < Ball.r;
}
if ( isect ) {
let dir = createVector(Ball.sideV, Ball.velocity);
R = reflect(dir, hitDir.normalize());
Ball.velocity = R.y;
Ball.sideV = R.x;
let oldY = Ball.y;
Ball.y = oT - Ball.r;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return true;
}
return false;
}
See the example, where I applied the changes to your original code:
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle();
}
function draw() {
background(75);
obstacle.display();
obstacle.update();
for (var i = 0; i < balls.length; i++) {
balls[i].display();
if (!RectCircleColliding(balls[i], obstacle)){
balls[i].update();
balls[i].edges();
}
//console.log(RectCircleColliding(balls[i], obstacle));
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY));
}
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.sideV = 0
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
this.x += this.sideV;
this.sideV *= 0.98;
}
this.edges = function() {
if (this.y >= height - this.r) {
this.y = height - this.r;
this.velocity = this.velocity * -1;
this.gravity = this.gravity * 1.1;
}
}
}
function Obstacle() {
this.x = width - width;
this.y = height / 2;
this.w = 200;
this.h = 25;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
function IsectRectRect(l1, r1, t1, b1, l2, r2, t2, b2) {
return l1 < r2 && l2 < r1 && t1 < b2 && t2 < b1;
}
function reflect( V, N ) {
R = V.copy().sub(N.copy().mult(2.0 * V.dot(N)));
return R;
}
function RectCircleColliding(Ball, Obstacle) {
let oL = Obstacle.x;
let oR = Obstacle.x + Obstacle.w;
let oT = Obstacle.y;
let oB = Obstacle.y + Obstacle.h;
let bL = Ball.x - Ball.r;
let bR = Ball.x + Ball.r;
let bT = Ball.y - Ball.r;
let bB = Ball.y + Ball.r;
let isect = false;
let hitDir = createVector(0, 1);
if ( IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB) ) {
isect = true;
} else if ( IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oL, oT))
isect = hitDir.mag() < Ball.r;
} else if ( IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oR, oT))
isect = hitDir.mag() < Ball.r;
}
if ( isect ) {
let dir = createVector(Ball.sideV, Ball.velocity);
R = reflect(dir, hitDir.normalize());
Ball.velocity = R.y;
Ball.sideV = R.x;
let oldY = Ball.y;
Ball.y = oT - Ball.r;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return true;
}
return false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>