我试图制作(基本上)简单的游泳池游戏,并且希望能够预测一旦它击中另一个球后射击的位置。
第一部分是,我相信,计算是否会撞击任何东西,如果碰撞,它会碰撞到哪里。我可以计算出一条线和一个球的碰撞点,但不是2个球。
因此,考虑到2个球的x / y位置和速度,我如何计算它们碰撞的点?
(PS:我知道我可以通过计算沿途每一步两个球之间的距离来做到这一点,但我希望能有一些更优雅和最优的东西。)
设置示例:尝试计算红点
答案 0 :(得分:14)
需要注意的一些事项:
r
碰撞时,它们的中心相距2r
。alpha
第二个。现在你有一些几何学要做。
做这个结构:
A
。B
。AB
。R
构建光线A
。2r
周围构建一个半径B
的圆圈。B
垂直于R
垂下一段,调用交叉点C
。AB
,您可以在alpha
和AB
之间找到角度R
,正弦律找到BC
的长度D
,其中圆圈与R
更接近A,并再次使用正弦法来找到距离AD。BD
现在你知道了一切。
从中构建有效的代码是一种练习。
BTW--如果两个球都在移动,这种结构将不起作用,但你可以变换成一个静止的框架,以那种方式解决,然后转换回来。只需确保在反向转换之后检查解决方案是否在允许的区域 ...
/物理学家不能不做出这样的评论。我试图抵制。我真的做了。
答案 1 :(得分:12)
绘制@dmckee的答案
修改
为了回应@ArtB necromancer的回答,可以写出上图中D点的解决方案:
1/2 {(Ax+Bx+2 d Dx Cos[alpha]- Dx Cos[2 alpha]+ 2 Dy (Cos[alpha]-d) Sin[alpha]),
(Ay+By+2 d Dy Cos[alpha]- Dy Cos[2 alpha]- 2 Dx (Cos[alpha]-d) Sin[alpha])
}
其中
Dx = Ax - Bx
Dy = Ay - By
并且
d = Sqrt[4 r^2 - (Dx^2 + Dy^2) Sin[alpha]^2]/Sqrt[Dx^2 + Dy^2]
HTH!
答案 2 :(得分:1)
我在看@dmckee的解决方案,我花了很多工作来完成它。以下是我对那些可能寻找更实际答案的人的说明,它是直接采取的
从他的帖子,所以信用归他/她,但任何错误都是我的。我通常使用Pascal类似的分配运算符(即:=
)来区分显示我的工作和实际必需的代码。
我使用标准Y = mX +b
格式和准oop表示法。我确实对段和结果行使用BC。也就是说,这几乎应该是'copy'n'paste-able Python代码
(删除“;”,用适当的版本替换“sqrt”和“sqr”等。)
AB.m := (b.y - a.y) / (b.x - a.x);
AB.b := A.y - AB.m * A.x;
R.m := A.v.y / A.v.x;
R.b := A.y - R.m * A.x;
BC.m := -A.v.x/A.v.y;
这是垂直斜率的标准公式BC.b := B.y - BC.m * B.x;
现在C
是AB
遇到BC
所以我们知道它们是相等的所以让我们让将C.y
等同于C.y == AB.m * C.x + AB.b == BC.m * C.x + BC.b;
所以C.x := (AB.m - BC.M) / (BC.b - AB.b);
然后插入C.x
以获取C.y := AB.m * C.x + AB.b;
BC
,BC.l := sqrt( sqr(B.x-C.x) + sqr(B.y-C.y) );
BC.l > A.r + B.r
,则解决方案为零,并且这些圈子不会触及,因为C
是A
的路径s perigee with respect to
B . If
BC.l == Ar + Br , there is only one solution, and
C == D . Otherwise, if
BC.l< A.r + B.r then there
are two solutions. You can think of this as such, if there are zero solutions the bullet missed, if there is one the bullet grazed, and if there are two then there is both an entry and exit wound. The one closer to
A`是我们想要的。D
是AC
上距离B A.r + B.r
(又名2r
)的点,因此:
sqrt( sqr(D.x - B.x) + sqr(D.y - B.y) ) == 2r
sqr(D.x - B.x) + sqr(D.y - B.y) == 4*r*r
。现在有一个方程的2个变量(即D.x
和D.y
)很麻烦,但我们也知道D
是
在AC
行D.y == AC.m*D.x + AC.b
。D.y
sqr(D.x - B.x) + sqr(AC.m*C.x + AC.b - B.y) == 4*r*r
。sqr(D.x) + 2*D.x - sqr(B.x) + sqr(AC.m*D.x) + 2*AC.b*D.x - 2*AC.m*D.x*B.y + sqr(AC.b) - 2*AC.b*B.y + sqr(B.y) == 4*r*r
(这个是我很可能犯了错误的部分如果我根本就这样做了。)D.x
是未知的;其余的我们可以将它们视为常量)得到
sqr(D.x) + 2*D.x - sqr(B.x) + sqr(AC.m*D.x) + 2*AC.b*D.x - 2*AC.m*D.x*B.y + sqr(AC.b) - 2*AC.b*B.y + sqr(B.y) == 4*r*r
(sqr(D.x) + sqr(AC.m*D.x)) + ( 2*D.x + 2*AC.b*D.x - 2*AC.m*B.y*D.x ) + ( - sqr(B.x) + sqr(AC.b) - 2*AC.b*B.y + sqr(B.y) ) == 4*r*r
(1 + sqr(AC.m)) * sqr(D.x) + 2*( 1 + AC.b - AC.m*B.y ) * D.x + ( sqr(B.y) - sqr(B.x) + sqr(AC.b) - 2*AC.b*B.y - 4*r*r ) == 0
x == (-bb +- sqrt( sqr(bb) - 4*aa*cc ) / 2*aa
)(使用aa
以避免与早期变量混淆),aa := 1 + sqr(AC.m);
,bb := 2*( 1 + AC.b - AC.m*B.y );
和{{ 1}}。cc := sqr(B.y) - sqr(B.x) + sqr(AC.b) - 2*AC.b*B.y - sqr(A.r+B.r);
保存部件:-bb/2aa +- sqrt(sqr(bb)-4*aa*cc)/2*aa
和first_term := -bb/(2*a);
。second_term := sqrt(sqr(bb)-4*aa*cc)/2*aa;
包含D1.x = first_term + second_term;
,第二个解决方案D1.y = AC.m * D1.x + AC.b
包含D2.x = first_term + second_term;
。D2.y = AC.m * D2.x + AC.b
的距离:A
和D1.l := sqrt( sqr(D1.x-A.x) + sqr(D1.y-A.y) );
(实际上,跳过两个平方根更有效,但没有区别。)D2.l = sqrt( sqr(D2.x-A.x) + sqr(D2.y-A.y) );
。D := D1 if D1.l < D2. l else D2;
的中点,我们称之为E,是碰撞(如果半径不相等,我不知道这是如何推断的。)DB
和DB.m := (B.y-D.y)/(B.x-D.x);
。 DB.b = B.y - DB.m*B.x;
,所以BD.l == A.r + B.r
。sqrt( sqr(E.x-B.x) + sqr(E.y-B.y) ) == B.r
,因为我们知道它位于E
所以BD
,正在E.y == BD.m * E.x + BD.b
。sqrt( sqr(E.x-B.x) + sqr(BD.m * E.x + BD.b-B.y) ) == B.r
sqr(E.x) - 2*E.x*B.x + sqr(B.x) + sqr(BD.m)*sqr(E.x) + 2*BD.m*E.x*BD.b - 2*BD.m*B.y + sqr(B.y) - 2*BD.b*B.y + sqr(B.y) == sqr(B.r)
sqr(E.x) + sqr(BD.m)*sqr(E.x) + 2*BD.m*E.x*BD.b - 2*E.x*B.x + sqr(B.x) - 2*BD.m*B.y + sqr(B.y) - 2*BD.b*B.y + sqr(B.y) == sqr(B.r)
(1 + sqr(BD.m)) * sqr(E.x) + 2*(BD.m*BD.b - B.x) * E.x + sqr(B.x) + sqr(B.y) - 2*BD.m*B.y + sqr(B.y) - 2*BD.b*B.y + sqr(B.y) - sqr(B.r) == 0
,二次公式,然后得到你的两个点并选择一个接近B的点。
我希望我只是因为业力或死灵法师徽章而嫖娼,但我真的需要解决这个问题,并认为我会分享。呃,我想我现在需要躺下。
答案 3 :(得分:1)
物理模拟的一部分是冲突解决。通常最好使用时间步长过程来完成此操作。从最早的碰撞开始,解决并在碰撞后重复进行更多的碰撞。
时间是单位时间。 0是最后一帧的时间,而1是我们正在渲染的帧。
因此,我们实际上仅对碰撞时间感兴趣,并且可以忽略实际位置,直到知道哪个对象首先碰撞。
此答案提供的功能将返回两个运动球的所有可能的碰撞时间(最多两个),非常适合基于时间的碰撞解决方案。
对于 TLDR ,请转到解决方案
有关如何继续阅读。
首先定义球(使用JS)
const ball = {
x, y, // position start of frame
r, // radius
vx, vy, // velocity vector per frame
}
// Two balls A, and B
A = {...ball, r: 20, etc}
B = {...ball, r: 20, etc}
如果我们想象球沿着其向量行进为一个点。我们首先看一下如何解决只有一个球移动的情况。
A
球不动,B
球不动,(以适应去除点符号A.x
的线为Ax
,B.vx
为Bvx
另外*
*表示JS中的功能)
通过以下两个步骤,我们可以找到一个点到直线的距离。
最接近点A
的直线上的第一个单位距离,然后使用距离dist u
来获得与球A的距离dA
与B线沿着的距离
u = (Bvx * (Ax - Bx) + Bvy * (Ay - By)) / ((Ax - Bx) ** 2 + (Ay - By) ** 2);
dA = (((Bx + Bvx * u) - Ax) ** 2 + ((By + Bvy * u) - Ay) ** 2) ** 0.5;
如果球“ B”不动且球A
不变,我们可以做同样的事情
u = (Avx * (Bx - Ax) + Avy * (By - Ay)) / ((Bx - Ax) ** 2 + (By - Ay) ** 2);
dB = (((Ax + Avx * u) - Bx) ** 2 + ((Ay + Avy * u) - By) ** 2) ** 0.5;
如果我们现在考虑如果两个球都在运动,那么如果发生碰撞,则必须同时发生碰撞。因此,以上两个函数中的值u
必须相同。
我们现在有两个方程,它们具有相同的值u
,但是还需要解决更多的问题。
我们还知道在时间u
(碰撞时),两个球必须是它们的半径之和。那就是dA + dB == Ar + Br
。要摆脱平方根,dA * dA + dB * dB == Ar * Ar + Br * Br
现在我们只有1个未知的u
和一个函数...
Ar*Ar+Br*Br = (((Bx+Bvx*u)-Ax)**2+((By+Bvy*u)-Ay)**2)+(((Ax+Avx*u)-Bx)**2+((Ay+Avy*u)-By)**2);
或
f(u) = Ar*Ar+Br*Br-(((Bx+Bvx*u)-Ax)**2+((By+Bvy*u)-Ay)**2)+(((Ax+Avx*u)-Bx)**2+((Ay+Avy*u)-By)**2)
这可能并不明显,但是该函数只是二次函数,例如f(u) = au^2 + bu + c
,如果我们知道系数a
,b
,c
,我们可以使用二次方程式(我们都是在高中学习的)
要解决这个问题,我们需要重新排列公式,以便我们可以插入a
,b
,c
// return array if 0, 1, or 2 roots
Math.quadRoots = (a, b, c) => {
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
重新排列会变成一整页,因此为了节省痛苦,以下函数返回两个运动球的值u
// For line segements
// args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
Math.interceptTime = (a, e, b, f, c, g, d, h, r1, r2) => {
const A = a*a, B = b*b, C = c*c, D = d*d;
const E = e*e, F = f*f, G = g*g, H = h*h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b*c + c*b + f*g + g*f + 2*(a*d - a*b - a*c - b*d - c*d - e*f + e*h - e*g - f*h - g*h);
const BB = 2 * (-A + a*b + 2*a*c - a*d - c*b - C + c*d - E + e*f + 2*e*g - e*h - g*f - G + g*h);
const CC = A - 2*a*c + C + E - 2*e*g + G - R;
return Math.quadRoots(AA, BB, CC);
}
使用
获取时间
const res = Math.interceptTime(A.x, A.y, A.x + A.vx, A.y + A.vy, B.x, B.y, B.x + B.vx, B.y + B.vy, A.r, B.r);
// remove out of range values and use the smallest as time of first contact
const time = Math.min(...res.filter(u => u >= 0 && u < 1));
使用该时间查找最早的碰撞,一旦发现该时间即可获取球的位置
// point of contact for balls A and B
ax = A.x + A.vx * time;
ay = A.y + A.vy * time;
bx = B.x + B.vx * time;
by = B.y + B.vy * time;
上面用于鲁棒的迭代冲突解决。注意小球可以以很高的速度行进。由于JS的运行速度很慢,因此每帧的迭代总数上限为200。
// See answer for details
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
const A = a * a, B = b * b, C = c * c, D = d * d;
const E = e * e, F = f * f, G = g * g, H = h * h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
return Math.quadRoots(AA, BB, CC);
}
Math.quadRoots = (a, b, c) => { // find roots for quadratic
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
mathExt(); // creates some additional math functions
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 40; // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 6;
const MAX_BALL_SIZE = 20;
const VEL_MIN = 1;
const VEL_MAX = 5;
const MAX_RESOLUTION_CYCLES = 200; // Put too many balls (or too large) in the scene and the
// number of collisions per frame can grow so large that
// it could block the page.
// If the number of resolution steps is above this value
// simulation will break and balls can pass through lines,
// get trapped, or worse. LOL
const SHOW_COLLISION_TIME = 30;
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
Line.prototype = {
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
},
reverse() {
const x = this.x1;
const y = this.y1;
this.x1 = this.x2;
this.y1 = this.y2;
this.x2 = x;
this.y2 = y;
return this;
}
}
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += GRAVITY;
},
draw() {
ctx.moveTo(this.x + this.r - 1.5, this.y);
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
},
interceptLineTime(l, time) {
const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2, this.r);
if (u >= time && u <= 1) {
return u;
}
},
checkBallBallTime(t, minTime) {
return t > minTime && t <= 1;
},
interceptBallTime(b, time) {
const x = this.x - b.x;
const y = this.y - b.y;
const d = (x * x + y * y) ** 0.5;
if (d > this.r + b.r) {
const times = Math.circlesInterceptUnitTime(
this.x, this.y,
this.x + this.vx, this.y + this.vy,
b.x, b.y,
b.x + b.vx, b.y + b.vy,
this.r, b.r
);
if (times.length) {
if (times.length === 1) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
return;
}
if (times[0] <= times[1]) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
if(this.checkBallBallTime(times[1], time)) { return times[1] }
return
}
if(this.checkBallBallTime(times[1], time)) { return times[1] }
if(this.checkBallBallTime(times[0], time)) { return times[0] }
}
}
},
collideLine(l, time) {
const x1 = l.x2 - l.x1;
const y1 = l.y2 - l.y1;
const d = (x1 * x1 + y1 * y1) ** 0.5;
const nx = x1 / d;
const ny = y1 / d;
const u = (this.vx * nx + this.vy * ny) * 2;
this.x += this.vx * time;
this.y += this.vy * time;
this.vx = (nx * u - this.vx) * WALL_LOSS;
this.vy = (ny * u - this.vy) * WALL_LOSS;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) {
const a = this;
const m1 = a.m;
const m2 = b.m;
a.x = a.x + a.vx * time;
a.y = a.y + a.vy * time;
b.x = b.x + b.vx * time;
b.y = b.y + b.vy * time;
const x = a.x - b.x
const y = a.y - b.y
const d = (x * x + y * y);
const u1 = (a.vx * x + a.vy * y) / d
const u2 = (x * a.vy - y * a.vx ) / d
const u3 = (b.vx * x + b.vy * y) / d
const u4 = (x * b.vy - y * b.vx ) / d
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
a.x = a.x - a.vx * time;
a.y = a.y - a.vy * time;
b.x = b.x - b.vx * time;
b.y = b.y - b.vy * time;
},
doesOverlap(ball) {
const x = this.x - ball.x;
const y = this.y - ball.y;
return (this.r + ball.r) > ((x * x + y * y) ** 0.5);
}
}
function canAdd(ball) {
for(const b of balls) {
if (ball.doesOverlap(b)) { return false }
}
return true;
}
function create(bCount) {
lines.push(new Line(-10, 20, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 30, ctx.canvas.width + 10, ctx.canvas.height - 3)).reverse());
lines.push((new Line(30, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 30, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(VEL_MIN, VEL_MAX);
const ball = new Ball(
Math.rand(MAX_BALL_SIZE + 30, canvas.width - MAX_BALL_SIZE - 30),
Math.rand(MAX_BALL_SIZE + 30, canvas.height - MAX_BALL_SIZE - 30),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.randP(MIN_BALL_SIZE, MAX_BALL_SIZE),
);
if (canAdd(ball)) {
balls.push(ball);
break;
}
}
}
}
function resolveCollisions() {
var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
while (resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
resolving = false;
minObj = undefined;
minBall = undefined;
minTime = 1;
idx = 0;
for (const b of balls) {
idx1 = idx + 1;
while (idx1 < balls.length) {
const b1 = balls[idx1++];
const time = b.interceptBallTime(b1, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = b1;
minBall = b;
resolving = true;
}
}
}
for (const l of lines) {
const time = b.interceptLineTime(l, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = l;
minBall = b;
resolving = true;
}
}
}
idx ++;
}
if (resolving) {
if (minObj instanceof Ball) {
minBall.collide(minObj, minTime);
} else {
minBall.collideLine(minObj, minTime);
}
after = minTime;
}
}
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for (const b of balls) { b.update() }
ctx.beginPath();
ctx.strokeStyle = "#00F";
ctx.lineWidth = 3;
for (const b of balls) { b.draw() }
ctx.stroke();
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
ctx.beginPath();
for(const l of lines) { l.draw() }
ctx.stroke();
requestAnimationFrame(mainLoop);
}
function mathExt() {
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randP = (min, max, pow = 2) => Math.random() ** pow * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2, r) => {
const xx = x2 - x1;
const yy = y2 - y1;
const d = vx * yy - vy * xx;
if (d > 0) { // only if moving towards the line
const dd = r / (xx * xx + yy * yy) ** 0.5;
const nx = xx * dd;
const ny = yy * dd;
return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
}
}
}
<canvas id="canvas"></canvas>