圆与圆的碰撞响应不如预期般有效

6
我正在制作一个HTML Canvas演示来学习有关圆形之间的碰撞检测和响应。我相信检测代码是正确的,但响应数学方面还不完全正确。
该演示使用TypeScript实现,TypeScript是JavaScript的类型上集,可转译为纯JavaScript。
我相信问题存在于Circle类的checkCollision方法中,具体是用于计算新速度的数学公式。
蓝色圆圈的位置由鼠标控制(使用事件监听器)。如果红色圆圈从蓝色圆圈的右侧碰撞,碰撞响应似乎可以正常工作,但如果从左侧接近,则无法正确响应。
我正在寻找一些指导,以便我可以修改checkCollision的数学公式,以正确处理任何角度的碰撞。
这是一个CodePen,用于演示和开发环境: CodePen
class DemoCanvas {
    canvasWidth: number = 500;
    canvasHeight: number = 500;
    canvas: HTMLCanvasElement = document.createElement('canvas');
    constructor() {
        this.canvas.width = this.canvasWidth;
        this.canvas.height = this.canvasHeight;
        this.canvas.style.border = '1px solid black';
        this.canvas.style.position = 'absolute';
        this.canvas.style.left = '50%';
        this.canvas.style.top = '50%';
        this.canvas.style.transform = 'translate(-50%, -50%)';
        document.body.appendChild(this.canvas);
    }

    clear() {
        this.canvas.getContext('2d').clearRect(0, 0, this.canvas.width, this.canvas.height);
    }

    getContext(): CanvasRenderingContext2D {
        return this.canvas.getContext('2d');
    }

    getWidth(): number {
        return this.canvasWidth;
    }

    getHeight(): number {
        return this.canvasHeight;
    }

    getTop(): number {
        return this.canvas.getBoundingClientRect().top;
    }

    getRight(): number {
        return this.canvas.getBoundingClientRect().right;
    }

    getBottom(): number {
        return this.canvas.getBoundingClientRect().bottom;
    }    

    getLeft(): number {
        return this.canvas.getBoundingClientRect().left;
    }
}

class Circle {
    x: number;
    y: number;
    xVelocity: number;
    yVelocity: number;
    radius: number;
    color: string;
    canvas: DemoCanvas;
    context: CanvasRenderingContext2D;

    constructor(x: number, y: number, xVelocity: number, yVelocity: number, color: string, gameCanvas: DemoCanvas) {
        this.radius = 20;
        this.x = x;
        this.y = y;
        this.xVelocity = xVelocity;
        this.yVelocity = yVelocity;
        this.color = color;
        this.canvas = gameCanvas;
        this.context = this.canvas.getContext();
    }

    public draw(): void {
        this.context.fillStyle = this.color;
        this.context.beginPath();
        this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
        this.context.fill();
    }

    public move(): void {
        this.x += this.xVelocity;
        this.y += this.yVelocity;
    }

    checkWallCollision(gameCanvas: DemoCanvas): void {
        let top = 0;
        let right = 500;
        let bottom = 500;
        let left = 0;

        if(this.y < top + this.radius) {
            this.y = top + this.radius;
            this.yVelocity *= -1;
        }

        if(this.x > right - this.radius) {
            this.x = right - this.radius;
            this.xVelocity *= -1;
        }

        if(this.y > bottom - this.radius) {
            this.y = bottom - this.radius;
            this.yVelocity *= -1;
        }

        if(this.x < left + this.radius) {
            this.x = left + this.radius;
            this.xVelocity *= -1;
        }
    }

    checkCollision(x1: number, y1: number, r1: number, x2: number, y2: number, r2: number) {
        let distance: number = Math.abs((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        // Detect collision
        if(distance < (r1 + r2) * (r1 + r2)) {
            // Respond to collision
            let newVelocityX1 = (circle1.xVelocity + circle2.xVelocity) / 2;
            let newVelocityY1 = (circle1.yVelocity + circle1.yVelocity) / 2;

            circle1.x = circle1.x + newVelocityX1;
            circle1.y = circle1.y + newVelocityY1;

            circle1.xVelocity = newVelocityX1;
            circle1.yVelocity = newVelocityY1;
        }
    }
}

let demoCanvas = new DemoCanvas();
let circle1: Circle = new Circle(250, 250, 5, 5, "#F77", demoCanvas);
let circle2: Circle = new Circle(250, 540, 5, 5, "#7FF", demoCanvas);
addEventListener('mousemove', function(e) {
    let mouseX = e.clientX - demoCanvas.getLeft();
    let mouseY = e.clientY - demoCanvas.getTop();
    circle2.x = mouseX;
    circle2.y = mouseY;
});

function loop() {
    demoCanvas.clear();
    circle1.draw();
    circle2.draw();
    circle1.move();
    circle1.checkWallCollision(demoCanvas);
    circle2.checkWallCollision(demoCanvas);
    circle1.checkCollision(circle1.x, circle1.y, circle1.radius, circle2.x, circle2.y, circle2.radius);
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
2个回答

4

弹性二维碰撞

问题可能是因为球没有彼此移开,然后在下一帧它们仍然重叠并且变得更糟。这只是从代码看我的猜测。

一个简单的解决方案。

在两个球改变方向之前,您必须确保它们的位置正确。它们必须只是接触(没有重叠),否则它们会相互卡住。

检测碰撞并修正位置。

   // note I am using javascript.
   // b1,b2 are the two balls or circles
   // b1.dx,b1.dy are velocity (deltas) to save space same for b2


   // get dist between them
   // first vect from one to the next
   const dx = b2.x - b1.x;
   const dy = b2.y - b1.y;

   // then distance
   const dist = Math.sqrt(dx*dx + dy*dy);

   // then check overlap
   if(b1.radius + b2.radius >= dist){ // the balls overlap
        // normalise the vector between them
         const nx = dx / dist;
         const ny = dy / dist;

         // now move each ball away from each other 
         // along the same line as the line between them
         // Use the ratio of the radius to work out where they touch
         const touchDistFromB1 = (dist * (b1.radius / (b1.radius + b2.radius)))         
         const contactX = b1.x + nx * touchDistFromB1;
         const contactY = b1.y + ny * touchDistFromB1;

         // now move each ball so that they just touch
         // move b1 back
         b1.x = contactX - nx * b1.radius;
         b1.y = contactY - ny * b1.radius;

         // and b2 in the other direction
         b2.x = contactX + nx * b2.radius;
         b2.y = contactY + ny * b2.radius;

如果一个是静止的

如果其中一个球是静止的,那么您可以保持它的位置并移动另一个球。

// from contact test for b1 is immovable
if(b1.radius + b2.radius >= dist){ // the balls overlap
    // normalise the vector between them
     const nx = dx / dist;
     const ny = dy / dist;

     // move b2 away from b1 along the contact line the distance of the radius summed
     b2.x = b1.x + nx * (b1.radius + b2.radius);
     b2.y = b1.y + ny * (b1.radius + b2.radius);

现在你已经正确地分离了球,可以计算新的轨迹。
改变轨迹的方法有很多种,但我最喜欢的是弹性碰撞。我从二维空间中的弹性碰撞维基来源中创建了一个函数,并在游戏中使用它已经有一段时间了。
该函数和信息在底部的片段中。
接下来,我将展示如何从上面的代码中调用该函数。
 // get the direction and velocity of each ball
 const v1 = Math.sqrt(b1.dx * b1.dx + b1.dy * b1.dy);
 const v2 = Math.sqrt(b2.dx * b2.dx + b2.dy * b2.dy);

 // get the direction of travel of each ball
 const dir1 = Math.atan2(b1.dy, b1.dx);
 const dir2 = Math.atan2(b2.dy, b2.dx);

 // get the direction from ball1 center to ball2 cenet
 const directOfContact = Math.atan2(ny, nx);

 // You will also need a mass. You could use the area of a circle, or the
 // volume of a sphere to get the mass of each ball with its radius
 // this will make them react more realistically
 // An approximation is good as it is the ratio not the mass that is important
 // Thus ball are spheres. Volume is the cubed radius
 const mass1 = Math.pow(b1.radius,3);
 const mass1 = Math.pow(b2.radius,3);

最后,您可以调用该函数

 ellastic2DCollistionD(b1, b2, v1, v2, d1, d2, directOfContact, mass1, mass2);

它将正确设置两个球的增量。

在碰撞函数之后,沿着它们的增量移动球的位置。

 b1.x += b1.dx;
 b1.y += b1.dy;
 b2.x += b1.dx;
 b2.y += b1.dy;

如果其中一个球是静止的,你可以忽略变化量。

弹性碰撞函数 2D

源自于二维空间中的弹性碰撞维基百科的信息。

// obj1, obj2 are the object that will have their deltas change
// velocity1, velocity2 is the velocity of each
// dir1, dir2 is the direction of travel
// contactDir is the direction from the center of the first object to the center of the second.
// mass1, mass2 is the mass of the first and second objects.
//
// function ellastic2DCollistionD(obj1, obj2, velocity1, velocity2, dir1, dir2, contactDir, mass1, mass2){
// The function applies the formula below twice, once fro each object, allowing for a little optimisation.


// The formula of each object's new velocity is 
//
// For 2D moving objects
// v1,v2 is velocity  
// m1, m2 is the mass 
// d1 , d2 us the direction of moment
// p is the angle of contact; 
//
//      v1* cos(d1-p) * (m1 - m2) + 2 * m2 * v2 * cos(d2- p)
// vx = ----------------------------------------------------- * cos(p) + v1 * sin(d1-p) * cos(p + PI/2)
//                    m1 + m2

//      v1* cos(d1-p) * (m1 - m2) + 2 * m2 * v2 * cos(d2- p)
// vy = ----------------------------------------------------- * sin(p) + v1 * sin(d1-p) * sin(p + PI/2)
//                     m1 + m2

// More info can be found at https://en.wikipedia.org/wiki/Elastic_collision#Two-dimensional

// to keep the code readable I use abbreviated names
function ellastic2DCollistionD(obj1, obj2, v1, v2, d1, d2, cDir, m1, m2){

    const mm = m1 - m2;
    const mmt = m1 + m2;
    const v1s = v1 * Math.sin(d1 - cDir);

    const cp = Math.cos(cDir);
    const sp = Math.sin(cDir);
    var cdp1 = v1 * Math.cos(d1 - cDir);
    var cdp2 = v2 * Math.cos(d2 - cDir);
    const cpp = Math.cos(cDir + Math.PI / 2)
    const spp = Math.sin(cDir + Math.PI / 2)

    var t = (cdp1 * mm + 2 * m2 * cdp2) / mmt;
    obj1.dx = t * cp + v1s * cpp;
    obj1.dy = t * sp + v1s * spp;
    cDir += Math.PI;
    const v2s = v2 * Math.sin(d2 - cDir);    
    cdp1 = v1 * Math.cos(d1 - cDir);
    cdp2 = v2 * Math.cos(d2 - cDir);    
    t = (cdp2 * -mm + 2 * m1 * cdp1) / mmt;
    obj2.dx = t * -cp + v2s * -cpp;
    obj2.dy = t * -sp + v2s * -spp;
}

注意:刚刚意识到您正在使用TypeScript,而上面的函数是特定于类型无关的。不关心obj1obj2的类型,并且将增加任何传递对象的增量。

您需要为TypeScript更改该函数。


2
速度向量应该在碰撞点处改变,这个改变是法线向量的倍数,同时也是两个圆心之间的归一化向量。
关于弹性圆形碰撞和冲量交换的计算,这里和其他地方都有几篇文章(例如Collision of circular objects,还有一个带有jsfiddle的行星台球https://dev59.com/Rn_aa4cB1Zd3GeqP45Fy#23671054)。
如果circle2绑定到鼠标上,则事件监听器也应使用与前一个点的差值和时间戳的差值来更新速度,或者更好的方法是使用某种移动平均值。在碰撞公式中,此圆的质量被认为是无限大。
由于您正在使用requestAnimationFrame,因此调用它的时间间隔应被视为随机的。最好使用实际时间戳并努力实现欧拉方法(或任何结果为一阶积分方法的方法),使用实际时间增量。碰撞过程不应包含位置更新,因为这是集成步骤的领域,反过来又需要添加测试以确保盘片实际上在一起移动。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接