Implementieren Sie den Boids-Algorithmus

18

Einführung

Der Boids-Algorithmus ist eine relativ einfache Demonstration des aufkommenden Verhaltens in einer Gruppe. Es hat drei Hauptregeln, wie von seinem Schöpfer Craig Reynolds beschrieben:

Das grundlegende Flockmodell besteht aus drei einfachen Lenkverhalten, die beschreiben, wie ein einzelner Boid anhand der Positionen und Geschwindigkeiten seiner in der Nähe befindlichen Flockkameraden manövriert:

  • Trennung : Lenken, um ein Überfüllen der örtlichen Herdenkameraden zu vermeiden.
  • Ausrichtung : Steuere in Richtung des durchschnittlichen Kurses der lokalen Herdenkameraden.
  • Kohäsion : Steuern Sie, um sich der durchschnittlichen Position lokaler Herdenkameraden zu nähern.

Jedes Boid hat direkten Zugriff auf die geometrische Beschreibung der gesamten Szene. Zum Beflocken muss es jedoch nur auf Flockkameraden in einer bestimmten kleinen Nachbarschaft um sich herum reagieren. Die Nachbarschaft ist durch einen Abstand (gemessen von der Mitte des Boids) und einen Winkel , gemessen von der Flugrichtung des Boids, gekennzeichnet. Flockmates außerhalb dieses Stadtviertels werden ignoriert. Die Nachbarschaft könnte als Modell für eine eingeschränkte Wahrnehmung angesehen werden (wie bei Fischen in trübem Wasser), aber es ist wahrscheinlich richtiger, sie als die Region zu betrachten, in der Herdenkameraden die Steuerung eines Boids beeinflussen.

Ich bin nicht perfekt, wenn ich Dinge erkläre, daher empfehle ich dringend, die Quelle zu überprüfen . Er hat auch einige super informative Bilder auf seiner Seite.

Herausforderung

Ausgehend von der Anzahl der Boids (simulierten Entities) und der Anzahl der Frames wird eine Animation der Simulation ausgegeben.

  • Die Boids sollten als roter Kreis gerendert werden, wobei eine Linie innerhalb des Kreises die Überschrift anzeigt, in die der Boid zeigt:

Rohzeichnung von zwei "Boids", von denen einer nach links und der andere nach rechts zeigt.

  • Der Winkel jedes Boids (wie von Reynolds beschrieben) sollte volle 300 Grad betragen. (nicht 360)
  • Die Startüberschrift und die Position jedes Boids sollten gleichmäßig zufällig sein (aber gesät sein, so dass die Ausgabe immer noch bestimmt ist) sowie die Position.
  • Wenn der Radius des Boids 1 ist, sollte der Radius der Nachbarschaft 3 sein.
  • Die Anzahl der Boids liegt zwischen 2 und 20.
  • Die Anzahl der Frames liegt zwischen 1-5000
  • Die Animation sollte mit mindestens 10 Millisekunden pro Bild und maximal 1 Sekunde mal der Anzahl der Boids abgespielt werden. (2 Boids = max. 2 Sekunden pro Frame, 3 Boids = max. 3 Sekunden pro Frame usw.)
  • Die Ausgabeanimation sollte mindestens 5 Boid-Radien mal 5 Boid-Radien mal die Hälfte der Boid-Anzahl betragen. Die minimale Größe für 2 Boids wäre also 10 Boid-Radien mal 10 Boid-Radien, das Minimum für 3 Boids wären 15 Boid-Radien mal 15 Boid-Radien usw.
  • Der Radius jedes Boids muss mindestens 5 Pixel und höchstens 50 Pixel betragen.
  • Die Geschwindigkeit jedes Boids muss begrenzt werden, damit er sich nicht mehr als 1/5 seines Radius in einem Frame bewegt.
  • Die Ausgabe muss bestimmt werden, damit dieselbe Eingabe dieselbe Ausgabe erzeugt, wenn sie mehrmals ausgeführt wird.
  • Wenn ein Boid eine Grenze erreicht, sollte es auf die andere Seite gewickelt werden. Ebenso sollte die Nachbarschaft um jedes Boid auch die Grenzen umschließen.

Regeln für den Algorithmus

In diesem Fall hat jedes Boid einen Sektor um sich herum, der sich über 300 Grad erstreckt und auf der Überschrift des Boids zentriert ist. Alle anderen Boids in dieser "Nachbarschaft" werden als "Nachbarn" oder (um Reynolds 'Begriff zu verwenden) "Herdenkameraden" angesehen.

  1. Jedes Boid sollte seinen Steuerkurs anpassen, um Kollisionen zu vermeiden und einen komfortablen Abstand von einem Boid-Radius zu seinen Nachbarn einzuhalten. (Dies ist der "Separations" -Aspekt des Algorithmus. Der eine Boid-Radius kann umgangen werden, sollte aber wie ein Gummiband aussehen und wieder einrasten.)

  2. Jedes Boid sollte zusätzlich seine Richtung so anpassen, dass es näher an der durchschnittlichen Richtung der anderen Boids in seiner Nachbarschaft liegt, sofern dies nicht die erste Regel stört. (Dies ist der "Ausrichtungs" -Aspekt des Algorithmus.)

  3. Jedes Boid sollte sich in Richtung der durchschnittlichen Position seiner Herdenmitglieder drehen, solange dies keine Kollision verursacht oder die zweite Regel erheblich beeinträchtigt.

In seiner Arbeit zu diesem Thema erklärt er dies wie folgt:

Um eine simulierte Herde zu bauen, beginnen wir mit einem Boid-Modell, das den geometrischen Flug unterstützt. Wir fügen Verhaltensweisen hinzu, die den entgegengesetzten Kräften der Kollisionsvermeidung und dem Drang, sich der Herde anzuschließen, entsprechen. Kurz als Regeln und in abnehmender Reihenfolge angegeben, sind die Verhaltensweisen, die zu einer simulierten Beflockung führen:

  • Kollisionsvermeidung: Vermeiden Sie Kollisionen mit in der Nähe befindlichen Herdenmitgliedern
  • Velocity Matching: Versucht, die Velocity mit in der Nähe befindlichen Flockmates abzugleichen
  • Flockenzentrierung: Versuchen Sie, in der Nähe von Flockenkameraden zu bleiben

Detailliertere Beschreibung der Bewegung:

  • Die Standardimplementierung des Boids-Algorithmus führt normalerweise eine Berechnung für jede der Regeln durch und führt diese zusammen.
  • Für die erste Regel durchläuft der Boid die Liste der benachbarten Boids in seiner Nachbarschaft. Wenn der Abstand zwischen sich und dem Nachbarn geringer als ein bestimmter Wert ist, wird ein Vektor, der den Boid von seinem Nachbarn wegdrückt, auf die Boid-Überschrift angewendet.
  • Für die zweite Regel berechnet das Boid die durchschnittliche Überschrift seiner Nachbarn und addiert einen kleinen Teil (wir verwenden 1/10 in dieser Herausforderung) der Differenz zwischen der aktuellen Überschrift und der durchschnittlichen Überschrift zu der aktuellen Überschrift.
  • Für die dritte und letzte Regel mittelt das Boid die Positionen seiner Nachbarn und berechnet einen Vektor, der auf diese Position zeigt. Dieser Vektor wird mit einer noch kleineren Zahl multipliziert als für Regel 2 verwendet (für diese Herausforderung wird 1/50 verwendet) und auf die Überschrift angewendet.
  • Das Boid wird dann in Richtung seiner Überschrift bewegt

Hier ist eine hilfreiche Pseudocode-Implementierung des Boids-Algorithmus.

Beispiel für Ein- und Ausgabe

Eingang:

5, 190 (5 Boids, 190 Frames)

Ausgabe:

190-Frame-Animation des Boids-Algorithmus mit 5 Boids.

Gewinnkriterium

Das ist , also gewinnt die kleinste Lösung in Bytes.

iPhoenix
quelle
7
"Da der Algorithmus natürlich noch mehr beinhaltet, empfehle ich dringend, die Quelle zu überprüfen." - ist hier alles nötig oder nicht? Wenn nicht, würde ich empfehlen, das zu beheben.
Jonathan Allan
1
Bitte benutze die Sandbox, bevor du Herausforderungen postest, wie auf der Ask-Seite empfohlen .
Fehler
@JonathanAllan Ja, alles Notwendige ist hier, aber ausführlichere Erklärungen, die für andere Benutzer möglicherweise sinnvoller sind, finden Sie an der Quelle.
iPhoenix
11
Dies ist eine interessante Herausforderung (ich finde das Flockverhalten faszinierend), aber sie muss genau spezifiziert werden, insbesondere für einen Code-Golf, da sonst der Druck, die Länge des Codes zu verringern, jede mögliche Abweichung vom Grundgedanken der Herausforderung zur Folge hat incentiviert werden.
Trichoplax

Antworten:

7

Verarbeitung 3.3.6 (Java) ,932 931 940 928 957 917 904 Bytes

-1 Byte von Jonathan Frech
+11 Byte, um besser mit der Spezifikation übereinzustimmen
-2 Byte von Kevin Cruijssen
-12 Byte für das Ändern von args in t ()
+29 Byte, weil ich Geisterbilder falsch
gemacht habe Schleifen anstelle von separaten Aufrufen für jeden Ghost
-13 Bytes zur Verwendung der Standard-FrameRate, 30

Nun, es ist ein Anfang für jemanden, der kein Java-Golf spielt. :)

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

Ich kenne keinen vernünftigen Weg, um Eingaben in der Verarbeitung vorzunehmen, daher sind die ersten beiden Variablen die Eingaben (und ich habe ihre Werte (5 Bytes) nicht in die Byteanzahl eingerechnet). Wenn dies ein Problem ist, kann ich andere Dinge ausprobieren.

Ich kenne auch keine gute Möglichkeit, es online zu versuchen (das Processing.js-Projekt kann diesen Codestil nicht verarbeiten), ohne die Dinge selbst zu hosten. und das ist etwas, was ich nicht unbedingt versuchen möchte. Lassen Sie mich wissen, ob ich etwas Kluges tun kann.

Formatierter Code mit Kommentaren

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

Beispielausgabe

n = 15, Frames = 400:

boids

Oder dieselbe Animation, jedoch mit der Nachbarschaft der einzelnen Boids.

Phlarx
quelle
1
Kann ein Byte 2*PInicht TAUgespeichert werden?
Jonathan Frech
@ JonathanFrech Ja, es kann; Ich hatte ursprünglich -PI, PI und ich ging diesen Weg, wurde aber abgelenkt.
Phlarx
Mein Programm (das in js und html geschrieben wurde) exportierte kein GIF, aber es zeichnete ein Bild und ich verwendete ein Bildschirmaufnahmeprogramm und konvertierte das exportierte Video in ein GIF. Eines ist mir allerdings aufgefallen. Die boids haben einen blauen Umriss, der nicht der Spezifikation folgt :)
iPhoenix
Nur eine weitere freundliche Erinnerung, diese Antwort entspricht nicht der Spezifikation und wird nicht belohnt.
iPhoenix
1
Ich kenne die Verarbeitung nicht, aber ich denke, Sie können die folgenden Dinge Golf spielen: ,i,Zu ,i=0,und dann das i=0Innere der for-Schleife entfernen . (-1 Byte); frameCount%f==0bis frameCount%f<1(1 Byte); &&bis &im letzten if 2*d>=p.dist(m)&q.angleBetween(v,q.sub(m,p))<=5*PI/6(-1 Byte). Auch hier bin ich mir nicht sicher, ob dies möglich ist, aber da die Verarbeitung Java ziemlich ähnlich zu sein scheint, denke ich, dass dies der Fall ist. Sie können auch versuchen, ein GIF mit screentogif.com zu erstellen .
Kevin Cruijssen
4

JavaScript (ES6) + HTML5, 1200 Byte

Hier ist meine aktuelle Lösung mit der Canvas-API. Das eval()Ergebnis ist eine Curry-Funktion, deren erste Eingabe die BoidGrundgesamtheit und die zweite die Anzahl der Animationsframes ist. Sie können Infinityfür die kontinuierliche Animation verwenden.

Das eval(...)ist 1187 Byte und <canvas id=c>13 Byte, was insgesamt 1200 Byte ergibt. Das CSS ist nicht erforderlich, aber der Einfachheit halber können Sie die Ränder der Zeichenfläche sehen.

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>

Bearbeiten

Auf Wunsch ein weiteres Snippet mit einer Eingabe für die Boid-Population:

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>

Patrick Roberts
quelle
Die Boids scheinen nicht zu interagieren, wenn ich das Snippet starte
Jo King
@JoKing es sollte jetzt behoben sein
Patrick Roberts
Das Problem war, dass der Babel Minifier eine globale Variable in einer Funktion mit einem Parameternamen beschattete und die implizite Typumwandlung in eine Zahl keinen Fehler auslöste, sodass die Funktion im Hintergrund fehlschlug und keine Nachbarn erkannte.
Patrick Roberts
Ich werde versuchen, morgen Abend eine interaktive Demo zu machen, aber heute Abend ist mir der Dampf ausgegangen.
Patrick Roberts
Nur eine Anmerkung: t.a+v+l/10+f/50Wenn Sie den Wert in ändern, t.a+v/3+l/10+f/50wird ein etwas interessanteres Verhalten erzeugt, aber das aktuelle Programm ist kleiner und entspricht immer noch den Spezifikationen.
Patrick Roberts