15

我在寻找什么

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

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

我试过的

您可以看到我的最佳尝试演示(通过 Javascript + WebGL)。请注意,由于涉及的计算,它会在较旧的计算机上缓慢加载。它似乎在 Windows 下的 Firefox/Chrome/IE11 中工作。

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

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

我还尝试了什么

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

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

我正在尝试什么

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

桌上的想法

  • Spektre 实现了一种非常快速的 RTS 风格的单位移动算法,效果非常好。它快速而优雅,但它存在 RTS 运动风格问题:突然的方向变化,单位可以突然停止以解决碰撞。此外,并非所有单位都同时到达目的地,这本质上是一个突然停止。这可能是一个很好的启发式方法来制作可行的非平滑路径,之后可以及时重新采样路径并且可以运行“平滑”算法(很像我在演示中使用的算法。)
  • Ashkan Kzme 认为问题可能与网络流量有关。看起来最小成本流问题是可行的,只要空间和时间可以以合理的方式进行区分,并且运行时间可以降低。这里的优点是它是一组经过充分研究的问题,但突然的速度变化仍然是一个问题,并且可能需要某种“平滑”的后期步骤。我目前遇到的绊脚石是决定时空网络表示不会导致光盘相互传送。
  • Jay Kominek 发布了一个答案,该答案使用非线性优化器来优化二次贝塞尔曲线,并取得了一些有希望的结果。
4

4 回答 4

7

玩了一会儿,结果如下:

算法:

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

    这是圆形到反向圆形路径的样子:

    示例1

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

    示例2

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

这是我使用的源代码(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. 画场景

如果你想改变它来使用t=<0,1>

  1. 循环迭代直到所有磁盘都在目的地或超时
  2. 记住列表中每个光盘的任何速度变化都需要位置或速度矢量以及它发生的时间
  3. 循环后重新调整光盘列表的范围<0,1>
  4. 渲染/动画重新缩放的列表

[笔记]

我的测试是实时运行的,但我没有应用<0,1>范围并且没有太多光盘。因此,您需要测试这对于您的设置是否足够快。

要加快速度,您可以:

  • 放大角度步长
  • 旋转后与最后一个碰撞的圆盘测试碰撞,并且只有在自由测试其余部分时...
  • 将光盘分割成(按半径重叠)区域分别处理每个区域
  • 我也认为这里的一些现场方法可以加快诸如偶尔创建现场地图之类的事情,以便更好地确定避障方向

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

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

示例 3

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

这是更改后的来源:

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++;
    }
于 2015-06-04T08:59:11.247 回答
4

它并不完美,但我最好的想法是沿着二次贝塞尔曲线移动圆盘。这意味着您尝试为其查找值的每张光盘只有 2 个自由变量。

此时,您可以将误差函数“插入”到非线性优化器中。您愿意等待的时间越长,您的解决方案就越好,就光盘相互避开而言。

只有一个实际命中:

在此处输入图像描述

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

在此处输入图像描述

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

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;
}

忽略和。n_ 描述了正在优化的具体问题、光盘数量以及它们开始和停止的位置。进行贝塞尔曲线插值,将其答案放入. 我们沿路径检查点,并在每一步测量光盘彼此之间的距离。为了使优化问题更顺畅,当圆盘开始接触时它没有硬开关,它只是尽量让它们彼此远离。graddatasetupquadbezier->scratch->steps

可在https://github.com/jkominek/discs获得完全可编译的代码、Makefile 和一些 Python,用于将一堆二次贝塞尔曲线转换为一系列图像

性能在大量点上有点迟钝,但有许多改进的选择。

  1. 如果用户对开始和结束位置进行细微调整,则在每次调整后,在后台重新运行优化,使用之前的解决方案作为新的起点。修复一个接近的解决方案应该比每次都从头开始重新创建它更快。
  2. 在所有点上并行化n^2循环。
  3. 检查其他优化算法是否会在此数据上做得更好。现在它从全局优化通道开始,然后进行局部优化通道。有些算法已经“知道”如何做这类事情,并且可能更聪明。
  4. 如果你能弄清楚如何免费或接近计算梯度函数,我相信这样做是值得的,并切换到可以利用梯度信息的算法。即使渐变不便宜,它也可能是值得的。
  5. 用次优化替换整个步骤,找到t两个圆盘最接近的位置,然后使用该距离来计算错误。计算出该次优化的梯度应该容易得多。
  6. 更好的中间点数据结构,因此您不必为相距很远的圆盘执行一堆不必要的距离计算。
  7. 可能更多?
于 2015-06-05T18:17:30.937 回答
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

Game AI Pro,第 2 卷,关于热图的章节

于 2015-06-08T20:46:03.750 回答
0

我还没有足够的代表发表评论,很抱歉没有回答。但是对于RTS角度,RTS一般使用A*算法进行寻路。你坚持使用基于物理的模型有什么原因吗?

其次,您链接的尝试运行得相当顺利,但中间有加速,表现出我最初的想法。由于您的模型将其视为橡皮筋,因此它基本上是在寻找以最短路径旋转到所需位置的方式。

如果您不担心物理方法,我会尝试如下:尝试直接向目标移动。如果它发生碰撞,它应该尝试围绕其最近的碰撞顺时针滚动,直到它位于向量上与从当前位置到目标位置的向量成 90 度的位置。

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

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

在像上面这样的循环测试中,如果所有磁盘都以相同的速度初始化,整个团块将进入中间,作为一个整体发生碰撞和扭曲大约四分之一圈,然后它们会脱离并前往它们的目标。

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

我希望这有帮助。

于 2015-06-08T19:26:01.290 回答