在平面上生成非相交圆盘运动的路径

时间:2015-05-21 13:45:40

标签: algorithm language-agnostic game-physics computational-geometry motion-planning

我正在寻找

我在飞机上有300或更少相等半径的光盘。在时间0,每个盘都在一个位置。在时间1,每个盘处于可能不同的位置。我希望为每个光盘生成0到1之间的2D路径,使得光盘不相交并且路径相对有效(短)并且如果可能的话具有低曲率。 (例如,直线优于波浪线)

  • 较低的计算时间通常比解决方案的准确性更重要。 (例如,一个小交叉点是可以的,我不一定需要一个最佳结果)
  • 然而,光盘不应该相互传送,突然停止或减速,或突然改变方向 - 光滑的"更平滑的"更好。唯一的例外是时间0和1。
  • 路径可以采样形式或分段线性(或更好)表示 - 我并不担心通过样条线获得真正平滑的路径。 (如果我需要,我可以估算一下。)

我尝试了什么

You can see a demo我最好的尝试(通过Javascript + WebGL)。请注意,由于涉及的计算,它将在旧计算机上缓慢加载。它似乎适用于Windows下的Firefox / Chrome / IE11。

在这个演示中,我将每个光盘代表一个"弹性乐队"在3D中(也就是说,每个光盘在每次都有一个位置)并运行一个简单的游戏式物理引擎来解决约束并将每个时间点视为具有弹簧的质量到上一次/下一次。 ('时间'在这种情况下只是第三个维度。)

这对于小N(< 20)实际上非常有效,但在常见的测试用例中(例如,从以圆圈排列的光盘开始,将每个光盘移动到圆圈上的相反点),这无法生成令人信服的路径因为约束和弹性在整个弹簧中缓慢传播。 (例如,如果我将时间切割成100个离散级别,则弹性带中的张力仅在每个模拟周期中传播一个级别)这使得良好的解决方案需要许多(> 10000)次迭代,并且对于我的应用来说这是非常慢的。它也无法合理地解决许多N> 40个案例,但这可能仅仅是因为我无法进行足够的迭代。

我还尝试了什么

我最初的尝试是一个爬山者,从直线路径开始,逐渐变异。比目前最佳解决方案更好的测量解决方案取代了目前最好的解决方案更好的测量结果来自交叉量(即完全重叠测量比仅仅放牧更糟糕)和路径长度(更短的路径更好)。

这产生了一些令人惊讶的好结果,但不可靠的是,很可能经常陷入局部极小。对于N> 20,它非常慢。我尝试应用一些技术(模拟退火,遗传算法方法等)试图绕过局部最小问题,但我从未取得太大成功。

我正在尝试

我正在优化"松紧带"模型使得张力和约束在时间维度上传播得更快。在许多情况下,这将节省大量所需的迭代,但是在高度受限的情况下(例如,许多光盘试图穿过相同的位置)仍然需要无法维持的迭代量。我不是如何解决约束或更快地传播弹簧的专家(我已经尝试过阅读一些关于不可拉伸布料模拟的论文,但我还没有弄清楚它们是否适用),所以我感兴趣的是,如果有一个好方法可以解决这个问题。

桌上的想法

  • Spektre实施了一种非常快速的RTS风格的单位运动算法,效果非常好。它快速而优雅,但它受到RTS运动风格问题的影响:突然改变方向,单位可以突然停止以解决碰撞。此外,单位并非全部同时到达目的地,这实际上是一个突然停止。这可能是一个很好的启发式方法,可以制作可行的非平滑路径,然后可以在时间上对路径进行重新采样,并且可以平滑"平滑"算法可以运行(很像我演示中使用的算法。)
  • Ashkan Kzme建议问题可能与网络流量有关。看起来minimum cost flow problem可以起作用,只要空间和时间可以合理的方式进行,并且可以保持运行时间。这里的优势在于它是一组研究得很好的问题,但突然的速度变化仍然是一个问题,并且某种“平滑”问题。可能需要后续步骤。我目前遇到的绊脚石是决定时空的网络表示,这不会导致光盘彼此传送。
  • Jay Kominek发布了一个答案,该答案使用非线性优化器来优化二次贝塞尔曲线,并得到一些有希望的结果。

4 个答案:

答案 0 :(得分:6)

为了好玩而玩了一下这里的结果:

<强>算法:

  1. 处理每张光盘
  2. 将速度设为constant*destination_vector
    • 乘法常量a
    • 并将之后的速度限制为v
  3. 测试新的迭代位置是否与任何其他光盘冲突
  4. 如果它确实将速度沿一个方向旋转某个角度步ang
  5. 循环直到找到自由方向或覆盖整个圆圈
  6. 如果没有找到自由方向标记光盘卡住

    这就是圆圈到反圆路径的样子:

    example1

    这是随机到随机路径的样子:

    example2

    卡住的光盘黄色(在这些情况下都没有),并且移动光盘已经到达目的地。 如果没有路径,如果光盘已经在目标圈子中,那么这也可能会卡住另一个目的地。为了避免这种情况,您还需要更换碰撞光盘...您可以使用ang,a,v常数来进行不同的外观,也可以尝试随机的角度旋转方向以避免旋转/扭曲运动

  7. 这里是我使用的源代码(C ++):

    //---------------------------------------------------------------------------
    const int    discs =23;     // number of discs
    const double disc_r=5;      // disc radius
    const double disc_dd=4.0*disc_r*disc_r;
    struct _disc
        {
        double x,y,vx,vy;       // actual position
        double x1,y1;           // destination
        bool _stuck;            // is currently stuck?
        };
    _disc disc[discs];          // discs array
    //---------------------------------------------------------------------------
    void disc_generate0(double x,double y,double r)     // circle position to inverse circle destination
        {
        int i;
        _disc *p;
        double a,da;
        for (p=disc,a=0,da=2.0*M_PI/double(discs),i=0;i<discs;a+=da,i++,p++)
            {
            p->x =x+(r*cos(a));
            p->y =y+(r*sin(a));
            p->x1=x-(r*cos(a));
            p->y1=y-(r*sin(a));
            p->vx=0.0;
            p->vy=0.0;
            p->_stuck=false;
            }
        }
    //---------------------------------------------------------------------------
    void disc_generate1(double x,double y,double r)     // random position to random destination
        {
        int i,j;
        _disc *p,*q;
        double a,da;
        Randomize();
        for (p=disc,a=0,da=2.0*M_PI/double(discs),i=0;i<discs;a+=da,i++,p++)
            {
            for (j=-1;j<0;)
                {
                p->x=x+(2.0*Random(r))-r;
                p->y=y+(2.0*Random(r))-r;
                for (q=disc,j=0;j<discs;j++,q++)
                 if (i!=j)
                  if (((q->x-p->x)*(q->x-p->x))+((q->y-p->y)*(q->y-p->y))<disc_dd)
                   { j=-1; break; }
                }
            for (j=-1;j<0;)
                {
                p->x1=x+(2.0*Random(r))-r;
                p->y1=y+(2.0*Random(r))-r;
                for (q=disc,j=0;j<discs;j++,q++)
                 if (i!=j)
                  if (((q->x1-p->x1)*(q->x1-p->x1))+((q->y1-p->y1)*(q->y1-p->y1))<disc_dd)
                   { j=-1; break; }
                }
            p->vx=0.0;
            p->vy=0.0;
            p->_stuck=false;
            }
        }
    //---------------------------------------------------------------------------
    void disc_iterate(double dt)                    // iterate positions
        {
        int i,j,k;
        _disc *p,*q;
        double v=25.0,a=10.0,x,y;
        const double ang=10.0*M_PI/180.0,ca=cos(ang),sa=sin(ang);
        const int n=double(2.0*M_PI/ang);
        for (p=disc,i=0;i<discs;i++,p++)
            {
            p->vx=a*(p->x1-p->x); if (p->vx>+v) p->vx=+v; if (p->vx<-v) p->vx=-v;
            p->vy=a*(p->y1-p->y); if (p->vy>+v) p->vy=+v; if (p->vy<-v) p->vy=-v;
            x=p->x; p->x+=(p->vx*dt);
            y=p->y; p->y+=(p->vy*dt);
            p->_stuck=false;
            for (k=0,q=disc,j=0;j<discs;j++,q++)
             if (i!=j)
              if (((q->x-p->x)*(q->x-p->x))+((q->y-p->y)*(q->y-p->y))<disc_dd)
                {
                k++; if (k>=n) { p->x=x; p->y=y; p->_stuck=true; break; }
                p->x=+(p->vx*ca)+(p->vy*sa); p->vx=p->x;
                p->y=-(p->vx*sa)+(p->vy*ca); p->vy=p->y;
                p->x=x+(p->vx*dt);
                p->y=y+(p->vy*dt);
                j=-1; q=disc-1;
                }
            }
        }
    //---------------------------------------------------------------------------
    

    用法很简单:

    1. 使用您要放置光盘的飞机的中心和半径来调用generate0/1
    2. 调用迭代(dt是以秒为单位的时间)
    3. 画出场景
    4. 如果您想将其更改为使用t=<0,1>

      1. 循环迭代直到目的地或超时的所有光盘
      2. 记住列表中每张光盘的速度变化 需要位置或速度矢量及其发生的时间
      3. 循环后重新缩放光盘列表全部到<0,1>
      4. 的范围
      5. 渲染/动画重新缩放的列表
      6. <强> [注释]

        我的测试是实时运行的,但我没有应用<0,1>范围且没有太多光盘。所以你需要测试它是否足够快你的设置。

        要加快速度,您可以:

        • 放大角度步骤
        • 在对着最后一个碰撞的光盘旋转后测试碰撞,并且仅在免费测试其余的时间...
        • 将光盘分割成(由半径重叠)区域分别处理每个区域
        • 我认为这里的一些现场方法可以加快创建田野地图的速度,以便更好地确定避障方向

        [edit1]进行一些调整以避免障碍物周围的无限振荡

        对于更多光盘,其中一些光盘卡在已经停止的光盘周围弹跳。为避免这种情况,只需偶尔更改ang步进方向,这就是结果:

        exampe3

        你可以在完成之前看到摆动的弹跳

        这是改变的来源:

        void disc_iterate(double dt)                    // iterate positions
            {
            int i,j,k;
            static int cnt=0;
            _disc *p,*q;
            double v=25.0,a=10.0,x,y;
            const double ang=10.0*M_PI/180.0,ca=cos(ang),sa=sin(ang);
            const int n=double(2.0*M_PI/ang);
            // process discs
            for (p=disc,i=0;i<discs;i++,p++)
                {
                // compute and limit speed
                p->vx=a*(p->x1-p->x); if (p->vx>+v) p->vx=+v; if (p->vx<-v) p->vx=-v;
                p->vy=a*(p->y1-p->y); if (p->vy>+v) p->vy=+v; if (p->vy<-v) p->vy=-v;
                // stroe old and compute new position
                x=p->x; p->x+=(p->vx*dt);
                y=p->y; p->y+=(p->vy*dt);
                p->_stuck=false;
                // test if coliding
                for (k=0,q=disc,j=0;j<discs;j++,q++)
                 if (i!=j)
                  if (((q->x-p->x)*(q->x-p->x))+((q->y-p->y)*(q->y-p->y))<disc_dd)
                    {
                    k++; if (k>=n) { p->x=x; p->y=y; p->_stuck=true; break; }   // if full circle covered? stop
                    if (int(cnt&128))   // change the rotation direction every 128 iterations
                        {
                        // rotate +ang
                        p->x=+(p->vx*ca)+(p->vy*sa); p->vx=p->x;
                        p->y=-(p->vx*sa)+(p->vy*ca); p->vy=p->y;
                        }
                    else{
                        //rotate -ang
                        p->x=+(p->vx*ca)-(p->vy*sa); p->vx=p->x;
                        p->y=+(p->vx*sa)+(p->vy*ca); p->vy=p->y;
                        }
                    // update new position and test from the start again
                    p->x=x+(p->vx*dt);
                    p->y=y+(p->vy*dt);
                    j=-1; q=disc-1;
                    }
                }
            cnt++;
            }
        

答案 1 :(得分:4)

这并不完美,但我最好的想法是将光盘移到quadratic Bezier curves。这意味着您每个光盘只有2个自由变量,而您正试图为其找到值。

此时,你可以'#34;插上&#34;错误函数到非线性优化器中。在光盘相互避开方面,你越愿意等待,你的解决方案就会越好。

只有一个实际命中:

enter image description here

不打扰显示匹配,光盘实际上开始重叠:

enter image description here

我已经制作了一个完整的例子,但关键是要最小化的错误函数,我在这里重现:

double errorf(unsigned n, const double *pts, double *grad,
              void *data)
{
  problem_t *setup = (problem_t *)data;
  double error = 0.0;

  for(int step=0; step<setup->steps; step++) {
    double t = (1.0+step) / (1.0+setup->steps);
    for(int i=0; i<setup->N; i++)
      quadbezier(&setup->starts[2*i],
                 &pts[2*i],
                 &setup->stops[2*i],
                 t,
                 &setup->scratch[2*i]);

    for(int i=0; i<setup->N; i++)
      for(int j=i+1; j<setup->N; j++) {
        double d = distance(&setup->scratch[2*i],
                            &setup->scratch[2*j]);
        d /= RADIUS;
        error += (1.0/d) * (1.0/d);
      }
  }

  return error / setup->steps;
}

忽略ngraddatasetup描述了优化的具体问题,光盘数量以及启动和停止的位置。 quadbezier进行贝塞尔曲线插值,将其答案放入->scratch。我们在路径的一部分检查->steps点,并测量每个步骤中光盘彼此的接近程度。为了使优化问题更加顺畅,当光盘开始接触时,它没有硬开关,它只是试图让它们尽可能远离彼此。

https://github.com/jkominek/discs

可以使用完全可编译的代码,Makefile和一些用于将一组二次贝塞尔曲线转换为一系列图像的Python

大量积分表现有点迟钝,但还有很多改进选择。

  1. 如果用户对起始位置和结束位置进行微调,则在每次调整后,使用之前的解决方案作为新的起点,在后台重新运行优化。修复一个紧密的解决方案应该比每次从头开始重新创建它更快。
  2. n^2循环并行化在所有点上。
  3. 检查其他优化算法是否会对此数据做得更好。现在,它以全局优化传递开始,然后执行本地优化传递。有些算法已经知道了#34;如何做到这一点,并且可能更聪明。
  4. 如果您可以弄清楚如何计算自由或接近的渐变函数,我确信这样做是值得的,并切换到可以利用渐变信息的算法。即使梯度不便宜,也许值得。
  5. 使用子优化替换整个步骤,找到两个光盘最接近的t,然后使用该距离进行错误。确定该次优化的梯度应该更容易。
  6. 为中间点提供更好的数据结构,因此您不必为相距很远的光盘执行一系列不必要的距离计算。
  7. 可能更多?

答案 2 :(得分:3)

这种问题的通常解决方案是使用所谓的“热图”(或“影响图”)。对于该字段中的每个点,您计算“热量”值。磁盘向高值移动并远离冷值。热图适用于您的问题类型,因为它们编程非常简单,但可以生成复杂的类似AI的行为。

例如,想象一下只有两个磁盘。如果您的热图规则是等径向的,那么磁盘将仅朝向彼此移动,然后向后移动,来回摆动。如果你的规则在不同的径向上随机化强度,那么行为将是混乱的。您还可以使规则取决于速度,在这种情况下,磁盘在移动时会加速和减速。

通常,说热图规则应该使区域“更热”,它们接近磁盘的最佳距离。太靠近磁盘或太远的地方变得“更冷”。通过更改此最佳距离,您可以确定磁盘聚集的距离。

以下是一些文章,其中的示例代码展示了如何使用热图:

http://haufler.org/2012/05/26/beating-the-scribd-ai-challenge-implementing-traits-through-heuristics-part-1/

http://www.gamedev.net/page/resources/_/technical/artificial-intelligence/the-core-mechanics-of-influence-mapping-r2799

游戏AI Pro,第2卷,热图章节

答案 3 :(得分:0)

我还没有足够的代表发表评论,很抱歉没有回答。 但是对于RTS角度,RTS通常使用A *算法进行路径寻找。您是否有理由坚持使用基于物理的模型?

其次,你的联系尝试相当顺利,但中间加速,表现我最初的想法。由于您的模型将其视为橡皮筋,因此它主要是寻找哪种方式旋转以获得到达所需位置的最短路径。

如果你不担心物理方法,我会尝试如下: 尝试直接朝目标移动。如果碰撞,它应该尝试顺时针绕最近的碰撞滚动,直到它在矢量上与当前位置到目标位置的矢量成90度的位置。

如果我们假设一个盒子顶部的连续5个测试用例和底部连续五个测试用例,它们将直接相互移动直到它们发生碰撞。整个顶行将向右滑动,直到它们向下移动到底行的边缘,因为它向左移动并漂浮在顶行的边缘上方。 (想想威士忌和水杯玻璃技巧在开始时的样子)

由于运动不是由存储在弹簧中的势能确定的,这会在旋转过程中加速物体,因此您可以完全控制模拟过程中速度的变化。

在如上所述的循环测试中,如果所有磁盘都以相同的速度进行初始化,整个团块将转到中间,碰撞并扭转为一个单位大约四分之一圈,此时它们将脱离并且为他们的目标前进。

如果时间是轻微随机的,我认为你会得到你正在寻找的行为。

我希望这会有所帮助。