概要
Boids 3.1で、いよいよEnemyを導入する。大まかな流れは以下の通り。
Creature
クラスを継承したEnemyCreature
クラスを定義する- EnemyCreature特有の行動はEnemy同士の分離(separation)のみ
- BoidCreatureはEnemyCreatureを避ける行動をとる
- BoidCreatureの群とEnemyCreatureの群を、それぞれのClusterオブジェクトに登録。
EnemyCreatureクラスの追加
Creature
クラスを継承したEnemyCreature
クラスを追加する。そのメソッド構成は以下の通り。specificBehaviorについては、Enemyに特別な行動ルールはなく、分離(separation)のみ実装している。separation()
メソッドはBoidCreature
とまったく同じロジック。
1 2 3 4 5 6 7 8 9 10 11 |
class EnemyCreature extends Creature constructor: (cluster, field) -> draw: (ctx) -> specificBehavior: (intervalInSec) -> @separation() .... separation: -> .... |
Controlerクラスの変更
パラメータ関係
Controler
クラスのパラメータ設定用setterを追加(後述)
Enemy発生用メソッドの追加
BoidCreature
クラスと同じ内容で、createEnemyCreature()
メソッドを追加
generate()
メソッドを変更
generate()
メソッドで、Boidに加えてEnemyも指定個体数だけ発生させるよう追加。
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 |
class Controler .... createBoidCreature: -> .... createEnemyCreature: -> cr = new EnemyCreature(@enemyCluster, @field) x = Math.floor(Math.random() * @field.xMax) y = Math.floor(Math.random() * @field.yMax) v = Math.random() * (EnemyCreature.vMax - EnemyCreature.vMin) + EnemyCreature.vMin ang = Math.random() * Math.PI * 2 cr.setPosition(x, y) cr.setVelocity(v * Math.cos(ang), v * Math.sin(ang)) return cr generate: (boidPopulation, enemyPopulation) -> @enemyCluster.clearCreatures() if enemyPopulation > 0 for i in [1..enemyPopulation] cr = @createEnemyCreature() @enemyCluster.addCreature(cr) @boidCluster.clearCreatures() for i in [1..boidPopulation] cr = @createBoidCreature() @boidCluster.addCreature(cr) # 個体発生後に画面をクリアして描画 @context.fillStyle = "#000080" @context.fillRect(0, 0, @canvas.width, @canvas.height) @boidCluster.draw(@context) @enemyCluster.draw(@context) .... |
BoidがEnemyから逃げる動作
BoidCreature
クラスに新たにavoidEnemy()
メソッドを追加し、specificBehavior()
でこれを呼び出すよう変更。avoidEnemy()
メソッドの内容は以下の通り。
- 基本のロジックは
sepration()
メソッドと同じ - ただし分離の相手方がEnemyの群になる
- 敵から回避する角度は、仲間との分離の角度より大きくする(パラメータで与える)
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 41 42 43 44 |
class BoidCreature extends Creature .... specificBehavior: (intervalInSec) -> @separation() @cohesion() @alignment() @avoidEnemy() return .... avoidEnemy: -> # 回避行動の方向転換計算用cos/sin cs = Math.cos(BoidCreature.antiEnemyAngle) sn = Math.sin(BoidCreature.antiEnemyAngle) # 自身に最も近いEnemyの個体 @nearestCreature = null # Enemyのすべての個体についてサーチ for enemy in @enemyCluster.creatures # その個体が自分の前方にいるなら if enemy.isInFrontOf(this) d = Vector.distance(this.pos, enemy.pos) # その個体が視野の中にいるなら if d <= BoidCreature.antiEnemyFieldDepth # 既に個体がセットされていて、今回の個体の方が近ければ入れ替え # 最初に認識した個体ならそのままセット if @nearestEnemy? if d <= Vector.distance(@nearestEnemy.pos, this.pos) @nearestEnemy = enemy else @nearestEnemy = enemy # 近くの個体が視野内に存在するなら回避計算 if @nearestEnemy? # Enemyが自分の右にいるなら左へ、自分の左にいるなら右へ向きを変更 if @nearestEnemy.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 |
実行部分
ページ読み込み時の実行部分は以下の通りで、2つのボタンのクリックに対する処理を定義。
- GENERATEボタンが押された時は、
BoidCreature
の群とEnemyCreature
の群を発生させる - START/STOPボタンが押されたときは、
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 |
# Generateボタンが押されたとき、指定された数でBoidとEnemyの群をつくる # Boids3.1では、VMax = VMinとして、全個体が同じ速さを持つようにしている $("#bd31_generate").click -> v = $("#bd31_boid_velocity").val() v = Math.max(v, BD31_BOID_VMIN) v = Math.min(v, BD31_BOID_VMAX) $("#bd31_boid_velocity").val(v) controler.setBoidVMax(v) controler.setBoidVMin(v) boidPop = $("#bd31_boid_population").val() boidPop = Math.max(boidPop, BD31_BOID_POPULATION_MIN) boidPop = Math.min(boidPop, BD31_BOID_POPULATION_MAX) $("#bd31_boid_population").val(boidPop) v = $("#bd31_enemy_velocity").val() v = Math.max(v, BD31_ENEMY_VMIN) v = Math.min(v, BD31_ENEMY_VMAX) $("#bd31_enemy_velocity").val(v) controler.setEnemyVMax(v) controler.setEnemyVMin(v) enemyPop = $("#bd31_enemy_population").val() enemyPop = Math.max(enemyPop, BD31_ENEMY_POPULATION_MIN) enemyPop = Math.min(enemyPop, BD31_ENEMY_POPULATION_MAX) $("#bd31_enemy_population").val(enemyPop) controler.generate(boidPop, enemyPop) # Start/Stopボタンが押されたとき、アニメーションの動作を切り替える $("#bd31_start_and_stop").click -> controler.startAndStop() |
コード全体
HTML
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Boids 3.1/CoffeeScript</title> <script type="text/javascript" src="../lib/jquery.js"></script> <script type="text/javascript" src="../lib/taustation_geom2d.js"></script> </head> <body> <h2>Boids 3.1</h2> <script> //<![CDATA[ BD31_WALL_DETECTION_LENGTH = 100; BD31_WALL_REPULSION_PARAM = 16; BD31_BOID_VMAX = 200; BD31_BOID_VMIN = 50; BD31_BOID_SIZE = 10; BD31_BOID_COLOR = "#FFFFFF"; BD31_BOID_POPULATION_MIN = 1; BD31_BOID_POPULATION_MAX = 300; BD31_BOID_SEPARATION_FIELD_DEPTH = 50; BD31_BOID_SEPARATION_ANGLE = 12 * Math.PI / 180; BD31_COHESION_ANGLE = 5 * Math.PI / 180; BD31_ALIGNMENT_FIELD_DEPTH = 250; BD31_ALIGNMENT_ANGLE = 5 * Math.PI / 180; BD31_ANTI_ENEMY_FIELD_DEPTH = 50; BD31_ANTI_ENEMY_ANGLE = 30 * Math.PI / 180; BD31_ENEMY_VMAX = 200; BD31_ENEMY_VMIN = 50; BD31_ENEMY_SIZE = 15; BD31_ENEMY_COLOR = "#FF0000"; BD31_ENEMY_POPULATION_MIN = 0; BD31_ENEMY_POPULATION_MAX = 5; BD31_ENEMY_SEPARATION_FIELD_DEPTH = 50; BD31_ENEMY_SEPARATION_ANGLE = 10 * Math.PI / 180; BD31_INTERVAL_SEC = 0.05; //]]> </script> <!-- Boids3.1の入力表示部 --> <p> <label><code>個体数 :</code></label><input type="number" id="bd31_boid_population" value="100" min="1" max="300" /><br /> <label><code>個体の速さ:</code></label><input type="number" id="bd31_boid_velocity" value="100" min="50" max="200" /><br /> <label><code>敵の数 :</code></label><input type="number" id="bd31_enemy_population" value="0" min="0" max="5" /><br /> <label><code>敵の速さ :</code></label><input type="number" id="bd31_enemy_velocity" value="75" min="50" max="200" /><br /> <input type="button" id="bd31_generate" value="Generate" /> <input type="button" id="bd31_start_and_stop" value="Start/Stop" /> </p> <canvas id="bd31_canvas" width="600" height="400" style="background-color: #000080"> </canvas> <script type="text/javascript" src="../coffee/boids/boids_3_1.js"></script> </body> </html> |
CoffeeScript
クラス構成
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
Boids31 = {} # パッケージBoids30の本体 do -> # 独自クラスのエイリアス # パッケージtaustation_geom2d.jsが必要。 Vector = taustation_geom2d.Vector MovingAgent = taustation_geom2d.MovingAgent # Boidsが活動する空間のクラス # このバージョンでは長方形 class Field # Field内の壁を避けながら移動する一般化Creeatureクラス # Boids3.0で、それまでのCreatureクラスをCreatureとBoidCreatureに分けて継承させた # Boidsの行動ルールは、このクラスを継承したBoidsCreatureクラスで実装する class Creature extends MovingAgent # Boidの個体を実現するクラス # Boids3.0で、Creatureクラスを継承するように変更 # 敵を回避するためのプロパティやメソッドを追加 class BoidCreature extends Creature # Boids3.1で導入 # Boidsに対する「敵」を表すクラス class EnemyCreature extends Creature # BoidsとEnemyの群を管理するクラスで、以下を保持する # - 群を構成する複数の個体 # - 個体を描画するcanvasのcontextオブジェクト class Cluster # Boidsアプリケーションの動作を統括するクラス # 唯一、ブラウザから直接呼び出され、初期値の設定やClusterの操作を担当 class Controler # 外部からアクセス可能な名前の設定 Boids31.Controler = Controler # スクリプトが読み込まれた時の実行部分 jQuery ($) -> # Controlerを作成し、canvasオブジェクトを渡す canvas = $("#bd31_canvas")[0] context = canvas.getContext("2d") if canvas.getContext controler = new Boids31.Controler(canvas, context) # 固定パラメータのセット # パラメータの値は、Javascript形式でHTMLに埋め込まれている想定 controler.setWallDetectionLength(BD31_WALL_DETECTION_LENGTH) controler.setWallRepulsionParam(BD31_WALL_REPULSION_PARAM) controler.setBoidSize(BD31_BOID_SIZE) controler.setBoidColor(BD31_BOID_COLOR) controler.setBoidSeparationFieldDepth(BD31_BOID_SEPARATION_FIELD_DEPTH) controler.setBoidSeparationAngle(BD31_BOID_SEPARATION_ANGLE) controler.setCohesionAngle(BD31_COHESION_ANGLE) controler.setAlignmentFieldDepth(BD31_ALIGNMENT_FIELD_DEPTH) controler.setAlignmentAngle(BD31_ALIGNMENT_ANGLE) controler.setAntiEnemyFieldDepth(BD31_ANTI_ENEMY_FIELD_DEPTH) controler.setAntiEnemyAngle(BD31_ANTI_ENEMY_ANGLE) controler.setEnemySize(BD31_ENEMY_SIZE) controler.setEnemyColor(BD31_ENEMY_COLOR) controler.setEnemySeparationFieldDepth(BD31_ENEMY_SEPARATION_FIELD_DEPTH) controler.setEnemySeparationAngle(BD31_ENEMY_SEPARATION_ANGLE) controler.setIntervalSec(BD31_INTERVAL_SEC) # Generateボタンが押されたとき、指定された数でBoidとEnemyの群をつくる # Boids3.1では、VMax = VMinとして、全個体が同じ速さを持つようにしている $("#bd31_generate").click -> v = $("#bd31_boid_velocity").val() v = Math.max(v, BD31_BOID_VMIN) v = Math.min(v, BD31_BOID_VMAX) $("#bd31_boid_velocity").val(v) controler.setBoidVMax(v) controler.setBoidVMin(v) boidPop = $("#bd31_boid_population").val() boidPop = Math.max(boidPop, BD31_BOID_POPULATION_MIN) boidPop = Math.min(boidPop, BD31_BOID_POPULATION_MAX) $("#bd31_boid_population").val(boidPop) v = $("#bd31_enemy_velocity").val() v = Math.max(v, BD31_ENEMY_VMIN) v = Math.min(v, BD31_ENEMY_VMAX) $("#bd31_enemy_velocity").val(v) controler.setEnemyVMax(v) controler.setEnemyVMin(v) enemyPop = $("#bd31_enemy_population").val() enemyPop = Math.max(enemyPop, BD31_ENEMY_POPULATION_MIN) enemyPop = Math.min(enemyPop, BD31_ENEMY_POPULATION_MAX) $("#bd31_enemy_population").val(enemyPop) controler.generate(boidPop, enemyPop) # Start/Stopボタンが押されたとき、アニメーションの動作を切り替える $("#bd31_start_and_stop").click -> controler.startAndStop() |
Fieldクラス
1 2 3 4 5 6 7 8 |
# Boidsが活動する空間のクラス # このバージョンでは長方形 class Field constructor: (width, height) -> @xMin = 0 @yMin = 0 @xMax = width @yMax = height |
Creatureクラス
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# Field内の壁を避けながら移動する一般化Creeatureクラス # Boids3.0で、それまでのCreatureクラスをCreatureとBoidCreatureに分けて継承させた # Boidsの行動ルールは、このクラスを継承したBoidsCreatureクラスで実装する class Creature extends MovingAgent # 壁の衝突回避を認識する距離 @wallDetectionLength: 100 # 反発力パラメータ @wallRepulsionParam: 4 constructor: (field) -> super() # 加速度ベクトル @a = new Vector(0, 0) @field = field # 個体を動かすメソッド # 群を表すClusterクラスのmove()メソッドから呼ばれる move: (intervalInSec) -> # 壁を避ける @wallAvoidance(intervalInSec) # このクラスを継承した具象クラスでの行動は、このメソッドをオーバーライド @specificBehavior(intervalInSec) # 最終的な位置を決定 @determinePosition(intervalInSec) # 壁を避ける行動 # 壁からの距離に応じた斥力を(repulsion)想定し、x/y方向の加速度を計算 wallAvoidance: (intervalInSec) -> 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 # 加速度を考慮した速度の変化を計算 # 速度の絶対値が増え続けないようにキャップで抑える # 速度成分の符号を得るのに、Math.signがサポートされていないブラウザが多いため、 # 値/絶対値で符号を得ている @v.plusEq(@a.times intervalInSec) @v.x = @v.x / Math.abs(@v.x) * Math.min(Math.abs(@v.x), BoidCreature.vMax) @v.y = @v.y / Math.abs(@v.y) * Math.min(Math.abs(@v.y), BoidCreature.vMax) # このクラスを継承するサブクラスで、このメソッドをオーバーライドする specificBehavior: (intervalInSec) -> console.log("specificBehavior() of Creature class is abstract.") # Creatureクラスの現在位置と速度から、次の時間ステップでの位置を決定する determinePosition: (intervalInSec) -> # インターバルの間の移動量を計算 @pos.plusEq(@v.times intervalInSec) # 壁にぶつかった場合は全反射 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 |
BoidCreatureクラス
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# Boidの個体を実現するクラス # Boids3.0で、Creatureクラスを継承するように変更 # 敵を回避するためのプロパティやメソッドを追加 class BoidCreature extends Creature # private staticなクラス・プロパティ @vMax: 0 @vMin: 0 # 描画の基本となるサイズと色 @drawSize: 8 @color: "#000000" # 視野の深さと分離行動の回避角 @separationFieldDepth: 100 @separationAngle: 0 # 結合行動の変化角 @cohesionAngle: 0 # 整列高度の視野の長さと変化角 @alignmentFieldDepth: 100 @alignmentAngle: 0 # 敵を回避する視野の深さと変化角 @antiEnemyFieldDepth: 100 @antiEnemyAngle: 0 # 自分が属する群と敵の群への参照を保持する constructor: (boidCluster, enemyCluster, field) -> super(field) @boidCluster = boidCluster @enemyCluster = enemyCluster @nearestCreature = null # 個体を描画するメソッド # canvasのコンテキストを引数で受け取る draw: (ctx) -> v = @v.abs() sx = BoidCreature.drawSize * @v.x / v sy = BoidCreature.drawSize * @v.y / v p = 0.25 ctx.fillStyle = BoidCreature.color 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()メソッドから呼ばれる # intervalInSecは秒単位のアニメーション・フレームのインターバル specificBehavior: (intervalInSec) -> # 分離行動(separation) @separation() # 結合行動(cohesion) @cohesion() # 整列行動(alignment) @alignment() # 敵回避 @avoidEnemy() return # 分離行動(separation) # 前方視角180度内で一定距離内の視野の中で、最も近い個体を避けるよう方向を変える # 相手方の速度の向きには関係なく、相手側と反対方向に速度の向きをを少し変更する。 separation: -> # 回避行動の方向転換計算用cos/sin cs = Math.cos(BoidCreature.separationAngle) sn = Math.sin(BoidCreature.separationAngle) # 自身に最も近いBoidとEnemyの個体 @nearestCreature = null @nearestEnemy = null # 自分以外の群のすべての個体についてサーチ for other in @boidCluster.creatures if other != this # その個体が自分の前方にいるなら if other.isInFrontOf(this) d = Vector.distance(this.pos, other.pos) # その個体が視野の中にいるなら if d <= BoidCreature.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 # 結合行動(cohesion) # 前方視角180度以内の全個体の重心に向かって少し方向を変える cohesion: -> # 群のうち認識した個体の数とそのグループの重心 n = 0 clusterCenter = null # 結合行動の方向転換計算用cos/sin cs = Math.cos(BoidCreature.cohesionAngle) sn = Math.sin(BoidCreature.cohesionAngle) # 自分以外の群のすべての個体についてサーチ for other in @boidCluster.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 # 整列行動(alignment) # 分離行動と同じ視野内の個体群の平均速度に合わせるよう少し方向を変える alignment: -> vMean = null n = 0 # 整列行動の方向転換計算用cos/sin cs = Math.cos(BoidCreature.alignmentAngle) sn = Math.sin(BoidCreature.alignmentAngle) # 自分以外の群のすべての個体についてサーチ for other in @boidCluster.creatures if other != this # その個体が自分の前方にいるなら if other.isInFrontOf(this) d = Vector.distance(this.pos, other.pos) # その個体が視野の中にいるなら if d <= BoidCreature.alignmentFieldDepth # 既に個体が視野内にあれば平均速度の計算を続行 n++ if vMean? vMean.plusEq(other.v) else vMean = new Vector(0, 0) # 視野内に個体がいるなら速度変更の計算へ if vMean? vMean.divEq(n) 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 # Boids3.1で追加 # 敵を回避する行動で、分離行動と同じロジック avoidEnemy: -> # 回避行動の方向転換計算用cos/sin cs = Math.cos(BoidCreature.antiEnemyAngle) sn = Math.sin(BoidCreature.antiEnemyAngle) # 自身に最も近いEnemyの個体 @nearestCreature = null # Enemyのすべての個体についてサーチ for enemy in @enemyCluster.creatures # その個体が自分の前方にいるなら if enemy.isInFrontOf(this) d = Vector.distance(this.pos, enemy.pos) # その個体が視野の中にいるなら if d <= BoidCreature.antiEnemyFieldDepth # 既に個体がセットされていて、今回の個体の方が近ければ入れ替え # 最初に認識した個体ならそのままセット if @nearestEnemy? if d <= Vector.distance(@nearestEnemy.pos, this.pos) @nearestEnemy = enemy else @nearestEnemy = enemy # 近くの個体が視野内に存在するなら回避計算 if @nearestEnemy? # Enemyが自分の右にいるなら左へ、自分の左にいるなら右へ向きを変更 if @nearestEnemy.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 |
EnemyCreatureクラス
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# Boids3.1で導入 # Boidsに対する「敵」を表すクラス class EnemyCreature extends Creature # private staticなクラス・プロパティ @vMax: 0 @vMin: 0 # 描画の基本となるサイズと色 @drawSize: 8 @color: "#000000" # 視野の長さ(奥行)と変化角 @separationFieldDepth: 100 @separationAngle: 0 # 自分が属する群への参照を保持する constructor: (cluster, field) -> super(field) @enemyCluster = cluster @nearestCreature = null # 個体を描画するメソッド # canvasのコンテキストを引数で受け取る draw: (ctx) -> v = @v.abs() sx = EnemyCreature.drawSize * @v.x / v sy = EnemyCreature.drawSize * @v.y / v p = 0.25 ctx.fillStyle = EnemyCreature.color 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()メソッドから呼ばれる # intervalInSecは秒単位のアニメーション・フレームのインターバル specificBehavior: (intervalInSec) -> # Enemy同士の分離行動 @separation() # 分離行動(sepration) # BoidCreatureと同じ内容 # EnemyCreatureとの衝突のみ避ける separation: -> # 回避行動の方向転換計算用cos/sin cs = Math.cos(EnemyCreature.separationAngle) sn = Math.sin(EnemyCreature.separationAngle) # 自身に最も近い個体 @nearestCreature = null # 自分以外の群のすべての個体についてサーチ for other in @enemyCluster.creatures if other != this # その個体が自分の前方にいるなら if other.isInFrontOf(this) d = Vector.distance(this.pos, other.pos) # その個体が視野の中にいるなら if d <= EnemyCreature.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 |
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 30 31 32 |
# BoidsとEnemyの群を管理するクラスで、以下を保持する # - 群を構成する複数の個体 # - 個体を描画するcanvasのcontextオブジェクト class Cluster # canvasのcontextをプロパティに登録 constructor: (context) -> @context = context @creatures = [] return # すべての個体オブジェクトを群から削除する clearCreatures: -> @creatures = [] # 引数で与えられた個体を群に加える addCreature: (creature) -> @creatures.push(creature) return # 群の全個体を描画する # 群の全個体のdraw()メソッドを呼び出している # 画面のクリアはより上位の処理で行う draw: (context) -> for creature in @creatures creature.draw(context) return # 秒単位のインターバル受け取り、すべての個体を移動させる move: (intervalInSec) -> for creature in @creatures creature.move(intervalInSec) return |
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# Boidsアプリケーションの動作を統括するクラス # 唯一、ブラウザから直接呼び出され、初期値の設定やClusterの操作を担当 class Controler # BoidとEnemyの発生個体数の最小値と最大値 constructor: (canvas, context) -> @canvas = canvas @context = context # Boidsの活動領域 @field = new Field(canvas.width, canvas.height) # Boidの群とEnemyの群のオブジェクト @boidCluster = new Cluster(context) @enemyCluster = new Cluster(context) # アニメーション用のフラグとtimer @isRunning = false @timer = undefined # 秒単位のアニメーション・フレーム間隔 @intervalInSec = 1 # animation frame interval in seconds # 定数として扱うクラス変数を設定するメソッド群 # 壁を避けるための認識距離と斥力係数 setWallDetectionLength: (pix) -> Creature.wallDetectionLength = pix setWallRepulsionParam: (param) -> Creature.wallRepulsionParam = param # Boid個体の実体に関するパラメータ setBoidVMax: (vMax) -> BoidCreature.vMax = vMax setBoidVMin: (vMin) -> BoidCreature.vMin = vMin setBoidSize: (size) -> BoidCreature.drawSize = size setBoidColor: (color) -> BoidCreature.color = color # Boid個体の分離・結合・整列に関するパラメータ setBoidSeparationFieldDepth: (length) -> BoidCreature.separationFieldDepth = length setBoidSeparationAngle: (angle) -> BoidCreature.separationAngle = angle setCohesionAngle: (angle) -> BoidCreature.cohesionAngle = angle setAlignmentFieldDepth: (length) -> BoidCreature.alignmentFieldDepth = length setAlignmentAngle: (angle) -> BoidCreature.alignmentAngle = angle # Boid個体が敵を避ける行動のパラメータ setAntiEnemyFieldDepth: (length) -> BoidCreature.antiEnemyFieldDepth = length setAntiEnemyAngle: (angle) -> BoidCreature.antiEnemyAngle = angle # Enemy個体の実体に関するパラメータ setEnemyVMax: (vMax) -> EnemyCreature.vMax = vMax setEnemyVMin: (vMin) -> EnemyCreature.vMin = vMin setEnemySize: (size) -> EnemyCreature.drawSize = size setEnemyColor: (color) -> EnemyCreature.color = color # Enemy個体の分離に関するパラメータ setEnemySeparationFieldDepth: (length) -> EnemyCreature.separationFieldDepth = length setEnemySeparationAngle: (angle) -> EnemyCreature.separationAngle = angle # アニメーションフレーム間隔 setIntervalSec: (@intervalInSec) -> # 一つのBoidの個体を発生させるメソッド # 位置はFieldに収まるよう、速度はvMax~vMinの範囲でランダム createBoidCreature: -> cr = new BoidCreature(@boidCluster, @enemyCluster, @field) x = Math.floor(Math.random() * @field.xMax) y = Math.floor(Math.random() * @field.yMax) v = Math.random() * (BoidCreature.vMax - BoidCreature.vMin) + BoidCreature.vMin ang = Math.random() * Math.PI * 2 cr.setPosition(x, y) cr.setVelocity(v * Math.cos(ang), v * Math.sin(ang)) return cr # Boids3.1で追加 # 一つの敵の個体を発生させるメソッド # 位置はFieldに収まるよう、速度はvMax~vMinの範囲でランダム createEnemyCreature: -> cr = new EnemyCreature(@enemyCluster, @field) x = Math.floor(Math.random() * @field.xMax) y = Math.floor(Math.random() * @field.yMax) v = Math.random() * (EnemyCreature.vMax - EnemyCreature.vMin) + EnemyCreature.vMin ang = Math.random() * Math.PI * 2 cr.setPosition(x, y) cr.setVelocity(v * Math.cos(ang), v * Math.sin(ang)) return cr # Boids3.1で変更 # 引数で指定された数のBoidとEnemyの個体を発生させ、Clusterオブジェクトに登録する generate: (boidPopulation, enemyPopulation) -> @enemyCluster.clearCreatures() if enemyPopulation > 0 for i in [1..enemyPopulation] cr = @createEnemyCreature() @enemyCluster.addCreature(cr) @boidCluster.clearCreatures() for i in [1..boidPopulation] cr = @createBoidCreature() @boidCluster.addCreature(cr) # 個体発生後に画面をクリアして描画 @context.fillStyle = "#000080" @context.fillRect(0, 0, @canvas.width, @canvas.height) @boidCluster.draw(@context) @enemyCluster.draw(@context) # Boids3.1でEnemyCreatureにも対応 # アニメーションの開始/停止の切り替え startAndStop: () -> if @isRunning clearInterval(@timer) @isRunning = false else @timer = setInterval => # 移動・再描画前に画面をクリア @context.fillStyle = "#000080" @context.fillRect(0, 0, @canvas.width, @canvas.height) @boidCluster.move(@intervalInSec) @boidCluster.draw(@context) @enemyCluster.move(@intervalInSec) @enemyCluster.draw(@context) return , @intervalInSec * 1000 @isRunning = true return |