[New] JavaScript ライブラリ tmlib.js で円同士の衝突プログラムを作ってみた

phiary に引っ越しました. 毎日プログラミングやWebに関する情報を発信しています! RSS 登録してたまに覗いたり, tweet やハテブして拡散してもらえると幸いです.

Pocket

以前『JavaScript ライブラリ tmlib.js で円同士の衝突プログラムを作ってみた』 というエントリーを書いたのですが, tmlib.js の仕様が色々と変わってしまい動かない状態だったので, 修正しました.

よかったら参考にしてください.

自作ライブラリ tmlib.js を使って, 円同士の衝突プログラムを作ってみました.

やっているのは,

  • 円同士の衝突判定
  • めり込みの補正
  • 衝突後の反発

です.

ちゃんと計算するなら, 衝突時間を調べてその分ベクトルの長さを調整して… ってやんないといけないのですが, 速度的に問題があるってのと難易度が上がってしまう(ホントはめんどくさい)のでちょっと省略しています.

サンプルは jsdo.it の方に移植してあるので, 実際に 動かしたり, fork してイジったりして頂けると嬉しいです.

Table of contents

Code

JavaScript 部分のコードです

/*
 * phi
 */

/*
 * グローバル
 */
var app             = null;
var visibleTrace    = true;
var circleList      = [];
var target          = null;

/*
 * 定数
 */
var SCREEN_WIDTH    = 640;
var SCREEN_HEIGHT   = 640;
var BOUNCINESS      = 0.8;
var FRICTION        = 0.99;
var GRAVITY         = tm.geom.Vector2(0, 0.8);
var CIRCLE_RADIUS   = 30;
var CIRCLE_MAX_NUM  = 15;
var CIRCLE_PURSUIT_RATE = 0.25;  // 収束率


/*
 * プレロード
 */
tm.preload(function() {
    
});

/*
 * メイン処理
 */
tm.main(function() {
    app = tm.app.CanvasApp("#world");
    app.resize(SCREEN_WIDTH, SCREEN_HEIGHT);
    app.fitWindow();
    app.background = "rgba(0, 0, 0, 0.25)";
    
    for (var i=0; i<CIRCLE_MAX_NUM; ++i) {
        var circle = Circle();
        app.currentScene.addChild(circle);
        circleList.push(circle);
    }
    
    for (var i=0; i<circleList.length-1; ++i) {
        var target = circleList&#91;i&#93;;
        for (var j=i+1; j<circleList.length; ++j) {
            var other = circleList&#91;j&#93;;
            target.collision.add(other);
        }
    }
    
    /*
    // Stats
    app.enableStats();
    
    // dat.GUI
    var gui = app.enableDatGUI();
    if (gui) {
        gui.add(window, "explode");
        gui.add(window, "visibleTrace").setValue(true).onChange(function(){
            app.background = (visibleTrace) ? "rgba(0, 0, 0, 0.25)" : "rgba(0, 0, 0, 1)";
        });
        gui.add(window, "BOUNCINESS", 0, 1, 0.1);
        gui.add(window, "FRICTION", 0, 1, 0.01);
        
        // gravity フォルダ
        var gravityFolder = gui.addFolder("gravity");
        gravityFolder.add(GRAVITY, "x", -2, 2, 0.1);
        gravityFolder.add(GRAVITY, "y", -2, 2, 0.1);
        gravityFolder.open();
    }
    */
    
    app.update = function() {
        var scene = this.currentScene;
        var key = this.keyboard;
        // ポーズ
        if (key.getKeyDown("space") == true) {
            (scene.isUpdate == true) ? scene.sleep() : scene.wakeUp();
        }
    }
    
    // mdlclick でキャプチャ
    tm.dom.Element(app.getElement()).event.mdlclick(function() {
        app.canvas.saveAsImage();
    });
    
    app.run();
});


window.explode = function() {
    for (var i=0; i<circleList.length; ++i) {
        var circle = circleList&#91;i&#93;;
        circle.explode();
    }
};


/*
 * サークルクラス
 */
var Circle = tm.createClass({
    superClass: tm.app.CanvasElement,
    
    init: function() {
        this.superInit(40, 40);
        
        // 位置をセット
        this.x = Math.rand(0, app.width);
        this.y = Math.rand(0, app.height);
        
        // パラメータセット
        this.radius = Math.rand(25, 50);
        this.m      = Math.PI*this.radius*this.radius;  // 面積 = 重さにする
        
        // 表示色設定
        this.colorAngle = Math.rand(0, 360);
        var canvas = document.createElement("canvas");
        var context= canvas.getContext("2d");
        var grad = context.createRadialGradient(0, 0, 0, 0, 0, this.radius);
        grad.addColorStop(0.0, "hsla({color}, 65%, 50%, 0.0)".format({color: this.colorAngle}));
        grad.addColorStop(0.95, "hsla({color}, 65%, 50%, 1.0)".format({color: this.colorAngle}));
        grad.addColorStop(1.0, "hsla({color}, 65%, 50%, 0.0)".format({color: this.colorAngle}));
        this.fillStyle  = grad;
        this.strokeStyle= "white";
        this.blendMode  = "lighter";
        
        // マウスやタッチ反応を有効化
        this.interaction;
        
        // 
        this.explode();
    },
    
    update: function(app) {
        // 掴んでいるサークルが自分だった場合
        if (this === target) {
            var p = app.pointing;
            this.position.set(this.x+this.pointing.x, this.y+this.pointing.y);
            this.velocity.set(p.dx, p.dy);
            return ;
        }
        
        this.velocity.mul(FRICTION);
        this.velocity.add(GRAVITY);
        this.position.add(this.velocity);
        
        // 
        var left    = this.radius;
        var right   = app.width-this.radius;
        var top     = this.radius;
        var bottom  = app.height-this.radius;
        if (this.x < left)  { this.x = left;    this.velocity.x*=-1; }
        if (this.x > right) { this.x = right;   this.velocity.x*=-1; }
        if (this.y < top)   { this.y = top;     this.velocity.y*=-1; }
        if (this.y > bottom){ this.y = bottom;  this.velocity.y*=-1; }
    },
    
    draw: function(canvas) {
        canvas.fillCircle(0, 0, this.radius);
    },
    
    explode: function() {
        // 向きをセット
        this.velocity = tm.geom.Vector2.random(0, 360, 32);
    },
    
    oncollisionstay: function(e) {
        var other = e.other;
        var abVec = tm.geom.Vector2.sub(other.position, this.position); // 自分から相手へのベクトル
        var len = abVec.length();
        if (len == 0) return ;
        abVec.normalize();
        var distance = (this.radius + other.radius)-len;    // 自分と相手の距離
        var sinkVec  = tm.geom.Vector2.mul(abVec, distance/2);
        this.position.sub(sinkVec);
        other.position.add(sinkVec);
        
        // 向きベクトルを調整する
        var V   = tm.geom.Vector2;
        var m0  = this.m;
        var m1  = other.m;
        var e   = BOUNCINESS;
        
        var ma = ( (m1 / (m0+m1))*(1+e) ) * V.dot(V.sub(other.velocity,  this.velocity), abVec);
        var mb = ( (m0 / (m0+m1))*(1+e) ) * V.dot(V.sub( this.velocity, other.velocity), abVec);
        
        this.velocity.add( V.mul(abVec, ma) );
        other.velocity.add( V.mul(abVec, mb) );
    },
    
    onmousedown: function() {
        console.log("a");
        target = this;
        this.velocity.set(0, 0);
    },
    
    onmouseup: function() {
        target = null;
    },
});

up

Tips

衝突判定の流れ

tm.app.CanvasElement の collision に衝突判定させたい 要素を登録すると自動的に衝突判定するようになります.

    for (var i=0; i<circleList.length-1; ++i) {
        var target = circleList[i];
        for (var j=i+1; j<circleList.length; ++j) {
            var other = circleList[j];
            target.collision.add(other);
        }
    }

そして, 衝突時に collisionstay イベントを発行します.

    oncollisionstay: function(e) {
        ...
    }

めり込みの補正

めり込んだ距離の半分の長さだけそれぞれの円を移動させることでめり込んだ分を補正しています.

ベクトルの計算には自作の Vector3 クラスを使用しています. 使い方は C++ ゲームライブラリや C# の XNA にあるような Vector3 と基本同じです.

        var other = e.other;
        var abVec = tm.geom.Vector2.sub(other.position, this.position); // 自分から相手へのベクトル
        var len = abVec.length();
        if (len == 0) return ;
        abVec.normalize();
        var distance = (this.radius + other.radius)-len;    // 自分と相手の距離
        var sinkVec  = tm.geom.Vector2.mul(abVec, distance/2);
        this.position.sub(sinkVec);
        other.position.add(sinkVec);

衝突後の反発

互いの向きベクトル, 質量を考慮して衝突後の向きベクトルを計算しています.

        // 向きベクトルを調整する
        var V   = tm.geom.Vector2;
        var m0  = this.m;
        var m1  = other.m;
        var e   = BOUNCINESS;
        
        var ma = ( (m1 / (m0+m1))*(1+e) ) * V.dot(V.sub(other.velocity,  this.velocity), abVec);
        var mb = ( (m0 / (m0+m1))*(1+e) ) * V.dot(V.sub( this.velocity, other.velocity), abVec);
        
        this.velocity.add( V.mul(abVec, ma) );
        other.velocity.add( V.mul(abVec, mb) );

up

TRACK BACK URL

POST COMMENT

メールアドレスが公開されることはありません。

COMMENT