いつ録画したもんだったか分りませんが、先日NHKの番組で、ホッケの群について学びました。
その中で、Boidsというものを知りました。
面白そうだったので、なでしこさんでもできるかなぁ? と、やってみることにしました。
Boidsとは?
グレイグ・レイノルズさんという人が考えた、個々に複雑な命令を与えなくても、全体にごく簡単なルールを適用するだけで、複雑な群の動きが再現出来るというものです。
bird-oidで、鳥っぽいの。縮めて、群なので複数形で、Boids。鳥モドキなんですが、TVでは、魚の群の仕組みを解明するのに使ってました。
私はこの時初めて知りましたが、ゲームや映画で集団を表現するのに使われる、有名な理論だそうです。
なので、知ってる方も多いのでしょうが、なんかこんな感じ。
- ぶつからない(分離)
- 集まりたい(結合)
- 一緒に動く(整列)
たった、こんだけだと言うんですね。しゅごい!
プログラムにする準備
全体の流れは、こんな感じでしょうかね。
- 準備。
- 画面を用意する。
- 群を作る。
- ボイド理論の3つのルールを作る。
- 回避行動(分離)
- 集合行動(結合)
- 整列行動(整列)
- ルールの反映と再描画。
- 群の各個体にルールを当てはめ、移動量を決める。
- 移動量はバカみたいな数にならないよう、最大値を超えないようにする。
- 画面の外には出ないようにする。
- 画面に描画する。
※3の一連を、タイマーでループし続けることで、アニメーションする。
画面を用意する
希望としては、鳥よりは魚・・・メダカっぽくしたいです。メダカを飼ってるんですw 池(?)にあたる画面は、取りあえず簡易エディタのキャンバスをそのまま使うことにします。
公式ページ標準の310*150ではさすがに狭いので、タートルグラフィックス用大画面とかでもいいんですが、今回は、EZNavi.netさんのエディタを使うことにします。
canvasを使う場合、こちらの方が広く自由に使えていい感じです。
群を作る
群は配列で、その中に各個体のデータを入れておくことにします。
色と、初期位置と、最初の移動量を、ランダムに決めて与えます。
#---設定------
群=空配列。個体数=30。体長=20。体高=6。最大速度=10。
#---群作成-----
(個体数)回
No=回数-1。
位置x=((池幅-体長)の乱数)+体長/2。位置y=((池高-体高)の乱数)+体高/2。
x移動量=((最大速度*2+1)の乱数)-最大速度。y移動量=((最大速度*2+1)の乱数)-最大速度。
ランダム色=(0xFFFFFFの乱数)のHEXを6でゼロ埋め。ランダム色=「#」&ランダム色。
群[No]={"x":位置x,"y":位置y,"x移動量":x移動量,"y移動量":y移動量,"元x":位置x,"元y":位置y,"色":ランダム色}
ここまで。
配列を使うと群[No]["x"]
みたいになって、なんか急に日本語感が薄れるけど、「群」の一員である「No」の「"x"」みたいに頭の中で読み換えればOK。
ボイド理論の実装
効率的ではないかもですが、全部別々の関数にして、その場で移動量を適用してから、次へ行くようにしています。
「回避度」「集合度」「整列度」は、その傾向がどれだけ強く表れるかを微調整するパラメーターで、なんかあんまよく分りませんが、コンランしたくないので、1で大体なんとなく無難に動く感じにしておいて、普通にパラメーターの数値の大小で微調整出来るように・・・したつもり。
ルール1:ぶつからない(分離)
ある種当然のアレですが、仲間とぶつからないよう、近付き過ぎたら離れるように動くということですね。
その為にはまず、相手との距離を知らねばなりません。・・・って、どうやって? 見たところ、なでしこにそういう便利な命令はありません。
・・・・・ぐ、ぐふぅ。・・・さんへいほうのていり???
小学校の算数の段階で挫折しているので、そんなもん知りません。が、有り難いことにこの時代、ぐぐっと検索すれば大概のことを教えて頂けます。
ユキノはレベルアップした。「みんな大好き三平方の定理」の呪文を覚えた!
●(abとcdの|abから)距離計算
a=ab[0]。b=ab[1]。c=cd[0]。d=cd[1]。
(((c-a)^2)+((d-b)^2))の平方根で戻る。
ここまで。
で、こんな感じ? あってる??
さて今回は、20px(体長)以下を近過ぎと考え、回避行動を取るようにしようと思います。
尻尾つつかれてピュッと逃げるような、お魚っぽい動きになれば良いなと思います。
後から変えたり出来るよう、先に定義しておきます。
#---設定------
間隔={"近過":20}。回避度=1。
#---ルールその1-----
●(自身の)回避行動
(個体数)回。
もし、自身=回数-1でなければ、
相手とは変数=回数-1。
距離とは変数=[群[自身]["x"],群[自身]["y"]]と[群[相手]["x"],群[相手]["y"]]の距離計算。
もし、距離≦間隔["近過"]ならば、
群[自身]["x移動量"]=群[自身]["x移動量"]-(群[相手]["x"]-群[自身]["x"])*回避度。
群[自身]["y移動量"]=群[自身]["y移動量"]-(群[相手]["y"]-群[自身]["y"])*回避度。
ここまで。
ここまで。
ここまで。
ここまで。
相手のいる座標から、自分がいる座標を引けば、x、y方向へそれぞれ何px進めば相手と重なれるかが分るので、それをマイナスして、逆に行くようにするってことですね。
ルール2:集まりたい(結合)
自然界では、ちょっと群から離れた瞬間、餌食になってしまったりするんですよね。なので、多くの仲間がいる方向へ動き、集合するということですね。
そういうことを考えて集合しているのではなく、本能にプログラムされていて、条件反射的に行動しているのだという?
自分以外の仲間がいる座標の平均を取って、そこに向かうようにしますが、それだけだと団子になってしまうので、一定以上離れた個体は無視することにして、遠過ぎも定義します。
いい感じに群がばらけると良いなと思います。
それ以外は、回避と同じですね。マイナスではなくプラスで集まる。
そのままだと一気に中心まで走ってしまうことになるので、集合度で調整するにしても価がアレなんで、予め弱めます。
#---設定------
間隔={"近過":20,"遠過":80}。集合度=1。
#---ルールその2-----
●(自身の)集合行動
群中心とは変数={"x":0,"y":0}。仲間とは変数=0。
(個体数)回
もし、自身=回数-1でなければ、
相手とは変数=回数-1。
距離とは変数=[群[自身]["x"],群[自身]["y"]]と[群[相手]["x"],群[相手]["y"]]の距離計算。
もし、距離≦間隔["遠過"]ならば、
群中心["x"]=群中心["x"]+群[相手]["x"]。
群中心["y"]=群中心["y"]+群[相手]["y"]。
仲間=仲間+1。
ここまで。
ここまで。
ここまで。
もし、仲間=0でなければ、
群中心["x"]=群中心["x"]/仲間。
群中心["y"]=群中心["y"]/仲間。
群[自身]["x移動量"]=群[自身]["x移動量"]+(群中心["x"]-群[自身]["x"])*0.02*集合度。
群[自身]["y移動量"]=群[自身]["y移動量"]+(群中心["y"]-群[自身]["y"])*0.02*集合度。
ここまで。
ここまで。
ルール3:一緒に動く(整列)
折角集まっても、集団行動取れないとすぐまた離れてしまいますからね。群の仲間と向きや速度を合わせて動く必要があるわけです。
集団行動って言っても、号令で一斉に動くようなのとは違い、周囲を見て追随する感じですよね。
一定の範囲内にいる仲間の平均値に自身の移動量を合わせます。
集合の時と、ほぼほぼ一緒ですね。
これも、そのままだと全員がぴたーっと揃ってしまうので弱めます。
#---設定-----
整列度=1。
#---ルールその3-----
●(自身の)整列行動
群平均速度とは変数={"x方向":0,"y方向":0}。仲間とは変数=0。
(個体数)回。
もし、自身=回数-1でなければ、
相手とは変数=回数-1。
距離とは変数=[群[自身]["x"],群[自身]["y"]]と[群[相手]["x"],群[相手]["y"]]の距離計算。
もし、距離≦間隔["遠過"]ならば、
群平均速度["x方向"]=群平均速度["x方向"]+群[相手]["x移動量"]。
群平均速度["y方向"]=群平均速度["y方向"]+群[相手]["y移動量"]。
仲間=仲間+1。
ここまで。
ここまで。
ここまで。
もし、仲間=0でなければ、
群平均速度["x方向"]=群平均速度["x方向"]/仲間。
群平均速度["y方向"]=群平均速度["y方向"]/仲間。
群[自身]["x移動量"]=群[自身]["x移動量"]+(群平均速度["x方向"]-群[自身]["x移動量"])*0.1*整列度。
群[自身]["y移動量"]=群[自身]["y移動量"]+(群平均速度["y方向"]-群[自身]["y移動量"])*0.1*整列度。
ここまで。
ここまで。
ルールの反映と再描画
メインループ
順番に実行。
#---設定-----
タイマー=0.05。
#---メインループ-----
(タイマー)秒毎には
(個体数)回
自身とは変数=回数-1。
自身の回避行動。自身の集合行動。自身の整列行動。
自身の移動反映。
ここまで。
移動描画。
ここまで。
移動を反映
以下の3つは、1つの関数で処理します。
最大速度以上の時、最大速度にする。
移動はマイナス方向もあるので、絶対値を見る。
v1には「絶対値」「符号」の命令があったのに、なにゆえv3では日本語化されておらぬのだろうか。
もし、(群[自身]["x移動量"]の絶対値)>最大速度ならば、群[自身]["x移動量"]=最大速度*(群[自身]["x移動量"]の符号)。
もし、(群[自身]["y移動量"]の絶対値)>最大速度ならば、群[自身]["y移動量"]=最大速度*(群[自身]["y移動量"]の符号)。
端から外に出ないようにする。
深く考えず、跳ね返るやつ。
もし、(群[自身]["x"]+群[自身]["x移動量"]<体長/2)または、(群[自身]["x"]+群[自身]["x移動量"]+体長/2>池幅)_または、(群[自身]["y"]+群[自身]["y移動量"]<体長/2)または、(群[自身]["y"]+群[自身]["y移動量"]+体高/2>池高)ならば、
群[自身]["x移動量"]=群[自身]["移動x"]*-1。
群[自身]["y移動量"]=群[自身]["移動y"]*-1。
ここまで。
今の位置を記憶して、位置を移動先に移す。
記憶した元位置は、角度計算に使う。
群[自身]["元x"]=群[自身]["x"]。群[自身]["元y"]=群[自身]["y"]
群[自身]["x"]=群[自身]["x"]+群[自身]["x移動量"]。
群[自身]["y"]=群[自身]["y"]+群[自身]["y移動量"]。
描画する
円なら簡単だったんですけど、やはり魚が○じゃ変だろうというわけで。楕円www
しかし、楕円描画が・・・よく分からない。てきとー;
しかも、ちゃんと進行方向を向かなくちゃいけません(重要)
でないと、横向きに進むとか、○の方がまだマシですからね!
というわけで、楕円を回転させなきゃなりません。
でも、大丈夫! 角度を求める術は、さっき2点間の距離を測るのと同時に学びました。
●(abとcdの)角度計算
a=ab[0]。b=ab[1]。c=cd[0]。d=cd[1]。
ARCTAN((c-a)/(d-b))
ここまで。
こんな感じであろう。
そして、
●移動描画
背景色に塗り色設定。背景色に線色設定。
[0,0,池幅,池高]に四角描画。
(個体数)回
No=回数-1。群[No]["色"]に塗り色設定。群[No]["色"]に線色設定。
方向とは変数=([群[No]["元x"],群[No]["元y"]]と[群[No]["x"],群[No]["y"]]の角度計算)。
方向=方向*-1+(90をラジアン変換)
[群[No]["x"],群[No]["y"],体長/2,体高/2,方向,0,(360をラジアン変換),0]の楕円描画。
ここまで。
ここまで。
できました! あってる??
追加機能
その後、パラメータをいじったり、壁に当たった時の処理を変えてみたり、なんやかやして、少しは魚らしくなったような気もしないこともないような、でもやっぱりメダカらしくは無いような;
本当は、壁に当たる前に自然に方向転換出来ると良いのだろう。
追加で、餌をやる(?)機能を付けました。
画面上をクリック(タッチ)すると、群がそこに集まるってだけのこってす。
あっという間に集まってきて、めちゃくちゃ群がって、餌が無くなるともう興味ないって感じで散っていく感じは、それっぽいと思いますw
しかし、クリックした瞬間に魚たち(楕円)が太くなるのはなぜなんだ;
餌キター! みたいな感じが出ていて面白いから、ま、いいけど(えっ;)
動作確認
なでしこアプリ掲示板に上げました。
「なでしこ3実行」で動きます。試してくれたら嬉しいです。
一応ソースも見られます。パラメーター変えてお試しも出来ます。
参考
わかりやすし!
ここをとても参考にさせて頂きました。Javascript全然マッタクこれっぽっちも分りませんが、ボイド理論の実装部分はタブンほぼこれ(のつもり)
これに、遠過ぎの場合の処理を加えたような感じです(のつもり)
おわります
なでしこさんでも、群を作ること出来ました! やったね☆
しかし、動きがメダカっぽいかというとビミョー。
3つのルールで群は表現出来るけど、やはり生き物の特性に合わせて個別にルールを追加しなきゃなのでしょう(ホッケも2つのルールを追加して再現してました)
それでも、眺めていると楽しい。時折一匹だけ群からはぐれてしまったのが出たりして、「ああっ、早く誰か迎えに行って上げて!」と、つい心配したりしてしまう(バカ)
ちなみに、ナニか間違っていたり変だったりしたら、それはなでしこさんのせいではなく私の問題です。なにぶんドシロウトな上に、本文中にも記した通りのおつむのクオリティなので;
もし気付いたことがあれば、お教え頂ければ幸いです。