Boids 0.5
全体構成
Boidsを構築していくために、以下のような全体構成とした。
- HTML
- WordPressのテキストエディタで、入力やボタン、canvasなどの必要な要素を書き込む。
- 外部スクリプトファイル
- 各バージョンのBoidsを実行するための外部スクリプトファイルを用意する。バージョンごとにパッケージ化する。
HTML
個体数をセットするためのinput要素のタイプは”number”とした。こうすることにより、ボックスの右側に値をコントロールする矢印が出たり、最小値の指定などができる。
ボタンはWordPressでonclickを使うと問題が生じるので、HTMLでは単にボタンを表示させるだけとし、リスナについてはコード側で対応する。
canvas要素は書き方によってWordPressで勝手に変更されてしまうので注意。
input要素とcanvasについては、コード側から参照するためのidを付与している。
スクリプトについては、jQueryはWordPress側で読み込まれるとのこと。外部スクリプトファイルは、</body>
タグの直前あたりを想定して、WordPressのテキストモードで最後尾にscript
要素を書いた。<script type="..." src="..."></script>
とテキストモードで書いても、ビジュアルモードを経由すると以下のように書き換えられてしまう。
- 勝手にCDATAのパターンが挿入され、間に改行が入ってしまう
- srcとtypeの指定順序が逆になってしまう
|
<!-- Boids 0.5--> <input id="boids05_population" min="1" type="number" value="1" /> <input id="boids05_generate" type="button" value="Generate" /> <canvas id="boids05_canvas" style="background-color: #e0e0e0;" width="400" height="300"> </canvas> <!-- テキストモードで最後尾に以下を追加 --> <script src="../coffee/boids/boids_0_5.js" type="text/javascript">// <![CDATA[ // ]]></script> |
クラス構成
クラスは以下の4つを準備
- Boidsの活動領域となる
Field
クラス
- 個々の個体を表現する
Creature
クラス
- 複数のCreatureが集まった群を表現する
Cluster
クラス
- 各オブジェクトを生成し、ボタン操作に対応してアニメーションをコントロールする
Controler
クラス
パッケージ
異なるバージョンのBoidsを同じページで動かすため、バージョンごとのパッケージに上記4つのクラスを入れることとした。
初めのバージョンに対応したパッケージ名はboids05
で、グローバルにアクセスするのはControler
オブジェクトだけとし、Boids05.Controler
を定義している。
そして最後に、このスクリプトファイルが読み込まれたときにボタンに対するリスナを登録するための処理を記述している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
# Boidsの準備として、個体を運動させる # Boids 0.5 # 指定した数の個体を発生させ、canvas内のランダムな位置に表示させる Boids05 = {} # パッケージBoids05の本体 do -> # Boidsが活動する空間のクラス # このバージョンでは、canvasオブジェクトの上下左右の座標を持つ長方形 class Field # Boidsの個体を実現するクラス # 位置座標と速度ベクトルの成分を保持する # 自らが活動するFieldクラスのオブジェクトへの参照も保持する class Creature # Boidsの群を管理するクラスで、以下を保持する # ・群を構成する複数の個体 # ・個体を描画するcanvasとそのcontextのオブジェクトを保持する class Cluster # Boidsアプリケーションの動作を統括するクラス # Creatureや class Controler # Boids 0.5で用いるための名前空間を設定 Boids05.Controler = Controler # Boids 0.5に対応したDOMの操作に対応し、このパッケージを操作する # DOMがすべて読み込まれてから以下を実行するよう保証 jQuery ($) -> |
各ブロックとクラスの説明
リスナ登録
jQueryの構文を用いている。WordPressでjQueryを用いる場合、”$”が別のエイリアスに定義されているらしいので、1行目のように書くとのこと。
1つ目のブロックではcanvas
要素のDOMオブジェクトを取得し、controler
オブジェクトを生成して初期値をセットしている。
この段階では速度関係は必要ないが、次のステップの前にとりあえず実装したため、Controler.setVMax()
メソッドを読んでいる。
2つ目のブロックでは、ボタンが押されたときの処理を記述しており、numberボックスから値を読み込み、それに基づいて個体生成の指令を出している。
|
jQuery ($) -> # Boids0.5のControlerを作成し、canvasオブジェクトを渡す canvas = $("#boids05_canvas")[0] controler = new Boids05.Controler(canvas) controler.setVMax(50) # Generateボタンが押されたとき、指定された個体数で群をつくる $("#boids05_generate").click -> pop = $("#boids05_population").val() if pop < 1 pop = 1 $("#boids05_population").val(pop) controler.generate(pop) |
Controlerクラス
ブラウザ側からの指令と、スクリプト内の各クラスとの仲立ちを一手に引き受けるクラスで、この段階では以下の機能を提供する。
- constructor
- canvasオブジェクトを渡して
Controler
オブジェクトを生成する。
- constructor(canvas)
- canvasオブジェクトを渡して
Constructor
オブジェクトを生成する。
- setVMax(vMax)
- ランダムに生成する速さの最大値を
Constructor
オブジェクトにセットする。
- createCreature()
- 位置と速度(速さと方向)をランダムに与えて個体を生成する。
- generate(population)
- 指定した数の個体を生成し、Clusterオブジェクトに格納する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
class Controler constructor: (canvas) -> @vMax = 0 @field = new Field(canvas) @cluster = new Cluster(canvas) @population = 0 # number of creatures # 個体の速度を発生させるときの最大値 setVMax: (@vMax) -> # 一つの個体を発生させるメソッド # 位置はcanvasに収まるよう、速度はvMax/2~vMaxの範囲でランダム createCreature: -> cr = new Creature(@field) x = Math.floor(Math.random() * @field.xMax) y = Math.floor(Math.random() * @field.yMax) v = (Math.random() + 1) * @vMax / 2 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() |
Fieldクラス
将来の拡張も考慮して、一つのクラスとした。現時点では単なる矩形で、上下左右の座標値をプロパティに持つ。
|
class Field constructor: (canvas) -> @xMin = 0 @yMin = 0 @xMax = canvas.width @yMax = canvas.height |
Clusterクラス
Creature
オブジェクトを1~複数保持する「群」のクラス。提供する機能は以下の通り。
- constructor(canvas)
- 個体を描画する
canvas
を渡してCluster
オブジェクトを生成。
- clearCreatures()
- 群の個体を空にする。
- addCreature(creature)
Creature
オブジェクトを渡し、群に追加する。
- draw()
- 群の全ての個体を描画する。実際には、保持している個体の
draw()
メソッドを呼び出しているだけ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
class Cluster constructor: (canvas) -> @creatures = [] @canvas = canvas @context = @canvas.getContext("2d") if @canvas.getContext return # 群の個体をすべてクリアする clearCreatures: -> @creatures = [] # 群に与えられた個体を加える addCreature: (creature) -> @creatures.push(creature) return # 群の善個体を描画する draw: -> @context.fillStyle = "#e0e0e0" @context.fillRect(0, 0, @canvas.width, @canvas.height) for creature in @creatures creature.draw(@context) return |
Creatureクラス
個々のクラスを表現するクラス。このバージョンでは運動はさせないが、速度関係のプロパティや初期化は行っている。
- constructor(canvas)
- Fieldオブジェクトを指定してCreatureオブジェクトを生成。
- setPosition(x, y)
- 個体が存在する座標を指定。
- setVelocity(vx, vy)
- 速度の成分をセット。このバージョンでは必要ないが実装。
- draw(context)
- 描画コンテキストを指定して、個体を描画する。このバージョンでは単純な円を描いている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
class Creature # 描画の基本となるサイズ # private staticなクラス・プロパティ _drawSize = 4 constructor: (field) -> @x = 0 @y = 0 @vx = 0 @vy = 0 @field = field # 個体の位置をセットするメソッド setPosition: (@x, @y) -> # 個体の速度をセットするメソッド setVelocity: (@vx, @vy) -> # 個体を描画するメソッド # canvasのコンテキストを引数で受け取る draw: (ctx) -> ctx.fillStyle = "black" ctx.beginPath() ctx.arc(@x, @y, _drawSize, 0, Math.PI*2, true) ctx.fill() return |
Boids 1.0
HTML
Boids 1.0用のボックスやボタンを用意し、外部スクリプトファイルを読み込んでいる。
このバージョンから個体の運動をさせるので、Start/Stopボタンを追加している。
|
<input type="number" id="boids10_population" value="1" min="1" /><br /> <input type="button" id="boids10_generate" value="Generate" /> <input type="button" id="boids10_start_and_stop" value="Start/Stop" /> <canvas id="boids10_canvas" width="400" height="300" style="background-color: #e0e0e0"> <!-- テキストモードの最後尾 --> ... <script src="../coffee/boids/boids_1_0.js" type="text/javascript">// <![CDATA[ // ]]></script> |
パッケージとクラスの変更
パッケージ構成
パッケージについては、パッケージ名やエイリアスを変更。
また、個体の運動を追加するため、以下のメソッドを追加。
Creature.move(interval_sec)
Cluster.move()
Controler.setIntervalSec(interva._sec)
Controler.startAndStop()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
Boids10 = {} do -> class Field constructor: (canvas) -> class Creature constructor: (field) -> setPosition: (@x, @y) -> setVelocity: (@vx, @vy) -> draw: (ctx) -> move: (interval_sec) -> class Cluster constructor: (canvas) -> clearCreatures: -> draw: -> move: (interval_sec) -> class Controler constructor: (canvas) -> setVMax: (@vMax) -> setIntervalSec: (@interval_sec) -> createCreature: -> generate: (@population) -> startAndStop: () -> Boids10.Controler = Controler jQuery ($) -> |
Creature.move()
秒単位のフレームタイムを指定して呼び出される。
- 各個体の速度で等速度運動
- 壁に当たった場合は全反射
|
class Field ... # Boids 1.0で追加 # interval_msはアニメーションのフレームタイム # 秒単位で与えられることを想定 move: (interval_sec) -> @x += @vx * interval_sec @y += @vy * interval_sec # 壁にぶつかった場合は全反射 if @x <= @field.xMin or @x >= @field.xMax then @vx = -@vx if @y <= @field.yMin or @y >= @field.yMax then @vy = -@vy return |
Cluster.move()
秒単位のフレームタイムを指定し、群の個体の数の分だけCreature.move()
メソッドを呼び出す。
|
class Cluster ... # Boids 1.0で追加 # フレームタイムを秒単位で受け取り、すべての個体を移動させる move: (interval_sec) -> for creature in @creatures creature.move(interval_sec) return |
Controlerクラス
変更内容は以下の通り。
- プロパティ追加
- アニメーションをコントロールするプロパティとして以下を追加。
- 運動中かどうかを保持する
isRunning
フラグ
- 作動中の
interval
タイマを保持するtimer
- 秒単位のフレームタイムを保持する
interval_sec
- setIntervalSec(interval_sec)
- アニメーションのフレームタイムをセット。
- startAndStop()
- Start/Stopボタンを押すごとに、作動状態と停止状態を切り替え。特にリスナにコールバック関数を無名関数で登録するときに、thisをバインドするためのファットアローを使っていることと、無名関数で引数を表示する記法に留意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
class Controler constructor: (canvas) -> ... # Boids 1.0で追加/アニメーション用 @isRunning = false @timer = undefined @interval_sec = 1 # animation frame interval in seconds ... # Boids 1.0で追加 # フレームタイムのセット setIntervalSec: (@interval_sec) -> ... # Boids 1.0で追加 # この関数を呼び出すたびに、アニメーションの開始と停止が切り替わる # アニメーションにはsetInterval()に以下の引数を渡している # ・アニメーション1フレームの操作 # ・ミリ秒単位の時間間隔 startAndStop: () -> if @isRunning clearInterval(@timer) @isRunning = false else @timer = setInterval => @cluster.move(@interval_sec) @cluster.draw() return , @interval_sec * 1000 @isRunning = true return |
Boids 1.1
このバージョンでは、個体を壁にぶつかってから全反射させるのではなく、壁が近づくとともに斥力を感じるように壁を避けようとする動作を導入する。
HTML
Boids 1.0用のボックスやボタンを用意し、外部スクリプトファイルを読み込んでいる。
|
<input type="number" id="boids11_population" value="1" min="1" /><br /> <input type="button" id="boids11_generate" value="Generate" /> <input type="button" id="boids11_start_and_stop" value="Start/Stop" /> <canvas id="boids11_canvas" width="400" height="300" style="background-color: #e0e0e0"> </canvas> <!-- テキストモードの最後尾 --> ... <script src="../coffee/boids/boids_1_1.js" type="text/javascript">// <![CDATA[ // ]]></script> |
パッケージとクラスの変更
パッケージ構成
クラス構成は特に変わらず、パッケージ名とエイリアスを本バージョンにあったものとする。以下のバージョンでも同じ。
|
Boids11 = {} do -> class Field class Creature class Cluster class Controler Boids11.Controler = Controler jQuery ($) -> |
Creatureのクラスプロパティ
このバージョンから、速度の最大値をControler
からCreature
のクラスプロパティに変更(6行目)。
また、壁の接近を認識する距離(_detectionLength
)と回避加速度計算時のパラメータ(_repulsionParam
)を定義。
|
class Creature # private staticなクラス・プロパティ # 最大速度 # Boids1.0ではControlerクラスに置いていたが、 # Boids1.1からCreatureクラスのクラスプロパティとした _vMax = 0 # 描画の基本となるサイズ _drawSize = 4 # 壁の衝突回避を認識する距離 _detectionLength = 100 # 反発力パラメータ _repulsionParam = 4 |
Creature.move
認識距離以内に壁に近づき始めると、壁からの斥力を想定した加速度を計算し、速度を変更。
加速度は重力加速度をイメージし、距離の2乗に反比例させた。
そもそもピクセル単位の計算では最小でも距離の値が”1″となるため、重力モデルでは大きな加速度が出ない。ここでは_detectionLength
を長さの基準としたが、壁との距離に応じて自然に避けるような運動をさせるためにパラメータ(_repulsionParam
)を導入しなければならなかった。
なお、速度ベクトルの値のキャップを判定する際に速度成分の符号を用いているが、Math.sign()はほとんどのブラウザで使えないため、「値/絶対値」で符号を得ている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
move: (interval_sec) -> # Boids 1.1から追加 # 壁からの距離に応じた斥力を想定し、x/y方向の加速度を計算 if @x <= _detectionLength ax = (_detectionLength * _repulsionParam / @x) ** 2 else if @x >= @field.xMax - _detectionLength ax = -(_detectionLength * _repulsionParam / (@field.xMax - @x)) ** 2 else ax = 0 if @y <= _detectionLength ay = (_detectionLength * _repulsionParam / @y) ** 2 else if @y >= @field.yMax - _detectionLength ay = -(_detectionLength * _repulsionParam / (@field.yMax - @y)) ** 2 else ay = 0 # Boids 1.1から追加 # 加速度を考慮した速度の変化 # 速度の絶対値が増え続けないようにキャップで抑える # 速度成分の符号を得るのに、Math.signがサポートされていないブラウザが多いため、 # 値/絶対値で符号を得ている @vx += ax * interval_sec @vx = @vx / Math.abs(@vx) * Math.min(Math.abs(@vx), Creature._vMax) @vy += ay * interval_sec @vy = @vy / Math.abs(@vy) * Math.min(Math.abs(@vy), Creature._vMax) # インターバルの間の移動量 @x += @vx * interval_sec @y += @vy * interval_sec # 壁にぶつかった場合は全反射 if @x <= @field.xMin or @x >= @field.xMax then @vx = -@vx if @y <= @field.yMin or @y >= @field.yMax then @vy = -@vy return |
Controlerクラス
vMaxの変更のみ。
|
class Controler constructor: (canvas) -> # @vMax = 0 は削除 @field = new Field(canvas) @cluster = new Cluster(canvas) ... # 実際にはCreatureクラスのクラスプロパティにセットしている setVMax: (vMax) -> Creature._vMax = vMax ... |
Boids 1.2
このバージョンで、以下の点を変更している。
- 座標単位のプロパティ定義・計算を、独自クラス
geom2d.Vector
で書き換え
- 描画サイズなど各種パラメータをHTMLで設定できるようにする
- 各種パラメータをstaticなクラス変数とする
- HTMLでグローバルな変数としてパラメータ群を設定
- 実行時にControlerオブジェクトのセッターでパラメータ群を設定
Creature
オブジェクトの形状を自由に設定可能にする
HTML
独自クラスパッケージファイルの読み込み
本体のスクリプトファイルに先立って、独自クラスのパッケージファイル(taustation_geom2d.js
)を読み込ませている。
パラメータ群の設定
HTMLのscript要素でパラメータ用のグローバル変数を定義する。これらはその後に読み込まれたスクリプトで利用される。
script要素内の変数定義はJavascriptで書くため、行末にコロン(;)をつけている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
<input type="number" id="boids12_population" value="1" min="1" /><br /> <input type="button" id="boids12_generate" value="Generate" /> <input type="button" id="boids12_start_and_stop" value="Start/Stop" /> <canvas id="boids12_canvas" width="400" height="300" style="background-color: #e0e0e0"> </canvas> <!-- テキストモードの最後尾 --> <!-- Boids 1.2用のパラメータ --> <script type="text/javascript">// <![CDATA[ V_MAX = 100; CREATURE_SIZE = 8; WALL_DETECTION_LENGTH = 100; WALL_REPULSION_PARAM = 16; INTERVAL_SEC = 0.05; // ]]></script> <script src="../lib/taustation_geom2d.js" type="text/javascript">// <![CDATA[ // ]]></script> ... <script src="../coffee/boids/boids_1_2.js" type="text/javascript">// <![CDATA[ // ]]></script> |
script要素をWordPressのテキストモードで書くと、ビジュアルモードを経由したときに//<![CDATA[~改行~//]]>
が挿入される。
また、Javascriptをscript要素に直接書くと、改行が除かれて一行になってしまう。
パッケージの準備とクラスの変更
Vectorクラスの利用
独自クラスgeom2d.Vectorを使って、位置・速度・加速度のコーディングを見やすくした。
各オブジェクトの生成と計算時にVectorオブジェクトを生成するnew演算子が介在するが、400個体程度の計算でもパフォーマンスの問題は見られなかった。
個体の形
これまではdraw()
メソッドの中で直接円を描いていたが、今回はヘルパーメソッドdrawShape()
を実装。
パラメータ設定
HTML内のスクリプト要素で定義されたグローバル変数を参照し、パラメータとしてクラス変数に格納。
この操作はControler
クラスに集約した。
変更に関係する箇所
Creatureクラスのクラス変数
@var: valの形式でクラス変数として定義。
|
class Creature # 最大速度 @_vMax: 100 # 描画の基本となるサイズ @_drawSize: 8 # 壁の衝突回避を認識する距離 @_wallDetectionLength: 100 # 反発力パラメータ @_wallRepulsionParam: 4 |
Creatureの基本メソッド
これまでx, y, vx, vy
のように個別の座標値で扱っていたプロパティを、Vectorクラスで置き換える。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
# 位置、速度、加速度をプロパティとして持つ constructor: (field) -> @pos = new Vector(0, 0) @v = new Vector(0, 0) @a = new Vector(0, 0) @field = field # 個体の位置をセットするメソッド setPosition: (x, y) -> @pos.x = x @pos.y = y return # 個体の速度をセットするメソッド setVelocity: (vx, vy) -> @v.x = vx @v.y = vy return |
Creature.draw()
単純な円ではなく、方向を持った細長い三角形を描くように変更。
下図のように、基準点(x, y)から速度ベクトルの方向に頂点を、速度ベクトルと直角な報告に底辺を描く。ここでで、これによって三角形の細長さが決まる。
ここでと置くと下図のようになる。
コードは以下の通り。
|
# 個体を描画するメソッド # 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 |
Creature.move()
Vectorオブジェクトの導入に対応して書き直した。
斥力加速度計算のパラメータ(Creature._wallRepulsionParam
)は()の外に出した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
move: (interval_sec) -> # Boids 1.1から追加 # 壁からの距離に応じた斥力を想定し、x/y方向の加速度を計算 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 # Boids 1.1から追加 # 加速度を考慮した速度の変化 # 速度の絶対値が増え続けないようにキャップで抑える @v.plusEq(@a.times interval_sec) @v.x = Math.sign(@v.x) * Math.min(Math.abs(@v.x), Creature._vMax) @v.y = Math.sign(@v.y) * Math.min(Math.abs(@v.y), Creature._vMax) # インターバルの間の移動量 @pos.plusEq(@v.times interval_sec) # 壁にぶつかった場合は全反射 if @pos.x <= @field.xMin or @pos.x >= @field.xMax then @v.x = -@v.x if @pos.y <= @field.yMin or @pos.y >= @field.yMax then @v.y = -@v.y return |
Clusterクラス
変更箇所はない。
Controlerクラスのセッター
以下のメソッド群で整理。Creatureのクラス変数をセットしている。
|
setVMax: (vMax) -> Creature._vMax = vMax setCreatureSize: (size) -> Creature._drawSize = size setWallDetectionLength: (pix) -> Creature._wallDetectionLength = pix setWallRepulsionParam: (param) -> Creature._wallRepulsionParam = param |
実行部分
Controlerオブジェクトのセッター群を呼び出している。
|
jQuery ($) -> canvas = $("#boids12_canvas")[0] controler = new Boids12.Controler(canvas) controler.setVMax(V_MAX) controler.setCreatureSize(CREATURE_SIZE) controler.setWallDetectionLength(WALL_DETECTION_LENGTH) controler.setWallRepulsionParam(WALL_REPULSION_PARAM) controler.setIntervalSec(INTERVAL_SEC) |