实现Boids算法


18

介绍

类鸟群算法是自发行为一组在一个相对简单的演示。正如其创建者Craig Reynolds所述,它具有三个主要规则:

基本的植绒模型由三个简单的转向行为组成,这些行为描述了单个波德人如何根据其附近的植群的位置和速度进行操纵:

  • 分隔:避免拥挤当地羊群。
  • 对齐:对当地flockmates的平均航向转向。
  • 凝聚力:转向向当地flockmates的平均位置移动。

每个辫子都可以直接访问整个场景的几何描述,但是植绒要求它仅对自身周围某个特定区域内的植绒动物做出反应。邻域的特征是距离(从主体的中心开始测量)和角度(从主体的飞行方向开始测量)。该本地社区之外的队友将被忽略。可以将邻域视为感知受限的模型(例如在浑浊的水中的鱼),但将其视为定义了一群人影响波伊德转向的区域可能更正确。

我在解释事物时并不完美,因此我强烈建议您查看原始资料。他的网站上还有一些超级有用的图片。

挑战

给定主体(模拟实体)的数量和帧的数量,输出模拟的动画。

  • Boid应该呈现为红色圆圈,并在圆圈内部显示一条线,指示其标题,这是Boid指向的方向:

粗略绘制的两个“ boid”,一个面向左,另一个面向右。

  • 每个波蒂的角度(如雷诺所描述的)应为整300度。(不是360)
  • 每个boid的起始航向和位置以及位置均应是均匀随机的(但要播种,以便仍能确定输出)。
  • 如果boid的半径为1,则邻居的半径应为3。
  • 伯德的数量从2到20不等。
  • 帧数为1-5000
  • 动画的播放时间至少应为每帧10毫秒,最长应为boid数量的1秒。(2个波西德=每帧最大2秒,3个波西德=每帧最大3秒,等等)
  • 输出动画应至少为5个boid半径乘以5个boid半径,乘以boid数量的一半。因此,两个boid的最小大小将是10 boid半径乘以10 boid半径,三个boid的最小大小将是15 boid半径乘以15 boid半径,等等。
  • 每个投标的半径必须至少为5个像素,最大为50个像素。
  • 需要限制每个Boid的速度,以使其在一帧中的移动幅度不超过其半径的1/5。
  • 需要确定输出,以便如果多次运行相同的输入将产生相同的输出。
  • 如果辫子到达边界,则应回绕到另一侧。同样,每个投标附近的区域也应环绕边界。

算法规则

在这种情况下,每个Boid周围都有一个跨度为300度的扇形,以Boid的航向为中心。这个“邻居”中的任何其他小伙子都被认为是“邻居”,或(用雷诺兹的术语称)“队友”。

  1. 每个BOID都应调整其航向以避免碰撞,并保持一个BOID半径与其邻居的舒适距离。(这是算法的“分离”方面。一个波德半径可能会被绕过,但应该像橡皮筋一样,重新卡入到位。)

  2. 每个boid都应另外调整其航向,使其更接近其附近其他boid的平均航向,只要它不干扰第一条规则即可。(这是算法的“对齐”方面)

  3. 只要不引起碰撞或不会显着干扰第二条规则,每个船长都应将自己转向其队友的平均位置。

有关该主题的论文中,他解释如下:

为了建立模拟群,我们从支持几何飞行的布依德模型开始。我们添加了与避免碰撞的相反力量和加入羊群的冲动相对应的行为。简要地描述为规则,并按优先级从高到低的顺序,导致模拟植绒的行为为:

  • 避免碰撞:避免与附近的队友发生碰撞
  • 速度匹配:尝试将速度与附近的队友匹配
  • 羊群居中:尝试靠近附近的羊群

运动的更详细描述:

  • Boids算法的标准实现通常会对每个规则进行计算,然后将其合并在一起。
  • 对于第一个规则,Boid会遍历其邻域内的相邻Boid列表,并且如果其自身与邻居之间的距离小于某个值,则将Boid推离其邻居的矢量应用于Boid的航向。
  • 对于第二条规则,Boid计算其邻居的平均航向,并将当前航向与平均航向之间的差值的一小部分(在此挑战中,我们将使用1/10)添加到当前航向。
  • 对于第三条规则(也是最后一条规则),该boid将其邻居的位置平均,然后计算指向该位置的向量。此向量乘以比规则2所用的数字还要小的数字(对于此挑战,将使用1/50)并将其应用于航向。
  • 然后将标本朝其方向移动

是Boids算法的有用伪代码实现。

输入和输出示例

输入:

5、190(5个辫子,190帧)

输出:

带有5个物体的Boids算法的190帧动画。

获奖标准

这是,因此以字节为单位的最小解决方案获胜。


7
“当然,算法还有更多,所以我强烈建议检查源代码。” -这里是否有一切必要?如果没有,我建议您修复它。
乔纳森·艾伦

1
请按照询问页面上的建议,在发布挑战之前使用沙盒
瑕疵的

@JonathanAllan是的,所有必要的信息都在这里,但是在源头上可以找到可能对其他用户更有意义的更深入的解释。
iPhoenix

11
这是一个有趣的挑战(我发现植绒行为令人着迷),但需要对它进行详细说明,尤其是对于代码高尔夫球,否则减小代码长度的压力将导致与挑战精神的一切可能偏差。受到激励。
trichoplax

Answers:


7

处理3.3.6(Java)932 931 940 928 957年 917 904字节

乔纳森·弗雷奇Jonathan Frech)的 -1字节
+11字节以更好地匹配凯文·克鲁伊森Kevin Cruijssen)规范
-2字节 -12字节,用于将args更改为t() +29字节,因为我做鬼影,请参见 -40字节以下的注释版本,用于 使用默认frameRate,30 循环而不是为每个重影-13个字节单独调用




好吧,对于没有Java高尔夫的人来说,这是一个开始。:)

int n=15,f=400,i,j,z=255,w=500;float d=200./n;PVector m;B[]a=new B[n];void setup(){size(500,500);fill(z,0,0);randomSeed(n);for(i=0;i<n;a[i++]=new B(new PVector(random(w),random(w)),m.fromAngle(random(TAU))));}void draw(){background(z);for(B b:a)b.u();if(frameCount%f<1)setup();}class B{PVector p,v,e,q,r;ArrayList<B>n;B(PVector m,PVector o){p=m;v=o;}void u(){e=v.copy();n=new ArrayList();for(B b:a){if(b!=this)for(i=-w;i<=w;i+=w)for(j=-w;j<=w;j+=w)t(i,j,b);}if(n.size()>0){q=new PVector();r=q.copy();for(B b:n){q.add(b.v);r.add(b.p);if(p.dist(b.p)<=d)e.add(p).sub(b.p);}e.add(q.div(n.size()).sub(v).div(10));e.add(r.div(n.size()).sub(p).div(50));}p.add(e.limit(d/10));v=e.mult(10);p.set((p.x+w)%w,(p.y+w)%w);noStroke();ellipse(p.x,p.y,d,d);stroke(0,0,z);line(p.x,p.y,p.x+v.x,p.y+v.y);}void t(int x,int y,B o){m=o.p.copy().add(x,y);if(2*d>=p.dist(m)&q.angleBetween(v,q.sub(m,p))<=5*PI/6)n.add(new B(m,o.v));}}

我不知道在Processing中进行输入的任何合理方法,因此前两个变量是输入(我没有将其值(5个字节)计入字节数)。如果有问题,我可以尝试其他方法。

我也不知道有什么好的方法可以在线尝试(Processing.js项目无法处理这种代码样式),而无需自己托管东西。这是我不希望尝试的事情。让我知道我能做些什么。

格式化代码,带注释

int n=15, // Number of boids
    f=400, // Number of frames
    i,j,z=255,w=500; // temp*2, and two constants
float d=200./n; // Boid diameter
PVector m; // temp
B[]a=new B[n];
void setup(){ // This is automatically called at startup
  size(500,500); // Can't use variables for this without extra bytes for settings()
  fill(z,0,0);
  randomSeed(n); // seeded from number of Boids, so that n=19 is very different from n=20
  for(i=0;i<n;a[i++]=new B(new PVector(random(w),random(w)),m.fromAngle(random(TAU))));
}
void draw(){ // This is automatically called each frame
  background(z);
  for(B b:a)
    b.u();
  if(frameCount%f<1) // When desired frames length is hit, reset everything.
    setup();         // Could also use noLoop() instead of setup() to just stop instead.
                     // Or, remove this if statement altogether to go on to infinity.
}
class B{ // Boid
  PVector p,v,e,q,r; // Position, Velocity, Next velocity, and two temp vectors
  ArrayList<B>n; // List of neighbors
  B(PVector m,PVector o){
    p=m;
    v=o;
  }
  void u(){ // Update function, does rules and redraw for this Boid
    e=v.copy();
    n=new ArrayList();
    for(B b:a){ // Test a Boid and its eight ghosts for neighborship
      if(b!=this) // Note: Assumes neighborhood diameter < min(width,height)
        // The ghosts are to check if it'd be closer to measure by wrapping
        // We need eight for wrapping north, east, south, west, northeast,
        // northwest, southeast, and southwest. And also the non-wrapped one.
        // The above assumption ensures that each ghost is further apart than
        // the neighborhood diameter, meaning that only one neighbor might be
        // found for each boid. To test this, place a boid in each corner, right
        // to the edge, facing away from center. Each boid should find three
        // neighbors, that are the three other boids.
        for(i=-w;i<=w;i+=w)for(j=-w;j<=w;j+=w)t(i,j,b);
    }
    if(n.size()>0){
      q=new PVector();
      r=q.copy();
      for(B b:n){
        q.add(b.v); // Velocity matching, pt 1
        r.add(b.p); // Flock centering, pt 1
        if(p.dist(b.p)<=d)  
          e.add(p).sub(b.p); // Collision avoidance
      }
      e.add(q.div(n.size()).sub(v).div(10)); // Velocity matching, pt 2
      e.add(r.div(n.size()).sub(p).div(50)); // Flock centering, pt 2
    }
    p.add(e.limit(d/10)); // Update vectors
    v=e.mult(10);
    p.set((p.x+w)%w,(p.y+w)%w); // Wrapping
    noStroke();
    ellipse(p.x,p.y,d,d); // Draw Boid, finally
    stroke(0,0,z);
    line(p.x,p.y,p.x+v.x,p.y+v.y);
  }
  void t(int x,int y,B o){ // Test if a Boid (or a ghost) is a neighbor
    m=o.p.copy().add(x,y);
    if(2*d>=p.dist(m)&q.angleBetween(v,q.sub(m,p))<=5*PI/6)
      n.add(new B(m,o.v));
  }
}

样品输出

n = 15,帧= 400:

辫子

或者,使用相同的动画,但显示每个Boid的邻域。


1
2*PI不能成为TAU救一个字节?
Jonathan Frech '18

@JonathanFrech是的,可以;我最初使用-PI,PI,我一直沿这种方式前进,但后来却陷入困境。
Phlarx

我的程序(用js和html编写)没有导出gif,但它绘制了图像,并且我使用了屏幕捕获程序并将导出的视频转换为gif。不过,我确实注意到了一件事。辫子的轮廓是蓝色的,不符合规格:)
iPhoenix

只是另一个友善的提醒,此答案不符合规范,因此不会得到赏金。
iPhoenix

1
我不知道处理,但我认为您可以进行以下操作:,i,进入,i=0,并删除i=0内部for循环。(-1个字节); frameCount%f==0frameCount%f<1(1个字节);&&&最后的if 2*d>=p.dist(m)&q.angleBetween(v,q.sub(m,p))<=5*PI/6(-1字节)。同样,不确定是否可行,但是由于Processing似乎与Java非常相似,所以我认为是这样。另外,您可以尝试使用screentogif.com创建gif 。
凯文·克鲁伊森

4

JavaScript(ES6)+ HTML5,1200字节

这是我当前使用Canvas API的解决方案。所述eval()返回咖喱函数,其第一输入端是Boid人口,并且第二个是动画帧的数量。您可以使用Infinity连续动画。

eval(...)长度为1187字节,<canvas id=c>长度为13字节,总计为1200。CSS是不必要的,但是为了方便起见,它允许您查看画布的边缘。

eval("L7F7{function B8{t=this,t.a=o8*T,t.x=o8*S,t.y=o8*S}C=this.c,D=C.getContext`2d`,({abs:z,random:o,atan2:k,cos:u,sin:g,PI:P,T=2*P,G={c:_7A[r='filter'](b7b!=t)[i](9)79)),n:_7A[r](b7b!=t)[i](9)7({a,x,y:y-S})),s:_7A[r](b7b!=t)[i](9)7({a,x,y:y+S})),e:_7A[r](b7b!=t)[i](9)7({a,x:x-S,y})),w:_7A[r](b7b!=t)[i](9)7({a,x:x+S,y}))},M=I7[I,I+T,I-T][p]((a,x)7z(x)<z(a)?x:a)}=Math),B.prototype={d8{with(D)save8,translate(x,y),rotate(a),beginPath8,arc(0,0,5,0,T),fillStyle='red',fill8,beginPath8,moveTo(0,0),lineTo(10,0),strokeStyle='blue',stroke8,restore8},n:_7(({c,n,s,e,w}=G),c8.concat(n8,s8,e8,w8)[r](b7(d=b.x-x,f=b.y-y,400>d*d+f*f&&z(z(k(f,d)-a)/P-1)>1/6))),s8{q=(j=t.n8).length,v=t.v8||0,l=t.l8||0,f=t.f8||0,a=t.a=(t.a+v+l/10+f/50)%T,t.x=(x+u(a)+S)%S,t.y=(y+g(a)+S)%S},v:_7([d,f]=j[r](b7225>(b.x-x)**2+(b.y-y)**2)[p='reduce'](([d,f],b)7[x+d-b.x,y+f-b.y],[0,0]),d||f?M(k(f,d)-a):0),l:_7j[i](b7M(b.a-a))[p]((a,x)7a+x,0)/q,f:_7([d,f]=j[p](([d,f],b)7[d+b.x,f+b.y],[-x*q,-y*q]),d||f?M(k(f,d)-a):0)},S=C.width=C.height=50*L,A=Array(L).fill().map(_7new B),R=_7{D.clearRect(0,0,S,S),A[i='map'](b79=b).d8),A[i](b79=t=b).s8),F--&&setTimeout(R,10)},R8}".replace(/[789]/g,m=>['=>','()','({a,x,y}'][m-7]))
(10)(Infinity)
canvas{border:1px solid}
<canvas id=c>

编辑

根据要求,提供了另一个用于输入Boid人口的代码段:

b.onchange=()=>{eval("L7F7{function B8{t=this,t.a=o8*T,t.x=o8*S,t.y=o8*S}C=this.c,D=C.getContext`2d`,({abs:z,random:o,atan2:k,cos:u,sin:g,PI:P,T=2*P,G={c:_7A[r='filter'](b7b!=t)[i](9)79)),n:_7A[r](b7b!=t)[i](9)7({a,x,y:y-S})),s:_7A[r](b7b!=t)[i](9)7({a,x,y:y+S})),e:_7A[r](b7b!=t)[i](9)7({a,x:x-S,y})),w:_7A[r](b7b!=t)[i](9)7({a,x:x+S,y}))},M=I7[I,I+T,I-T][p]((a,x)7z(x)<z(a)?x:a)}=Math),B.prototype={d8{with(D)save8,translate(x,y),rotate(a),beginPath8,arc(0,0,5,0,T),fillStyle='red',fill8,beginPath8,moveTo(0,0),lineTo(10,0),strokeStyle='blue',stroke8,restore8},n:_7(({c,n,s,e,w}=G),c8.concat(n8,s8,e8,w8)[r](b7(d=b.x-x,f=b.y-y,400>d*d+f*f&&z(z(k(f,d)-a)/P-1)>1/6))),s8{q=(j=t.n8).length,v=t.v8||0,l=t.l8||0,f=t.f8||0,a=t.a=(t.a+v/3+l/10+f/50)%T,t.x=(x+u(a)+S)%S,t.y=(y+g(a)+S)%S},v:_7([d,f]=j[r](b7225>(b.x-x)**2+(b.y-y)**2)[p='reduce'](([d,f],b)7[x+d-b.x,y+f-b.y],[0,0]),d||f?M(k(f,d)-a):0),l:_7j[i](b7M(b.a-a))[p]((a,x)7a+x,0)/q,f:_7([d,f]=j[p](([d,f],b)7[d+b.x,f+b.y],[-x*q,-y*q]),d||f?M(k(f,d)-a):0)},S=C.width=C.height=50*L,A=Array(L).fill().map(_7new B),R=_7{D.clearRect(0,0,S,S),A[i='map'](b79=b).d8),A[i](b79=t=b).s8),F--&&setTimeout(R,10)},R8}".replace(/[789]/g,m=>['=>','()','({a,x,y}'][m-7]))(+b.value)(Infinity);b.remove()}
input{display:block}canvas{border:1px solid}
<input id=b><canvas id=c>


当我运行摘录片段时,辫子似乎没有相互作用
Jo King

@JoKing,现在应该修复它
Patrick Roberts

问题是因为babel缩小器在一个带有参数名称的函数中隐藏了全局变量,并且隐式类型转换为数字不会引发错误,因此该函数只是默默地失败,并且从未检测到任何邻居。
帕特里克·罗伯茨

明天晚上我将尝试进行交互式演示,但今晚我精疲力尽。
帕特里克·罗伯茨

请注意:t.a+v+l/10+f/50如果将其更改为t.a+v/3+l/10+f/50,它会显示一些更有趣的行为,但是当前程序较小,并且仍符合规范。
帕特里克·罗伯茨
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.