JavaScript, HTML5で『群れ』をシミュレーションするレシピ (ボイド)

おっ立ち野郎が2014年12月30日21:01:51に投稿しました。

JavaScript

8

お気に入り

準備中

Views

このレシピで作るもの

ボイドについて

ボイドを知っていますか?ボイド(Boids)はCraig Raynoldsによって発表された人工生命シミュレーションプログラムです。Boidsとはによると、以下のように記述されています。

Boid(ボイド)とは、1987年にCraig Raynoldsによって発表された理論です。 この理論は、3つのルールを規定するだけで鳥の群れをシミュレーションできるというものです。 ちなみにBoidという名の由来は、鳥もどきという意味の言葉birdoid(バードイド)が短くなりこのように呼ばれるようになりました。

3つのルールだけで簡単に群れを表現することができます。

JavaScriptで作っていく

今回はボイドをJavaScriptで作っていきます。


目次

今回作るボイドの動作の流れ

実際に書く前にこれから作るボイドがどんなものか少し考えてみましょう。ボイドには最低限、以下の3つのルールが必要です。

  • ルール1: ボイドは群れの中心へ向かおうとする
  • ルール2: ボイドは他のボイドと最低限の距離を取ろうとする
  • ルール3: ボイドは群れの平均速度ベクトルに合わせようとする

ではこれを踏まえて全体の動作の流れを考えます。

  1. ボイドをキャンバスに描画する
  2. 全ボイドに3つのルールを適用し、それぞれの速度を更新
  3. 更新された速度に従って座標の変更
  4. 1へ

早速書いていきましょう。

プログラミングの下準備

今回の簡易ボイドに必要なファイルは以下。

  • index.html
  • main.js

今回はmain.jsをメインに書いていくので、index.htmlをちゃちゃっと書いておきましょう。

index.htmlを作る

index.htmlを以下のように記述してください。

<!DOCTYPE html>
<meta charset="utf-8">
<title>シンプルなボイド</title>
<style>body{margin:0;padding:0;}</style>
<canvas id="world"></canvas>
<script src="main.js"></script>

HTML5の宣言、文字コード、タイトルとちょっとしたスタイルシート、それからキャンバス要素を記述してmain.jsを読み込みます。

早速main.jsを書いていきましょう。

main.jsを書いていく

コード全体の構成を考える

引数や内容は詳しく考えずに抽象的に考えてみましょう。

こんな感じでボイドは完成します。

// グローバル変数の宣言

window.onload = function() {
    // 初期化
    setInterval(simulate, 1000/FPS);
};

/**
 * 1000/FPS毎に呼び出される。ループの内容。
 */
var simulate = function() {
    draw();  // ボイドの描画
    move();  // ボイドの座標の更新
};

/**
 * ボイドの描画
 */
var draw = function() {
    // キャンバスに全てのボイドを描画
};

/**
 * ボイドの座標の更新
 */
var move = function() {
    // 全てのボイドに3つのルールを適用
    // 速度の微調整(壁から出た場合と最大速度を超えていた場合の処理)
    // ボイドの速度より座標を更新
};

var rule1 = function(index) {
    // ボイドは群れの中心へ向かおうとする
};

var rule2 = function(index) {
    // ボイドは他のボイドと最低限の距離を取ろうとする
};

var rule3 = function(index) {
    // ボイドは群れの平均速度ベクトルに合わせようとする
};

/**
 * ボイドとボイドの距離を返す
 */
var getDistance = function(b1, b2) {
    return ボイド間の距離
};
  • winload.onloadにボイドを初期化を処理する関数を指定します。
  • simulate関数はボイドの描画と座標の更新をします。(1000/指定したフレーム数)ごとに実行されます。
  • draw関数はボイドの描画
  • move関数はボイドへのルールの適用、座標の更新をします。
  • rule1~3は群れを作るための最低限のルールです。ボイドの速度を他のボイドの状態から足し引きします。

では実際にコードを書いていきましょう。

グローバル変数を宣言する

まず定数などをグローバル変数として宣言しましょう。

var FPS = 30;                   // フレームレート
var SCREEN_SIZE = 500;          // 画面サイズ
var NUM_BOIDS = 100;            // ボイドの数
var BOID_SIZE = 5;              // ボイドの大きさ
var MAX_SPEED = 7;              // ボイドの最大速度
var canvas = document.getElementById('world');
var ctx = canvas.getContext('2d');
var boids = [];                 // ボイド
  • FPSは一秒間に何回simulate関数を呼ぶかの数です
  • SCRREEN_SIZEはキャンバスのサイズです
  • NUM_BOIDSはボイドの数です
  • MAX_SPEEDはボイドの最大速度です
  • canvasはキャンバス要素です
  • ctxはキャンバスのコンテキストです。キャンバスの筆のようなものです。
  • boidsは全てのボイドを格納する配列です。

ページが読み込まれた時の処理を書こう

window.onloadに代入する関数を書きます。これの関数はボイドの状態を初期化します。

window.onload = function() {
    /* 初期化 */
    canvas.width = canvas.height = SCREEN_SIZE;
    ctx.fillStyle = "rgba(33, 33, 33, 0.8)"; // ボイドの色
    for (var i=0; i<NUM_BOIDS; ++i) {
        boids[i] = {
            x: Math.random()*SCREEN_SIZE, // x座標
            y: Math.random()*SCREEN_SIZE, // y座標
            vx: 0,                        // x方向の速度
            vy: 0                         // y方向の速度
        }
    }
    /* ループ開始 */
    setInterval(simulate, 1000/FPS);
};
  • canvas.width, canvas.heightを指定することでキャンバスの大きさを変更することができます。
  • ctx.fillStyleで指定している色はつまりボイドの色です。透明度を0.8にすることで少しだけですが重なっている場合を見て判断することができます。
  • for文ではボイドを初期化しています。ボイドオブジェクトは座標の情報、速度の情報のみを持っています。
  • setIntervalでsimulate関数が1秒間にFPS回実行されるようにしています。

メインループ処理の内容を書こう

ここの処理はシンプルです。draw関数とmove関数を呼び出すだけです。

var simulate = function() {
    draw();                     // ボイドの描画
    move();                     // ボイドの座標の更新
};

ボイドの描画用関数を書こう

boidsに格納された全てのボイドをキャンバスに描画します。

/**
 * ボイドの描画
 */
var draw = function() {
    ctx.clearRect(0, 0, SCREEN_SIZE, SCREEN_SIZE); // 画面をクリア
    // 全てのボイドの描画
    for (var i=0,len=boids.length; i<len; ++i) {
        ctx.fillRect(boids[i].x-BOID_SIZE/2, boids[i].y-BOID_SIZE/2, BOID_SIZE, BOID_SIZE);
    }
};

ボイドの位置を更新する関数を書こう

各ボイドの速度を更新し、座標を更新します。

/**
 * ボイドの位置の更新
 */
var move = function() {
    for (var i=0,len=boids.length; i<len; ++i) {
        // ルールを適用して速さを変更
        rule1(i);    // 近くの群れの真ん中に向かおうとする
        rule2(i);    // ボイドは他のボイドと距離を取ろうとする
        rule3(i);    // ボイドは他のボイドの平均速度に合わせようとする
        // limit speed
        var b = boids[i];
        var speed = Math.sqrt(b.vx*b.vx + b.vy*b.vy);
        if (speed >= MAX_SPEED) {
            var r = MAX_SPEED / speed;
            b.vx *= r;
            b.vy *= r;
        }
        // 壁の外に出てしまった場合速度を内側へ向ける
        if (b.x<0 && b.vx<0 || b.x>SCREEN_SIZE && b.vx>0) b.vx *= -1;
        if (b.y<0 && b.vy<0 || b.y>SCREEN_SIZE && b.vy>0) b.vy *= -1;
        // 座標の更新
        b.x += b.vx;
        b.y += b.vy;
    }    
};
  • rule1,2,3の関数を呼び出し、ボイドへ基本的なルールをあてていきます。
  • ルールを適用した後は速さがMAX_SPEEDを超えていないかチェックし、超えていたら調整します。
  • もし壁の外に出ていて速度が壁の外向きであれば内側へ戻します。
  • 最後に座標の更新をします

ルール1: ボイドは群れの中心へ向かおうとする

20130925045810.png

では基本的な3つのルールのうち、始めのルールを書きましょう。

各ボイドは群れの中心へ向かおうとします。このルールがあることによって群れがまとまります。今回は、簡単のために群れは最大で1つとします。つまり、全てのボイドが1つの群れに属していると考えます。

/**
 * ルール1: ボイドは近くに存在する群れの中心に向かおうとする
 */
var rule1 = function(index) {
    var c = {x: 0, y:0};        // 自分を除いた群れの真ん中
    for (var i=0,len=boids.length; i<len; ++i) {
        if (i != index) {
            c.x += boids[i].x;
            c.y += boids[i].y;
        }
    }
    c.x /= boids.length - 1;
    c.y /= boids.length - 1;
    boids[index].vx += (c.x-boids[index].x) / 100;
    boids[index].vy += (c.y-boids[index].y) / 100;
};
  • cは群れの中心です。自分自身を除いて計算しています。
  • 最後に速度に群れの中心から自分自身の座標を引いたものを足しています。この時100で割ることによって少しずつ中心へ速度ベクトルが向くようにしています。

ルール2: ボイドは他のボイドと最低限の距離を取ろうとする

20130925045826.png

各ボイドは他のボイドと密着しないようにします。つまり少しだけ間隔を開けようとします。この関数がなければボイド達が重なりあってしまいます。今回は距離間隔が5px以内になったら離れるように速度を調節しています。

/**
 * ルール2: ボイドは隣のボイドとちょっとだけ距離をとろうとする
 */
var rule2 = function(index) {
    for (var i=0,len=boids.length; i<len; ++i) {
        if (i != index) {
            var d = getDistance(boids[i], boids[index]); // ボイド間の距離
            if (d < 5) {
                boids[index].vx -= boids[i].x - boids[index].x;
                boids[index].vy -= boids[i].y - boids[index].y;
            }
        }
    }
};
  • getDistance関数はボイド間の距離を返す関数です。あとで書きます。

ルール3: ボイドは群れの平均速度ベクトルに合わせようとする

20130925045834.png

最後のルールです。各ボイドは群れの平均速度ベクトルに合わせようとします。この関数があることによって集まったボイド達は統一性のある動きをとるようになります。

/**
 * ルール3: ボイドは近くのボイドの平均速度に合わせようとする
 */
var rule3 = function(index) {
    var pv = {x: 0, y: 0};      // 自分を除いた群れの平均速度
    for (var i=0,len=boids.length; i<len; ++i) {
        if (i != index) {
            pv.x += boids[i].vx;
            pv.y += boids[i].vy;
        }
    }
    pv.x /= boids.length - 1;
    pv.y /= boids.length - 1;
    boids[index].vx += (pv.x-boids[index].vx) / 8;
    boids[index].vy += (pv.y-boids[index].vy) / 8;
};
  • 今回は8で割っていますが、そこの数値は適当に調節してみてください。

2つのボイド間の距離を返す関数を書こう

この関数はボイド間の距離を返します。

/**
 * 2つのボイド間の距離
 */
var getDistance = function(b1, b2) {
    var x = b1.x - b2.x;
    var y = b1.y - b2.y;
    return Math.sqrt(x*x + y*y);
};

ボイド完成!!

いかがでしたでしょうか。

これで簡易ボイドが完成しました。お疲れ様でした。

ここからどんどんカスタマイズしていくことにより、敵を作ったり餌をあげたり、より自然な感じを出すことなどなどができます。オリジナルの群れを作るのはとても楽しいですね。

このレシピを書いたシェフ

おっ立ち野郎

@ottatiyarou

コードレシピの管理人です。
「for文とか関数とかの基礎はやったけど、実際アプリとかどうやって作るのか分からない」を解決するようなサービスにしていきたいです。


このシェフが書いたレシピ