ボイドアルゴリズムを実装する


18

前書き

Boidsアルゴリズムは、グループ内の緊急行動の比較的簡単なデモです。作成者のクレイグ・レイノルズが説明するように、3つの主要なルールがあります。

基本的な植毛モデルは、個々のボイドが近くの植生仲間の位置と速度に基づいてどのように操作するかを記述する3つの単純なステアリング動作で構成されます。

  • 分離:地元の群れが混雑しないように操縦します。
  • アライメント:地元の群れの平均的な方向に向かってください。
  • 結束:地元の群れ仲間の平均的な位置に向かって移動するように操縦します。

各boidは、シーン全体の幾何学的記述に直接アクセスできますが、群れを作るには、周囲の特定の小さな近傍内の群れだけに反応する必要があります。近傍は、ボイドの中心から測定された距離とボイドの飛行方向から測定された角度によって特徴付けられます。この近隣地域の外の群れは無視されます。近隣は、限られた知覚のモデルと考えることができます(濁った水中の魚など)が、群れ仲間がボイドのステアリングに影響を与える領域を定義するものと考える方がおそらく正しいでしょう。

私は物事を説明するときに完璧ではないので、ソースをチェックアウトすることを強くお勧めします。また、彼のサイトには非常に有益な写真がいくつかあります。

チャレンジ

ボイド(シミュレートされたエンティティ)の数とフレームの数を指定して、シミュレーションのアニメーションを出力します。

  • ボイドは赤い円としてレンダリングされる必要があり、円の内側の線はヘディングを示します。これは、ボイドが指している方向です。

1つは左向き、もう1つは右向きの2つの「ボイド」の粗図。

  • 各ボイドの角度(レイノルズによる説明)は、完全な300度である必要があります。(360ではない)
  • 各boidの開始見出しと位置は、位置と同様に、ランダムに(ただし、シードされているため、出力はまだ確定されている)必要があります。
  • ボイドの半径が1の場合、近傍の半径は3でなければなりません。
  • boidの数は2〜20の範囲です。
  • フレームの数は1〜5000のいずれかになります
  • アニメーションは、フレームごとに最小10ミリ秒、最大1秒でボイドの数で再生する必要があります。(2ボイド=フレームあたり最大2秒、3ボイド=フレームあたり最大3秒など)
  • 出力アニメーションは、少なくとも5ボイド半径x 5ボイド半径、半分のボイド数の倍数である必要があります。したがって、2ボイドの最小サイズは10ボイド半径x 10ボイド半径であり、3ボイドの最小サイズは15ボイド半径x 15ボイド半径などです。
  • 各boidの半径は、最小で5ピクセル、最大で50ピクセルでなければなりません。
  • 各boidの速度は、1フレームで半径の1/5を超えて移動しないように制限する必要があります。
  • 出力は確定的である必要があるため、同じ入力が複数回実行された場合に同じ出力を生成します。
  • ボイドが境界に達すると、反対側に戻ります。同様に、各boidの周囲も境界を囲む必要があります。

アルゴリズムの規則

この場合、各Boidの周囲には、Boidの見出しを中心とした300度に及ぶセクターがあります。この「近傍」の他のboidは、「近傍」、または(レイノルズの用語を使用すると)「群れ」と見なされます。

  1. 各ボイドは、衝突を回避し、1つのボイド半径と隣接するボイドの快適な距離を維持するために、見出しを調整する必要があります。(これはアルゴリズムの「分離」の側面です。1つの半径をバイパスすることもできますが、ゴムバンドのように、元の位置に戻ります。)

  2. 各ボイドは、最初のルールに干渉しない限り、さらに他のボイドの平均ヘディングに近づくように、ヘディングを調整する必要があります。(これはアルゴリズムの「アライメント」の側面です)

  3. 衝突が発生しないか、2番目のルールを大幅に妨害しない限り、各ボイドは群れの平均位置に向かって回転します。

この件に関する彼の論文で、彼はこれを次のように説明しています。

シミュレートされた群れを構築するには、幾何学的飛行をサポートするボイドモデルから始めます。衝突回避の反対の力と群れに参加する衝動に対応する行動を追加します。ルールとして簡単に述べますが、優先順位の高い順に、群れのシミュレーションにつながる動作は次のとおりです。

  • 衝突回避:近くの群れとの衝突を回避
  • 速度マッチング:速度を近くの群れと一致させようとします
  • 群れの中心化:近くの群れの近くにとどまることを試みる

動きのより詳細な説明:

  • Boidsアルゴリズムの標準実装では、通常、各ルールの計算を行い、それを一緒にマージします。
  • 最初のルールでは、boidは近傍内の隣接boidのリストを通過し、それ自体と近傍との距離が特定の値よりも小さい場合、boidを隣接から遠ざけるベクトルがboidの見出しに適用されます。
  • 2番目のルールでは、ボイドはその隣の平均ヘディングを計算し、現在のヘディングと平均ヘディングの差のわずかな部分(このチャレンジでは1/10を使用します)を現在のヘディングに追加します。
  • 3番目の最後のルールでは、boidはその近傍の位置を平均し、この位置を指すベクトルを計算します。このベクトルは、ルール2に使用されたものよりもさらに小さい数で乗算され(このチャレンジでは、1/50が使用されます)、見出しに適用されます。
  • 次に、ボイドは見出しの方向に移動します

ここ Boidsアルゴリズムの有用な擬似コードの実装があります。

入力と出力の例

入力:

5、190(5ボイド、190フレーム)

出力:

5ボイドのボイドアルゴリズムの190フレームのアニメーション。

勝利基準

これはであるため、バイト単位の最小のソリューションが優先されます。


7
「もちろん、アルゴリズムには他にもあるので、ソースをチェックアウトすることを強くお勧めします。」-ここですべてが必要ですか?そうでない場合は、修正することをお勧めします。
ジョナサンアラン

1
質問ページでアドバイスされているように、チャレンジを投稿する前にサンドボックスを使用しください
flawr

@JonathanAllanええ、必要なものはすべてここにありますが、他のユーザーにとってより理にかなっているかもしれないより詳細な説明がソースで利用可能です。
iPhoenix

11
これは興味深い課題です(群がる行動が魅力的だと思います)が、特にコードゴルフの場合は、明確に指定する必要があります。インセンティブが与えられます。
-trichoplax

回答:


7

処理3.3.6(Java)932 931 940 928 957 917 904バイト

-1からバイトジョナサンFRECH
11が良く一致するバイトスペックを
-2からバイトケビンCruijssen
-12(トンへの引数を変更するためのバイト)
私はゴースト間違ってやっていたので、29バイトを、下記のコメントバージョンを確認
するために使用するために-40バイトをゴーストごとに個別の呼び出しの代わりにループ
します。デフォルトのframeRate、30を使用する場合は-13バイト

まあ、それはJava-golfを知らない人にとっては始まりです。:)

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で入力を行う合理的な方法がわからないので、最初の2つの変数は入力です(そして、その値(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:

ボイド

または、同じアニメーションですが、各ボイドの近傍を示しています。


1
バイトを保存する2*PIようになることはできませんTAUか?
ジョナサンフレッチ

@JonathanFrechはい、できます。私はもともと-PI、PIを持っていて、そのようにしていましたが、脇道になりました。
プラックス

私のプログラム(jsとhtmlで作成された)はgifをエクスポートしませんでしたが、画像を描画し、スクリーンキャプチャプログラムを使用して、エクスポートしたビデオをgifに変換しました。ただし、気づいたことが1つあります。ボイドの輪郭は青いのですが、仕様に準拠していません:)
iPhoenix

別のわかりやすいリマインダー、この答えは仕様に従っていないため、賞金は得られません。
iPhoenix

1
私が処理を知らないが、私はあなたがゴルフ、以下のことができると思う:,i,にし,i=0,、その後、削除i=0forループ内で。(-1バイト); frameCount%f==0to frameCount%f<1(1バイト); もし決勝で(-1バイト)。繰り返しますが、これらが可能かどうかはわかりませんが、ProcessingはJavaにかなり似ているように見えるので、そうだと思います。また、screentogif.comで gifを作成することもできます。&&&2*d>=p.dist(m)&q.angleBetween(v,q.sub(m,p))<=5*PI/6
ケビンクルーッセン

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>

編集

要求に応じて、ボイドの母集団の入力を含む別のスニペット:

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>


スニペットを実行するとき、ボイドは相互作用しないようです
ジョーキング

@JoKing今すぐ修正する必要があります
パトリックロバーツ

問題は、バベルミニファイアーがパラメーター名を持つ1つの関数のグローバル変数をシャドウし、数値への暗黙の型キャストがエラーをスローしなかったため、関数が黙って失敗し、近隣を検出しなかったためです。
パトリックロバーツ

明日の夜、インタラクティブなデモを作成しようと思いますが、今夜は気が尽きました。
パトリックロバーツ

注:を読み取りt.a+v+l/10+f/50、に変更するとt.a+v/3+l/10+f/50、より興味深い動作が生成されますが、現在のプログラムは小さく、まだ仕様どおりです。
パトリックロバーツ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.