HTML Canvas 球碰撞问题

HTML Canvas Ball Collision Issue

提问人:Liam 提问时间:11/14/2023 更新时间:11/15/2023 访问量:41

问:

我正在创建一个简单的程序来模拟围绕圆圈弹跳的球。我遇到过一个问题,球偶尔会与内圈碰撞并卡住。我不完全确定为什么会发生这种情况,但当我提高速度时,它确实会更频繁地发生。 这是我的代码:

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();

球.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
JavaScript HTML 画布

评论


答:

1赞 user3297291 11/15/2023 #1

问题

当球与边缘碰撞时,您会改变方向。如果新的方向和速度无法将球移动到框架内的碰撞之外,您将获得双重碰撞。

当有时颜色多次闪烁并且返回角度看起来不正常时,您可以看到这种情况发生。

在某些角度下,快速连续的碰撞会不断翻转方向,球永远无法解开。

可能的修复

您经常看到的两个修复已实现:

  • 检查碰撞时向前看一帧,并改变路线以防止碰撞
  • 发生碰撞时,将碰撞的物体向后移动到刚好接触它的位置。

第二个通常是更好的修复,但第一个很容易实现,并且在您的示例中效果很好。

展望未来

下面我做了一个简单的更改:我没有检查与电流的碰撞,而是虚拟地添加到它并使用球的“下一个”位置。ball.positionball.speed

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;

换档到碰撞的确切位置

如果你想选择另一个,你可以:

  • 计算过冲距离 (distance - innerCircle.radius)
  • 从内圆以球的速度与当前位置成一定角度的线上找到位置-overshoot
  • 在翻转速度之前,更新到这个“仅接触内圆表面”点。ball.position

运行示例

注意:下面的示例显示了第一种方法的实现。

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>