Thực hiện thuật toán Boids


18

Giới thiệu

Các Boids Thuật toán là một cuộc biểu tình tương đối đơn giản về hành vi nổi trong một nhóm. Nó có ba quy tắc chính, như được mô tả bởi người tạo ra nó, Craig Reynold:

Mô hình đổ xô cơ bản bao gồm ba hành vi lái đơn giản mô tả cách thức một cá nhân điều khiển cơ động dựa trên các vị trí và vận tốc của các bầy gần đó:

  • Tách biệt : chỉ đạo để tránh đông đúc đàn chiên địa phương.
  • Sắp xếp : chỉ đạo hướng tới tiêu đề trung bình của các đàn chiên địa phương.
  • Sự gắn kết : chỉ đạo để di chuyển đến vị trí trung bình của các đàn chiên địa phương.

Mỗi boid có quyền truy cập trực tiếp vào mô tả hình học của toàn cảnh, nhưng việc đổ xô yêu cầu nó chỉ phản ứng với những người bạn trong một khu phố nhỏ nhất định xung quanh nó. Vùng lân cận được đặc trưng bởi một khoảng cách (được đo từ tâm của boid) và một góc , được đo từ hướng bay của boid. Flockmate bên ngoài khu phố địa phương này được bỏ qua. Vùng lân cận có thể được coi là một mô hình về nhận thức hạn chế (như cá trong nước đục) nhưng có lẽ đúng hơn khi nghĩ về nó như xác định khu vực trong đó các bầy đồng ảnh hưởng đến việc chèo lái.

Tôi không hoàn hảo khi giải thích mọi thứ, vì vậy tôi khuyên bạn nên kiểm tra nguồn . Ông cũng có một số hình ảnh siêu thông tin trên trang web của mình.

Thử thách

Cho số lượng boids (thực thể mô phỏng) và số lượng khung hình, xuất ra hình ảnh động của mô phỏng.

  • Các boid phải được hiển thị dưới dạng một vòng tròn màu đỏ, với một đường bên trong vòng tròn hiển thị tiêu đề của nó, đó là hướng mà boid đang chỉ vào:

Bản vẽ thô của hai "boids", một mặt trái và mặt kia phải.

  • Góc của mỗi boid (như được mô tả bởi Reynold) phải là 300 độ. (không phải 360)
  • Tiêu đề và vị trí bắt đầu của mỗi boid phải là ngẫu nhiên đồng nhất (nhưng được gieo hạt, sao cho đầu ra vẫn là xác định), cũng như vị trí.
  • Nếu bán kính của boid là 1, thì bán kính của vùng lân cận phải là 3.
  • Số lượng boids sẽ ở bất cứ đâu từ 2-20.
  • Số lượng khung hình sẽ ở bất cứ đâu từ 1-5000
  • Hoạt hình nên được phát với tối thiểu 10 mili giây trên mỗi khung hình và tối đa là 1 giây so với số lượng boids. (2 boids = 2 giây mỗi khung hình tối đa, 3 boids = 3 giây mỗi khung hình tối đa, et cetera)
  • Hoạt hình đầu ra phải có ít nhất 5 boid-radii bằng 5 boid-radii, gấp rưỡi số lượng boids. Vì vậy, kích thước tối thiểu cho 2 boid sẽ là 10 boid-radii bằng 10 boid-radii, tối thiểu cho 3 boid sẽ là 15 boid-radii bởi 15 boid-radii, et cetera.
  • Bán kính của mỗi boid phải tối thiểu là 5 pixel và tối đa là 50 pixel.
  • Tốc độ của mỗi boid cần phải được giới hạn để nó không di chuyển hơn 1/5 bán kính của nó trong một khung.
  • Đầu ra cần phải được xác định, sao cho cùng một đầu vào sẽ tạo ra cùng một đầu ra nếu chạy nhiều lần.
  • Nếu một con boid đạt đến một biên giới, nó sẽ quấn lại phía bên kia. Tương tự như vậy, các khu phố xung quanh mỗi boid cũng nên bao quanh các biên giới.

Quy tắc cho thuật toán

Trong trường hợp này, mỗi boid có một khu vực xung quanh nó kéo dài 300 độ, tập trung vào tiêu đề của boid. Bất kỳ những kẻ thù nào khác trong "khu phố" này đều được coi là "hàng xóm" hoặc (sử dụng thuật ngữ của Reynold) "bầy bạn".

  1. Mỗi boid nên điều chỉnh tiêu đề của nó để tránh va chạm và duy trì khoảng cách thoải mái của một bán kính boid với các nước láng giềng. (Đây là khía cạnh "Tách" của thuật toán. Bán kính bo có thể được bỏ qua, nhưng nó phải giống như một dải cao su, chụp lại đúng vị trí.)

  2. Mỗi boid nên điều chỉnh bổ sung tiêu đề của nó để gần với tiêu đề trung bình của các nhóm khác trong khu vực lân cận, miễn là nó không can thiệp vào quy tắc đầu tiên. (Đây là khía cạnh "Sắp xếp" của thuật toán)

  3. Mỗi con nhím nên tự quay về vị trí trung bình của các bầy của nó, miễn là điều này không gây ra va chạm hoặc can thiệp đáng kể vào quy tắc thứ hai.

Trong bài viết của mình về chủ đề này , ông giải thích điều này như sau:

Để xây dựng một đàn mô phỏng, chúng tôi bắt đầu với một mô hình boid hỗ trợ bay hình học. Chúng tôi thêm các hành vi tương ứng với các lực lượng đối lập tránh va chạm và mong muốn gia nhập đàn. Nói ngắn gọn là các quy tắc và theo thứ tự giảm dần quyền ưu tiên, các hành vi dẫn đến đổ xô mô phỏng là:

  • Tránh va chạm: tránh va chạm với những người bạn gần đó
  • Kết hợp vận tốc: cố gắng khớp vận tốc với những người bạn gần đó
  • Flock Centering: cố gắng ở gần những người bạn gần đó

Mô tả chi tiết hơn về phong trào:

  • Việc thực hiện chuẩn của Thuật toán Boids thường thực hiện một phép tính cho từng quy tắc và hợp nhất nó lại với nhau.
  • Đối với quy tắc đầu tiên, boid đi qua danh sách các boong lân cận trong vùng lân cận của nó và nếu khoảng cách giữa nó và hàng xóm nhỏ hơn một giá trị nhất định, một vectơ đẩy boid ra khỏi là hàng xóm được áp dụng cho tiêu đề của boid.
  • Đối với quy tắc thứ hai, boid tính toán tiêu đề trung bình của các nước láng giềng và thêm một phần nhỏ (chúng ta sẽ sử dụng 1/10 trong thử thách này) về sự khác biệt giữa tiêu đề hiện tại và tiêu đề trung bình so với tiêu đề hiện tại.
  • Đối với quy tắc thứ ba và cuối cùng, boid tính trung bình các vị trí của các lân cận, tính toán một vectơ chỉ về vị trí này. Vectơ này được nhân với một số thậm chí nhỏ hơn so với số được sử dụng cho quy tắc 2 (đối với thử thách này, 1/50 sẽ được sử dụng) và được áp dụng cho tiêu đề.
  • Các boid sau đó được di chuyển theo hướng của tiêu đề của nó

Dưới đây là một triển khai mã giả hữu ích của Thuật toán Boids.

Ví dụ đầu vào và đầu ra

Đầu vào:

5, 190 (5 boids, 190 khung hình)

Đầu ra:

Hoạt hình 190 khung hình của Thuật toán Boids với 5 boids.

Tiêu chí chiến thắng

Đây là , vì vậy giải pháp nhỏ nhất trong byte sẽ thắng.


7
"Tất nhiên, có nhiều hơn về thuật toán, vì vậy tôi khuyên bạn nên kiểm tra nguồn." - có phải mọi thứ cần thiết ở đây hay không? Nếu không tôi khuyên bạn nên sửa nó.
Jonathan Allan

1
Vui lòng sử dụng hộp cát trước khi đăng các thách thức, như được tư vấn trên trang hỏi .
flawr

@Jonathan ALLan Vâng, mọi thứ cần thiết đều có ở đây, nhưng những giải thích sâu hơn có thể có ý nghĩa hơn đối với những người dùng khác có sẵn tại nguồn.
iPhoenix

11
Đây là một thử thách thú vị (tôi thấy các hành vi đổ xô rất hấp dẫn) nhưng nó sẽ cần được chỉ định rõ, đặc biệt đối với môn đánh gôn, nếu không, áp lực giảm độ dài của mã sẽ gây ra mọi sai lệch có thể xảy ra so với tinh thần của thử thách được khuyến khích.
trichoplax

Câu trả lời:


7

Xử lý 3.3.6 (Java) ,932 931 940 928 957 917 904 byte

-1 byte từ Jonathan Frech
+11 byte để phù hợp hơn với thông số
-2 byte từ Kevin Cruijssen
-12 byte để thay đổi args thành t ()
+29 byte vì tôi đã làm sai, hãy xem phiên bản được nhận xét bên dưới
-40 byte để sử dụng cho các vòng lặp thay vì các cuộc gọi riêng biệt cho mỗi ma
-13 byte để sử dụng frameRate mặc định, 30

Chà, đó là một khởi đầu, cho một người không chơi 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));}}

Tôi không biết bất kỳ cách hợp lý nào để thực hiện đầu vào trong Xử lý, vì vậy hai biến đầu tiên là đầu vào (và tôi đã không đếm giá trị của chúng (5 byte) đối với số byte). Nếu đây là một vấn đề, tôi có thể thử những thứ khác.

Tôi cũng không biết một cách hay để cho phép dùng thử trực tuyến (dự án Treatment.js không thể xử lý kiểu mã này) mà không tự lưu trữ mọi thứ; và đó là điều tôi không muốn thử. Hãy cho tôi biết nếu có bất cứ điều gì thông minh tôi có thể làm.

Mã được định dạng, có ý kiến

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

Sản lượng mẫu

n = 15, khung = 400:

nhọt

Hoặc, cùng một hình ảnh động, nhưng hiển thị các vùng lân cận của mỗi boid.


1
Không thể 2*PItrở thành TAUđể lưu một byte?
Jonathan Frech

@JonathanFrech Có, nó có thể; Ban đầu tôi có -PI, PI và tôi đã đi theo cách đó, nhưng bị lạc hướng.
Phlarx

Chương trình của tôi (được viết bằng js và html) đã không xuất gif, nhưng nó đã vẽ một hình ảnh và tôi đã sử dụng một chương trình chụp màn hình và chuyển đổi video mà nó đã xuất thành gif. Có một điều tôi đã thông báo, mặc dù. Các boids có đường viền màu xanh lam, không tuân theo thông số kỹ thuật :)
iPhoenix

Chỉ cần một lời nhắc nhở thân thiện khác, câu trả lời này không tuân theo thông số kỹ thuật, vì vậy nó sẽ không nhận được tiền thưởng.
iPhoenix

1
Tôi không biết Xử lý, nhưng tôi nghĩ bạn có thể chơi gôn những điều sau: ,i,đến ,i=0,và sau đó loại bỏ i=0vòng lặp for. (-1 byte); frameCount%f==0đến frameCount%f<1(1 byte); &&đến &cuối cùng nếu 2*d>=p.dist(m)&q.angleBetween(v,q.sub(m,p))<=5*PI/6(-1 byte). Một lần nữa, không chắc chắn nếu những điều này là có thể, nhưng vì Xử lý có vẻ khá giống với Java, tôi nghĩ rằng nó là. Ngoài ra, bạn có thể thử tạo một gif với screentogif.com .
Kevin Cruijssen

4

JavaScript (ES6) + HTML5, 1200 byte

Đây là giải pháp hiện tại của tôi bằng API Canvas. Hàm eval()trả về một hàm bị cong có đầu vào đầu tiên là Boiddân số và thứ hai là số khung hình động. Bạn có thể sử dụng Infinitycho hình ảnh động liên tục.

Các eval(...)là 1187 byte và <canvas id=c>là 13 byte, nâng tổng số 1200. Các CSS là không cần thiết, nhưng để thuận tiện, nó cho phép bạn xem các cạnh của vải.

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>

Biên tập

Theo yêu cầu, một đoạn mã khác có đầu vào cho dân số 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>


Các boids dường như không tương tác khi tôi chạy đoạn trích
Jo King

@JoKing nó nên được sửa ngay bây giờ
Patrick Roberts

Vấn đề là do công cụ khai thác babel che khuất một biến toàn cục trong một hàm với tên tham số và kiểu chữ ngầm cho một số không gây ra lỗi, do đó, hàm chỉ thất bại trong âm thầm và không bao giờ phát hiện ra bất kỳ hàng xóm nào.
Patrick Roberts

Tôi sẽ cố gắng thực hiện một bản demo tương tác vào tối mai nhưng tôi đã hết hơi cho tối nay.
Patrick Roberts

Chỉ cần một lưu ý: nơi nó đọc t.a+v+l/10+f/50, nếu bạn thay đổi điều đó thành t.a+v/3+l/10+f/50, nó sẽ tạo ra một số hành vi thú vị hơn, nhưng chương trình hiện tại nhỏ hơn và vẫn để thông số kỹ thuật.
Patrick Roberts
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.