In case Flash no longer exists; a copy of this site is included in the Flashpoint archive's "ultimate" collection.

Dead Code Preservation :: Archived AS3 works from wonderfl.net

降り注ぐボール / Falling balls

ライブラリ無しで剛体物理演算をしてみました。
参考までにどうぞ。
(一通りコメントに目を通すと色々分かると思います)

このシミュレーションでは三つ以上の物体が同時に
衝突した時のことを考えていません(=APEみたいな感じ)。
そのため積み上げ処理には極端に弱いです。
安定して積み上げたい場合は接触点を保持しておいて、
力積を連立方程式として解いてやると安定します(=Box2Dとか)。

図形を追加したい場合はその図形の質量属性と描画関数を作って、
衝突検出の部分にその図形を追加してやると動きます。
Get Adobe Flash player
by saharan 28 Mar 2011
/**
 * Copyright saharan ( http://wonderfl.net/user/saharan )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/sQhB
 */

/* 
 * 降り注ぐボール / Falling balls
 * 
 * ライブラリ無しで剛体物理演算をしてみました。
 * 参考までにどうぞ。
 * (一通りコメントに目を通すと色々分かると思います)
 *
 * このシミュレーションでは三つ以上の物体が
 * 同時に衝突した時のことを考えていません(=APEみたいな感じ)。
 * そのため積み上げ処理には極端に弱いです。
 * 安定して積み上げたい場合は接触点を保持しておいて、
 * 力積を連立方程式として解いてやると安定します(=Box2Dとか)。
 *
 * 図形を追加したい場合はその図形の質量属性と描画関数を作って、
 * 衝突検出の部分にその図形を追加してやると動きます。
 */
package {
    import flash.events.MouseEvent;
    import flash.events.Event;
    import flash.display.Sprite;
    import net.hires.debug.Stats;
    [SWF(frameRate="60")]

    public class Physics extends Sprite {
        private var bodies:Vector.<Body>;
        private var gravity:Vector2D;
        private var dt:Number;
        private const WALL_BODY:Circle = new Circle(0, 0, 1, 0, 0, 0.5, 0.5);
        public function Physics() {
            initialize();
        }

        private function initialize():void {
            bodies = new Vector.<Body>();
            // Circle(x座標, y座標, 半径, 角度, 密度, 反発係数, 摩擦係数)
            for(var i:int = 0; i < 3; i++) {
                bodies.push(new Circle(2.5 + i * 3.5, 18, 0.5, 0, 0, 0.5, 0.5));
                bodies.push(new Circle(20.35 - i * 3.5, 18, 0.5, 0, 0, 0.5, 0.5));
            }
            bodies.push(new Circle(11.625, 12, 2, 0, 0, 0.5, 0.5));
            gravity = new Vector2D(0, 9.80665);
            dt = 1 / 60; // フレームレートにあわせて変更
            addEventListener(Event.ENTER_FRAME, frame);
            var s:Stats = new Stats();
            s.alpha = 0.8;
            addChild(s);
        }

        private function frame(e:Event):void {
            graphics.clear();
            graphics.beginFill(0x282828);
            graphics.drawRect(0, 0, 465, 465);
            var b:Body;
            for each(b in bodies) {
                b.draw(graphics, 20); // 20px = 1m
            }
            if(Math.random() > 0.5) {
                b = new Circle(11.625 + Math.random() * 20 - 10, -3,
                    Math.random() * 0.5 + 0.25, 0, 1, Math.random(), Math.random());
                b.linearVelocity.setVector(Math.random() * 10 - 5, Math.random() * 2);
                b.angularVelocity = Math.random() * 4 - 2;
                bodies.push(b);
            }
            for each(b in bodies) {
                b.preMove(gravity, dt);
            }
            broadPhase();
            for each(b in bodies) {
                b.postMove(dt);
            }
        }

        private function broadPhase():void { // 広域衝突判定(総当り = O(n^2))
            for(var i:int = 0; i < bodies.length; i++) {
                const b:Body = bodies[i];
                for(var j:int = 0; j < i; j++) {
                    narrowPhase(b, bodies[j]);
                }
                if(b is Circle) {
                    const c:Circle = b as Circle;
                    // 壁境界
                    if(c.position.x - c.radius < 0) {
                        solve(c, WALL_BODY, new Vector2D(0, c.position.y),
                            new Vector2D(1, 0));
                    }
                    if(c.position.x + c.radius > 23.25) {
                        solve(c, WALL_BODY, new Vector2D(23.25, c.position.y),
                            new Vector2D(-1, 0));
                    }
                    if(c.position.y - c.radius > 23.25) {
                        bodies.splice(i, 1);
                        i--;
                    }
                }
            }
        }

        private function narrowPhase(b1:Body, b2:Body):void { // 詳細衝突判定
            
            // 図形を追加したならここを弄る
            
            if(b1 is Circle && b2 is Circle) { // 両方丸だった
                const c1:Circle = b1 as Circle;
                const c2:Circle = b2 as Circle;
                if(c1.position.x + c1.radius < c2.position.x - c2.radius)
                    return;
                if(c1.position.y + c1.radius < c2.position.y - c2.radius)
                    return; // AABBもどき
                var d:Vector2D = c1.position.sub(c2.position);
                if(d.length() < c1.radius + c2.radius) { // 接触あり
                    d.normalize();
                    solve(c1, c2, c1.position.add(d.mul(-c1.radius)), d);
                }
            }
        }

        private function solve(b1:Body, b2:Body, p:Vector2D,
            n:Vector2D):void { // 重要! いわゆる物理の公式で2物体間の衝突を計算
                               // p = 衝突座標, n = 法線ベクトル
            // 相乗平均で反発係数を算出
            const restitution:Number = Math.sqrt(b1.restitution * b2.restitution);
            
            // 相乗平均で摩擦係数を算出
            const friction:Number = Math.sqrt(b1.friction * b2.friction);
            
            // 接線ベクトル
            const t:Vector2D = new Vector2D(n.y, -n.x);
            
            // 相対速度
            const relVel:Vector2D = calcRelativeVelocity(b1, b2, p);
            
            // 法線方向の相対速度(相対速度N)
            const dotN:Number = n.dot(relVel);
            // 接線方向の相対速度(相対速度T)
            const dotT:Number = t.dot(relVel);
            
            // 法線方向の適正質量(適正質量N)
            const massN:Number = calcEffectiveMass(b1, b2, p, n);
            // 接線方向の適正質量(適正質量T)
            const massT:Number = calcEffectiveMass(b1, b2, p, t);
            
            // 衝突後の理想相対速度N
            var idealN:Number = 0;
            if(dotN < -0.5) idealN = -dotN * restitution;
            
            // ((理想相対速度N - 現在の相対速度N) * 適正質量N)を力積Nとする
            var impulseN:Number = (idealN - dotN) * massN;
            if(impulseN < 0) impulseN = 0;
            
            b1.applyImpulse(n.mul(impulseN), // 法線方向の力積適応
                p.sub(b1.position));
            b2.applyImpulse(n.mul(-impulseN), // 作用・反作用の法則
                p.sub(b2.position));
            
            // (-現在の相対速度T * 適正質量T)で完全にずれを打ち消す力積T
            // しかし摩擦力には上限があるので(力積N * 摩擦係数)でクランプ
            var impulseT:Number = clamp(massT * -dotT,
                -impulseN * friction, impulseN * friction);
            
            b1.applyImpulse(t.mul(impulseT), // 接線方向の力積適応
                p.sub(b1.position));
            b2.applyImpulse(t.mul(-impulseT), // 作用・反作用の法則
                p.sub(b2.position));
            
            // Debug
            // graphics.lineStyle(1, 0);
            // graphics.moveTo(p.x * 20, p.y * 20);
            // graphics.lineTo(p.x * 20 + n.x * 5, p.y * 20 + n.y * 5);
            // graphics.lineStyle();
            // graphics.beginFill(0xff0000);
            // graphics.drawCircle(p.x * 20, p.y * 20, 2);
            // graphics.endFill();
        }

        private function clamp(v:Number, min:Number, max:Number):Number {
            return v < min ? min : v > max ? max: v;
        }

        private function calcRelativeVelocity(b1:Body, b2:Body,
            p:Vector2D):Vector2D { // 相対速度を計算
            var rel:Vector2D = new Vector2D();
            rel.addEqual(b1.linearVelocity);
            rel.addEqual(p.sub(b1.position).crossReverse(b1.angularVelocity));
            rel.subEqual(b2.linearVelocity);
            rel.subEqual(p.sub(b2.position).crossReverse(b2.angularVelocity));
            return rel;
        }

        private function calcEffectiveMass(b1:Body, b2:Body,
            p:Vector2D, n:Vector2D):Number { // 適正な質量を計算
            
            // 物体に与えた力は速度に変換される際質量(= M)で割られるので、
            // 与えた力より実際は小さく(M > 1)あるいは大きく(M < 1)反映されてしまう。
            // しかし適当な値(この場合はM)を力に掛けてやることで元の力をそのまま速度に反映できる。
            // (実際は質量だけでなく慣性モーメントも影響してきます)
            
            const r1:Vector2D = p.sub(b1.position); // それぞれに対する相対位置
            const r2:Vector2D = p.sub(b2.position);
            // 相対位置と法線の内積(= 法線方向に力を与えたときの回りにくさ)を計算
            const dot1:Number = n.dot(r1);
            const dot2:Number = n.dot(r2);
            // 1 / ...になっているのは逆数を元に戻すため。
            // 逆数を使えば無限大の質量も表現できる(1 / ∞ = 0)
            return 1 / (
                // それぞれの質量の逆数を加算
                b1.invMass +
                b2.invMass +
                // 慣性モーメント・相対位置・法線から
                // 物体の角速度に与える影響を計算
                b1.invInertia * (r1.x * r1.x + r1.y * r1.y - dot1 * dot1) +
                b2.invInertia * (r2.x * r2.x + r2.y * r2.y - dot2 * dot2)
                );
        }
    }
}

import flash.display.Graphics;

class Body { // 剛体
    public var mass:Number; // 質量(kg)
    public var invMass:Number; // 質量の逆数
    public var inertia:Number; // 慣性モーメント(kg・m^2)
    public var invInertia:Number; // 慣性モーメントの逆数
    public var position:Vector2D; // 位置
    public var linearVelocity:Vector2D; // 並進速度
    public var rotation:Number; // 角度
    public var angularVelocity:Number; // 角速度
    public var restitution:Number; // 反発係数
    public var friction:Number; // 摩擦係数
    public var color:uint;

    public function applyImpulse(force:Vector2D,
        relativePosition:Vector2D):void { // 剛体に力を与える
        // 並進加速度は位置に関係なく加算される
        linearVelocity.x += force.x * invMass;
        linearVelocity.y += force.y * invMass;
        // 角加速度は相対位置と力のベクトルとの外積で求まる
        angularVelocity += relativePosition.cross3D(force) * invInertia;
    }

    public function draw(g:Graphics, scale:Number):void {} // For override.

    public function preMove(gravity:Vector2D, dt:Number):void {
        if(mass > 0) {
            linearVelocity.addEqual(gravity.mul(dt));
        } else { // 固定物体
            linearVelocity.setVector(0, 0);
            angularVelocity = 0;
        }
    }

    public function postMove(dt:Number):void {
        if(mass > 0) {
            position.addEqual(linearVelocity.mul(dt));
            rotation += angularVelocity * dt;
        }
    }

}

class Circle extends Body { // 円(2Dの球体)
    public var radius:Number; // 半径

    public function Circle(x:Number, y:Number, radius:Number, rotation:Number,
        density:Number, restitution:Number, friction:Number) {
        position = new Vector2D(x, y);
        this.radius = radius;
        this.rotation = rotation;
        this.restitution = restitution;
        this.friction = friction;
        linearVelocity = new Vector2D();
        angularVelocity = 0;
        if(density > 0) {
            mass = radius * radius * Math.PI * density; // 密度 * 面積 = 質量
            invMass = 1 / mass;
            inertia = 2 / 5 * radius * radius * mass; // 慣性モーメント(円ではなく球体とする)
            invInertia = 1 / inertia;
        } else { // 密度が0以下なら固定物体に
            mass = invMass = 0;
            inertia = invInertia = 0;
        }
        color = (Math.random() * 128 + 96) << 16 | (Math.random() * 128 + 96) << 8 | (Math.random() * 128 + 96);
    }

    public override function draw(g:Graphics, scale:Number):void {
        // Standard
        // g.lineStyle(1, 0x000000);
        // g.beginFill(color);
        // g.drawCircle(position.x * scale, position.y * scale, radius * scale);
        // g.endFill();
        // g.moveTo(position.x * scale, position.y * scale);
        // g.lineTo((position.x + Math.cos(rotation) * radius) * scale,
        //     (position.y + Math.sin(rotation) * radius) * scale);
        
        // QB (Colored from http://wonderfl.net/c/qoTQ)
        // 僕と契約して、物理演算少女に(ry
        //     Hi, let's contract to me and become physics g...(abbreviated)
        g.beginFill(0xff0060);
        g.drawCircle(position.x * scale, position.y * scale,radius * scale);
        g.endFill();
        g.beginFill(0xc00000);
        g.drawCircle(position.x * scale, position.y * scale, radius * scale * 0.625);
        g.endFill();
        g.beginFill(0xffffff);
        g.drawCircle((position.x + Math.cos(rotation - 0.5) * radius * 0.5) * scale,
            (position.y + Math.sin(rotation - 0.5) * radius * 0.5) * scale, radius * scale * 0.25);
        g.endFill();
    }

}

class Vector2D { // 2Dベクトル
    public var x:Number; // x要素
    public var y:Number; // y要素

    public function Vector2D(x:Number = 0, y:Number = 0) {
        this.x = x;
        this.y = y;
    }

    public function setVector(x:Number, y:Number):void {
        this.x = x;
        this.y = y;
    }

    public function addEqual(v:Vector2D):void { // 加算
        x += v.x;
        y += v.y;
    }

    public function subEqual(v:Vector2D):void { // 減算
        x -= v.x;
        y -= v.y;
    }

    public function mulEqual(s:Number):void { // 乗算
        x *= s;
        y *= s;
    }

    public function add(v:Vector2D):Vector2D { // 加算
        return new Vector2D(x + v.x, y + v.y);
    }

    public function sub(v:Vector2D):Vector2D { // 減算
        return new Vector2D(x - v.x, y - v.y);
    }

    public function mul(s:Number):Vector2D { // 乗算
        return new Vector2D(x * s, y * s);
    }

    public function dot(v:Vector2D):Number { // 内積
        return x * v.x + y * v.y;
    }

    public function cross(z:Number):Vector2D { // 外積
        return new Vector2D(y * z, -x * z);
    }

    public function crossReverse(z:Number):Vector2D { // 外積
        return new Vector2D(-y * z, x * z);
    }

    public function cross3D(v:Vector2D):Number { // 外積(z要素を取り出す)
        return x * v.y - y * v.x;
    }

    public function length():Number { // ベクトルの大きさ
        return Math.sqrt(x * x + y * y);
    }

    public function normalize():void { // 正規化する
        if(x == y && y == 0)
            return; // 正規化できない
        const invLength:Number = 1 / length();
        x *= invLength;
        y *= invLength;
    }
}