与正方形碰撞后如何保持圆周速度?
How to maintain circle velocity after colliding with a square?
我正在开发一款游戏,玩家是一个圆圈,方块是正方形。用户用键盘移动头像(圆圈),应该不能和方块(方块)碰撞。
此外,我希望圆圈在碰到角时沿着正方形滑动,这样如果玩家一直按着键向同一方向移动,它们就会沿着正方形滑动而不是卡在上面它。
我已经完全重现了我在这里面临的问题:
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; }
<canvas width="800" height="800"></canvas>
如果您 运行 代码片段,您会看到圆圈正确地绕着正方形移动,但之后又向下和向右移动。我不确定为什么会这样。它应该保持完全直线移动,然后向右移动。
不幸的是,我的数学不是很好,所以我很难弄清楚为什么会这样。学习了主要算法,也参考了下面的答案:
One
Two
Three
我注意到的另一个问题是,如果将 circlePos
的 y 位置从 80
更改为 240
,那么它仍然会沿着正方形的顶部滑动,而不是占用沿着正方形底部滑动的更自然的路径。如果可能的话,我也想解决这个问题。
此外,理想情况下,如果圆圈直接击中瓷砖,则根本不应该有任何滑动,如果这有意义的话。在那种情况下,它应该卡在正方形上。
我建议进行以下更改:
在你的class中再定义两个方法:
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
。这将保证一旦障碍物离开,运动将继续像以前一样。
在下面的代码片段中进行了这些更改。此外,我在圆圈的路径中添加了第二个正方形,一旦圆圈不可见,我添加了第二个 运行,圆圈采用不同的路径:
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; }
<canvas width="800" height="800"></canvas>
注意:您在碰撞和位移函数中有一些代码重复。他们都计算几乎相同的东西。这可以优化。
我正在开发一款游戏,玩家是一个圆圈,方块是正方形。用户用键盘移动头像(圆圈),应该不能和方块(方块)碰撞。
此外,我希望圆圈在碰到角时沿着正方形滑动,这样如果玩家一直按着键向同一方向移动,它们就会沿着正方形滑动而不是卡在上面它。
我已经完全重现了我在这里面临的问题:
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; }
<canvas width="800" height="800"></canvas>
如果您 运行 代码片段,您会看到圆圈正确地绕着正方形移动,但之后又向下和向右移动。我不确定为什么会这样。它应该保持完全直线移动,然后向右移动。
不幸的是,我的数学不是很好,所以我很难弄清楚为什么会这样。学习了主要算法
我注意到的另一个问题是,如果将 circlePos
的 y 位置从 80
更改为 240
,那么它仍然会沿着正方形的顶部滑动,而不是占用沿着正方形底部滑动的更自然的路径。如果可能的话,我也想解决这个问题。
此外,理想情况下,如果圆圈直接击中瓷砖,则根本不应该有任何滑动,如果这有意义的话。在那种情况下,它应该卡在正方形上。
我建议进行以下更改:
在你的class中再定义两个方法:
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
。这将保证一旦障碍物离开,运动将继续像以前一样。
在下面的代码片段中进行了这些更改。此外,我在圆圈的路径中添加了第二个正方形,一旦圆圈不可见,我添加了第二个 运行,圆圈采用不同的路径:
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; }
<canvas width="800" height="800"></canvas>
注意:您在碰撞和位移函数中有一些代码重复。他们都计算几乎相同的东西。这可以优化。