Balls bouncing off of each other

末鹿安然 提交于 2020-05-13 14:32:26

问题


I am working on this script where I have x-number bouncing balls (in this case 20 balls) in a canvas. My question is, how do I make them bounce off each other when they hit, as well as bounce off the yellow rectangle when they hit it?

var mycanvas =document.getElementById("mycanvas");
var ctx=mycanvas.getContext("2d");
var w=500,h=500;

mycanvas.height=h;
mycanvas.width=w;

var ball=[];

function Ball(x,y,r,c,vx,vy){
  this.x=x; //starting x coordinate
  this.y=y; //starting x coordinate
  this.r=r; //radius  
  this.c=c; //color 
  this.vx=vx; // x direction speed
  this.vy=vy; // y direction speed
  this.update=function(){
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, Math.PI*2, false);
        ctx.fillStyle = this.c;
        ctx.fill();
        ctx.closePath();
        this.x += this.vx;
        this.y += this.vy;
        //changing direction on hitting wall
        if(this.y>=(w-10)||this.y<=10){
        this.vy=-this.vy;
         }
        if(this.x>=(h-10)||this.x<=10){
        this.vx=-this.vx;
         }
}
}

function clearCanvas(){
ctx.clearRect(0, 0, w, h);
}

var count;
for (count = 0; count < 20; count++) {
  var rndColor=Math.floor((Math.random() * 9) + 1); //random color
    ball[count]= new Ball(Math.floor((Math.random() * 490) + 1),Math.floor((Math.random() * 490)+1),5,'red',5,5);
}

function update(){
  var i;
  clearCanvas();
    //draw rectangle 
    ctx.rect(250, 200, 10, 100);
    ctx.fillStyle = 'yellow';
    ctx.fill();

  for(i=0;i<count;i++){
        ball[i].update();
    }
}

setInterval(update, 1000/60);

回答1:


To bounce balls off of one another, he's what you need to know

  1. Have the balls collided? The way to determine is to measure the distance between the centers of the two circles. If this is less than the combined radiuses, the balls have collided

  2. What direction should they have after colliding? Use use atan2 to calculate the angle between the centers of the two balls. Then set them in opposite directions on that angle, in a way that they don't end up deeper within each other. Of course, this simple solution ignores existing momentum that the balls may have. But doing the momentum calculation (which involves mass, speed, and current angle) is more complicated.




回答2:


There are several methods you can use. The following methods are about the simplest.

Update

I have added an example that uses the second method. See snippet at the bottom.

Defining the balls

Each example is as an object called Ball.

// x,y position of center, 
// vx,vy is velocity, 
// r is radius defaults 45, 
// m is mass defaults to the volume of the sphere of radius r
function Ball(x, y, vx, vy, r = 45, m = (4 / 3 * Math.PI * (r ** 3)) {
    this.r = r;
    this.m = m;
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
}
Ball.prototype = {
    // add collision functions here
};

The code assumes the balls are touching.

Elastic collisions

The logic used can be found at wikis elastic collision page

The calculation splits the forces into two parts for each ball. (4 in total for 2 balls)

  1. The transfer of energy along the line between the balls,
  2. The adjustment of energy per ball along the tangent of the collision point

Equal mass

Each ball has the same mass which means that the transfer of energy is balanced and can be ignored

After the function is called each ball has a new velocity vector.

Note that if you call collision and the velocities mean that the balls are moving away from each other (collision paradox) then the result will have the balls moving into each other (resolution paradox)

To keep the math simple the vector magnitudes u1, u2, u3, and u4 are converted into a unit that is the length of the line between the ball centers (square root of d)

collide(b) {  // b is the ball that the collision is with
    const a = this;
    const x = a.x - b.x;
    const y = a.y - b.y;  
    const d = x * x + y * y;

    const u1 = (a.vx * x + a.vy * y) / d;  // From this to b
    const u2 = (x * a.vy - y * a.vx) / d;  // Adjust self along tangent
    const u3 = (b.vx * x + b.vy * y) / d;  // From b to this
    const u4 = (x * b.vy - y * b.vx) / d;  // Adjust b  along tangent

    // set new velocities
    b.vx = x * u1 - y * u4;
    b.vy = y * u1 + x * u4;
    a.vx = x * u3 - y * u2;
    a.vy = y * u3 + x * u2;     
},

Different masses

Each ball has its own mass and thus the transfer needs to calculate the amount of energy related to the mass that is transferred.

Only the energy transferred along the line between the balls is effect by the mass differences

collideMass(b) {
    const a = this;
    const m1 = a.m;
    const m2 = b.m;        
    const x = a.x - b.x;
    const y = a.y - b.y;
    const d = x * x + y * y;

    const u1 = (a.vx * x + a.vy * y) / d; 
    const u2 = (x * a.vy - y * a.vx) / d;  
    const u3 = (b.vx * x + b.vy * y) / d;    
    const u4 = (x * b.vy - y * b.vx) / d; 

    const mm = m1 + m2;
    const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
    const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;

    b.vx = x * vu1 - y * u4;
    b.vy = y * vu1 + x * u4;
    a.vx = x * vu3 - y * u2;
    a.vy = y * vu3 + x * u2;     
},

Example

Simple ball collision example. Balls bound by lines (Note lines have an outside and inside, if looking from the start to the end the inside is on the right)

Collisions are fully resolved in chronological order between frames. The time used is a frame where 0 is the previous frame and 1 is the current frame.

canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 20;  // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 13;
const MAX_BALL_SIZE = 20;
const VEL_MIN = 1;
const VEL_MAX = 5; 
const MAX_RESOLUTION_CYCLES = 100;
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
// contact points of two circles radius r1, r2 moving along two lines (a,e)-(b,f) and (c,g)-(d,h) [where (,) is coord (x,y)]
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
    const A = a * a, B = b * b, C = c * c, D = d * d;
    const E = e * e, F = f * f, G = g * g, H = h * h;
    var R = (r1 + r2) ** 2;
    const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
    const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
    const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
    return Math.quadRoots(AA, BB, CC);
}   
Math.quadRoots = (a, b, c) => { // find roots for quadratic
    if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : []  }
    b /= a;
    var d = b * b - 4 * (c / a);
    if (d > 0) {
        d = d ** 0.5;
        return  [0.5 * (-b + d), 0.5 * (-b - d)]
    }
    return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2,  r) => {
    const xx = x2 - x1;
    const yy = y2 - y1;
    const d = vx * yy - vy * xx;
    if (d > 0) {  // only if moving towards the line
        const dd = r / (xx * xx + yy * yy) ** 0.5;
        const nx = xx * dd;
        const ny = yy * dd;
        return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
    }
}
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
}
Line.prototype = {
    draw() {
        ctx.moveTo(this.x1, this.y1);
        ctx.lineTo(this.x2, this.y2);
    },
    reverse() {
        const x = this.x1;
        const y = this.y1;
        this.x1 = this.x2;
        this.y1 = this.y2;
        this.x2 = x;
        this.y2 = y;
        return this;
    }
}
    
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
    this.r = r;
    this.m = m
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
}
Ball.prototype = {
    update() {
        this.x += this.vx;
        this.y += this.vy;
        this.vy += GRAVITY;
    },
    draw() {
        ctx.moveTo(this.x + this.r, this.y);
        ctx.arc(this.x, this.y, this.r, 0, Math.TAU);
    },
    interceptLineTime(l, time) {
        const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2,  this.r);
        if(u >= time && u <= 1) {
            return u;
        }
    },
    checkBallBallTime(t, minTime) {
        return t > minTime && t <= 1;
    },
    interceptBallTime(b, time) {
        const x = this.x - b.x;
        const y = this.y - b.y;
        const d = (x * x + y * y) ** 0.5;
        if(d > this.r + b.r) {
            const times = Math.circlesInterceptUnitTime(
                this.x, this.y, 
                this.x + this.vx, this.y + this.vy, 
                b.x, b.y,
                b.x + b.vx, b.y + b.vy, 
                this.r, b.r
            );
            if(times.length) {
                if(times.length === 1) {
                    if(this.checkBallBallTime(times[0], time)) { return times[0] }
                    return;
                }
                if(times[0] <= times[1]) {
                    if(this.checkBallBallTime(times[0], time)) { return times[0] }
                    if(this.checkBallBallTime(times[1], time)) { return times[1] }
                    return
                }
                if(this.checkBallBallTime(times[1], time)) { return times[1] }                
                if(this.checkBallBallTime(times[0], time)) { return times[0] }
            }
        }
    },
    collideLine(l, time) {
        const x1 = l.x2 - l.x1;
        const y1 = l.y2 - l.y1;
        const d = (x1 * x1 + y1 * y1) ** 0.5;
        const nx = x1 / d;
        const ny = y1 / d;            
        const u = (this.vx  * nx + this.vy  * ny) * 2;
        this.x += this.vx * time;   
        this.y += this.vy * time;   
        this.vx = (nx * u - this.vx) * WALL_LOSS;
        this.vy = (ny * u - this.vy) * WALL_LOSS;
        this.x -= this.vx * time;
        this.y -= this.vy * time;
    },
    collide(b, time) {
        const a = this;
        const m1 = a.m;
        const m2 = b.m;
        const x = a.x - b.x
        const y = a.y - b.y  
        const d = (x * x + y * y);
        const u1 = (a.vx * x + a.vy * y) / d
        const u2 = (x * a.vy - y * a.vx ) / d
        const u3 = (b.vx * x + b.vy * y) / d
        const u4 = (x * b.vy - y * b.vx ) / d
        const mm = m1 + m2;
        const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
        const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
        a.x = a.x + a.vx * time;
        a.y = a.y + a.vy * time;
        b.x = b.x + b.vx * time;
        b.y = b.y + b.vy * time;
        b.vx = x * vu1 - y * u4;
        b.vy = y * vu1 + x * u4;
        a.vx = x * vu3 - y * u2;
        a.vy = y * vu3 + x * u2;
        a.x = a.x - a.vx * time;
        a.y = a.y - a.vy * time;
        b.x = b.x - b.vx * time;
        b.y = b.y - b.vy * time;
    },
    doesOverlap(ball) {
        const x = this.x - ball.x;
        const y = this.y - ball.y;
        return  (this.r + ball.r) > ((x * x + y * y) ** 0.5);  
    }       
}

function canAdd(ball) {
    for(const b of balls) {
        if (ball.doesOverlap(b)) { return false }
    }
    return true;
}
function create(bCount) {
    lines.push(new Line(-10, 10, ctx.canvas.width + 10, 5));
    lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 10)).reverse());
    lines.push((new Line(10, -10, 4, ctx.canvas.height + 10)).reverse());
    lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 10, ctx.canvas.height + 10)); 
    while (bCount--) {
        let tries = 100;
        debugger
        while (tries--) {
            const dir = Math.rand(0, Math.TAU);
            const vel = Math.rand(VEL_MIN, VEL_MAX);
            const ball = new Ball(
                Math.rand(MAX_BALL_SIZE + 10, canvas.width - MAX_BALL_SIZE - 10), 
                Math.rand(MAX_BALL_SIZE + 10, canvas.height - MAX_BALL_SIZE - 10),
                Math.cos(dir) * vel,
                Math.sin(dir) * vel,
                Math.rand(MIN_BALL_SIZE, MAX_BALL_SIZE),
            );
            if (canAdd(ball)) {
                balls.push(ball);
                break;
            }
        }
    }
}
function resolveCollisions() {
    var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
    while(resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
        resolving = false;
        minObj = undefined;
        minBall = undefined;
        minTime = 1;
        idx = 0;
        for(const b of balls) {
            idx1 = idx + 1;
            while(idx1 < balls.length) {
                const b1 = balls[idx1++];
                const time = b.interceptBallTime(b1, after);
                if(time !== undefined) {
                    if(time <= minTime) {
                        minTime = time;
                        minObj = b1;
                        minBall = b;
                        resolving = true;
                    }
                }
            }
            for(const l of lines) {
                const time = b.interceptLineTime(l, after);
                if(time !== undefined) {
                    if(time <= minTime) {
                        minTime = time;
                        minObj = l;
                        minBall = b;
                        resolving = true;
                    }
                }
            }
            idx ++;
        }
        if(resolving) {
            if (minObj instanceof Ball) {
                minBall.collide(minObj, minTime);
            } else {
                minBall.collideLine(minObj, minTime);
            }
            after = minTime;
        }
    }
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
    resolveCollisions();
    for(const b of balls) { b.update() }
    ctx.strokeStyle = "#000";
    ctx.beginPath();
    for(const b of balls) { b.draw() }
    for(const l of lines) { l.draw() }
    ctx.stroke();
    requestAnimationFrame(mainLoop);
}
<canvas id="canvas"></canvas>


来源:https://stackoverflow.com/questions/60727534/balls-bouncing-off-of-each-other

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!