与正方形碰撞后如何保持圆周速度?

问题描述

我正在开发一个游戏,其中玩家是一个圆圈,瓷砖是正方形。用户键盘移动头像(圆圈),不能与瓷砖(方块)碰撞。

另外,我希望圆圈在碰到角落时沿着正方形滑动,这样如果玩家一直按下键向同一方向移动,他们将沿着正方形滑动而不是卡在上面。

我已经完整地重现了我在这里面临的问题:

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

class Vec2 {
  constructor(x,y) {
    this.x = x || 0;
    this.y = y || 0;
  }

  distance(v) {
    let x = v.x - this.x;
    let y = v.y - this.y;

    return Math.sqrt(x * x + y * y);
  }

  magnitude() { 
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }

  dot(v) { 
    return this.x * v.x + this.y * v.y;
  }

  normalize() {
    let magnitude = this.magnitude();
    
    return new Vec2(this.x / magnitude,this.y / magnitude);
  }
  
  multiply(val) {
    return typeof val === "number" ? new Vec2(this.x * val,this.y * val) : new Vec2(this.x * val.x,this.y * val.y);
  }

  subtract(val) {
    return typeof val === "number" ? new Vec2(this.x - val,this.y - val) : new Vec2(this.x - val.x,this.y - val.y);
  }

  add(val) {
    return typeof val === "number" ? new Vec2(this.x + val,this.y + val) : new Vec2(this.x + val.x,this.y + val.y);
  }
}

function clamp(value,min,max) {
  return Math.min(Math.max(value,min),max);
}

function drawCircle(xCenter,yCenter,radius) {
  ctx.beginPath();
  ctx.arc(xCenter,radius,2 * Math.PI);
  ctx.fill();
}

function drawSquare(x,y,w,h) {
  ctx.beginPath();
  ctx.rect(x,h);
  ctx.stroke();
}

function circleRectangleCollision(cX,cY,cR,rX,rY,rW,rH) {
  let x = clamp(cX,rX + rW);
  let y = clamp(cY,rY + rH);

  let cPos = new Vec2(cX,cY);

  return cPos.distance(new Vec2(x,y)) < cR;
}

function getCircleRectangledisplacement(rX,rH,cX,cVel) {
  let circle = new Vec2(cX,cY);

  let nearestX = Math.max(rX,Math.min(cX,rX + rW));
  let nearestY = Math.max(rY,Math.min(cY,rY + rH));    
  let dist = new Vec2(cX - nearestX,cY - nearestY);

  let tangentVel = dist.normalize().dot(cVel);

  // The original answer had `cVel.subtract(tangentVel * 2);` here
  // but that was giving me issues as well
  return cVel.add(tangentVel);
}

let circlePos = new Vec2(150,80);
let squarePos = new Vec2(240,110);

let circleR = 50;

let squareW = 100;
let squareH = 100;

let circleVel = new Vec2(5,0);

draw = () => {
  ctx.fillStyle = "#b2c7ef";
  ctx.fillRect(0,800,800); 

  ctx.fillStyle = "#ffffff";

  drawCircle(circlePos.x,circlePos.y,circleR);
  drawSquare(squarePos.x,squarePos.y,squareW,squareH);
}

update = () => {
  draw();

  if (circleRectangleCollision(circlePos.x,circleR,squarePos.x,squareH)) {
    circleVel = getCircleRectangledisplacement(squarePos.x,squareH,circlePos.x,circleVel);
  }

  circlePos = circlePos.add(circleVel);
}

setInterval(update,30);
canvas { display: flex; margin: 0 auto; }
<canvas width="800" height="800"></canvas>

如果您运行该代码段,您会看到圆圈正确地绕着正方形移动,但随后它向下和向右移动。我不确定为什么会这样。之后它应该保持完全笔直并向右移动。

不幸的是,我的数学不是很好,所以我无法弄清楚为什么会发生这种情况。我学习了主要算法through this answer,但也使用了以下答案作为参考: One Two Three

我注意到的另一个问题是,如果您将 circlePos 的 y 位置从 80 更改为 240,那么它仍然沿着正方形的顶部滑动,而不是取更多沿着正方形底部滑动的自然路径。如果可能,我也想解决这个问题。

此外,理想情况下,如果圆圈直接击中瓷砖,那么理想情况下根本不应该有任何滑动,如果这是有道理的。在这种情况下,它应该卡在正方形上。

解决方法

我建议进行以下更改:

在类中再定义两个方法:

  crossProductZ(v) {
    return this.x * v.y - v.x * this.y;
  }
  
  perpendicular() {
    return new Vec2(this.y,-this.x);
  }

getCircleRectangleDisplacement 中将 return 语句替换为:

return dist.perpendicular().normalize()
           .multiply(cVel.magnitude() * Math.sign(cVel.crossProductZ(dist)));

这个想法是圆应该垂直于通过圆心和命中点(即 dist)的线移动。垂线上当然有两个方向:它应该是与当前速度矢量在dist 同一侧的那个。这样圆就会选择正方形的右边。

那个移动的幅度应该等于当前速度的幅度(这样速度没有变化,只是方向变化)。

最后,还要对 update 函数进行此更改:

  let nextCirclePos = circlePos.add(circleVel);
  if (circleRectangleCollision(nextCirclePos.x,nextCirclePos.y,circleR,squarePos.x,squarePos.y,squareW,squareH)) {
    let currentVel = getCircleRectangleDisplacement(squarePos.x,squareH,circlePos.x,circlePos.y,circleVel);
    nextCirclePos = circlePos.add(currentVel);
  }
  circlePos = nextCirclePos;

这里的想法是我们首先像往常一样移动 (circleVel),看看这是否意味着碰撞。在这种情况下,我们不会采取这种行动。相反,我们从当前位置获取位移

而且,我们从不更新 currentVel。这将保证一旦障碍物被移开,运动将像以前一样继续。

在下面的代码段中进行了这些更改。此外,我在圆的路径中添加了第二个正方形,一旦圆不在视野中,我添加了第二次运行,其中圆采用了不同的路径:

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

class Vec2 {
  constructor(x,y) {
    this.x = x || 0;
    this.y = y || 0;
  }

  distance(v) {
    let x = v.x - this.x;
    let y = v.y - this.y;

    return Math.sqrt(x * x + y * y);
  }

  magnitude() { 
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }

  dot(v) { 
    return this.x * v.x + this.y * v.y;
  }

  normalize() {
    let magnitude = this.magnitude();
    
    return new Vec2(this.x / magnitude,this.y / magnitude);
  }
  
  multiply(val) {
    return typeof val === "number" ? new Vec2(this.x * val,this.y * val) : new Vec2(this.x * val.x,this.y * val.y);
  }

  subtract(val) {
    return typeof val === "number" ? new Vec2(this.x - val,this.y - val) : new Vec2(this.x - val.x,this.y - val.y);
  }

  add(val) {
    return typeof val === "number" ? new Vec2(this.x + val,this.y + val) : new Vec2(this.x + val.x,this.y + val.y);
  }
  
  crossProductZ(v) {
    return this.x * v.y - v.x * this.y;
  }
  
  perpendicular() {
    return new Vec2(this.y,-this.x);
  }
}

function clamp(value,min,max) {
  return Math.min(Math.max(value,min),max);
}

function drawCircle(xCenter,yCenter,radius) {
  ctx.beginPath();
  ctx.arc(xCenter,radius,2 * Math.PI);
  ctx.fill();
}

function drawSquare(x,y,w,h) {
  ctx.beginPath();
  ctx.rect(x,h);
  ctx.stroke();
}

function circleRectangleCollision(cX,cY,cR,rX,rY,rW,rH) {
  let x = clamp(cX,rX + rW);
  let y = clamp(cY,rY + rH);

  let cPos = new Vec2(cX,cY);

  return cPos.distance(new Vec2(x,y)) < cR;
}

function getCircleRectangleDisplacement(rX,rH,cX,cVel) {
  let circle = new Vec2(cX,cY);

  let nearestX = clamp(cX,rX + rW);
  let nearestY = clamp(cY,rY + rH);
  let dist = new Vec2(cX - nearestX,cY - nearestY);

  return dist.perpendicular().normalize().multiply(cVel.magnitude() * Math.sign(cVel.crossProductZ(dist)));
}

let circlePos = new Vec2(100,80);
let squarePosList = [new Vec2(240,110),new Vec2(480,-50)];

let circleR = 50;

let squareW = 100;
let squareH = 100;

let circleVel = new Vec2(5,0);

draw = () => {
  ctx.fillStyle = "#b2c7ef";
  ctx.fillRect(0,800,800); 

  ctx.fillStyle = "#ffffff";

  drawCircle(circlePos.x,circleR);
  for (let squarePos of squarePosList) {
    drawSquare(squarePos.x,squareH);
  }
}

update = () => {
  draw();

  let nextCirclePos = circlePos.add(circleVel);
  for (let squarePos of squarePosList) {
    if (circleRectangleCollision(nextCirclePos.x,squareH)) {
      let currentVel = getCircleRectangleDisplacement(squarePos.x,circleVel);
      nextCirclePos = circlePos.add(currentVel);
      break; // we only deal with one collision (otherwise it becomes more complex)
    }
  }
  circlePos = nextCirclePos;
  if (circlePos.x > 800 + circleR) { // Out of view: Repeat the animation but with a diagonal direction
       circlePos = new Vec2(100,400);
       circleVel = new Vec2(3.6,-3.6);
  }
}

let interval = setInterval(update,30);
canvas { display: flex; margin: 0 auto; }
<canvas width="800" height="800"></canvas>

注意:碰撞和位移函数中有一些代码重复。他们都计算出几乎相同的东西。这可以优化。