読者です 読者をやめる 読者になる 読者になる

ux00ff

ビールとプログラミングと

ScalaでFlockingの公式サンプルコードを書き直してみた

Processing Scala

Processingの公式コードサンプルのFlockingは自分が好きなサンプルの一つです。

Flocking \ Examples \ Processing.org

これは Boid理論に基づいてわらわらと動作する「鳥の群れ」っぽい動きをシミュレーションするというものです。

Boid理論というのは「仲間に近づきすぎたら離れる」「群れの中心に近づこうとする」「群れの方向に合わせて並んで移動する」という簡単なルールを適応することで群れっぽい動きになるぜというもので、こちらのサイト (Boid理論 群行動生成アルゴリズム| ActionScriptテックラボ | [Smart])がわかりやすく参考になりました。

移植

で、この公式サンプルをまずは簡単に Scala に移植してみたのが以下となります。メソッドなども含めてだいたい忠実に移植しています。

元のサンプルに加えて、マウスボタンを押しっぱなしにすると Boidインスタンスが生成され続けます。Gistにもあげておきました

import processing.core.PConstants._
import processing.core.{PApplet, PVector}

import scala.collection.mutable.ListBuffer

class Boid(x: Float,y: Float, private[this] val pApplet: PApplet) {
  private[this] val R = 2.0f
  private[this] val MAX_FORCE = 0.03f
  private[this] val MAX_SPEED = 2.0f
  private[this] val NEIGHBORHOOD_DIST = 50f
  private[this] val DESIRED_SEPARATION = 25f

  private[this] val acceleration = PVector.random2D(pApplet)
  private[this] val angle = pApplet.random(TWO_PI)

  private[Boid] val position: PVector = new PVector(x, y)
  private[Boid] val velocity: PVector = new PVector(Math.cos(angle.toDouble).toFloat, Math.sin(angle.toDouble).toFloat)

  def run(boids: ListBuffer[Boid]): Unit = {
    this.flock(boids)
    this.update()
    this.borders()
    this.render()
  }

  private[this] def render() = {
    val theta = velocity.heading() + Math.toRadians(90)
    pApplet.fill(200)
    pApplet.stroke(255)
    pApplet.pushMatrix()
    pApplet.translate(position.x, position.y)
    pApplet.rotate(theta.toFloat)
    pApplet.beginShape(TRIANGLES)
    pApplet.vertex(0, -R * 2)
    pApplet.vertex(-R, R * 2)
    pApplet.vertex(R, R * 2)
    pApplet.endShape()
    pApplet.popMatrix()
  }

  private[this] def borders() = {
    if (position.x < -R) position.x = pApplet.width + R
    if (position.y < -R) position.y = pApplet.height + R
    if (position.x > pApplet.width + R) position.x = -R
    if (position.y > pApplet.height + R) position.y = -R
  }

  private[this] def update() = {
    velocity.add(acceleration)
    velocity.limit(MAX_SPEED)
    position.add(velocity)
    acceleration.mult(0)
  }

  private[this] def flock(boids: ListBuffer[Boid]) = {
    val sep: PVector = this.separate(boids)
    val ali: PVector = this.align(boids)
    val coh: PVector = this.cohesion(boids)

    sep.mult(1.5f)
    ali.mult(1.0f)
    coh.mult(1.0f)

    this.applyForce(sep)
    this.applyForce(ali)
    this.applyForce(coh)
  }

  private[this] def applyForce(force: PVector) = {
    acceleration.add(force)
  }

  private[this] def cohesion(boids: ListBuffer[Boid]): PVector = {
    val sum = new PVector(0, 0)
    // Start with empty vector to accumulate all positions
    var count = 0
    boids.foreach({ other =>
      val d = PVector.dist(position, other.position)
      if ((d > 0) && (d < NEIGHBORHOOD_DIST)) {
        sum.add(other.position) // Add position
        count += 1
      }
    })
    if (count > 0) {
      sum.div(count)
      seek(sum) // Steer towards the position
    }
    else {
      new PVector(0, 0)
    }
  }

   private[this]  def seek(target: PVector): PVector = {
    val desired = PVector.sub(target, position)
    desired.normalize()
    desired.mult(MAX_SPEED)
    val steer = PVector.sub(desired, velocity)
    steer.limit(MAX_FORCE)
    steer
  }

  private[this] def align(boids: ListBuffer[Boid]): PVector = {
    val sum = new PVector(0, 0)
    var count = 0
    boids.foreach({ other =>
      val d = PVector.dist(position, other.position)
      if ((d > 0) && (d < NEIGHBORHOOD_DIST)) {
        sum.add(other.velocity)
        count += 1
      }
    })
    if (count > 0) {
      sum.div(count)
      sum.normalize()
      sum.mult(MAX_SPEED)
      val steer = PVector.sub(sum, velocity)
      steer.limit(MAX_FORCE)
      steer
    }
    else {
      new PVector(0, 0)
    }
  }

  private[this] def separate(boids: ListBuffer[Boid]): PVector = {
    val steer = new PVector(0, 0)
    var count = 0
    boids.foreach({ other =>
      val d = PVector.dist(position, other.position)
      if ((d > 0) && (d < DESIRED_SEPARATION)) {
        val diff = PVector.sub(position, other.position)
        diff.normalize()
        diff.div(d) // Weight by distance
        steer.add(diff)
        count += 1 // Keep track of how many
      }
    })

    if (count > 0) {
      steer.div(count)
    }
    if (steer.mag() > 0) {
      steer.normalize()
      steer.mult(MAX_SPEED)
      steer.sub(velocity)
      steer.limit(MAX_FORCE)
    }
    steer
  }
}

class Flock {
  private[this] val boids: ListBuffer[Boid] = ListBuffer[Boid]()

  def run(): Unit = {
    boids.foreach({ _.run(boids) })
  }

  def addBoid(b: Boid): Unit = {
    boids.append(b)
  }

  def size: Int = {
    boids.size
  }
}

class App extends PApplet {
  private[this] val flock: Flock = new Flock()

  override def settings(): Unit = {
    size(1000, 700, JAVA2D)
  }

  override def setup(): Unit = {
    for (_ <- 1 to 600) {
      flock.addBoid(new Boid(width / 2, height / 2, this))
    }
  }

  override def draw(): Unit = {
    background(100)
    textAlign(LEFT, TOP)
    textSize(12)
    text("%2.3f fps".format(frameRate), 0, 0)
    text("boid size = %d".format(flock.size), 0, 15)
    if(this.mousePressed) {
      flock.addBoid(new Boid(mouseX, mouseY, this))
    }
    flock.run()
  }
}

実行結果はこちら。起動直後、真ん中から均等に周囲に Boid ちゃんが広がっていき・・・

f:id:ux00ff:20170210104925p:plain

しばらくするとなんとなく整列して動くようになります。今回は右下から左上に向かうように揃いました。

f:id:ux00ff:20170210105034p:plain

マウスの左ボタンを押しっぱなしにすると、Boidちゃんが増えていくのですが、手元の MacBook Pro(Late 2016) では1000を超えるあたりでフレームレートが30を切り、明らかに動きから滑らかさが失われます。処理としては1フレームの表示ごとに、全ての要素に対して、他の全ての要素との距離を調べ・・・というようなことをしているのでそりゃあ大変だよなという気分ではあります。

f:id:ux00ff:20170210105242p:plain

こいつもう少し高速化してやりたいなぁ。