red graphics shows the last iteration number
blue graphics shows the last delta time from iteration
try pushing and stressing the little balls to the wall with the big one
/**
* Copyright Albert ( http://wonderfl.net/user/Albert )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/dWrc
*/
package
{
import flash.geom.ColorTransform;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
[SWF(backgroundColor="#DDDDDD", frameRate=60)]
public class ElasticCollisionDemo extends Sprite
{
private var simulator:Simulator;
private var timeHistory:Vector.<Number> = new Vector.<Number>();
private var iterationHistory:Vector.<int> = new Vector.<int>();
private var sizeHistory:int = 450;
public function ElasticCollisionDemo()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
initSimulation();
}
private function initSimulation() : void
{
simulator = new Simulator();
simulator.addWall( new Wall( 40, 40, 400, 10 ) );
simulator.addWall( new Wall( 400, 10, 420, 200 ) );
simulator.addWall( new Wall( 420, 200, 400, 400 ) );
simulator.addWall( new Wall( 400, 400, 10, 400 ) );
simulator.addWall( new Wall( 10, 400, 40, 40 ) );
/*simulator.addWall( new Wall( 200, 100, 100, 100 ) );
simulator.addWall( new Wall( 100, 200, 200, 200 ) );
simulator.addWall( new Wall( 200, 200, 200, 100 ) );
simulator.addWall( new Wall( 100, 100, 100, 200 ) );*/
/*simulator.addWall( new Wall( 100, 100, 200, 100 ) );
simulator.addWall( new Wall( 200, 200, 100, 200 ) );
simulator.addWall( new Wall( 200, 100, 200, 200 ) );
simulator.addWall( new Wall( 100, 200, 100, 100 ) ); */
1
simulator.addParticle( new Particle( 100, 300, 0, 0, 200, 70 ) );
for( var i:int = 0; i < 200; i++ )
{
var canAdd:Boolean = true;
var tx:Number = 50 + Math.random() * 240;
var ty:Number = 50 + Math.random() * 240;
for each( var particle:Particle in simulator.particles )
{
if( Math.abs( particle.x.x - tx ) < particle.r + 5 && Math.abs( particle.x.y - ty ) < particle.r + 5 )
{
canAdd = false;
i--;
break;
}
}
if( canAdd ) simulator.addParticle( new Particle( tx, ty, Math.random() * 900, Math.random() * 900 ) );
}
simulator.addEventListener( Simulator.STEP, onSimulationStep );
simulator.run( this, Event.ENTER_FRAME );
}
private function onSimulationStep( event:Event ) : void
{
render();
var mouse:Vec2D = new Vec2D (mouseX, mouseY);
for each (var p:Particle in simulator.particles) {
var direction:Vec2D = p.x.minus (mouse);
var distance:Number = direction.magnitude;
direction = direction.times (20 / distance / distance);
p.v.plusEquals (direction);
}
iterationHistory.push(simulator.getLastIter());
if (iterationHistory.length > sizeHistory)
iterationHistory.shift();
timeHistory.push(simulator.getLastTime());
if (timeHistory.length > sizeHistory)
timeHistory.shift();
}
private function render() : void
{
graphics.clear();
transform.colorTransform = new ColorTransform(1.01, 1.01, 1.01);
graphics.lineStyle( 1 );
for each( var wall:Wall in simulator.walls )
{
graphics.moveTo( wall.A.x, wall.A.y );
graphics.lineTo( wall.B.x, wall.B.y );
graphics.moveTo( (wall.A.x+wall.B.x)/2, (wall.A.y+wall.B.y)/2);
graphics.lineTo( (wall.A.x+wall.B.x)/2, (wall.A.y+wall.B.y)/2 );
}
for each( var particle:Particle in simulator.particles )
{
graphics.lineStyle( 1, 0x000000 );
graphics.drawCircle( particle.x.x, particle.x.y, particle.r );
graphics.lineStyle( 1, 0xcd0d0d0);
graphics.moveTo (particle.x.x, particle.x.y);
graphics.lineTo (particle.x.x + particle.v.x, particle.x.y + particle.v.y);
}
graphics.lineStyle( 2, 0x550000, 0.5 );
var histI:int=0;
{
for each( var hist:int in iterationHistory )
{
if (histI++==0)
{
graphics.moveTo (histI, Simulator.MAX_ITERATIONS + 5 - hist);
continue;
}
graphics.lineTo (histI, Simulator.MAX_ITERATIONS + 5 - hist);
}
}
graphics.lineStyle( 2, 0x000055, 0.5 );
histI=0;
for each( var histtime:Number in timeHistory )
{
if (histI++==0)
{
graphics.moveTo (histI, Simulator.MAX_ITERATIONS + 5 - histtime * 1000);
continue;
}
graphics.lineTo (histI, Simulator.MAX_ITERATIONS + 5 - histtime * 1000 );
}
}
}
}
import __AS3__.vec.Vector;
import flash.events.EventDispatcher;
import flash.events.Event;
import flash.utils.getTimer;
[Event(name="step", type="Simulator")]
class Simulator extends EventDispatcher
{
public static const MAX_ITERATIONS:uint = 100;
public static const STEP:String = "step";
public var particles:Vector.<Particle>;
public var walls:Vector.<Wall>;
//holds all the pairs that passed coarse collision detection
private var coarsePass:Vector.<ICollidablePair>;
private var time:uint;
private var lastTime:Number;
private var lastIterNum:int;
public function Simulator()
{
super();
particles = new Vector.<Particle>();
walls = new Vector.<Wall>();
}
public function addParticle( particle:Particle ) : void
{
particles.push( particle );
}
public function addWall( wall:Wall ) : void
{
walls.push( wall );
}
//advances the simulation at each dispatch of the passed event type
public function run( updateDispatcher:EventDispatcher, eventType:String = Event.ENTER_FRAME ) : void
{
time = getTimer();
updateDispatcher.addEventListener( eventType, step, false, 0, true );
}
//advances the simulation by the amount of time that has passed since the last step
private function step( event:Event ) : void
{
//delta time in milliseconds
var dtms:uint = getTimer() - time;
//delta time in seconds
var elapsed:Number = dtms / 1000;
//start this step at 0 and advance to elapsed
var t:Number = 0;
var dt:Number;
var iteration:uint;
while( t < elapsed && ++iteration <= MAX_ITERATIONS )
{
//start by trying to step over the entire remainder
dt = elapsed - t;
//neglect pairs whose bounding boxes don't overlap
doCoarsePhase( dt );
//holds the next future collision
var minPair:ICollidablePair = null;
var minT:Number = Number.POSITIVE_INFINITY;
for each( var pair:ICollidablePair in coarsePass )
{
//if the collision will happen within the current time-step
//compare the time against the current minimum
if( pair.willCollide( dt ) )
{
//if it's less, store it as the min and proceed
if( pair.timeToCollision < minT )
{
minT = pair.timeToCollision;
minPair = pair;
}
}
}
//change the actual time to integrate
if( minT < Number.POSITIVE_INFINITY ) dt = minT;
//update the simulation to the time of collision
for each( var particle:Particle in particles )
{
particle.integrate( dt - 1e-8 );
}
//resolve the collision instantaneously
if( minPair != null )
{
minPair.resolve();
}
//update time by the stepped amount
t += dt;
}
time += dtms;
lastTime = t;
lastIterNum=iteration;
dispatchEvent( new Event( Simulator.STEP ) );
}
public function getLastIter() : int
{
return lastIterNum;
}
public function getLastTime() : Number
{
return lastTime;
}
//rules out some unnecessary collision checks
private function doCoarsePhase( dt:Number ) : void
{
coarsePass = new Vector.<ICollidablePair>();
var aabb:AABB;
for each( var particle:Particle in particles )
{
//update the particle's bounding box to account for its velocity
particle.update( dt );
aabb = particle.aabb;
//check each particle against each wall
for each( var wall:Wall in walls )
{
if( aabb.isOverlapping( wall.aabb ) )
{
coarsePass.push( new ParticleWallPair( particle, wall ) );
}
}
}
var n:int = particles.length;
//check each particle against each other
for( var i:int = 0; i < n - 1; i++ )
{
var p1:Particle = particles[ i ];
aabb = p1.aabb;
for( var j:int = i + 1; j < n; j++ )
{
var p2:Particle = particles[ j ];
if( aabb.isOverlapping( p2.aabb ) )
{
coarsePass.push( new ParticleParticlePair( p1, p2 ) );
}
}
}
}
}
//describes a common interface for collision pairs
interface ICollidablePair
{
function get timeToCollision() : Number;
function willCollide( dt:Number ) : Boolean;
function resolve() : void;
}
class ParticleParticlePair implements ICollidablePair
{
public var p1:Particle;
public var p2:Particle;
private var t:Number;
public function ParticleParticlePair( p1:Particle, p2:Particle )
{
this.p1 = p1;
this.p2 = p2;
}
public function get timeToCollision() : Number
{
return t;
}
public function willCollide( dt:Number ) : Boolean
{
const EPSILON:Number = 1e-4;
//points from 1 -> 2
var dx:Vec2D = p2.x.minus( p1.x );
//if the circle's are already overlapped, return true (this brings the sim to a halt)
var c:Number = dx.dot( dx ) - ( p1.r + p2.r ) * ( p1.r + p2.r );
if( c < 0 )
{
t = EPSILON;
return true;
}
//relative velocity
var dv:Vec2D = p2.v.minus( p1.v );
var a:Number = dv.dot( dv );
if( a < EPSILON ) return false; //not moving enough toward each other to warrant a response
var b:Number = dv.dot( dx );
if( b >= 0 ) return false; //moving apart
var d:Number = b * b - a * c;
if( d < 0 ) return false; //no intersection
t = ( -b - Math.sqrt( d ) ) / a;
//circle's collide if the time of collision is within the current time-step
return t <= dt;
}
//simulation has been updated so that the particles are just colliding
public function resolve() : void
{
//points from 1 -> 2
var cn:Vec2D = p2.x.minus( p1.x );
cn.normalize();
//relative velocity
var dv:Vec2D = p2.v.minus( p1.v );
//perfectly elastic impulse
var impulse:Number = cn.dot( dv.times( -2 ) ) / cn.dot( cn.times( 1 / p1.mass + 1 / p2.mass ) );
//scale normal by the impulse
p1.v.plusEquals( cn.times( -impulse / p1.mass ) );
p2.v.plusEquals( cn.times( impulse / p2.mass ) );
//damping
p1.v.x *= p1.restitution;
p1.v.y *= p1.restitution;
p2.v.x *= p2.restitution;
p2.v.y *= p2.restitution;
}
}
class ParticleWallPair implements ICollidablePair
{
public var p:Particle;
public var w:Wall;
private var t:Number;
public function ParticleWallPair( p:Particle, w:Wall )
{
this.p = p;
this.w = w;
}
public function get timeToCollision() : Number
{
return t;
}
public function willCollide( dt:Number ) : Boolean
{
//this is line/line intersection
//A is the position of the particle
//B is the position + velocity
//together they make the segment AB
//CD is the line segment made up of the wall's end points
var A:Vec2D = p.x;
var B:Vec2D = p.x.plus( p.v );
var AB:Vec2D = B.minus( A );
//inflate the normal by the particle's radius
var normScaledRadius:Vec2D = w.normal.times( -p.r );
//push the wall segment in by this amount
var C:Vec2D = w.A.plus( normScaledRadius );
var D:Vec2D = w.B.plus( normScaledRadius );
var CD:Vec2D = D.minus( C )
var AC:Vec2D = C.minus( A );
t = w.normal.dot( AC ) / w.normal.dot( AB );
if( isNaN( t ) ) t = 0;
return t <= dt && t >= 0;
}
//simulation has been updated so that the particles are coincident
public function resolve() : void
{
var cn:Vec2D = w.normal;
//relative velocity
var dv:Vec2D = p.v;
//perfectly elastic
var impulse:Number = cn.dot( dv.times( -2 ) ) / ( 1 / p.mass );
p.v.plusEquals( cn.times( impulse / p.mass ) );
//damping
p.v.x *= p.restitution;
p.v.y *= p.restitution;
}
}
class Wall
{
public var A:Vec2D;
public var B:Vec2D;
public var aabb:AABB;
public var normal:Vec2D;
public function Wall( ax:Number, ay:Number, bx:Number, by:Number )
{
A = new Vec2D( ax, ay );
B = new Vec2D( bx, by );
normal = new Vec2D( B.y - A.y, -( B.x - A.x ) );
normal.normalize();
aabb = new AABB();
aabb.minx = Math.min( ax, bx );
aabb.maxx = Math.max( ax, bx );
aabb.miny = Math.min( ay, by );
aabb.maxy = Math.max( ay, by );
}
}
class Particle
{
public var restitution:Number = 0.9;
//position
public var x:Vec2D;
//velocity
public var v:Vec2D;
public var mass:Number;
//radius
public var r:Number;
//bounding box
public var aabb:AABB;
public function Particle( xx:Number, xy:Number, vx:Number, vy:Number, mass:Number = 1.0, radius:Number = 5 )
{
x = new Vec2D( xx, xy );
v = new Vec2D( vx, vy );
this.mass = mass;
this.r = radius;
aabb = new AABB();
}
public function update( t:Number ) : void
{
var xt:Number = x.x + v.x * t;
var yt:Number = x.y + v.y * t;
var minx:Number = Math.min( x.x, xt );
var maxx:Number = Math.max( x.x, xt );
var miny:Number = Math.min( x.y, yt );
var maxy:Number = Math.max( x.y, yt );
aabb.minx = minx - r;
aabb.maxx = maxx + r;
aabb.miny = miny - r;
aabb.maxy = maxy + r;
}
public function integrate( dt:Number ) : void
{
x.x += v.x * dt;
x.y += v.y * dt;
}
}
class AABB
{
public var minx:Number = 0;
public var maxx:Number = 0;
public var miny:Number = 0;
public var maxy:Number = 0;
public function isOverlapping( aabb:AABB ) : Boolean
{
if( minx > aabb.maxx ) return false;
if( miny > aabb.maxy ) return false;
if( maxx < aabb.minx ) return false;
if( maxy < aabb.miny ) return false;
return true;
}
}
class Vec2D
{
public var x:Number;
public var y:Number;
public function Vec2D( x:Number = 0.0, y:Number = 0.0 )
{
this.x = x;
this.y = y;
}
public function plusEquals( vec2D:Vec2D ) : void
{
x += vec2D.x;
y += vec2D.y;
}
public function plus( vec2D:Vec2D ) : Vec2D
{
return new Vec2D( x + vec2D.x, y + vec2D.y );
}
public function minus( vec2D:Vec2D ) : Vec2D
{
return new Vec2D( x - vec2D.x, y - vec2D.y );
}
public function times( s:Number ) : Vec2D
{
return new Vec2D( x * s, y * s );
}
public function dot( vec2D:Vec2D ) : Number
{
return x * vec2D.x + y * vec2D.y;
}
public function get magnitude() : Number
{
return Math.sqrt( x * x + y * y );
}
public function normalize() : void
{
var length:Number = magnitude;
if( length == 0 ) return;
x /= length;
y /= length;
}
}