为什么球在我的乒乓 JavaScript canvas 游戏中不能完全弹起?

Why won't the balls bounce fully in my pong JavaScript canvas game?

我在 javascript 中的 canvas 上有多个省略号,我希望它们都能相互反弹。我尝试使用距离公式,然后在距离小于球半径 *2 时改变球的 x 和 y 方向。

这对一个球很管用,但对很多球就不太管用了,因为它经常会导致 'bounce loop' 所描绘的 Here

为了解决这个问题,我决定根据球彼此碰撞的位置来改变球的弹跳方式,以避免弹跳循环,并使游戏更接近现实生活中的物理特性。

如果发生左右碰撞,我想反转两个球的 x 方向,如果发生上下碰撞,我想反转两个球的 y 方向。

因此,我计算了所有与度数相关的点,例如 45 度到 135 度之间(即 90 点),并将它们与 225 度到 315 度之间的所有 90 点进行比较,反之亦然。

如果圆的边缘上的任何点与所有其他球的中心点之间的距离小于半径,我希望两个球的Y方向都反转

我重复了同样的过程,135度和225度到315度和405度(相当于45度),并且颠倒了两个球的X方向。

截至目前,我认为球应该按照我希望的方式相互反弹,但事实并非如此。它们从彼此的侧面和顶部、底部反弹,偶尔会以一定角度反弹,但它们往往会浸入彼此内部,然后改变方向。这是一个video of the output.

下面是从上到下比较的代码:

    // radius is the same for all the balls and is at 25.
let ballToBallDistance = (x1, y1, x2, y2) => {
    return Math.sqrt((Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)));
}
const ballCollisionY = (start, end) => {
    for (let i = start; i <= end; i++) {
        return ballObjects[0].ballRadius * Math.sin((i * Math.PI / 180));
    }
}
const ballCollisionX = (start, end) => {
    for (let i = start; i <= end; i++) {
        return ballObjects[0].ballRadius * Math.cos((i * Math.PI / 180));
    }
}
const upperYBall = {
    bounceTopBottom() {
        let n = 0;
        for (let i = 0; i < ballObjects.length; i++) {
            if (ballObjects.length == 1) {
                return;
            }
            if (n == i) {
                continue;
            }
            let yUpXPoint = ballObjects[n].ballXPos - ballCollisionX(45, 135);
            let yUpYPoint = ballObjects[n].ballYPos - ballCollisionY(45, 135);
            let centerBallX = ballObjects[i].ballXPos;
            let centerBallY = ballObjects[i].ballYPos;
            let pointDistance = ballToBallDistance(yUpXPoint, yUpYPoint, centerBallX, centerBallY);
            if (pointDistance <= 25) {
                ballObjects[n].ballMotionY = ballObjects[n].ballMotionY * -1;
            }
            if (i == ballObjects.length - 1) {
                ++n;
                i = -1;
                continue;
            }
        }
    }
}
const lowerYBall = {
    bounceBottomTop() {
        let n = 0;
        for (let i = 0; i < ballObjects.length; i++) {
            if (ballObjects.length == 1) {
                return;
            }
            if (n == i) {
                continue;
            }
            let yDownXPoint = ballObjects[n].ballXPos - ballCollisionX(225, 315);
            let yDownYPoint = ballObjects[n].ballYPos - ballCollisionY(225, 315);
            let centerBallX = ballObjects[i].ballXPos;
            let centerBallY = ballObjects[i].ballYPos;
            let pointDistance = ballToBallDistance(yDownXPoint, yDownYPoint, centerBallX, centerBallY);
            if (pointDistance <= 25) {
                ballObjects[n].ballMotionY = ballObjects[n].ballMotionY * -1;
            }
            if (i == ballObjects.length - 1) {
                ++n;
                i = -1;
                continue;
            }
        }
    }
}

我已经在这个功能上停留了两个星期了。如果有人对我做错了什么有任何见解,并且可能有实现预期结果的解决方案,我们将不胜感激。

我建议您从特殊情况编码切换到更通用的方法。

当两个球相撞时:

  1. 计算碰撞法线(角度)
  2. 根据之前的速度和法线计算新的速度
  3. 重新定位球,使它们不再重叠,从而防止 'bounce loop'。

您将需要:

两球夹角计算方法:

function ballToBallAngle(ball1,ball2) {
    return Math.atan2(ball2.y-ball1.y,ball2.x-ball1.x)
}

一种从角度导出法向量的方法:

function calcNormalFromAngle(angle){
  return [
    Math.cos(angle),
    Math.sin(angle)
  ]
}

一种计算两个向量点积的方法:

function dotproduct (a, b){
    return a.map((x, i) => a[i] * b[i]).reduce((m, n) => m + n)
}

终于找到了一种计算弹跳角度的方法。 Read this,描述的很完美

所以要将它们放在一起,请看下面的代码片段:

let canvas = document.querySelector('canvas')
let ctx = canvas.getContext('2d')


let balls = [
 {x:40,y:40,radius:25,vx:4,vy:3},
    {x:300,y:300,radius:50,vx:-2,vy:-3},
    {x:100,y:220,radius:25,vx:4,vy:-3},
    {x:400,y:400,radius:50,vx:-1,vy:-3},
    {x:200,y:400,radius:32,vx:2,vy:-3}
]

function tick() {
 balls.forEach((ball, index) => {
  ball.x += ball.vx
  ball.y += ball.vy

  //check for x bounds collision
  if (ball.x - ball.radius < 0) {
   bounceBall(ball, Math.PI)
   ball.x = ball.radius
  } else if (ball.x + ball.radius > 500) {
   bounceBall(ball, 0)
   ball.x = 500 - ball.radius
  }

  //check for y bounds collision
  if (ball.y - ball.radius < 0) {
   bounceBall(ball, Math.PI / 2)
   ball.y = ball.radius
  } else if (ball.y + ball.radius > 500) {
   bounceBall(ball, -Math.PI / 2)
   ball.y = 500 - ball.radius
  }

  balls.forEach((other_ball, other_index) => {
   if (index == other_index)
    return

   // how many px the balls intersect
   let intersection = ball.radius + other_ball.radius - ballToBallDistance(ball, other_ball)

   // if its greater than 0, they must be colliding
   if (intersection > 0) {
    let angle = ballToBallAngle(ball, other_ball)
    let normal = calcNormalFromAngle(angle)

    bounceBall(ball, angle)
    bounceBall(other_ball, angle + Math.PI)

    // set positions so that they are not overlapping anymore
    ball.x -= normal[0] * intersection / 2
    ball.y -= normal[1] * intersection / 2

    other_ball.x += normal[0] * intersection / 2
    other_ball.y += normal[1] * intersection / 2
   }
  })
 })

 render()
 requestAnimationFrame(tick)
}

function render() {
 ctx.clearRect(0, 0, canvas.width, canvas.height)

 balls.forEach(ball => {
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.radius, 0, 2 * Math.PI);
  ctx.stroke();
 })
}



function bounceBall(ball, angle) {
 let normal = calcNormalFromAngle(angle)
 let velocity = [ball.vx, ball.vy]

 let ul = dotproduct(velocity, normal) / dotproduct(normal, normal)
 let u = [
  normal[0] * ul,
  normal[1] * ul
 ]

 let w = [
  velocity[0] - u[0],
  velocity[1] - u[1]
 ]

 let new_velocity = [
  w[0] - u[0],
  w[1] - u[1]
 ]

 ball.vx = new_velocity[0]
 ball.vy = new_velocity[1]
}

function dotproduct(a, b) {
 return a.map((x, i) => a[i] * b[i]).reduce((m, n) => m + n)
}

function ballToBallDistance(ball1, ball2) {
 return Math.sqrt((Math.pow(ball2.x - ball1.x, 2) + Math.pow(ball2.y - ball1.y, 2)));
}

function ballToBallAngle(ball1, ball2) {
 return Math.atan2(ball2.y - ball1.y, ball2.x - ball1.x)
}

function calcNormalFromAngle(angle) {
 return [
  Math.cos(angle),
  Math.sin(angle)
 ]
}

tick();
body{
  background-color: #eee;
}

canvas{
  background-color: white;
}
<canvas width="500" height="500"></canvas>