【问题标题】:How to maintain circle velocity after colliding with a square?与正方形碰撞后如何保持圆周速度?
【发布时间】:2021-03-29 06:32:27
【问题描述】:

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

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

我已经开发出我在这里面临的问题的完整再现:

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, yCenter, radius, 0, 2 * Math.PI);
  ctx.fill();
}

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

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

  let cPos = new Vec2(cX, cY);

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

function getCircleRectangleDisplacement(rX, rY, rW, rH, cX, cY, cR, 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, 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, circlePos.y, circleR, squarePos.x, squarePos.y, squareW, squareH)) {
    circleVel = getCircleRectangleDisplacement(squarePos.x, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR, circleVel);
  }

  circlePos = circlePos.add(circleVel);
}

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

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

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

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

此外,理想情况下,如果圆圈直接撞到瓷砖上,则根本不应该有任何滑动,如果这有意义的话。在这种情况下,它应该会卡在正方形上。

【问题讨论】:

  • 至于您的最后一段:演示中的圆圈“直上”(我理解为:垂直于正方形的一侧),那么它为什么要滑动呢?还是圆碰到角上的正方形的滑动条件?如果撞到一边怎么办?如果它撞到一边,但不是垂直方向怎么办?
  • @trincot 抱歉,我认为我没有正确解释。我不确定解释它的正确方法,但基本上,如果圆圈直接在正方形上,但几乎没有夹住它的边缘,它应该在它周围滑动。与此 GIF 中发生的情况类似:dl.dropboxusercontent.com/s/joa8cdeddygje5f/round.gif 但如果圆圈接近正方形,使得大部分圆圈都夹在正方形中,那么它应该卡在它旁边。它只有在几乎没有碰撞到正方形的情况下才会在正方形周围滑动,如果这有意义的话。
  • 圆的初始运动总是纯水平的还是纯垂直的?如果圆在对角线路径上完全撞到正方形怎么办……它也应该粘住吗?
  • 圆的初始移动肯定是对角线的,因为玩家(圆)可以使用 WASD 在 8 个方向中的任何一个方向移动。老实说,我很难理解确切什么时候应该坚持,而且我不确定是否有客观的答案。基本上,我只是想改善目前玩家在四处走动时总是卡在可碰撞方块边缘的情况,因为玩起来非常令人沮丧。我喜欢在尽可能多的情况下滑动。抱歉,我的回答不是很专业。
  • 嘿,我刚醒来,很抱歉回复晚了。我现在正在查看您的答案。

标签: javascript algorithm math collision-detection game-physics


【解决方案1】:

我建议进行以下更改:

在你的类中再定义两个方法:

  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, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR, circleVel);
    nextCirclePos = circlePos.add(currentVel);
  }
  circlePos = nextCirclePos;

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

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

在下面的 sn-p 中进行了这些更改。此外,我在圆圈的路径中添加了第二个正方形,一旦圆圈不在视野范围内,我添加了第二次运行,圆圈采用不同的路径:

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, yCenter, radius, 0, 2 * Math.PI);
  ctx.fill();
}

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

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

  let cPos = new Vec2(cX, cY);

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

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

  let nearestX = clamp(cX, rX, rX + rW);
  let nearestY = clamp(cY, rY, 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, 0, 800, 800); 

  ctx.fillStyle = "#ffffff";

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

update = () => {
  draw();

  let nextCirclePos = circlePos.add(circleVel);
  for (let squarePos of squarePosList) {
    if (circleRectangleCollision(nextCirclePos.x, nextCirclePos.y, circleR, squarePos.x, squarePos.y, squareW, squareH)) {
      let currentVel = getCircleRectangleDisplacement(squarePos.x, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR, 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; }
&lt;canvas width="800" height="800"&gt;&lt;/canvas&gt;

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

【讨论】:

  • 感谢您的精彩回答!不过,我现在正在尝试将其实现到我的游戏中,但遇到了一些麻烦。似乎玩家有时会卡在方格内。我注意到这也发生在您提供的代码中。例如,如果将circleVel 设置为(3, 3),将circlePos 设置为(35, 80),将squarePosList 设置为[new Vec2(140, 180), new Vec2(140, 280), new Vec2(140, 380)];,则圆圈不仅有时会在内部出现故障,而且有时还会卡住,如以下屏幕截图所示: i.imgur.com/J1f2q0l.pngi.imgur.com/6uxbhpj.png
  • 这个问题可以通过删除我的解决方案中的break 来解决,但我知道如果你有可以同时被击中的复杂方块排列,事情会变得很棘手。这有点超出了原来的问题。理想情况下,您应该将相邻的正方形合并为一个矩形,并调整代码以使用矩形。
  • 嗯,我不能在我的游戏中真正做到这一点(将一组正方形变成矩形),因为玩家在瓷砖网格上移动。就像想象 2d 自上而下的塞尔达传说游戏一样,坚固的墙壁和可碰撞的物体都是你无法碰撞的实心方块。
  • 好吧,试试不带break,看看你会得到什么。
  • 哦,是的,好电话。嗯,这个错误仍然在发生,但是这对你来说太难调试了,我只是发布随机的 sn-ps。不管怎么说,多谢拉!我会试着弄清楚。你给了我一个很好的合作基础。
猜你喜欢
  • 1970-01-01
  • 2013-01-17
  • 2012-01-15
  • 1970-01-01
  • 1970-01-01
  • 2016-07-01
  • 2014-09-03
  • 2014-04-08
  • 1970-01-01
相关资源
最近更新 更多