Issue
I'm creating a simple program to simulate balls bouncing around a circle. I've run across a problem where occasionally, the balls will collide with the inner circle and get stuck. I'm not entirely sure why this happens, but it does happen more frequently when I increase the velocity. Here is my code:
main.js
import Ball from "./balls.js";
import GenericObject from "./genericObject.js";
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
canvas.width = innerWidth;
canvas.height = innerHeight;
const colors = ['#e81416', '#ffa500', '#faeb36', '#79c314', '#487de7', '#4b369d', '#70369d'];
const ballRadius = 40; //Radius for all balls
let balls = [];
let innerCircle = new GenericObject({
x: canvas.width / 2,
y: canvas.height / 2,
radius: 300,
color: '#ffffff'
});
//Function to spawn new ball at click inside inner circle
function getMousePosition(event) {
let mouseX = event.clientX;
let mouseY = event.clientY;
//Calculate distance between mouse click and center of inner circle
let distance = Math.sqrt((mouseX - innerCircle.position.x) ** 2 + (mouseY - innerCircle.position.y) ** 2) + ballRadius;
//If clicked inside circle, spawn a new ball
if (distance <= innerCircle.radius) {
balls.push(new Ball({
x: mouseX,
y: mouseY,
velX: 0,
velY: 6,
radius: ballRadius,
color: colors[Math.floor(Math.random() * colors.length)]
}));
}
}
canvas.addEventListener("mousedown", function (e) {
getMousePosition(e);
});
function animate() {
requestAnimationFrame(animate);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
innerCircle.update();
balls.forEach((ball) => {
ball.update();
//Calculate distance between ball and center of inner circle
let distance = Math.sqrt((ball.position.x - innerCircle.position.x) ** 2 + (ball.position.y - innerCircle.position.y) ** 2) + ball.radius;
//Check for collision with inner circle
if (distance >= innerCircle.radius) {
//Filter color array to exclude current ball's color
const filteredArray = colors.filter(element => element !== ball.color);
//Randomly choose color from filtered color array
const randomIndex = Math.floor(Math.random() * filteredArray.length);
//Change ball's color
ball.color = filteredArray[randomIndex];
//Find collision angle
const angle = Math.atan2(ball.position.y, ball.position.x);
//Find angle perpendicular to collision angle
const normalAngle = angle + Math.PI / 2;
//Create new velocity for x and y
const newVelX = ball.velocity.x * Math.cos(2 * normalAngle) - ball.velocity.y * Math.sin(2 * normalAngle);
const newVelY = ball.velocity.x * Math.sin(2 * normalAngle) + ball.velocity.y * Math.cos(2 * normalAngle);
ball.velocity.x = newVelX;
ball.velocity.y = newVelY;
//Move ball away from collision point
ball.position.y += ball.velocity.y;
ball.position.x += ball.velocity.x;
} else {
//If no collision, continue moving ball
ball.position.y += ball.velocity.y;
ball.position.x += ball.velocity.x;
}
});
}
animate();
balls.js
const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d')
class Ball {
constructor({x, y, velX, velY, radius, color, mass}) {
this.position = {
x: x,
y: y
}
this.velocity = {
x: velX,
y: velY
}
this.radius = radius
this.color = color
}
draw() {
c.beginPath();
c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2, false);
c.fillStyle = this.color;
c.fill();
c.closePath();
}
update() {
this.draw()
}
}
export default Ball
genericObject.js
const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d')
class GenericObject {
constructor({x, y, radius, color}) {
this.position = {
x: x,
y: y
}
this.radius = radius
this.color = color
}
draw() {
c.beginPath();
c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2, false);
c.fillStyle = this.color;
c.fill();
c.closePath();
}
update() {
this.draw()
}
}
export default GenericObject
Solution
The issue
You change direction when the ball collides with the edge. If the new direction and speed is not able to move the ball outside of the collision within the frame, you will get double collisions.
You can see this happen when sometimes the color flashes multiple times and the return angle looks off.
For certain angles, the quick successions of collisions keep flipping the direction and the ball is never able to get unstuck.
Possible fixes
Two fixes you often see implemented:
- Look one frame ahead when checking for collision and change course to prevent it.
- When a collision happens, move back the object that collides to a position where it just touches it.
The second one is usually the better fix, but the first one is easy to implement and works quite well in your example.
Looking ahead
Below I've made a simple change: instead of checking for collision with the current ball.position
, I virtually add ball.speed
to it and use the balls "next" position.
const nextX = ball.position.x + ball.velocity.x;
const nextY = ball.position.y + ball.velocity.y;
let distance = Math.sqrt((nextX - innerCircle.position.x) ** 2 + (nextY - innerCircle.position.y) ** 2) + ball.radius;
Shifting to the exact point of collision
If you want to go for the other one you can:
- calculate the overshoot distance (
distance - innerCircle.radius
) - find the position on the line from the inner circle at the angle of the ball's velocity that is
-overshoot
away from the current position - Update
ball.position
with to this "only just touching the inner circle surface" spot before flipping the velocity.
Running example
Note: The example below shows the implementation of the first method.
class GenericObject {
constructor({
x,
y,
radius,
color
}) {
this.position = {
x: x,
y: y
}
this.radius = radius
this.color = color
}
draw() {
c.beginPath();
c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2, false);
c.fillStyle = this.color;
c.fill();
c.closePath();
}
update() {
this.draw()
}
}
class Ball {
constructor({
x,
y,
velX,
velY,
radius,
color,
mass
}) {
this.position = {
x: x,
y: y
}
this.velocity = {
x: velX,
y: velY
}
this.radius = radius
this.color = color
}
draw() {
c.beginPath();
c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2, false);
c.fillStyle = this.color;
c.fill();
c.closePath();
}
update() {
this.draw()
}
}
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
const ctx = c;
canvas.width = innerWidth;
canvas.height = innerHeight;
const colors = ['#e81416', '#ffa500', '#faeb36', '#79c314', '#487de7', '#4b369d', '#70369d'];
const ballRadius = 40; //Radius for all balls
let balls = [];
let innerCircle = new GenericObject({
x: canvas.width / 2,
y: canvas.height / 2,
radius: 300,
color: '#ffffff'
});
//Function to spawn new ball at click inside inner circle
function getMousePosition(event) {
let mouseX = event.clientX;
let mouseY = event.clientY;
//Calculate distance between mouse click and center of inner circle
let distance = Math.sqrt((mouseX - innerCircle.position.x) ** 2 + (mouseY - innerCircle.position.y) ** 2) + ballRadius;
//If clicked inside circle, spawn a new ball
if (distance <= innerCircle.radius) {
balls.push(new Ball({
x: mouseX,
y: mouseY,
velX: 0,
velY: 6,
radius: ballRadius,
color: colors[Math.floor(Math.random() * colors.length)]
}));
}
}
canvas.addEventListener("mousedown", function(e) {
getMousePosition(e);
});
function animate() {
requestAnimationFrame(animate);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
innerCircle.update();
balls.forEach((ball) => {
ball.update();
// Calculate distance between ball and center of inner circle
// Do this for the _next_ frame so we're sure we bounce _before_ we collide
const nextX = ball.position.x + ball.velocity.x;
const nextY = ball.position.y + ball.velocity.y;
let distance = Math.sqrt((nextX - innerCircle.position.x) ** 2 + (nextY - innerCircle.position.y) ** 2) + ball.radius;
//Check for collision with inner circle
if (distance >= innerCircle.radius) {
//Filter color array to exclude current ball's color
const filteredArray = colors.filter(element => element !== ball.color);
//Randomly choose color from filtered color array
const randomIndex = Math.floor(Math.random() * filteredArray.length);
//Change ball's color
ball.color = filteredArray[randomIndex];
//Find collision angle
const angle = Math.atan2(ball.position.y, ball.position.x);
//Find angle perpendicular to collision angle
const normalAngle = angle + Math.PI / 2;
//Create new velocity for x and y
const newVelX = ball.velocity.x * Math.cos(2 * normalAngle) - ball.velocity.y * Math.sin(2 * normalAngle);
const newVelY = ball.velocity.x * Math.sin(2 * normalAngle) + ball.velocity.y * Math.cos(2 * normalAngle);
ball.velocity.x = newVelX;
ball.velocity.y = newVelY;
//Move ball away from collision point
ball.position.y += ball.velocity.y;
ball.position.x += ball.velocity.x;
} else {
//If no collision, continue moving ball
ball.position.y += ball.velocity.y;
ball.position.x += ball.velocity.x;
}
});
}
animate();
<canvas></canvas>
Answered By - user3297291
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.