class Vertex {
constructor(world, body, x, y) {
this.position = new Phaser.Math.Vector2(x,y);
this.oldPosition = new Phaser.Math.Vector2(x,y);
this.acceleration = new Phaser.Math.Vector2();
this.parent = body;
}
}
class Edge {
constructor( world, body, pV1, pV2, pBoundary) {
this.v1 = pV1;
this.v2 = pV2;
this.length = Math.hypot(pV2.position.x - pV1.position.x, pV2.position.y - pV1.position.y); //Calculate the original length
this.parent = body;
this.boundary = pBoundary;
}
};
class PhysicsBody {
constructor(world, pMass) {
this.mass = pMass;
this.vertices = [];
this.vertexCount = 0;
this.edges = [];
this.edgeCount = 0;
this.parent = world;
this.center = new Phaser.Math.Vector2();
this.minX; // these are for the bounding box;
this.maxX;
this.minY;
this.maxY;
this.mass;
world.addBody(this)
}
addVertex(V ) {
this.vertices.push(V);
this.vertexCount++;
this.parent.addVertex(V);
}
addEdge(E) {
this.edges.push(E);
this.edgeCount++;
this.parent.addEdge(E);
}
ProjectToAxis(Axis) {
var data = new MinMax();
let dotP = Axis.x this.vertices[ 0 ].position.x + Axis.y this.vertices[ 0 ].position.y;
data.min = dotP;
data.max = dotP; //Set the minimum and maximum values to the projection of the first vertex
for (let i = 0; i < this.vertexCount; i++ ) {
dotP = Axis.x this.vertices[ i ].position.x + Axis.y this.vertices[ i ].position.y; //Project the rest of the vertices onto the axis and extend the interval to the left/right if necessary
data.min = Math.min( dotP, data.min );
data.max = Math.max( dotP, data.max );
}
return data;
}
/**
* Calculates the center of mass
*/
calculateCenter() {
this.center.x = 0;
this.center.y = 0;
this.minX = 10000.0;
this.minY = 10000.0;
this.maxX = -10000.0;
this.maxY = -10000.0;
for (let i = 0; i < this.vertexCount; i++) {
this.center.x += this.vertices[ i ].position.x;
this.center.y += this.vertices[ i ].position.y;
this.minX = Math.min( this.minX, this.vertices[ i ].position.x );
this.minY = Math.min( this.minY, this.vertices[ i ].position.y );
this.maxX = Math.max( this.maxX, this.vertices[ i ].position.x );
this.maxY = Math.max( this.maxY, this.vertices[ i ].position.y );
}
this.center.x /= this.vertexCount;
this.center.y /= this.vertexCount;
}
/**
* Helper function to create a box primitive.
* @param x
* @param y
* @param width
* @param height
*/
createBox( world, x, y, width, height ) {
let V1 = new Vertex( world, this, x , y );
this.addVertex(V1)
let V2 = new Vertex( world, this, x + width, y );
this.addVertex(V2)
let V3 = new Vertex( world, this, x + width, y + height );
this.addVertex(V3)
let V4 = new Vertex( world, this, x , y + height );
this.addVertex(V4)
this.addEdge(new Edge( world, this, V1, V2, true )) ;
this.addEdge(new Edge( world, this, V2, V3, true )) ;
this.addEdge(new Edge( world, this, V3, V4, true ));
this.addEdge(new Edge( world, this, V4, V1, true ));
this.addEdge(new Edge( world, this, V1, V3, false ));
this.addEdge(new Edge( world, this, V2, V4, false ));
}
createTriangle( world, X, Y, w, h) {
let V1 = new Vertex( world, this, X , Y + h );
this.addVertex(V1)
let V2 = new Vertex( world, this, X + w/2, Y );
this.addVertex(V2)
let V3 = new Vertex( world, this, X + w, Y + h );
this.addVertex(V3)
this.addEdge(new Edge( world, this, V1, V2 ));
this.addEdge(new Edge( world, this, V2, V3 ));
this.addEdge(new Edge( world, this, V3, V1 ));
}
};
class Physics {
constructor(scene, width, height, GravitationX, GravitationY, pIterations) {
this.scene = scene;
this.screenWidth = width;
this.screenHeight = height;
this.bodies = [];
this.bodyCount = 0;
this.vertices = [];
this.vertexCount = 0;
this.edgeCount = 0;
this.edges = [];
this.gravity = new Phaser.Math.Vector2(GravitationX, GravitationY);
this.iterations = pIterations;
this.MAX_BODIES = 256; //Maximum body/vertex/edgecount the physics simulation can handle
this.MAX_VERTICES = 1024;
this.MAX_EDGES = 1024;
this.MAX_BODY_VERTICES = 10; //Maximum body/edge count a body can contain
this.MAX_BODY_EDGES = 10;
this.axis = new Phaser.Math.Vector2()
this.collisionInfo = new CollisionInfo();
}
update(dt) {
this.updateForces(dt);
this.updateVerlet(dt);
this.iterateCollisions();
}
render() { // pass the graphics object
this.graphics.clear();
this.graphics.lineStyle(1,'0xff0000')
for (let i = 0; i < this.edgeCount; i++ ) {
this.graphics.lineBetween(this.edges[ i ].v1.position.x, this.edges[ i ].v1.position.y, this.edges[ i ].v2.position.x, this.edges[ i ].v2.position.y );
}
this.graphics.fillStyle('0xffffff');
for (let i = 0; i < this.vertexCount; i++ ) {
this.graphics.fillCircle(this.vertices[ i ].position.x, this.vertices[ i ].position.y, 4);
}
}
/**
* Adds new elements to the simulation
* @param Body
*/
addBody(Body) {
this.bodies.push(Body);
this.bodyCount++;
}
addEdge(E) {
this.edges.push(E);
this.edgeCount++;
}
addVertex(V) {
this.vertices.push(V);
this.vertexCount ++;
}
findVertex( X, Y ) {
var nearestVertex = null;
var minDistance = 1000;
var coords = new Phaser.Math.Vector2(X, Y);
for (let i = 0; i < this.vertexCount; i++ ) {
var distance = Math.hypot(this.vertices[ i ].position.x - coords.x, this.vertices[ i ].position.y - coords.y);
if (distance < minDistance ) {
nearestVertex = this.vertices[ i ];
minDistance = distance;
}
}
return nearestVertex;
}
updateForces(dt) {
for (let I=0; I<this.vertexCount; I++ ) {
this.vertices[ I ].acceleration = this.gravity;
}
}
updateVerlet(dt) {
var tempX;
var tempY;
for (let i = 0; i < this.vertexCount; i++) {
let v = this.vertices[i];
tempX = v.position.x;
tempY = v.position.y;
v.position.x += v.position.x - v.oldPosition.x + v.acceleration.x * dt;
v.position.y += v.position.y - v.oldPosition.y + v.acceleration.y * dt;
v.oldPosition.x = tempX;
v.oldPosition.y = tempY;
}
}
updateEdges() {
for (let i = 0; i < this.edgeCount; i++) {
var e = this.edges[ i ];
// Calculate the vector between the two vertices
let v1v2x = e.v2.position.x - e.v1.position.x;
let v1v2y = e.v2.position.y - e.v1.position.y;
let v1v2Length = Math.hypot(v1v2x, v1v2y); //Calculate the current distance
let diff = v1v2Length - e.length; //Calculate the difference from the original length
// Normalise
var len = 1.0/Math.hypot(v1v2x, v1v2y);
v1v2x *= len;
v1v2y *= len;
// Push both vertices apart by half of the difference respectively so the distance between them equals the original length
e.v1.position.x += v1v2x diff 0.5;
e.v1.position.y += v1v2y diff 0.5;
e.v2.position.x -= v1v2x diff 0.5;
e.v2.position.y -= v1v2y diff 0.5;
}
}
iterateCollisions() {
for (let i = 0; i < this.iterations; i++ ) { //Repeat this a few times to give more exact results
//A small 'hack' that keeps the vertices inside the screen. You could of course implement static objects and create
//four to serve as screen boundaries, but the max/min method is faster
for (let t = 0; t < this.vertexCount; t++ ) {
var pos = this.vertices[ t ].position;
pos.x = Math.max( Math.min( pos.x, this.screenWidth ), 0.0 );
pos.y = Math.max( Math.min( pos.y, this.screenHeight ), 0.0 );
}
this.updateEdges(); //Edge correction step
for (let j = 0; j < this.bodyCount; j++) {
this.bodies[j].calculateCenter(); //Recalculate the center
}
for (let b1 = 0; b1 < this.bodyCount; b1++) { //Iterate trough all bodies
for (let b2 = 0; b2 < this.bodyCount; b2++) {
if (b1 != b2) {
if (this.bodiesOverlap(this.bodies[b1], this.bodies[b2])) { //Test the bounding boxes
if (this.detectCollision(this.bodies[b1], this.bodies[b2])) { //If there is a collision, respond to it
this.processCollision();
}
}
}
}
} // for loop iterating through all the bodies for intra-body collision
} // for loop this.iterations
} // end of iterateCollision method
detectCollision( b1, b2 ) {
var minDistance = 10000.0; //Initialize the length of the collision vector to a relatively large value
for (let i = 0; i < b1.edgeCount + b2.edgeCount; i++ ) { //Just a fancy way of iterating through all of the edges of both bodies at once
var e;
if (i < b1.edgeCount) {
e = b1.edges[i];
}
else {
e = b2.edges[i - b1.edgeCount];
}
//This will skip edges that lie totally inside the bodies, as they don't matter.
//The boundary flag has to be set manually and defaults to true
if (e.boundary == false) {
continue;
}
// Calculate the perpendicular to this edge and normalize it
this.axis.x = e.v1.position.y - e.v2.position.y;
this.axis.y = e.v2.position.x - e.v1.position.x;
// Normalise
var len = 1.0/ Math.hypot(this.axis.x, this.axis.y);
this.axis.x *= len;
this.axis.y *= len;
// Project both bodies onto the perpendicular
var dataA = b1.ProjectToAxis( this.axis );
var dataB = b2.ProjectToAxis( this.axis );
var distance = this.IntervalDistance( dataA.min, dataA.max, dataB.min, dataB.max ); //Calculate the distance between the two intervals
// If the intervals don't overlap, return, since there is no collision
if (distance > 0.0) {
return false;
}
else if (Math.abs(distance) < minDistance ) {
minDistance = Math.abs(distance);
// Save collision information for later
this.collisionInfo.normal.x = this.axis.x;
this.collisionInfo.normal.y = this.axis.y;
this.collisionInfo.e = e; //Store the edge, as it is the collision edge
}
}
this.collisionInfo.depth = minDistance;
if (this.collisionInfo.e.parent != b2) { //Ensure that the body containing the collision edge lies in B2 and the one conatining the collision vertex in B1
var temp = b2;
b2 = b1;
b1 = temp;
}
// int Sign = SGN( CollisionInfo.Normal.multiplyVal( B1.Center.minus(B2.Center) ) ); //This is needed to make sure that the collision normal is pointing at B1
var mult = this.collisionInfo.normal.x xx + this.collisionInfo.normal.y yy;
// Remember that the line equation is N*( R - R0 ). We choose B2->Center as R0; the normal N is given by the collision normal
if (mult < 0) {
// Revert the collision normal if it points away from B1
this.collisionInfo.normal.x = 0-this.collisionInfo.normal.x;
this.collisionInfo.normal.y = 0-this.collisionInfo.normal.y;
}
var smallestD = 10000.0; //Initialize the smallest distance to a large value
for (let i=0; i<b1.vertexCount; i++) {
// Measure the distance of the vertex from the line using the line equation
// float Distance = CollisionInfo.Normal.multiplyVal( B1.Vertices[I].Position.minus(B2.Center) );
var xx = b1.vertices[i].position.x - b2.center.x;
var yy = b1.vertices[i].position.y - b2.center.y;
var distance = this.collisionInfo.normal.x xx + this.collisionInfo.normal.y yy;
if (distance < smallestD) { //If the measured distance is smaller than the smallest distance reported so far, set the smallest distance and the collision vertex
smallestD = distance;
this.collisionInfo.v = b1.vertices[i];
}
}
return true; //There is no separating axis. Report a collision!
}
processCollision() {
var e1 = this.collisionInfo.e.v1;
var e2 =this.collisionInfo.e.v2;
var collisionVectorX = this.collisionInfo.normal.x * this.collisionInfo.depth;
var collisionVectorY = this.collisionInfo.normal.y * this.collisionInfo.depth;
var t;
if (Math.abs( e1.position.x - e2.position.x ) > Math.abs( e1.position.y - e2.position.y ) ) {
t = ( this.collisionInfo.v.position.x - collisionVectorX - e1.position.x )/( e2.position.x - e1.position.x );
}
else {
t = ( this.collisionInfo.v.position.y - collisionVectorY - e1.position.y )/( e2.position.y - e1.position.y );
}
var lambda = 1.0/( t*t + ( 1 - t )*( 1 - t ) );
var edgeMass = t*e2.parent.mass + ( 1 - t )*e1.parent.mass; //Calculate the mass at the intersection point
var invCollisionMass = 1.0/( edgeMass + this.collisionInfo.v.parent.mass );
var ratio1 = this.collisionInfo.v.parent.mass*invCollisionMass;
var ratio2 = edgeMass*invCollisionMass;
e1.position.x -= collisionVectorX (( 1 - t )ratio1*lambda);
e1.position.y -= collisionVectorY (( 1 - t )ratio1*lambda);
e2.position.x -= collisionVectorX ( t ratio1*lambda);
e2.position.y -= collisionVectorY ( t ratio1*lambda);
this.collisionInfo.v.position.x += collisionVectorX * ratio2;
this.collisionInfo.v.position.y += collisionVectorY * ratio2;
}
IntervalDistance( minA, maxA, minB, maxB) {
if (minA < minB) {
return minB - maxA;
}
else {
return minA - maxB;
}
}
/**
* Used for optimization to test if the bounding boxes of two bodies overlap.
* @param B1
* @param B2
* @return
*/
bodiesOverlap( B1, B2 ) {
return ( B1.minX <= B2.maxX ) && ( B1.minY <= B2.maxY ) && ( B1.maxX >= B2.minX ) && ( B2.maxY >= B1.minY );
}
}
Useful References
Commenti