【Processing】公式サイトにあったFlockingを理解する過程2

Webデザイン
Photo by Shamin Haky on Unsplash

コードを理解する過程を書いていきます。

この記事はこちら↓の記事の続きです。

細かい動作は他のファイルに書かれていると予想する

メインのファイルでやっている動作は次の2つだとわかりました。

  1. 初期のボイド(三角形)のセットを加える
  2. クリックしたら、新しいボイドを加える

でも、この2つの機能をメインのファイル「flocking.pde」だけに書いてあるようには見えません。

そう思う理由の1つはコードの量が少なすぎるためです。

メインのファイルをもう一度見てください。

Flock flock;

void setup() {
  size(640, 360);
  // fullScreen();
  frameRate(120);
  flock = new Flock();
  // Add an initial set of boids into the system
  for (int i = 0; i < 150; i++) {
    flock.addBoid(new Boid(width/2,height/2));
  }
}

void draw() {
  background(50);
  flock.run();
}

// Add a new boid into the System
void mousePressed() {
  flock.addBoid(new Boid(mouseX,mouseY));
}

コードが明らかに短いですよね。

ということは、他のファイルに先ほどの2つの動作を書いていると予想できます。

他の2ファイルに書かれていることを予想する

メインのファイル以外に細かい処理が書かれていることがわかりました。

メインファイル以外に、2つのファイルがあります。

「boid.pde」と「flock.pde」というファイルです。

これら2つを細かく見ていく前に、それぞれ何が書かれているか予想してみます。

予想の手がかりはいくつかあります。

しかし、じっくり見ないとわからないことは手がかりにしないようにします。

ぱっと見てわかるようなことだけを手がかりに予想できるように意識します。

ファイル名から予想する

ファイル名を見れば何の処理が書かれているかわかることもあります。

今回は、英語なので訳してみましょう。

boid = 人工生命体

flock = 群れ

このことから、それぞれ次のようなことが書かれていると予想できます。

boid.pde → ボイド1つ1つの情報や動作

flock → ボイドが集まってできた群れとしての情報や動作

予想なので、あっているかどうかはわかりません。

ただ、全く予想しないよりは、コードの意味が頭に入ってきやすくなると思います。

コメントから予想する

それぞれのファイルには複数のコメントが書いてありました。

いくつか訳したものを取り上げてみます。

boid.pde

  • Boidクラス
  • 最大速度
  • 次の3つのルールに基づいて、毎回新しい加速度を蓄積する
  • 衝突回避
  • 整列
  • 接近
  • 力のベクトルを加速度に加える
  • 速度の方向に回転した三角形を描く

やっぱり「boid.pde」では、1つ1つのボイドの動作を書いているようです。

初めの方に調べた3つのルール「衝突回避」「整列」「接近」の機能も、このファイルで設定していると予想できます。

flock.pde

このファイルには次の3つコメントがありませんでした。

  • 群れ(Boidオブジェクトのリスト)
  • 全ボイドのためのArrayList
  • ボイドのリスト全体を各ボイドに個別に渡す

コメントだけでは何をしているのかイマイチわかりませんでした。

ただやっぱり群れに関することが記述してあるようです。

主従関係から予想する

ここでいう主従関係とは次のようなことを意味しています。

主従関係の例

ファイルAとファイルBがある。

ファイルBの中で定義されているクラスが、ファイルAでインスタンス化されている。

この場合、

ファイルA → 従

ファイルB → 主


このようにどちらのファイルで、どのクラスがインスタンス化されているのかを確認します。

すると、「boid.pde」の中で定義された、Boidクラスが「flock.pde」の中でインスタンス化されていました。

このことから、次のことがわかります。

flock.pde → 主

boid.pde → 従

また、主従関係がわかったので、次のような予想ができます。

「flock.pde」に書かれているを実現するための処理が、「boid.pde」に書かれている

必ずしもそうでないですが、細かい部分を理解するときの手がかりになってくれると思います。

細かい部分を理解していく

ここまで、「大体書かれている処理はこんな感じのことかな?」という予想をしてきました。

ここからは、ついに1行1行コードを見ていきます。

1行1行コードを見て、意味がわからない部分は、ネットで調べます。

理解していく順番

どのコードから理解を始めるかは、非常に重要だと思います。

適当な順番でコードの理解をしようとすると、コード同士の関わり合いがわからなくなるかもしれません。

では、どのような順番で理解していくかというと、僕は次のような順番で理解していくようにしています。

主従関係の 主 →  の順番

先ほど、「flock.pde」が主で、「boid.pde」が従と書きました。

そのため、まずは「flock.pde」のコードを1行1行見ていきます。

その中で、の方のファイルで定義されているものが使われている行が出てきたら、その度に従の方のファイルを見ます。

今回の場合は、「flock.pde」のファイルを見て、その定義がどのようなものなのかを確認します。

flock.pdeを理解する

「flock.pde」はこちら↓です。

// The Flock (a list of Boid objects)

class Flock {
  ArrayList<Boid> boids; // An ArrayList for all the boids

  Flock() {
    boids = new ArrayList<Boid>(); // Initialize the ArrayList
  }

  void run() {
    for (Boid b : boids) {
      b.run(boids);  // Passing the entire list of boids to each boid individually
    }
  }

  void addBoid(Boid b) {
    boids.add(b);
  }

}

↓まず、「Flock」というクラスを定義していますね。

class Flock {

↓その定義はどのようになっているかを見てみます。

ArrayList<Boid> boids; // An ArrayList for all the boids

↑Boid型の値を要素に持つ「boids」というArrayListを宣言しています。

↓次

Flock() {
  boids = new ArrayList<Boid>(); // Initialize the ArrayList
}

↑Flockクラスのコンストラクターを定義しています。先ほどのboidsというArryListにBoidクラスのインスタンスを入れています。ここで、ファイル「boid.pde」を見て、Boidクラスがどのように定義されているのかを見てきます。

Boidクラスの定義を見ていきます。

↓ただその前に変数の宣言をしていて、重要そうなので見ておきます。

PVector position;
PVector velocity;
PVector acceleration;
float r;
float maxforce;    // Maximum steering force
float maxspeed;    // Maximum speed

↑ボイドの位置や速度、加速度などの変数を宣言しているようです。

「PVector」という型に聞き覚えがなかったので、「Processing PVector」で調べます。

するとどうやら、PVectorはベクトルを扱うためのクラスらしいです。

特に、位置と速度と加速度を表すときによく使われるみたいです。

ベクトルを作るときは、「PVector 変数名 = new Pvector(第1成分, 第2成分, 第3成分)」のように書きます。

後からベクトルの成分を使うときは「変数名.x」で第1成分を取得できます。

第2成分は「変数名.y」で取得でき、同じように第3成分は「変数名.z」と書けば取得できます。

PVectorについては少し理解できたので、次に進みます。

↓変数の下には、Boidクラスのコンストラクターがありました。

Boid(float x, float y) {
    acceleration = new PVector(0, 0);

    // This is a new PVector method not yet implemented in JS
    // velocity = PVector.random2D();

    // Leaving the code temporarily this way so that this example runs in JS
    float angle = random(TWO_PI);
    velocity = new PVector(cos(angle), sin(angle));

    position = new PVector(x, y);
    r = 2.0;
    maxspeed = 2;
    maxforce = 0.03;
  }

↑コンストラクターの引数に「x」と「y」が与えられることを確認しておきます。

コンストラクターの中身を見ていきます。

↓まずは1行目を見てみます。

acceleration = new PVector(0, 0);

この行では加速度を初期化するようです。

先ほど調べたPVectorクラスが出てきました。

このクラスを使うと値をベクトルみたいに扱えるんでした。

「new PVector(0, 0)」となっていることから、この行では(0,0)という2次元ベクトルを作っているとイメージすれば良さそうです。

↓コンストラクターの残りの部分も見てみます。

float angle = random(TWO_PI);
velocity = new PVector(cos(angle), sin(angle));
position = new PVector(x, y);
r = 2.0;
maxspeed = 2;
maxforce = 0.03;

色々な変数を初期化している感じです。

PVectorクラスを使っていることから、角度と位置はベクトルで表そうとしていることがわかります。

位置に関してはPVectorのコンストラクターの引数としてxとyを渡しています。

この「x」と「y」は、確かBoidクラスのコンストラクターの引数でしたよね。

「r」「maxspeed」「maxforce」に関してはまだどういう変数なのかよくわかりません。

ただ、まとめて言えることは、新しく生成されるボイド(三角形)のデータを設定するためコードだということです。

Boidクラスのコンストラクターを見終わったので、見る範囲をBoidクラスを定義している部分に戻します。

class Boid {

  PVector position;
  PVector velocity;
  PVector acceleration;
  float r;
  float maxforce;    // Maximum steering force
  float maxspeed;    // Maximum speed

  Boid(float x, float y) {
    acceleration = new PVector(0, 0);

    // This is a new PVector method not yet implemented in JS
    // velocity = PVector.random2D();

    // Leaving the code temporarily this way so that this example runs in JS
    float angle = random(TWO_PI);
    velocity = new PVector(cos(angle), sin(angle));

    position = new PVector(x, y);
    r = 2.0;
    maxspeed = 2;
    maxforce = 0.03;
  }

  void run(ArrayList<Boid> boids) {
    flock(boids);
    update();
    borders();
    render();
  }

↑次は、コンストラクタの定義の下にある「run」というメソッドを見ていきます。

void run(ArrayList<Boid> boids) {
  flock(boids);
  update();
  borders();
  render();
}

↑引数にBoid型の値を要素に持つArrayListを受け取るようです。

  • ちなみにBoid型の値を要素に持つArrayListといえば、「flock.pde」 の方で宣言していました。

このメソッドの中では、次の4つのメソッドを呼び出しています。

  • flockメソッド
  • updateメソッド
  • bordersメソッド
  • renderメソッド

どれも見覚えがないメソッドです。

このファイルのどこかに宣言しているはずなので、探してみます。

すると、4つメソッド全ての宣言を発見できました。

その宣言を見てみます。

↓まずは、flockの宣言です。

// We accumulate a new acceleration each time based on three rules
void flock(ArrayList<Boid> boids) {
  PVector sep = separate(boids);   // Separation
  PVector ali = align(boids);      // Alignment
  PVector coh = cohesion(boids);   // Cohesion
  // Arbitrarily weight these forces
  sep.mult(1.5);
  ali.mult(1.0);
  coh.mult(1.0);
  // Add the force vectors to acceleration
  applyForce(sep);
  applyForce(ali);
  applyForce(coh);
}

1つ目のコメントの意味は「次の3つのルールに基づいて、毎回新しい加速度を蓄積していきます」です。

この部分で「衝突回避」「整列」「接近」のルールを決めている部分ということですね。

このメソッドが受け取る引数も確認しておきます。

引数はBoid型の値を要素に持つArrayListです。

↓初めの3行に注目してみます。

PVector sep = separate(boids);   // Separation(衝突回避)
PVector ali = align(boids);      // Alignment(整列)
PVector coh = cohesion(boids);   // Cohesion(接近)

↑これらが先ほどの3つのルールだと思います。

PVector型の変数にそれぞれ違う方法で値を入れている部分が気になります。

それぞれ次のようになっています。

sep → separateメソッド

ali → alignメソッド

cohesion → cohesionメソッド

この3つのメソッドも見覚えがありません。

同じファイル内に定義しているか探してみます。

探した結果、3つとも同じファイル内に宣言されていることがわかりました。

↓まずは、separateメソッドです。

// Separation
// Method checks for nearby boids and steers away
PVector separate (ArrayList<Boid> boids) {
  float desiredseparation = 25.0f;
  PVector steer = new PVector(0, 0, 0);
  int count = 0;
  // For every boid in the system, check if it's too close
  for (Boid other : boids) {
    float d = PVector.dist(position, other.position);
    // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
    if ((d > 0) && (d < desiredseparation)) {
      // Calculate vector pointing away from neighbor
      PVector diff = PVector.sub(position, other.position);
      diff.normalize();
      diff.div(d);        // Weight by distance
      steer.add(diff);
      count++;            // Keep track of how many
    }
  }
  // Average -- divide by how many
  if (count > 0) {
    steer.div((float)count);
  }

  // As long as the vector is greater than 0
  if (steer.mag() > 0) {
    // First two lines of code below could be condensed with new PVector setMag() method
    // Not using this method until Processing.js catches up
    // steer.setMag(maxspeed);

    // Implement Reynolds: Steering = Desired - Velocity
    steer.normalize();
    steer.mult(maxspeed);
    steer.sub(velocity);
    steer.limit(maxforce);
  }
  return steer;
}

Boid型のArrayListを引数として受けって、PVector型の値を戻り値として返すということを覚えておきます。

そして、コメント2つ目のコメントには次のように書かれています。

メソッドは、近くのボイドをチェックし、それを避けるように操縦します。

このメソッドが呼ばれたときに実行されるがある程度わかりました。

このメソッドは、ボイドを操縦してくれるメソッドということですね。

操縦するというよりも、操縦するために必要な値が入ったベクトルを返してくれると言った方がいいかもしれません。

そして、戻り値のそのベクトルは何らかの過程を経て、ボイドの向きや速度を変えてくれるのだと思います。

しかし、操縦する仕組みを理解しようとすると、時間がかかり過ぎてしまいます。

そのため、ぶつからないようにしてくれるメソッドという理解にとどめておくことにします。

↓では、flockメソッドを定義している部分に戻ります。

// We accumulate a new acceleration each time based on three rules
void flock(ArrayList<Boid> boids) {
  PVector sep = separate(boids);   // Separation
  PVector ali = align(boids);      // Alignment
  PVector coh = cohesion(boids);   // Cohesion
  // Arbitrarily weight these forces
  sep.mult(1.5);
  ali.mult(1.0);
  coh.mult(1.0);
  // Add the force vectors to acceleration
  applyForce(sep);
  applyForce(ali);
  applyForce(coh);
}

↑今見ていたのは色が濃くなっている行にあるseparateメソッドでした。

↓次は、その下にあるalignメソッドを見てみます。

// Alignment
// For every nearby boid in the system, calculate the average velocity
PVector align (ArrayList<Boid> boids) {
  float neighbordist = 50;
  PVector sum = new PVector(0, 0);
  int count = 0;
  for (Boid other : boids) {
    float d = PVector.dist(position, other.position);
    if ((d > 0) && (d < neighbordist)) {
      sum.add(other.velocity);
      count++;
    }
  }
  if (count > 0) {
    sum.div((float)count);
    // First two lines of code below could be condensed with new PVector setMag() method
    // Not using this method until Processing.js catches up
    // sum.setMag(maxspeed);

    // Implement Reynolds: Steering = Desired - Velocity
    sum.normalize();
    sum.mult(maxspeed);
    PVector steer = PVector.sub(sum, velocity);
    steer.limit(maxforce);
    return steer;
  } 
  else {
    return new PVector(0, 0);
  }
}

このalignメソッドは、先ほどのseparateメソッドと同じく、Boid型のArrayListを引数として受けって、PVector型の値を戻り値として返します。

また、2行目にあるコメントには次のようなことが書かれています。

システム内の近くにいるすべてのボイドについて、平均速度を計算する。

このメソッドもコメントからやっていることを推測します。

コメントを読んだ限りでは、全てのボイドの平均速度を求めて、その平均速度と個別のボイドの速度を比べているのだと思います。

その2つの速度の差が大きくなりすぎないような、ベクトルを戻り値として返していると予測します。

これもまたそのベクトルが、そのボイドが進む方向や速度に影響していると思います。

ただ、詳しくはまだわかりません。

↓では、もう一度flockメソッドを定義している部分に戻ります。

// We accumulate a new acceleration each time based on three rules
void flock(ArrayList<Boid> boids) {
  PVector sep = separate(boids);   // Separation
  PVector ali = align(boids);      // Alignment
  PVector coh = cohesion(boids);   // Cohesion
  // Arbitrarily weight these forces
  sep.mult(1.5);
  ali.mult(1.0);
  coh.mult(1.0);
  // Add the force vectors to acceleration
  applyForce(sep);
  applyForce(ali);
  applyForce(coh);
}

↑今見ていたのは、色が濃くなっている行にあるalignメソッドでした。

↓3つのうち最後のメソッドであるcohesionメソッドの定義も見てみます。

// Cohesion
// For the average position (i.e. center) of all nearby boids, calculate steering vector towards that position
PVector cohesion (ArrayList<Boid> boids) {
  float neighbordist = 50;
  PVector sum = new PVector(0, 0);   // Start with empty vector to accumulate all positions
  int count = 0;
  for (Boid other : boids) {
    float d = PVector.dist(position, other.position);
    if ((d > 0) && (d < neighbordist)) {
      sum.add(other.position); // Add position
      count++;
    }
  }
  if (count > 0) {
    sum.div(count);
    return seek(sum);  // Steer towards the position
  } 
  else {
    return new PVector(0, 0);
  }
}

↑このalignメソッドは、separateメソッドとalignメソッドと同じく、Boid型のArrayListを引数として受けって、PVector型の値を戻り値として返します。

2行目にあるコメントには次のようなことが書かれています。

近くにいるすべてのボイドの平均的な位置(つまり中心)に対して、その位置に向かうステアリング・ベクトルを計算する

コメントに書いてあるような計算をすることで、他のボイドから離れないようにしたり、離れすぎた場合は、他のボイドを見つけるということができるようになるのだと思います。

Flock flock;

void setup() {
  size(640, 360);
  // fullScreen();
  frameRate(120);
  flock = new Flock();
  // Add an initial set of boids into the system
  for (int i = 0; i < 150; i++) {
    flock.addBoid(new Boid(width/2,height/2));
  }
}

void draw() {
  background(50);
  flock.run();
}

// Add a new boid into the System
void mousePressed() {
  flock.addBoid(new Boid(mouseX,mouseY));
}

上のファイルにはコメントが書かれている部分が2つあったので、それぞれコメントを読んでいきます。

// Add an initial set of boids into the system
for (int i = 0; i < 150; i++) {
  flock.addBoid(new Boid(width/2,height/2));
}

まずはこの部分です。

コメントが意味しているのは次のようなことです。

システムに初期のボイド(三角形)のセットを加える

そういえばシステムを起動したときに、いくつかボイド(三角形)が表示されていました。

コメントが示しているのはこの↓動作のことだと思います。

2つ目のコメントがある部分がこちらです。

// Add a new boid into the System
void mousePressed() {
  flock.addBoid(new Boid(mouseX,mouseY));
}

こちらのコメントはおそらくこんな感じの意味になると思います。

システムに新しいボイドを加える

ということで、この部分のコードは、クリックしたらボイドが出現する処理の部分だと思います。

下に、クリックしたらボイドが出現する処理の動画を示しておきます。

続きの記事

ここまで見てくださってありがとうございます。長くなったので、下の記事に続きを書きました。

コメント

  1. […] […]

タイトルとURLをコピーしました