【问题标题】:Calculate x/y point that 2 moving balls will collide计算 2 个移动球会碰撞的 x/y 点
【发布时间】:2020-07-20 08:53:19
【问题描述】:

我正在尝试制作(本质上)简单的台球游戏,并且希望能够预测一旦击中另一个球后击球的位置。

我相信,第一部分是计算主球是否会击中任何东西,如果会击中,它会在哪里发生碰撞。我可以计算出一条线和一个球的碰撞点,但不能计算出两个球。

那么给定 2 个球的 x/y 位置和速度,我如何计算它们碰撞的点?

(PS:我知道我可以通过沿途每一步计算两个球之间的距离来做到这一点,但我希望得到更优雅和最优的东西。)

设置示例:尝试计算红点

http://dl.dropbox.com/u/6202117/circle.PNG

【问题讨论】:

    标签: math language-agnostic collision-detection physics


    【解决方案1】:

    注意事项:

    • 当两个球相撞时,半径为r,它们的中心相距2r
    • 可以假设您的第一个球沿直线行进(嗯,第一个近似值,但从这个开始),您可以找到该路径与从第一个球到第二个球的方向之间的角度 alpha .
    • 你知道静止球的中心,不是吗?

    现在你有一些几何图形要做。

    做这个结构:

    1. 将第一个(移动)球的当前中心标记为点A
    2. 将静止球的中心标记为点B
    3. 构造线段AB
    4. A 沿运动方向构造射线R
    5. 围绕B 构造一个半径为2r 的圆。
    6. B 垂直于R 删除一条线段,调用交点C
    7. 你知道AB的距离,你可以找到ABR之间的角度alpha,用正弦定理求BC的长度。
    8. 根据该长度确定是否有 0、1 或 2 个解。如果有 0 或 1,你就完成了。
    9. 在 A 附近构造圆与 R 相交的点 D,并再次使用正弦定理求距离 AD。
    10. 碰撞点是BD的中点

    现在你什么都知道了。

    由此构建高效的代码留作练习。


    顺便说一句——如果两个球都在移动,这种结构将不起作用,但你可以转换成一个框架,其中一个是静止的,这样解决它,然后再转换回来。请务必检查解决方案是否在允许的区域内反向转换之后...

    / 物理学家不能制造这样的 cmets。我试图反抗。我真的做到了。

    【讨论】:

    • 呸,我是个白痴。以前有好几个人给我解释过,现在才明白。出于某种原因,我确信执行 2r 会导致碰撞变得不稳定。感谢您的帮助,我应该能够从这里解决这个问题。
    • 在你做这种事情几次后,你会自动伸手去拿一个垫子或白板。这就是你永远迷失在男人世界中的时刻......
    • 这个答案让我觉得我又回到了高中几何……而且有点喜欢它,这很糟糕吗?
    • 刚刚检查了您的个人资料,dmckee。中微子——干得好。听起来它在多年后开始得到回报。我真的很喜欢这个答案,期待你的更多。
    【解决方案2】:

    @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!

    【讨论】:

      【解决方案3】:

      我正在查看@dmckee 的解决方案,我花了很多功夫才完成它。以下是我为那些可能正在寻找更实际答案的人的笔记,它是直接采取的 来自他的职位,所以功劳归他/她所有,但任何错误都是我的。我通常使用类似 Pascal 的赋值运算符(即:=)来区分显示我的工作和实际必要的代码。 我正在使用标准的Y = mX +b 格式和准循环表示法。我确实将 BC 用于段和结果行。也就是说,这应该“几乎”是可复制可粘贴的 Python 代码 (删除“;”,用正确的版本替换“sqrt”和“sqr”等)。

      1. A.x 和 A.y 是 x 和 y 位置,A.r 是 A 的半径,A.v 是速度,A.v.x 是它的 x 分量,A.v.y 是它的 y 分量。
      2. B 相同,但没有速度(或者更准确地说,从 A 中减去 B 的速度,因此 B 相对而言是静止的)。
      3. AB.m := (b.y - a.y) / (b.x - a.x);AB.b := A.y - AB.m * A.x;
      4. R.m := A.v.y / A.v.x;R.b := A.y - R.m * A.x;
      5. 没有必要
      6. BC.m := -A.v.x/A.v.y; 是垂直坡度的标准方程,BC.b := B.y - BC.m * B.x; 现在 CABBC 相交的位置,所以我们知道它们相等,所以让 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;
      7. 你可以忽略正弦定律,因为我们有 AB 和 BC,所以我们可以使用勾股定理得到 BCBC.l := sqrt( sqr(B.x-C.x) + sqr(B.y-C.y) ); 的长度
      8. 如果BC.l > A.r + B.r,则有零解,并且这些圆圈不接触,因为CA 的路径s perigee with respect toB. IfBC.l == A.r + B.r, there is only one solution, andC = = D. Otherwise, ifBC.l 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 toA` 是我们想要的。
        1. 现在,数学变得难看,所以我会展示我的工作以防万一我做错了什么。
        2. DAC 上的一个点,即A.r + B.r(又名2r)远离B 所以: sqrt( sqr(D.x - B.x) + sqr(D.y - B.y) ) == 2r
        3. 因此sqr(D.x - B.x) + sqr(D.y - B.y) == 4*r*r。现在 2 个变量(即 D.xD.y)和一个方程很麻烦,但我们也知道 D 是 在线AC 所以D.y == AC.m*D.x + AC.b
        4. 我们可以用sqr(D.x - B.x) + sqr(AC.m*C.x + AC.b - B.y) == 4*r*r代替D.y
        5. 这扩展为可爱: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*rthis 是我最有可能犯错误的部分如果我确实犯了)。
        6. 我们可以收集这些术语(请记住,此时只有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
        7. 改写成更整洁的 (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
        8. 现在这与 aa := 1 + sqr(AC.m);bb := 2*( 1 + AC.b - AC.m*B.y );cc := sqr(B.y) - sqr(B.x) + sqr(AC.b) - 2*AC.b*B.y - sqr(A.r+B.r); 非常适合二次公式(即 x == (-bb +- sqrt( sqr(bb) - 4*aa*cc ) / 2*aa)(使用 aa 以避免与早期变量混淆)。
        9. 现在我们可能会得到两个解决方案,所以让我们使用-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;
        10. 第一个解决方案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
        11. 找出到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) );(实际上跳过两个平方根更有效,但没有区别)。
        12. 更接近的就是你想要的D := D1 if D1.l < D2. l else D2;
        1. DB 的中点,我们称之为 E,是碰撞(如果半径不相等,我不知道这如何概括)。
        2. 因此构造DB.m := (B.y-D.y)/(B.x-D.x);DB.b = B.y - DB.m*B.x; 行。
        3. 我们不需要长度来确定长度,因为它应该是BD.l == A.r + B.r,所以sqrt( sqr(E.x-B.x) + sqr(E.y-B.y) ) == B.r
        4. 同样,我们可以替换 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
        5. 扩展为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)
        6. 其中汇入

        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 aa := (1 + sqr(BD.m)); bb := 2*(BD.m*BD.b - B.x); cc := sqr(B.y) - 2*BD.m*B.y + sqr(B.y) - 2*BD.b*B.y + sqr(B.y) - sqr(B.r);,二次公式,然后得到你的两个点,选择更接近B的那个。

      我希望我只是为了业力或死灵法师徽章而卖淫,但我真的需要解决这个问题并想我会分享。呃,我想我现在需要躺下。

      【讨论】:

      • 现在这很尴尬。好吧,至少我试过了。感谢您为我的混乱提供更简单的替代方案。
      • 不客气。请记住以@Username 开始您的 cmets,以便您的派对收到通知。我刚回到这里交叉检查我的答案!
      • 根据您的应用程序,您可能不需要知道构造中每个点的位置。如果你用向量的形式写出数学,你可能会发现这更容易:如果我早期尝试将所有数学都写成组件,我总是会迷路。
      【解决方案4】:

      计算碰撞时间

      物理模拟的一部分是碰撞解决。这通常最好使用时间步长过程来完成。从最早的碰撞开始,解决并在此碰撞后重复更多的碰撞。

      时间是单位时间。 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.xAxB.vxBvx 也是 ** 表示 JS 中的力量)

      我们可以通过以下两个步骤找到一个点到一条线的距离。

      距离A最近的点的直线上的第一个单位距离,然后使用单位距离u得到dA与球A的距离与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,如果我们知道系数abc,我们可以使用二次公式找到根(我们都在高中)

      解决方案

      为了解决我们需要重新排列公式,以便我们可以得到abc,我们可以插入它们

      // 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;
              }
          }
      }
      &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

      【讨论】:

        猜你喜欢
        • 2015-07-19
        • 1970-01-01
        • 2011-04-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-11
        • 1970-01-01
        • 2015-09-30
        相关资源
        最近更新 更多