圆形/矩形碰撞响应

5

我之前制作了一个小的打砖块克隆游戏,并希望对其进行升级,主要是为了改进碰撞。当我第一次制作它时,我的球和砖块之间有基本的"碰撞"检测,实际上将球视为另一个矩形。但这在边缘碰撞方面存在问题,因此我想改变它。问题是,我找到了一些解决方法:

例如这张图片:

enter image description here

还有这个帖子的最后一条评论:circle/rect collision reaction,但我找不到如何计算最终速度向量。

到目前为止,我已经:

- 找到了矩形上最近的点
- 创建了法向量和切向量

现在我需要以某种方式“将速度向量分成正常分量和切向分量;否定正常分量并添加正常和切向分量以获得新的速度向量”。如果这似乎非常简单,我很抱歉,但我无法想通... 代码如下:

function collision(rect, circle){
  var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
  var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.w));

  var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
  var dnormal = createVector(- dist.y, dist.x);
//change current circle vel according to the collision response
}

谢谢!

编辑:我还找到了这个,但我不确定它是否适用于矩形的所有点或仅适用于角落。


我不确定你使用的是什么类型的 rect 对象,但我有一种感觉,在计算 NearestY 时,你想要高度而不是宽度,就像 rect.h 而不是 rect.w - arbuthnott
3个回答

10
最好用一些图例来解释:
有一个入射角等于反射角的角度。我们将其称为θ。
θ = 法线角度 - 入射角度。 atan2 是计算向量与正x轴之间的角度的函数。
然后,下面的代码紧随其后:
function collision(rect, circle){
  var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
  var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));

  var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
  var dnormal = createVector(- dist.y, dist.x);

  var normal_angle = atan2(dnormal.y, dnormal.x);
  var incoming_angle = atan2(circle.vel.y, circle.vel.x);
  var theta = normal_angle - incoming_angle;
  circle.vel = circle.vel.rotate(2*theta);
}

另一种方法是获取沿切线的速度,然后将该值的两倍从圆形速度中减去。

angles3

代码如下:

function collision(rect, circle){
  var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
  var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));

  var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
  var tangent_vel = dist.normalize().dot(circle.vel);
  circle.vel = circle.vel.sub(tangent_vel.mult(2));
}

上述两个代码片段基本相同,执行时间也差不多(可能)。只需选择你最理解的那个。

另外,正如@arbuthnott所指出的,NearestY中存在复制粘贴错误,应该使用rect.h而不是rect.w

编辑:我忘记了位置分辨率。这是将两个物理对象移开,使它们不再相交的过程。在这种情况下,由于块是静态的,因此我们只需要移动小球。

penetration diagram

function collision(rect, circle){
  var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
  var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));    
  var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);

  if (circle.vel.dot(dist) < 0) { //if circle is moving toward the rect
    //update circle.vel using one of the above methods
  }

  var penetrationDepth = circle.r - dist.mag();
  var penetrationVector = dist.normalise().mult(penetrationDepth);
  circle.pos = circle.pos.sub(penetrationVector);
}

谢谢你的答案,我一定会尝试的!至于 rect.w/rect.h,这不是复制粘贴错误,在我的情况下,“矩形”实际上是正方形,所以只需要一个值即可,我保留了 rect.w ! - Alivanar
好的,就我测试过的情况来看(指第一种方法),它运行良好,非常感谢!不过我还有另一个问题,在游戏中,我有多个矩形/正方形/平铺物相邻,当球撞到交点时,它会穿过平铺物之间,并在所有附近的交点内出现故障,有什么办法可以解决吗?也许在计算中添加关于跟随/前置平铺物的条件? - Alivanar
啊,我忘记了位置分辨率。请看编辑后的答案。这通常仍然会在某些地方出现抖动,但通常会好得多。 - cdo256
感谢您的编辑,尽管在我的情况下,位置分辨率使事情变得更糟(检测到多个碰撞,计算点数时不好...),所以我只是删除了最后3行,现在它运行得很好!:)非常感谢!! - Alivanar
嗯,第二个代码块中有错误(或缺少细节),对吧?tangent_vel是一个点积(即一个数字),它从圆的速度向量中减去。 - exscape

5

球和棒的碰撞

处理球和矩形碰撞的最佳方法是利用系统的对称性。

将球视为点。

首先考虑球,它有一个半径r,定义了所有距离中心r的点。但是我们可以将球变成一个点,并将半径添加到矩形中。现在,球只是一个沿着时间移动的单个点,这是一条线。

矩形的所有边都增加了半径。下图显示了这是如何工作的。

enter image description here

绿色矩形是原始矩形。球A、B没有接触矩形,而球C、D接触。球A、D表示一个特殊情况,但很容易解决,您将看到。

所有运动均为一条线。

现在,我们有一个更大的矩形和一个作为时间点移动的球(一条线),但是矩形也在移动,这意味着随着时间的推移,边缘将扫出区域,这对我的大脑来说过于复杂,因此我们可以再次使用相对运动中的对称性。

从球棒的角度来看,它是静止的,而球在移动,而从球的角度来看,它仍在静止,而棒在移动。他们都看到彼此以相反的方向移动。

由于球现在是一个点,因此更改其运动只会更改其沿着行进的线路。因此,我们现在可以将棒固定在空间中并从球中减去其运动。由于棒现在是固定的,我们可以将其中心点移动到原点(0,0),并朝相反方向移动球。

此时,我们做出了一个重要的假设。球和棒始终处于不接触的状态,当我们移动球和/或棒时,它们可能接触。如果它们确实接触,我们计算一个新的轨迹,使它们不接触。

两种可能的碰撞

现在有两种可能的碰撞情况,一种是球击中棒的侧面,另一种是球击中棒的角落。

下图显示了棒位于原点,球相对于棒的运动和位置。它沿着从A到B的红线行驶,然后反弹到C

球撞边缘

enter image description here

球撞角落

enter image description here

由于对称性,无论击中哪一侧或角落都没有影响。事实上,我们可以根据球到球棒中心的距离来镜像整个问题。因此,如果球在球棒的左侧,则将其位置和运动在x轴方向上镜像,y轴方向也是如此(您必须通过信号量来跟踪此镜像,以便在找到解决方案后可以将其反转)。
代码
示例通过函数“doBatBall(bat,ball)”执行上述操作。球具有一些重力并将从画布的侧面弹起。通过鼠标移动球棒。球棒的移动将被传递给球,但球棒不会感受到任何来自球的力。

const ctx = canvas.getContext("2d");
const mouse  = {x : 0, y : 0, button : false}
function mouseEvents(e){
 mouse.x = e.pageX;
 mouse.y = e.pageY;
 mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
        
// short cut vars 
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;
const gravity = 1;


// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx,style) => { Object.keys(style).forEach(key=> ctx[key] = style[key] ) };

// the ball
const ball = {
    r : 50,
    x : 50,
    y : 50,
    dx : 0.2,
    dy : 0.2,
    maxSpeed : 8,
    style : {
        lineWidth : 12,
        strokeStyle : "green",
    },
    draw(ctx){
        setStyle(ctx,this.style);
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.r-this.style.lineWidth * 0.45,0,PI2);
        ctx.stroke();
    },
    update(){
        this.dy += gravity;
        var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
        var x = this.x + this.dx;
        var y = this.y + this.dy;
        
        if(y > canvas.height - this.r){
             y = (canvas.height - this.r) - (y - (canvas.height - this.r));
             this.dy = -this.dy;
        }
        if(y < this.r){
             y = this.r - (y - this.r);
             this.dy = -this.dy;
        }
        if(x > canvas.width - this.r){
             x = (canvas.width - this.r) - (x - (canvas.width - this.r));
             this.dx = -this.dx;
        }
        if(x < this.r){
             x = this.r - (x - this.r);
             this.dx = -this.dx;
        }
        
        this.x = x;
        this.y = y;
        if(speed > this.maxSpeed){  // if over speed then slow the ball down gradualy
            var reduceSpeed = this.maxSpeed + (speed-this.maxSpeed) * 0.9; // reduce speed if over max speed
            this.dx = (this.dx / speed) * reduceSpeed;
            this.dy = (this.dy / speed) * reduceSpeed;
        }
        
        
    }
}
const ballShadow = { // this is used to do calcs that may be dumped
    r : 50,
    x : 50,
    y : 50,
    dx : 0.2,
    dy : 0.2,
}
// Creates the bat
const bat = {
    x : 100,
    y : 250,
    dx : 0,
    dy : 0,
    width : 140,
    height : 10,
    style : {
        lineWidth : 2,
        strokeStyle : "black",
    },
    draw(ctx){
        setStyle(ctx,this.style);
        ctx.strokeRect(this.x - this.width / 2,this.y - this.height / 2, this.width, this.height);
    },
    update(){
        this.dx = mouse.x - this.x;
        this.dy = mouse.y - this.y;        
        var x = this.x + this.dx;
        var y = this.y + this.dy;
        x < this.width / 2  && (x = this.width / 2);
        y < this.height / 2  && (y = this.height / 2);
        x > canvas.width - this.width / 2  && (x = canvas.width  - this.width / 2);
        y > canvas.height - this.height / 2  && (y = canvas.height - this.height / 2);        
        this.dx = x - this.x;
        this.dy = y - this.y;
        this.x = x;
        this.y = y;
        
    }
}

//=============================================================================
// THE FUNCTION THAT DOES THE BALL BAT sim.
// the ball and bat are at new position
function doBatBall(bat,ball){
    var mirrorX = 1;
    var mirrorY = 1;

    const s = ballShadow; // alias
    s.x = ball.x;
    s.y = ball.y;
    s.dx = ball.dx;
    s.dy = ball.dy;
    s.x -= s.dx;
    s.y -= s.dy;

    // get the bat half width height
    const batW2 = bat.width / 2;
    const batH2 = bat.height / 2;  

    // and bat size plus radius of ball
    var batH = batH2 + ball.r;
    var batW = batW2 + ball.r;

    // set ball position relative to bats last pos
    s.x -= bat.x;
    s.y -= bat.y;
    
    // set ball delta relative to bat
    s.dx -= bat.dx;
    s.dy -= bat.dy;
    
    // mirror x and or y if needed
    if(s.x < 0){
        mirrorX = -1;
        s.x = -s.x;
        s.dx = -s.dx;
    }
    if(s.y < 0){
        mirrorY = -1;
        s.y = -s.y;
        s.dy = -s.dy;
    }
    
    
    // bat now only has a bottom, right sides and bottom right corner
    var distY = (batH - s.y); // distance from bottom 
    var distX = (batW - s.x); // distance from right

    if(s.dx > 0 && s.dy > 0){ return }// ball moving away so no hit

    var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy); // get ball speed relative to bat

    // get x location of intercept for bottom of bat
    var bottomX = s.x +(s.dx / s.dy) * distY;

    // get y location of intercept for right of bat
    var rightY = s.y +(s.dy / s.dx) * distX;

    // get distance to bottom and right intercepts
    var distB = Math.hypot(bottomX - s.x, batH - s.y);
    var distR = Math.hypot(batW - s.x, rightY - s.y);
    var hit = false;

    if(s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR){  // if hit is on bottom and bottom hit is closest
        hit = true;     
        s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
        s.dy = -s.dy;
    }
    if(! hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB){ // if hit is on right and right hit is closest
        hit = true;     
        s.x =  batW  - s.dx * ((ballSpeed - distR) / ballSpeed);;
        s.dx = -s.dx;
    }
    if(!hit){  // if no hit may have intercepted the corner. 
        // find the distance that the corner is from the line segment from the balls pos to the next pos
        const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy)/(ballSpeed * ballSpeed);
        
        // get the closest point on the line to the corner
        var cpx = s.x + s.dx * u;
        var cpy = s.y + s.dy * u;
        
        // get ball radius squared
        const radSqr = ball.r * ball.r;
        
        // get the distance of that point from the corner squared
        const dist  = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
        
        // is that distance greater than ball radius
        if(dist > radSqr){ return }  // no hit

        // solves the triangle from center to closest point on balls trajectory
        var d = Math.sqrt(radSqr - dist) / ballSpeed;

        // intercept point is closest to line start
        cpx -= s.dx * d;
        cpy -= s.dy * d;
        
        // get the distance from the ball current pos to the intercept point
        d = Math.hypot(cpx - s.x,cpy - s.y);
        
        // is the distance greater than the ball speed then its a miss
        if(d > ballSpeed){  return  } // no hit return
        
        s.x = cpx;  // position of contact
        s.y = cpy;        
        
        // find the normalised tangent at intercept point 
        const ty = (cpx - batW2) / ball.r;
        const tx = -(cpy - batH2) / ball.r;
        
        // calculate the reflection vector
        const bsx = s.dx / ballSpeed;   // normalise ball speed
        const bsy = s.dy / ballSpeed;
        const dot = (bsx * tx + bsy * ty) * 2;
        
        // get the distance the ball travels past the intercept
        d = ballSpeed - d;
        
        // the reflected vector is the balls new delta (this delta is normalised)
        s.dx = (tx * dot - bsx); 
        s.dy = (ty * dot - bsy);
        
        // move the ball the remaining distance away from corner
        s.x += s.dx * d;
        s.y += s.dy * d;        
        
        // set the ball delta to the balls speed
        s.dx *= ballSpeed;
        s.dy *= ballSpeed;
        hit = true;
    }
    
    // if the ball hit the bat restore absolute position
    if(hit){
        // reverse mirror
        s.x *= mirrorX;
        s.dx *= mirrorX;
        s.y *= mirrorY;
        s.dy *= mirrorY;

        // remove bat relative position
        s.x += bat.x;
        s.y += bat.y;
        
        // remove bat relative delta
        s.dx += bat.dx;
        s.dy += bat.dy;
        
        // set the balls new position and delta
        ball.x = s.x;
        ball.y = s.y;
        ball.dx = s.dx;
        ball.dy = s.dy;
    }
    
}





// main update function
function update(timer){

    if(w !== innerWidth || h !== innerHeight){
      cw = (w = canvas.width = innerWidth) / 2;
      ch = (h = canvas.height = innerHeight) / 2;
    }
      
  
    
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h);
    
    // move bat and ball
    bat.update();
    ball.update();
    
    // check for bal bat contact and change ball position and trajectory if needed
    doBatBall(bat,ball);        

    // draw ball and bat
    bat.draw(ctx);
    ball.draw(ctx);
   
    requestAnimationFrame(update);

}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
body {font-family : arial; }
Use the mouse to move the bat and hit the ball.
<canvas id="canvas"></canvas>

该方法的缺陷。

可能出现用球棒将球困住,使得没有有效的解决方案,例如将球按压到屏幕底部。在某些时候,球的直径大于墙和球棒之间的空间。当这种情况发生时,解决方案将失败,球将穿过球棒。

在演示中,尽一切努力不损失能量,但随着时间的推移,浮点误差会累积,如果模拟没有任何输入运行,这可能导致能量损失。

由于球棒具有无限动量,因此很容易将大量能量传递给球,为了防止球累积太多动量,我已经将球的最大速度设定为最大速度。如果球移动得比最大速度快,它将逐渐减速,直到达到或低于最大速度。

偶尔情况下,如果您以相同的速度将球棒移开,由于重力的额外加速度,导致球不能正确地从球棒上弹开。


我真的很喜欢这个解释,尽管我认为 OP 找到碰撞点的方法要简单得多。 - cdo256
3
谢谢@cdo256. 糟糕,我在回答中有点过度了。当时时间所剩不多,写代码时对角碰撞拦截进行了些许修改,最终使用了圆形射线拦截再寻找反射,我会在有机会时将其修复。我没有看到提问者想要的是一个球和移动的球拍,而是一个球和砖块(砖块不会移动),所以我的回答有些过度了(除非这些砖块开始移动),我还考虑了添加球碰撞旋转(幸好我跳过了这个)。好吧,至少这样可以练习我的几何数学技能。 - Blindman67
@Blindman67 我也在寻找OP最初寻找的东西。我们能否得到一个修改版,使墙不移动? - Lathryx
更新:我将您的纯JS示例移植到P5JS(https://editor.p5js.org/Lathryx/sketches/Brcf0Et0K),并使墙壁减少球的速度以抵消“球拍”具有“无限速度”的问题。我目前正在制作一个游戏,并且一直在寻找一个能够准确计算角边缘情况的圆形-矩形碰撞,因为这就是我现在拥有的:https://editor.p5js.org/Lathryx/sketches/WfnQgurCR您的答案是我见过的最接近此问题的解决方案,因此我正在尝试将您的想法重新实现到我的物理引擎中。请告诉我您的想法! - Lathryx

0

更正上面分享的一个想法,使用切向速度调整碰撞后的速度。

弹性 - 定义为表示碰撞后失去的力量的常数。

nv = vector                            # normalized vector from center of cricle to collision point        (normal)
pv = [-vector[1], vector[0]]           # normalized vector perpendicular to nv                             (tangental)
n = dot_product(nv, circle.vel)        # normal vector length
t = dot_product(pv, circle.vel)        # tangental_vector length
new_v = sum_vectors(multiply_vector(t*bounciness, pv), multiply_vector(-n*self.bounciness, nv))            # new velocity vector
circle.velocity = new_v

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