Circle/rectangle 碰撞响应
Circle/rectangle collision response
所以我前段时间构建了一个 Breakout clone,我想稍微升级一下,主要是为了碰撞。当我第一次做到这一点时,我在我的球和我的砖块之间进行了基本的“碰撞”检测,实际上将球视为另一个矩形。但这造成了边缘碰撞的问题,所以我想我会改变它。问题是,我找到了我的问题的一些答案:
例如这张图片
以及该线程的最后评论:circle/rect collision reaction 但我找不到如何计算最终速度矢量。
到目前为止我有:
-找到矩形上的最近点,
- 创建法线和切线向量,
现在我需要的是某种方式 "divide the velocity vector into a normal component and a tangent component; negate the normal component and add the normal and tangent components to get the new Velocity vector" 如果这看起来非常简单但我无法理解这一点,我很抱歉......
代码:
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.w));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
//change current circle vel according to the collision response
}
谢谢!
编辑:还发现 this 但我不知道它是适用于矩形的所有点还是仅适用于角。
最好用几张图来解释:
有入射角=反射角。将此值称为 θ。
有 θ = 法向角 - 入射角。
atan2是计算向量与正x轴的夹角的函数。
然后下面的代码紧随其后:
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
var normal_angle = atan2(dnormal.y, dnormal.x);
var incoming_angle = atan2(circle.vel.y, circle.vel.x);
var theta = normal_angle - incoming_angle;
circle.vel = circle.vel.rotate(2*theta);
}
另一种方法是获取沿切线的速度,然后从圆周速度中减去该值的两倍。
那么代码就变成了
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var tangent_vel = dist.normalize().dot(circle.vel);
circle.vel = circle.vel.sub(tangent_vel.mult(2));
}
上面的两个代码片段在大约相同的时间(可能)做了基本相同的事情。随便选一个你最了解的。
此外,正如@arbuthnott 指出的那样,存在复制粘贴错误,因为 NearestY
应该使用 rect.h
而不是 rect.w
。
编辑: 我忘记了位置分辨率。这是将两个物理对象分开以使它们不再相交的过程。在这种情况下,由于块是静止的,我们只需要移动球即可。
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
if (circle.vel.dot(dist) < 0) { //if circle is moving toward the rect
//update circle.vel using one of the above methods
}
var penetrationDepth = circle.r - dist.mag();
var penetrationVector = dist.normalise().mult(penetrationDepth);
circle.pos = circle.pos.sub(penetrationVector);
}
球拍碰撞
处理球和矩形碰撞的最佳方法是利用系统的对称性。
球作为一个点。
首先是球,它的半径 r
定义了所有点距中心的 r
距离。但是我们可以把球变成一个点,然后给矩形加上半径。球现在只是随时间移动的一个点,这是一条线。
矩形的所有边都按半径增长了。该图显示了这是如何工作的。
绿色矩形是原来的矩形。球 A、B 不接触矩形,而球 C、D 接触。球 A、D 代表一种特殊情况,但如您所见,很容易解决。
所有运动都是一条线。
所以现在我们有一个更大的矩形和一个球作为一个点随时间移动(一条线),但矩形也在移动,这意味着随着时间的推移边缘将扫除对我的大脑来说太复杂的区域, 因此我们可以再次使用对称性,这次是相对运动。
从球棒的角度看它是静止的而球在移动,而从球的角度来看它是静止的而球棒在移动。他们都看到对方朝相反的方向移动。
因为球现在是一个点,所以改变它的运动只会改变它行进的路线。所以我们现在可以将球棒固定在 space 中并从球中减去它的运动。由于球棒现在是固定的,我们可以将其中心点移动到原点 (0,0) 并向相反方向移动球。
此时我们做出一个重要的假设。球和球棒始终处于不接触的状态,当我们移动球 and/or 球棒时,它们可能会接触。如果他们确实接触了,我们会计算一条新的轨迹,这样他们就不会接触。
两次可能的碰撞
现在有两种可能的碰撞情况,一种是球击中球棒的侧面,一种是球击中球棒的角。
下一张图片显示了球棒在原点以及球相对于球棒的运动和位置。它沿着红线从 A 到 B 然后反弹到 C
球击中边缘
球击中角球
由于这里也存在对称性,因此击中哪一侧或哪个角没有任何区别。事实上,我们可以根据距离球棒中心的球的大小来反映整个问题。因此,如果球在球棒的左侧,则在 x 方向上镜像它的位置和运动,在 y 方向上也是如此(您必须通过信号量跟踪这个镜像,以便在找到解决方案后可以反转它)。
代码
该示例执行上面函数 doBatBall(bat, ball)
中描述的操作。球具有一定的重力,会从 canvas 的两侧反弹。蝙蝠通过鼠标移动。球棒的运动会转移到球上,但球棒不会感受到球的任何力量。
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 1;
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx,style) => { Object.keys(style).forEach(key=> ctx[key] = style[key] ) };
// the ball
const ball = {
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
maxSpeed : 8,
style : {
lineWidth : 12,
strokeStyle : "green",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.beginPath();
ctx.arc(this.x,this.y,this.r-this.style.lineWidth * 0.45,0,PI2);
ctx.stroke();
},
update(){
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if(y > canvas.height - this.r){
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if(y < this.r){
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if(x > canvas.width - this.r){
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if(x < this.r){
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if(speed > this.maxSpeed){ // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed-this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
}
}
const ballShadow = { // this is used to do calcs that may be dumped
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
}
// Creates the bat
const bat = {
x : 100,
y : 250,
dx : 0,
dy : 0,
width : 140,
height : 10,
style : {
lineWidth : 2,
strokeStyle : "black",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.strokeRect(this.x - this.width / 2,this.y - this.height / 2, this.width, this.height);
},
update(){
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
//=============================================================================
// THE FUNCTION THAT DOES THE BALL BAT sim.
// the ball and bat are at new position
function doBatBall(bat,ball){
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if(s.x < 0){
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if(s.y < 0){
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if(s.dx > 0 && s.dy > 0){ return }// ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy); // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x +(s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y +(s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if(s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR){ // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if(! hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB){ // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if(!hit){ // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy)/(ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if(dist > radSqr){ return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x,cpy - s.y);
// is the distance greater than the ball speed then its a miss
if(d > ballSpeed){ return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed;
s.dy *= ballSpeed;
hit = true;
}
// if the ball hit the bat restore absolute position
if(hit){
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
// main update function
function update(timer){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
// move bat and ball
bat.update();
ball.update();
// check for bal bat contact and change ball position and trajectory if needed
doBatBall(bat,ball);
// draw ball and bat
bat.draw(ctx);
ball.draw(ctx);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
body {font-family : arial; }
Use the mouse to move the bat and hit the ball.
<canvas id="canvas"></canvas>
此方法存在缺陷。
可以用球棒将球夹住,这样就没有有效的解决方案,例如将球向下压到屏幕底部。在某些时候,球的直径大于墙和球棒之间的 space。发生这种情况时,解决方案将失败,球将穿过球棒。
在演示中已尽一切努力不损失能量,但随着时间的推移,浮点错误会累积,如果 sim 运行 没有一些输入,这可能会导致能量损失。
由于球棒具有无限的动量,因此很容易将大量能量传递给球,为防止球积累过多的动量,我为球增加了最大速度。如果球的移动速度快于最大速度,它会逐渐减速直到达到或低于最大速度。
有时,如果您以相同的速度将球棒从球上移开,重力引起的额外加速度可能会导致球无法正确地从球棒上推开。
修正了上面分享的一个想法,在碰撞后使用切向速度调整速度。
弹性 - 定义为表示碰撞后失去的力的常量
nv = vector # normalized vector from center of cricle to collision point (normal)
pv = [-vector[1], vector[0]] # normalized vector perpendicular to nv (tangental)
n = dot_product(nv, circle.vel) # normal vector length
t = dot_product(pv, circle.vel) # tangental_vector length
new_v = sum_vectors(multiply_vector(t*bounciness, pv), multiply_vector(-n*self.bounciness, nv)) # new velocity vector
circle.velocity = new_v
所以我前段时间构建了一个 Breakout clone,我想稍微升级一下,主要是为了碰撞。当我第一次做到这一点时,我在我的球和我的砖块之间进行了基本的“碰撞”检测,实际上将球视为另一个矩形。但这造成了边缘碰撞的问题,所以我想我会改变它。问题是,我找到了我的问题的一些答案:
例如这张图片
以及该线程的最后评论:circle/rect collision reaction 但我找不到如何计算最终速度矢量。
到目前为止我有:
-找到矩形上的最近点,
- 创建法线和切线向量,
现在我需要的是某种方式 "divide the velocity vector into a normal component and a tangent component; negate the normal component and add the normal and tangent components to get the new Velocity vector" 如果这看起来非常简单但我无法理解这一点,我很抱歉...... 代码:
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.w));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
//change current circle vel according to the collision response
}
谢谢!
编辑:还发现 this 但我不知道它是适用于矩形的所有点还是仅适用于角。
最好用几张图来解释:
有入射角=反射角。将此值称为 θ。
有 θ = 法向角 - 入射角。
atan2是计算向量与正x轴的夹角的函数。
然后下面的代码紧随其后:
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
var normal_angle = atan2(dnormal.y, dnormal.x);
var incoming_angle = atan2(circle.vel.y, circle.vel.x);
var theta = normal_angle - incoming_angle;
circle.vel = circle.vel.rotate(2*theta);
}
另一种方法是获取沿切线的速度,然后从圆周速度中减去该值的两倍。
那么代码就变成了
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var tangent_vel = dist.normalize().dot(circle.vel);
circle.vel = circle.vel.sub(tangent_vel.mult(2));
}
上面的两个代码片段在大约相同的时间(可能)做了基本相同的事情。随便选一个你最了解的。
此外,正如@arbuthnott 指出的那样,存在复制粘贴错误,因为 NearestY
应该使用 rect.h
而不是 rect.w
。
编辑: 我忘记了位置分辨率。这是将两个物理对象分开以使它们不再相交的过程。在这种情况下,由于块是静止的,我们只需要移动球即可。
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
if (circle.vel.dot(dist) < 0) { //if circle is moving toward the rect
//update circle.vel using one of the above methods
}
var penetrationDepth = circle.r - dist.mag();
var penetrationVector = dist.normalise().mult(penetrationDepth);
circle.pos = circle.pos.sub(penetrationVector);
}
球拍碰撞
处理球和矩形碰撞的最佳方法是利用系统的对称性。
球作为一个点。
首先是球,它的半径 r
定义了所有点距中心的 r
距离。但是我们可以把球变成一个点,然后给矩形加上半径。球现在只是随时间移动的一个点,这是一条线。
矩形的所有边都按半径增长了。该图显示了这是如何工作的。
绿色矩形是原来的矩形。球 A、B 不接触矩形,而球 C、D 接触。球 A、D 代表一种特殊情况,但如您所见,很容易解决。
所有运动都是一条线。
所以现在我们有一个更大的矩形和一个球作为一个点随时间移动(一条线),但矩形也在移动,这意味着随着时间的推移边缘将扫除对我的大脑来说太复杂的区域, 因此我们可以再次使用对称性,这次是相对运动。
从球棒的角度看它是静止的而球在移动,而从球的角度来看它是静止的而球棒在移动。他们都看到对方朝相反的方向移动。
因为球现在是一个点,所以改变它的运动只会改变它行进的路线。所以我们现在可以将球棒固定在 space 中并从球中减去它的运动。由于球棒现在是固定的,我们可以将其中心点移动到原点 (0,0) 并向相反方向移动球。
此时我们做出一个重要的假设。球和球棒始终处于不接触的状态,当我们移动球 and/or 球棒时,它们可能会接触。如果他们确实接触了,我们会计算一条新的轨迹,这样他们就不会接触。
两次可能的碰撞
现在有两种可能的碰撞情况,一种是球击中球棒的侧面,一种是球击中球棒的角。
下一张图片显示了球棒在原点以及球相对于球棒的运动和位置。它沿着红线从 A 到 B 然后反弹到 C
球击中边缘
球击中角球
由于这里也存在对称性,因此击中哪一侧或哪个角没有任何区别。事实上,我们可以根据距离球棒中心的球的大小来反映整个问题。因此,如果球在球棒的左侧,则在 x 方向上镜像它的位置和运动,在 y 方向上也是如此(您必须通过信号量跟踪这个镜像,以便在找到解决方案后可以反转它)。
代码
该示例执行上面函数 doBatBall(bat, ball)
中描述的操作。球具有一定的重力,会从 canvas 的两侧反弹。蝙蝠通过鼠标移动。球棒的运动会转移到球上,但球棒不会感受到球的任何力量。
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 1;
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx,style) => { Object.keys(style).forEach(key=> ctx[key] = style[key] ) };
// the ball
const ball = {
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
maxSpeed : 8,
style : {
lineWidth : 12,
strokeStyle : "green",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.beginPath();
ctx.arc(this.x,this.y,this.r-this.style.lineWidth * 0.45,0,PI2);
ctx.stroke();
},
update(){
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if(y > canvas.height - this.r){
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if(y < this.r){
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if(x > canvas.width - this.r){
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if(x < this.r){
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if(speed > this.maxSpeed){ // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed-this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
}
}
const ballShadow = { // this is used to do calcs that may be dumped
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
}
// Creates the bat
const bat = {
x : 100,
y : 250,
dx : 0,
dy : 0,
width : 140,
height : 10,
style : {
lineWidth : 2,
strokeStyle : "black",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.strokeRect(this.x - this.width / 2,this.y - this.height / 2, this.width, this.height);
},
update(){
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
//=============================================================================
// THE FUNCTION THAT DOES THE BALL BAT sim.
// the ball and bat are at new position
function doBatBall(bat,ball){
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if(s.x < 0){
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if(s.y < 0){
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if(s.dx > 0 && s.dy > 0){ return }// ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy); // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x +(s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y +(s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if(s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR){ // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if(! hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB){ // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if(!hit){ // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy)/(ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if(dist > radSqr){ return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x,cpy - s.y);
// is the distance greater than the ball speed then its a miss
if(d > ballSpeed){ return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed;
s.dy *= ballSpeed;
hit = true;
}
// if the ball hit the bat restore absolute position
if(hit){
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
// main update function
function update(timer){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
// move bat and ball
bat.update();
ball.update();
// check for bal bat contact and change ball position and trajectory if needed
doBatBall(bat,ball);
// draw ball and bat
bat.draw(ctx);
ball.draw(ctx);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
body {font-family : arial; }
Use the mouse to move the bat and hit the ball.
<canvas id="canvas"></canvas>
此方法存在缺陷。
可以用球棒将球夹住,这样就没有有效的解决方案,例如将球向下压到屏幕底部。在某些时候,球的直径大于墙和球棒之间的 space。发生这种情况时,解决方案将失败,球将穿过球棒。
在演示中已尽一切努力不损失能量,但随着时间的推移,浮点错误会累积,如果 sim 运行 没有一些输入,这可能会导致能量损失。
由于球棒具有无限的动量,因此很容易将大量能量传递给球,为防止球积累过多的动量,我为球增加了最大速度。如果球的移动速度快于最大速度,它会逐渐减速直到达到或低于最大速度。
有时,如果您以相同的速度将球棒从球上移开,重力引起的额外加速度可能会导致球无法正确地从球棒上推开。
修正了上面分享的一个想法,在碰撞后使用切向速度调整速度。
弹性 - 定义为表示碰撞后失去的力的常量
nv = vector # normalized vector from center of cricle to collision point (normal)
pv = [-vector[1], vector[0]] # normalized vector perpendicular to nv (tangental)
n = dot_product(nv, circle.vel) # normal vector length
t = dot_product(pv, circle.vel) # tangental_vector length
new_v = sum_vectors(multiply_vector(t*bounciness, pv), multiply_vector(-n*self.bounciness, nv)) # new velocity vector
circle.velocity = new_v