# Boids 2.3
# Boids 2.2に整列行動(alignment)を加える
Boids23 = {}
# パッケージBoids23の本体
do ->
# 独自クラスのエイリアス
# パッケージtaustation_geom2d.jsが必要。
Vector = taustation_geom2d.Vector
MovingAgent = taustation_geom2d.MovingAgent
# Boidsが活動する空間のクラス
# このバージョンでは、canvasオブジェクトの上下左右の座標を持つ長方形
class Field
constructor: (canvas) ->
@xMin = 0
@yMin = 0
@xMax = canvas.width
@yMax = canvas.height
# Boidsの個体を実現するクラス
# 位置座標と速度ベクトルの成分を保持する
# 自らが活動するFieldクラスのオブジェクトへの参照も保持する
class Creature extends MovingAgent
# private staticなクラス・プロパティ
# ランダムに速度を発生させるときの最大速度と最小速度
@_vMax: 100
@_vMin: 50
# 描画の基本となるサイズ
@_drawSize: 8
# 壁の衝突回避を認識する距離
@_wallDetectionLength: 100
# 反発力パラメータ
@_wallRepulsionParam: 4
# 視野の長さ(奥行)
@_separationFieldDepth: 100
# 分離行動の回避角
@_separationAngle: 0
# 結合行動の回避角
@_cohesionAngle: 0
# 整列高度の視野の長さ(奥行)
@_alignmentFieldDepth: 100
# 整列行動の変化角
@_alignmentAngle: 0
# コンストラクタの引数にはFieldオブジェクトを渡す
# 位置、速度、加速度、自分が属する群、Fieldをプロパティとして持つ
constructor: (cluster, field) ->
super()
@a = new Vector(0, 0)
@cluster = cluster
@field = field
@nearestCreature = null
# 個体を描画するメソッド
# canvasのコンテキストを引数で受け取る
draw: (ctx) ->
v = @v.abs()
sx = Creature._drawSize * @v.x / v
sy = Creature._drawSize * @v.y / v
p = 0.25
ctx.fillStyle = "black"
ctx.beginPath()
ctx.moveTo(@pos.x + (1 - p) * sx, @pos.y + (1 - p) * sy)
ctx.lineTo(@pos.x + (-sx - sy) * p, @pos.y + (sx - sy) * p)
ctx.lineTo(@pos.x + (-sx + sy) * p, @pos.y + (-sx - sy) * p)
ctx.closePath()
ctx.fill()
return
# 個体を動かすメソッド
# 「群れ」を表すClusterクラスのmove()メソッドから呼ばれる
# interval_secは秒単位のアニメーション・フレームのインターバル
move: (interval_sec) ->
# 壁を避ける
@wallAvoidance()
# 分離行動(separation)
@separation()
# 結合行動(cohesion)
@cohesion()
# 整列行動(alignment)
@alignment()
# 加速度を考慮した速度の変化を計算
# 速度の絶対値が増え続けないようにキャップで抑える
# 速度成分の符号を得るのに、Math.signがサポートされていないブラウザが多いため、
# 値/絶対値で符号を得ている
@v.plusEq(@a.times interval_sec)
@v.x = @v.x / Math.abs(@v.x) * Math.min(Math.abs(@v.x), Creature._vMax)
@v.y = @v.y / Math.abs(@v.y) * Math.min(Math.abs(@v.y), Creature._vMax)
# インターバルの間の移動量を計算
@pos.plusEq(@v.times interval_sec)
# 壁にぶつかった場合は全反射
if @pos.x <= @field.xMin
@pos.x = @field.xMin * 2 - @pos.x
@v.x = -@v.x
else if @pos.x >= @field.xMax
@pos.x = @field.xMax * 2 - @pos.x
@v.x = -@v.x
if @pos.y <= @field.yMin
@pos.y = @field.yMin * 2 - @pos.y
@v.y = -@v.y
else if @pos.y >= @field.yMax
@pos.y = @field.yMax * 2 - @pos.y
@v.y = -@v.y
return
# 壁を避ける行動
# 壁からの距離に応じた斥力を(repulsion)想定し、x/y方向の加速度を計算
wallAvoidance: ->
if @pos.x <= Creature._wallDetectionLength
@a.x =
Creature._wallRepulsionParam *
(Creature._wallDetectionLength / @pos.x) ** 2
else if @pos.x >= @field.xMax - Creature._wallDetectionLength
@a.x =
-Creature._wallRepulsionParam *
(Creature._wallDetectionLength / (@field.xMax - @pos.x)) ** 2
else
@a.x = 0
if @pos.y <= Creature._wallDetectionLength
@a.y =
Creature._wallRepulsionParam *
(Creature._wallDetectionLength / @pos.y) ** 2
else if @pos.y >= @field.yMax - Creature._wallDetectionLength
@a.y =
-Creature._wallRepulsionParam *
(Creature._wallDetectionLength / (@field.yMax - @pos.y)) ** 2
else
@a.y = 0
# Boids2.1
# 分離行動(separation)
# 前方視角180度内で一定距離内の視野の中で、最も近い個体を避ける
# 相手方の速度の向きには関係なく、相手側と反対方向に速度の向きをを少し変更する。
separation: ->
# 回避行動の方向転換計算用cos/sin
cs = Math.cos(Creature._separationAngle)
sn = Math.sin(Creature._separationAngle)
# 自身に最も近い個体
@nearestCreature = null
# 自分以外の群のすべての個体についてサーチ
for other in @cluster.creatures
if other != this
# その個体が自分の前方にいるなら
if other.isInFrontOf(this)
d = Vector.distance(this.pos, other.pos)
# その個体が視野の中にいるなら
if d <= Creature._separationFieldDepth
# 周世に個体がセットされていて、今回の個体の方が近ければ入れ替え
# 最初に認識した個体ならそのままセット
if @nearestCreature?
if d <= Vector.distance(@nearestCreature.pos, this.pos)
@nearestCreature = other
else
@nearestCreature = other
# 近くの個体が視野内に存在するなら回避計算
if @nearestCreature?
# 相手が自分の右にいるなら左へ、自分の左にいるなら右へ向きを変更
if @nearestCreature.isOnTheRightOf(this)
vx = @v.x * cs - @v.y * sn
vy = @v.x * sn + @v.y * cs
else
vx = @v.x * cs + @v.y * sn
vy = -@v.x * sn + @v.y * cs
@v.x = vx
@v.y = vy
# Boids2.2
# 結合行動(cohesion)
# 前方視角180度以内の全個体の重心に向かって速度を少し変更する
cohesion: ->
# 群のうち認識した個体の数とそのグループの重心
n = 0
clusterCenter = null
# 結合行動の方向転換計算用cos/sin
cs = Math.cos(Creature._cohesionAngle)
sn = Math.sin(Creature._cohesionAngle)
# 自分以外の群のすべての個体についてサーチ
for other in @cluster.creatures
if other != this
# 相手が自身の前方にいるなら、その位置を考慮
if other.isInFrontOf(this)
n++
if clusterCenter?
clusterCenter.plusEq(other.pos)
else
clusterCenter = new Vector(other.pos.x, other.pos.y)
# 前方の相手が1つでも存在すれば、結合行動をとる
if clusterCenter?
clusterCenter.divEq(n)
# 重心が自分の右にあれば左に、自分の左にあれば右に進路を変更
if clusterCenter.isOnTheRightOf(this)
vx = @v.x * cs + @v.y * sn
vy = -@v.x * sn + @v.y * cs
else
vx = @v.x * cs - @v.y * sn
vy = @v.x * sn + @v.y * cs
@v.x = vx
@v.y = vy
# Boids2.3
# 整列行動(alignment)
# 分離行動と同じ視野内の個体群の平均速度に合わせるよう速度の方向を変更
alignment: ->
# 平均速度計算用
vMean = null
n = 0
# 整列行動の方向転換計算用cos/sin
cs = Math.cos(Creature._alignmentAngle)
sn = Math.sin(Creature._alignmentAngle)
# 自分以外の群のすべての個体についてサーチ
for other in @cluster.creatures
if other != this
# その個体が自分の前方にいるなら
if other.isInFrontOf(this)
d = Vector.distance(this.pos, other.pos)
# その個体が視野の中にいるなら
if d <= Creature._alignmentFieldDepth
# 既に個体が視野内にあれば平均速度の計算を続行
n++
if vMean?
vMean.plusEq(other.v)
else
vMean = new Vector(0, 0)
# 視野内に個体がいたなら速度変更の計算へ
if vMean?
vMean.divEq(n)
###
@v.plusEq(vMean)
newV = @v.abs()
if newV >= Creature._vMax
@v.timesEq(vMean.abs() / newV)
###
if vMean.isHeadingToRightOf(@v)
vx = @v.x * cs + @v.y * sn
vy = -@v.x * sn + @v.y * cs
else
vx = @v.x * cs - @v.y * sn
vy = @v.x * sn + @v.y * cs
@v.x = vx
@v.y = vy
# Boidsの群を管理するクラスで、以下を保持する
# ・群を構成する複数の個体
# ・個体を描画するcanvasとそのcontextのオブジェクト
class Cluster
# コンストラクタはcanvasオブジェクトを受け取り、そのcontextをプロパティに登録
constructor: (canvas) ->
@creatures = []
@canvas = canvas
@context = @canvas.getContext("2d") if @canvas.getContext
return
# すべての個体オブジェクトを群から削除する
clearCreatures: ->
@creatures = []
# 引数で与えられた個体を群に加える
addCreature: (creature) ->
@creatures.push(creature)
return
# 群の全個体を描画する
# 描画エリアをクリアした後、群の全個体のdraw()メソッドを呼び出している
draw: ->
@context.fillStyle = "#e0e0e0"
@context.fillRect(0, 0, @canvas.width, @canvas.height)
for creature in @creatures
creature.draw(@context)
return
# 秒単位のインターバル受け取り、すべての個体を移動させる
move: (interval_sec) ->
for creature in @creatures
creature.move(interval_sec)
return
# Boidsアプリケーションの動作を統括するクラス
# 唯一、ブラウザから直接呼び出され、初期値の設定やClusterの操作を担当
class Controler
# Boids 1.2からstaticなクラスプロパティとした
@interval_sec: 1 # animation frame interval in seconds
constructor: (canvas) ->
# Boidsの活動領域
@field = new Field(canvas)
# 群のオブジェクト
@cluster = new Cluster(canvas)
# 個体の数
@population = 0
# アニメーション用のフラグとtimer
@isRunning = false
@timer = undefined
# 定数として扱うクラス変数を設定するメソッド群
setVMax: (vMax) -> Creature._vMax = vMax
setVMin: (vMin) -> Creature._vMin = vMin
setCreatureSize: (size) -> Creature._drawSize = size
setWallDetectionLength: (pix) -> Creature._wallDetectionLength = pix
setWallRepulsionParam: (param) -> Creature._wallRepulsionParam = param
setSeparationFieldDepth: (length) -> Creature._separationFieldDepth = length
setSeparationAngle: (angle) -> Creature._separationAngle = angle
setCohesionAngle: (angle) -> Creature._cohesionAngle = angle
setAlignmentFieldDepth: (length) -> Creature._alignmentFieldDepth = length
setAlignmentAngle: (angle) -> Creature._alignmentAngle = angle
setIntervalSec: (@interval_sec) ->
# 一つの個体を発生させるメソッド
# 位置はcanvasに収まるよう、速度はvMax/2~vMaxの範囲でランダム
createCreature: ->
cr = new Creature(@cluster, @field)
x = Math.floor(Math.random() * @field.xMax)
y = Math.floor(Math.random() * @field.yMax)
v = Math.random() * (Creature._vMax - Creature._vMin) + Creature._vMin
a = Math.random() * Math.PI * 2
cr.setPosition(x, y)
cr.setVelocity(v * Math.cos(a), v * Math.sin(a))
return cr
# 引数で指定された数の個体を発生させ、Clusterオブジェクトに登録する
generate: (@population) ->
@cluster.clearCreatures()
for i in [1..@population]
cr = @createCreature()
@cluster.addCreature(cr)
@cluster.draw()
# アニメーションの開始/停止の切り替え
# アニメーションはsetInterval()で実装している
startAndStop: () ->
if @isRunning
clearInterval(@timer)
@isRunning = false
else
@timer = setInterval =>
@cluster.move(@interval_sec)
@cluster.draw()
return
, @interval_sec * 1000
@isRunning = true
return
# 外部からアクセス可能な名前の設定
Boids23.Controler = Controler
# スクリプトが読み込まれた時の実行部分
jQuery ($) ->
# Controlerを作成し、canvasオブジェクトを渡す
canvas = $("#Boids23_canvas")[0]
controler = new Boids23.Controler(canvas)
# 固定パラメータのセット
# パラメータの値は、Javascript形式でHTMLに埋め込まれている想定
controler.setVMax(BD23_V_MAX)
controler.setVMin(BD23_V_MIN)
controler.setCreatureSize(BD23_CREATURE_SIZE)
controler.setWallDetectionLength(BD23_WALL_DETECTION_LENGTH)
controler.setWallRepulsionParam(BD23_WALL_REPULSION_PARAM)
controler.setSeparationFieldDepth(BD23_SEPARATION_FIELD_DEPTH)
controler.setSeparationAngle(BD23_SEPARATION_ANGLE)
controler.setCohesionAngle(BD23_COHESION_ANGLE)
controler.setAlignmentFieldDepth(BD23_ALIGNMENT_FIELD_DEPTH)
controler.setAlignmentAngle(BD23_ALIGNMENT_ANGLE)
controler.setIntervalSec(BD23_INTERVAL_SEC)
# Generateボタンが押されたとき、指定された個体数で群をつくる
$("#boids23_generate").click ->
pop = $("#boids23_population").val()
if pop < 1
pop = 1
$("#boids23_population").val(pop)
controler.generate(pop)
# Start/Stopボタンが押されたとき、アニメーションの動作を切り替える
$("#boids23_start_and_stop").click ->
controler.startAndStop()