/**
This system is responsible for handling physics for entities. All entities that have the Position, Movable and Shape components are treated as dynamic physics bodies.
All entities that have the Position and Shape components, but don't have the Movable component are treated as static physics bodies. Entities that have the Collidable component are also valid candidates for collision detection.
To update a position of a static body, it should have one of the required components removed and added again. Directly changing a position of a static entity doesn't update its position in the system.
This physics system works with the standard 2D coordinate system where the x axis grows left to right and the y axis grows top to bottom. Please keep this in mind when working with this system and the components and objects it requires.
When the system handles collisions between entities, it will send events. A "contactDetected" event is sent when two entities overlap. A "collisionDetected" event is sent when two entities are collidable with one another.
A "collisionResolved" event is sent once the collision between two entities is resolved. All events send a contact object. The contact object is described in the documentation as a property on the system.
@class PhysicsSystem
@constructor
@param entitySystemManager {Manager} The entity system manager whose entities this system will be working on.
@extends EventHandler
*/
function PhysicsSystem(entitySystemManager){
//Inherit from the event handling object.
EventHandler.call(this);
//Helper variable, needed for event handling.
var thisPhysicsSystem = this;
//=====Engine flags=====
var TESTS_PER_FRAME_FOR_FAST_OBJECTS = 5;
//=====The world object=====
//Quad tree holding the static and dynamic entities.
var entityQuadTree;
/**
Interface for working with the world.
@property world
@type Object
@final
*/
this.world = {
/**
Interface for configuring the system.
@property configure
@type Object
@final
*/
configure : {
/**
This function specifies how many tests per frame should be performed to prevent
fast moving objects from passing through other objects.
@method world.configure.setNumberOfTestsPerFrameForFastObjects
@param number {Number} The amount of tests to be performed.
*/
setNumberOfTestsPerFrameForFastObjects : function(number){
TESTS_PER_FRAME_FOR_FAST_OBJECTS = number;
}
},
/**
Sets up the system's environment. This function needs to be called at least once before updating the system.
@method world.initialize
@param width {Number} The width of the world.
@param height {Number} The height of the world.
@param maximumEntitiesPerLeaf {Number} The maximum number of entities that a leaf of the entity quad tree used to parse space can contain before it splits. This variable should be dependant on the size of entities. It should be easy to tweak when using debug drawing.
@param maximumLeafDepth {Number} The maximum number of times that the entity quad tree used to parse space can split. This variable should be dependant on the size of entities. It should be easy to tweak when using debug drawing.
*/
initialize : function(width, height, maximumEntitiesPerLeaf, maximumLeafDepth){
var worldHalfWidth = width / 2,
worldHalfHeight = height / 2;
entityQuadTree = new EntityQuadTree(worldHalfWidth, worldHalfHeight, worldHalfWidth, worldHalfHeight, maximumEntitiesPerLeaf, maximumLeafDepth);
function insert(entity){
entityQuadTree.insert(entity);
}
staticEntities.iterate(insert);
dynamicEntities.iterate(insert);
},
/**
Returns an array of entities whose Axis-Aligned Bounding Boxes overlap the one specified as the parameter.
@method world.queryEntitiesWithAABB
@param AABB {AABB} The AABB describing area from which entities should be retrieved.
@return {Array} Array holding the retrieved entities. This function always returns the same array, so the data contained in it isn't persistent. If it's required for the data to be persistent, copy it using slice.
*/
queryEntitiesWithAABB : function(AABB){
return entityQuadTree.queryEntities(AABB);
},
/**
Returns an the first entity that intersects the ray from the ray's begin point.
@method world.queryEntitiesWithRay
@param ray {Ray} The querying ray.
@param [category] {Number} A bit mask used to filter the entities being queried. Same as the category on the Collidable component.
@param [collideWith] {Number} A bit mask used to filter the entities being queried. Same as the collideWith on the Collidable component.
@return {Object|null} Object holding the contact data if the ray intersects an entity, otherwise null. The object has three fields, entity holding the entity that intersects the ray, contactPoint of the type Vector2D storing the point of intersection and contactNormal of the type Vector2D which holds the surface normal at the point of intersection.
*/
queryEntitiesWithRay : (function(){
var queryData = {
entity : null,
contactPoint : new Vector2D(),
contactNormal : new Vector2D()
},
rayAABB = new AABB(),
lowestDistanceSquared;
var testIntersectionRayCircle = (function(){
var temporary = new Vector2D();
return function(ray, entity, circleCenter, circleShape){
var a = ray.vector.magnitudeSquared(),
b = 2 * (ray.vector.x * (ray.begin.x - circleCenter.x) + ray.vector.y * (ray.begin.y - circleCenter.y)),
c = circleCenter.magnitudeSquared() + ray.begin.magnitudeSquared() - 2 * (circleCenter.x * ray.begin.x + circleCenter.y * ray.begin.y) - circleShape.getRadius() * circleShape.getRadius(),
delta = b * b - 4 * a * c,
sqrtDelta = Math.sqrt(delta),
distanceSquared;
if(delta < 0){
//There is no intersection.
return;
}
var u1 = (-b + sqrtDelta) / (2 * a);
if(0 <= u1 && u1 <= 1){
//The ray and the circle intersect,
//calculate the distance to the point of intersection.
ray.vector.multiplied(u1, temporary);
distanceSquared = temporary.magnitudeSquared();
//If this is the shortest distance yet, store all the data for this query.
if(distanceSquared < lowestDistanceSquared){
lowestDistanceSquared = distanceSquared;
//Set the entity as closest.
queryData.entity = entity;
//Calculate the contact point.
queryData.contactPoint.x = temporary.x + ray.begin.x;
queryData.contactPoint.y = temporary.y + ray.begin.y;
//Calculate the contact normal.
queryData.contactNormal.x = queryData.contactPoint.x - circleCenter.x;
queryData.contactNormal.y = queryData.contactPoint.y - circleCenter.y;
queryData.contactNormal.normalize();
}
}
if(delta === 0){
//There's only one point of intersection, the function can exit.
return;
}
var u2 = (-b - sqrtDelta) / (2 * a);
if(0 <= u2 && u2 <= 1){
//The ray and the circle intersect,
//calculate the distance to the point of intersection.
ray.vector.multiplied(u2, temporary);
distanceSquared = temporary.magnitudeSquared();
//If this is the shortest distance yet, store all the data for this query.
if(distanceSquared < lowestDistanceSquared){
lowestDistanceSquared = distanceSquared;
//Set the entity as closest.
queryData.entity = entity;
//Calculate the contact point.
queryData.contactPoint.x = temporary.x + ray.begin.x;
queryData.contactPoint.y = temporary.y + ray.begin.y;
//Calculate the contact normal.
queryData.contactNormal.x = queryData.contactPoint.x - circleCenter.x;
queryData.contactNormal.y = queryData.contactPoint.y - circleCenter.y;
queryData.contactNormal.normalize();
}
}
};
})();
var testIntersectionRayPolygon = (function(){
function crossProduct(vectorA, vectorB){
return vectorA.x * vectorB.y - vectorA.y * vectorB.x;
}
var q = new Vector2D(),
s = new Vector2D(),
qMinusRayBegin = new Vector2D(),
temporary = new Vector2D();
return function(ray, entity, polygonCenter, polygonShape){
var u1, u2, rayVectorCrossS, distanceSquared,
vertices = polygonShape._vertices;
for(var i=0; i<vertices.length-1; ++i){
vertices[i].added(polygonCenter, q);
vertices[i+1].subtracted(vertices[i], s);
q.subtracted(ray.begin, qMinusRayBegin);
rayVectorCrossS = crossProduct(ray.vector, s);
if(rayVectorCrossS !== 0){
u1 = crossProduct(qMinusRayBegin, s) / rayVectorCrossS;
u2 = crossProduct(qMinusRayBegin, ray.vector) / rayVectorCrossS;
if(u1 >= 0 && u1 <= 1 && u2 >= 0 && u2 <= 1){
//The ray and the edge intersect,
//calculate the distance to the point of intersection.
ray.vector.multiplied(u1, temporary);
distanceSquared = temporary.magnitudeSquared();
//If this is the shortest distance yet, store all the data for this query.
if(distanceSquared < lowestDistanceSquared){
lowestDistanceSquared = distanceSquared;
//Set the entity as closest.
queryData.entity = entity;
//Calculate the contact point.
ray.begin.added(temporary, queryData.contactPoint);
//Calculate the contact normal.
queryData.contactNormal.x = s.y;
queryData.contactNormal.y = -s.x;
queryData.contactNormal.normalize();
}
}
}
}
vertices[i].added(polygonCenter, q);
vertices[0].subtracted(vertices[i], s);
q.subtracted(ray.begin, qMinusRayBegin);
rayVectorCrossS = crossProduct(ray.vector, s);
if(rayVectorCrossS !== 0){
u1 = crossProduct(qMinusRayBegin, s) / rayVectorCrossS;
u2 = crossProduct(qMinusRayBegin, ray.vector) / rayVectorCrossS;
if(u1 >= 0 && u1 <= 1 && u2 >= 0 && u2 <= 1){
//The ray and the edge intersect,
//calculate the distance to the point of intersection.
ray.vector.multiplied(u1, temporary);
distanceSquared = temporary.magnitudeSquared();
//If this is the shortest distance yet, store all the data for this query.
if(distanceSquared < lowestDistanceSquared){
lowestDistanceSquared = distanceSquared;
//Set the entity as closest.
queryData.entity = entity;
//Calculate the contact point.
ray.begin.added(temporary, queryData.contactPoint);
//Calculate the contact normal.
queryData.contactNormal.x = s.y;
queryData.contactNormal.y = -s.x;
queryData.contactNormal.normalize();
}
}
}
};
})();
return function(ray, category, collideWith){
//Calculate the AABB for the ray.
if(ray.begin.x > ray.begin.x + ray.vector.x){
rayAABB.topLeft.x = ray.begin.x + ray.vector.x;
rayAABB.bottomRight.x = ray.begin.x;
}else{
rayAABB.topLeft.x = ray.begin.x;
rayAABB.bottomRight.x = ray.begin.x + ray.vector.x;
}
if(ray.begin.y > ray.begin.y + ray.vector.y){
rayAABB.topLeft.y = ray.begin.y + ray.vector.y;
rayAABB.bottomRight.y = ray.begin.y;
}else{
rayAABB.topLeft.y = ray.begin.y;
rayAABB.bottomRight.y = ray.begin.y + ray.vector.y;
}
//Reset the lowest distance.
lowestDistanceSquared = Number.MAX_VALUE;
var possibleIntersections = entityQuadTree.queryEntities(rayAABB),
entity, center, shape, collidable;
//If the bit masks were passed in, test only
//entities that pass the bit flag test.
if((category !== undefined) && (collideWith !== undefined)){
for(var i=0; i<possibleIntersections.length; ++i){
entity = possibleIntersections[i];
center = entity.get(Position)._position;
shape = entity.get(Shape)._shape;
collidable = entity.get(Collidable);
if(collidable &&
(collideWith & collidable._category) &&
(collidable._collideWith & category)){
if(shape instanceof Box || shape instanceof Polygon){
testIntersectionRayPolygon(ray, entity, center, shape);
}else if(shape instanceof Circle){
testIntersectionRayCircle(ray, entity, center, shape);
}
}
}
}else{
for(var i=0; i<possibleIntersections.length; ++i){
entity = possibleIntersections[i],
center = entity.get(Position)._position,
shape = entity.get(Shape)._shape;
if(shape instanceof Box || shape instanceof Polygon){
testIntersectionRayPolygon(ray, entity, center, shape);
}else if(shape instanceof Circle){
testIntersectionRayCircle(ray, entity, center, shape);
}
}
}
if(lowestDistanceSquared < Number.MAX_VALUE){
return queryData;
}else{
return null;
}
};
})(),
/**
Returns an array of entities whose shapes overlap the specified shape.
@method world.queryEntitiesWithShape
@param position {Position} Position of the querying shape.
@param shape {Shape} The shape describing the area from which entities should be retrieved.
@param [category] {Number} A bit mask used to filter the queried entities. Same as the category on the Collidable component.
@param [collideWith] {Number} A bit mask used to filter the queried entities. Same as the collideWith on the Collidable component.
@return {Array} Array holding the retrieved entities. This function always returns the same array, so the data contained in it isn't persistent. If it's required for the data to be persistent, copy it using slice.
*/
queryEntitiesWithShape : (function(){
var overlappingEntities = [];
return function(position, shape, category, collideWith){
//Store the shape component's current AABB.
var AABB = shape._AABB;
calculateAABB(position, shape);
var possibleOverlaps = entityQuadTree.queryEntities(shape._AABB),
entity, entityPosition, entityShape, entityCollidable;
//Reset the array.
overlappingEntities.length = 0;
//If the bit masks were passed in, return only
//entities that pass the bit flag test.
if((category !== undefined) && (collideWith !== undefined)){
for(var i=0; i<possibleOverlaps.length; ++i){
entity = possibleOverlaps[i];
entityPosition = entity.get(Position);
entityShape = entity.get(Shape);
entityCollidable = entity.get(Collidable);
if(entityCollidable &&
(collideWith & entityCollidable._category) &&
(entityCollidable._collideWith & category) &&
testOverlap(position, shape, entityPosition, entityShape)){
overlappingEntities.push(entity);
}
}
}else{
for(var i=0; i<possibleOverlaps.length; ++i){
entity = possibleOverlaps[i];
entityPosition = entity.get(Position);
entityShape = entity.get(Shape);
if(testOverlap(position, shape, entityPosition, entityShape)){
overlappingEntities.push(entity);
}
}
}
//Restore the AABB;
shape._AABB = AABB;
return overlappingEntities;
};
})()
};
//=====Aspect setup code=====
var staticEntities = entitySystemManager.createAspect([Position, Shape],[Movable]),
dynamicEntities = entitySystemManager.createAspect([Position, Movable, Shape]);
(function(){
function added(entity){
calculateAABB(entity.get(Position), entity.get(Shape));
if(entityQuadTree){
entityQuadTree.insert(entity);
}
}
function removed(entity){
if(entityQuadTree){
entityQuadTree.remove(entity);
}
entity.get(Shape)._AABB = null;
}
staticEntities.subscribeAdded(added);
staticEntities.subscribeRemoved(removed);
dynamicEntities.subscribeAdded(added);
dynamicEntities.subscribeRemoved(removed);
})();
//=====Overlap tests=====
//This variable holds an array of edge normals necessary for SAT.
//Those normals are allocated once and reused in all separating axis tests, both in overlap
//as well as intersection tests. The array will be resized if necessary.
var separatingAxes = [];
function resizeSeparatingAxesArray(newSize){
separatingAxes.length = newSize;
for(var i=0; i<separatingAxes.length; ++i){
//If the given separating axis is undefined, then allocate a new vector for it.
if(!separatingAxes[i]){
separatingAxes[i] = new Vector2D();
}
}
}
var testOverlap = (function(){
function testOverlapPolygonPolygon(centerA, polygonA, centerB, polygonB){
var verticesA = polygonA.getVertices(),
verticesB = polygonB.getVertices(),
normal, numberOfAxes = 0, requiredNumberOfAxes;
//Resize the separating axes array, if necessary.
requiredNumberOfAxes = verticesA.length + verticesB.length;
if(requiredNumberOfAxes > separatingAxes.length){
resizeSeparatingAxesArray(requiredNumberOfAxes);
}
//=====Find potential separating axes for the first polygon=====
var x, y;
for(var i=0; i<verticesA.length - 1; ++i, ++numberOfAxes){
//Calculate a vector going along an edge.
normal = verticesA[i+1].subtracted(verticesA[i], separatingAxes[numberOfAxes]);
//Rotate that vector and normalize it to create an edge normal.
x = -normal.y;
y = normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
}
//Calculate the normal for the last edge.
normal = verticesA[0].subtracted(verticesA[i], separatingAxes[numberOfAxes]);
numberOfAxes++;
x = -normal.y;
y = normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
//=====Find potential separating axes for the second polygon=====
for(i=0; i<verticesB.length - 1; ++i, ++numberOfAxes){
//Calculate a vector going along an edge.
normal = verticesB[i+1].subtracted(verticesB[i], separatingAxes[numberOfAxes]);
//Rotate that vector and normalize it to create an edge normal.
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
}
//Calculate the normal for the last edge.
normal = verticesB[0].subtracted(verticesB[i], separatingAxes[numberOfAxes]);
numberOfAxes++;
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
//=====Project both polygons and check for overlap=====
var separatingAxis, projection,
//Projected extents of both polygons.
minA, maxA, minB, maxB,
//Projected centers of both polygons.
projectedCenterA, projectedCenterB;
for(i=0; i<numberOfAxes; ++i){
separatingAxis = separatingAxes[i];
//Project the centers of the polygons. They're necessary to offset the vertex projections when calculating overlap.
projectedCenterA = separatingAxis.dot(centerA);
projectedCenterB = separatingAxis.dot(centerB);
//=====Projection of the first polygon=====
//Project the first vertex separately to give initial values to the variables.
minA = maxA = separatingAxis.dot(verticesA[0]);
//Project the rest of the vertices.
for(var j=1; j<verticesA.length; ++j){
projection = separatingAxis.dot(verticesA[j]);
minA = Math.min(minA, projection);
maxA = Math.max(maxA, projection);
}
//=====Projection of the second polygon=====
//Project the first vertex separately to give initial values to the variables.
minB = maxB = separatingAxis.dot(verticesB[0]);
//Project the rest of the vertices.
for(j=1; j<verticesB.length; ++j){
projection = separatingAxis.dot(verticesB[j]);
minB = Math.min(minB, projection);
maxB = Math.max(maxB, projection);
}
//Offset the projections.
minA += projectedCenterA;
maxA += projectedCenterA;
minB += projectedCenterB;
maxB += projectedCenterB;
//Check for overlap.
if(maxA < minB){
return false;
}
if(maxB < minA){
return false;
}
}
return true;
}
var BAvector = new Vector2D();
function testOverlapCircleCircle(centerA, circleA, centerB, circleB){
//Calculate the vector "going" from the center of B to the center of A.
centerA.subtracted(centerB, BAvector);
var distance = BAvector.magnitude();
if(distance <= circleA.getRadius() + circleB.getRadius()){
return true;
}else{
return false;
}
}
var vertexPosition = new Vector2D(),
temporary = new Vector2D();
function testOverlapCirclePolygon(circleCenter, circle, polygonCenter, polygon){
var vertices = polygon.getVertices(),
normal, magnitude, distance, numberOfAxes = 1, requiredNumberOfAxes;
//Resize the separating axes array, if necessary.
requiredNumberOfAxes = vertices.length + 1;
if(requiredNumberOfAxes > separatingAxes.length){
resizeSeparatingAxesArray(requiredNumberOfAxes);
}
//=====Find potential separating axes for the circle=====
distance = Number.MAX_VALUE;
//Find the vertex closest to the circle's center.
for(var i=0; i<vertices.length; ++i){
//Calculate the vector from the polygon's vertex to the circle's center.
polygonCenter.added(vertices[i], vertexPosition);
circleCenter.subtracted(vertexPosition, temporary);
magnitude = temporary.magnitude();
if(magnitude < distance){
distance = magnitude;
//Use the already calculated magnitude to normalize the temporary vector and store it as a separation axis.
temporary.divided(magnitude, separatingAxes[0]);
}
}
//=====Find potential separating axes for the polygon=====
var x, y;
for(i=0; i<vertices.length - 1; ++i, ++numberOfAxes){
//Calculate a vector going along an edge.
normal = vertices[i+1].subtracted(vertices[i], separatingAxes[numberOfAxes]);
//Rotate that vector and normalize it to create an edge normal.
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
}
//Calculate the normal for the last edge.
normal = vertices[0].subtracted(vertices[i], separatingAxes[numberOfAxes]);
numberOfAxes++;
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
//=====Project both shapes and check for overlap=====
var separatingAxis, projection,
//Projected extents.
circleMin, circleMax, polygonMin, polygonMax,
//Projected centers.
projectedCircleCenter, projectedPolygonCenter;
for(i=0; i<numberOfAxes; ++i){
separatingAxis = separatingAxes[i];
//Project the centers of the polygons. They're necessary to offset the vertex projections when calculating overlap.
projectedCircleCenter = separatingAxis.dot(circleCenter);
projectedPolygonCenter = separatingAxis.dot(polygonCenter);
//=====Projection of the circle=====
circleMin = -circle.getRadius();
circleMax = circle.getRadius();
//=====Projection of the polygon=====
//Project the first vertex separately to give initial values to the variables.
polygonMin = polygonMax = separatingAxis.dot(vertices[0]);
//Project the rest of the vertices.
for(var j=1; j<vertices.length; ++j){
projection = separatingAxis.dot(vertices[j]);
polygonMin = Math.min(polygonMin, projection);
polygonMax = Math.max(polygonMax, projection);
}
//Offset the projections.
circleMin += projectedCircleCenter;
circleMax += projectedCircleCenter;
polygonMin += projectedPolygonCenter;
polygonMax += projectedPolygonCenter;
//Check for overlap.
if(circleMax < polygonMin){
return false;
}
if(polygonMax < circleMin){
return false;
}
}
return true;
}
return function(positionComponentA, shapeComponentA, positionComponentB, shapeComponentB){
var centerA = positionComponentA._position,
shapeA = shapeComponentA._shape,
centerB = positionComponentB._position,
shapeB = shapeComponentB._shape;
//Choose the correct collision detection algorithm.
if(shapeA instanceof Box || shapeA instanceof Polygon){
if(shapeB instanceof Box || shapeB instanceof Polygon){
return testOverlapPolygonPolygon(centerA, shapeA, centerB, shapeB);
}else if(shapeB instanceof Circle){
return testOverlapCirclePolygon(centerB, shapeB, centerA, shapeA);
}
}else if(shapeA instanceof Circle){
if(shapeB instanceof Box || shapeB instanceof Polygon){
return testOverlapCirclePolygon(centerA, shapeA, centerB, shapeB);
}else if(shapeB instanceof Circle){
return testOverlapCircleCircle(centerA, shapeA, centerB, shapeB);
}
}
//The shape could not be recognized.
return false;
};
})();
//=====Intersection tests=====
var testIntersection = (function(){
function testIntersectionPolygonPolygon(centerA, polygonA, centerB, polygonB, collisionData){
var verticesA = polygonA.getVertices(),
verticesB = polygonB.getVertices(),
normal, numberOfAxes = 0, requiredNumberOfAxes;
//Resize the separating axes array, if necessary.
requiredNumberOfAxes = verticesA.length + verticesB.length;
if(requiredNumberOfAxes > separatingAxes.length){
resizeSeparatingAxesArray(requiredNumberOfAxes);
}
collisionData._penetration = Number.MAX_VALUE;
//=====Find potential separating axes for the first polygon=====
var x, y;
for(var i=0; i<verticesA.length - 1; ++i, ++numberOfAxes){
//Calculate a vector going along an edge.
normal = verticesA[i+1].subtracted(verticesA[i], separatingAxes[numberOfAxes]);
//Rotate that vector and normalize it to create an edge normal.
x = -normal.y;
y = normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
}
//Calculate the normal for the last edge.
normal = verticesA[0].subtracted(verticesA[i], separatingAxes[numberOfAxes]);
numberOfAxes++;
x = -normal.y;
y = normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
//=====Find potential separating axes for the second polygon=====
for(i=0; i<verticesB.length - 1; ++i, ++numberOfAxes){
//Calculate a vector going along an edge.
normal = verticesB[i+1].subtracted(verticesB[i], separatingAxes[numberOfAxes]);
//Rotate that vector and normalize it to create an edge normal.
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
}
//Calculate the normal for the last edge.
normal = verticesB[0].subtracted(verticesB[i], separatingAxes[numberOfAxes]);
numberOfAxes++;
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
//=====Project both polygons and check for overlap=====
var separatingAxis, projection, overlap,
//Projected extents of both polygons.
minA, maxA, minB, maxB,
//Projected centers of both polygons.
projectedCenterA, projectedCenterB;
for(i=0; i<numberOfAxes; ++i){
separatingAxis = separatingAxes[i];
//Project the centers of the polygons. They're necessary to offset the vertex projections when calculating overlap.
projectedCenterA = separatingAxis.dot(centerA);
projectedCenterB = separatingAxis.dot(centerB);
//=====Projection of the first polygon=====
//Project the first vertex separately to give initial values to the variables.
minA = maxA = separatingAxis.dot(verticesA[0]);
//Project the rest of the vertices.
for(var j=1; j<verticesA.length; ++j){
projection = separatingAxis.dot(verticesA[j]);
minA = Math.min(minA, projection);
maxA = Math.max(maxA, projection);
}
//=====Projection of the second polygon=====
//Project the first vertex separately to give initial values to the variables.
minB = maxB = separatingAxis.dot(verticesB[0]);
//Project the rest of the vertices.
for(j=1; j<verticesB.length; ++j){
projection = separatingAxis.dot(verticesB[j]);
minB = Math.min(minB, projection);
maxB = Math.max(maxB, projection);
}
//Offset the projections.
minA += projectedCenterA;
maxA += projectedCenterA;
minB += projectedCenterB;
maxB += projectedCenterB;
//Check for overlap.
if(maxA < minB){
return false;
}
if(maxB < minA){
return false;
}
//If the function hasn't returned, there's overlap. Calculate the overlap.
overlap = maxB - minA;
//If the overlap is smaller than the currently stored penetration, update the contact object;
if(overlap < collisionData._penetration){
collisionData._collisionNormal.x = separatingAxis.x;
collisionData._collisionNormal.y = separatingAxis.y;
collisionData._penetration = overlap;
}
}
return true;
}
var BAvector = new Vector2D();
function testIntersectionCircleCircle(centerA, circleA, centerB, circleB, collisionData){
//Calculate the vector "going" from the center of B to the center of A.
centerA.subtracted(centerB, BAvector);
var distance = BAvector.magnitude();
if(distance <= circleA.getRadius() + circleB.getRadius()){
//Use the calculated magnitude to normalize the vector and store is as the contact normal.
BAvector.divided(distance, collisionData._collisionNormal);
collisionData._penetration = (circleA.getRadius() + circleB.getRadius()) - distance;
return true;
}else{
return false;
}
}
var vertexPosition = new Vector2D(),
temporary = new Vector2D();
function testIntersectionCirclePolygon(circleCenter, circle, polygonCenter, polygon, collisionData){
var vertices = polygon.getVertices(),
normal, magnitude, distance, numberOfAxes = 1, requiredNumberOfAxes;
//Resize the separating axes array, if necessary.
requiredNumberOfAxes = vertices.length + 1;
if(requiredNumberOfAxes > separatingAxes.length){
resizeSeparatingAxesArray(requiredNumberOfAxes);
}
collisionData._penetration = Number.MAX_VALUE;
//=====Find potential separating axes for the circle=====
distance = Number.MAX_VALUE;
//Find the vertex closest to the circle's center.
for(var i=0; i<vertices.length; ++i){
//Calculate the vector from the polygon's vertex to the circle's center.
polygonCenter.added(vertices[i], vertexPosition);
circleCenter.subtracted(vertexPosition, temporary);
magnitude = temporary.magnitude();
if(magnitude < distance){
distance = magnitude;
//Use the already calculated magnitude to normalize the temporary vector and store it as a separation axis.
temporary.divided(magnitude, separatingAxes[0]);
}
}
//=====Find potential separating axes for the polygon=====
var x, y;
for(i=0; i<vertices.length - 1; ++i, ++numberOfAxes){
//Calculate a vector going along an edge.
normal = vertices[i+1].subtracted(vertices[i], separatingAxes[numberOfAxes]);
//Rotate that vector and normalize it to create an edge normal.
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
}
//Calculate the normal for the last edge.
normal = vertices[0].subtracted(vertices[i], separatingAxes[numberOfAxes]);
numberOfAxes++;
x = normal.y;
y = -normal.x;
normal.x = x;
normal.y = y;
normal.normalize();
//=====Project both shapes and check for overlap=====
var separatingAxis, projection, overlap,
//Projected extents.
circleMin, circleMax, polygonMin, polygonMax,
//Projected centers.
projectedCircleCenter, projectedPolygonCenter;
for(i=0; i<numberOfAxes; ++i){
separatingAxis = separatingAxes[i];
//Project the centers of the polygons. They're necessary to offset the vertex projections when calculating overlap.
projectedCircleCenter = separatingAxis.dot(circleCenter);
projectedPolygonCenter = separatingAxis.dot(polygonCenter);
//=====Projection of the circle=====
circleMin = -circle.getRadius();
circleMax = circle.getRadius();
//=====Projection of the polygon=====
//Project the first vertex separately to give initial values to the variables.
polygonMin = polygonMax = separatingAxis.dot(vertices[0]);
//Project the rest of the vertices.
for(var j=1; j<vertices.length; ++j){
projection = separatingAxis.dot(vertices[j]);
polygonMin = Math.min(polygonMin, projection);
polygonMax = Math.max(polygonMax, projection);
}
//Offset the projections.
circleMin += projectedCircleCenter;
circleMax += projectedCircleCenter;
polygonMin += projectedPolygonCenter;
polygonMax += projectedPolygonCenter;
//Check for overlap.
if(circleMax < polygonMin){
return false;
}
if(polygonMax < circleMin){
return false;
}
//If the function hasn't returned, there's overlap. Calculate the overlap.
overlap = polygonMax - circleMin;
//If the overlap is smaller than the currently stored penetration, update the contact object;
if(overlap < collisionData._penetration){
collisionData._collisionNormal.x = separatingAxis.x;
collisionData._collisionNormal.y = separatingAxis.y;
collisionData._penetration = overlap;
}
}
return true;
}
return function(positionComponentA, shapeComponentA, positionComponentB, shapeComponentB, collisionData){
var collisionDetected = false,
centerA = positionComponentA._position,
shapeA = shapeComponentA._shape,
centerB = positionComponentB._position,
shapeB = shapeComponentB._shape;
//Choose the correct collision detection algorithm.
if(shapeA instanceof Box || shapeA instanceof Polygon){
if(shapeB instanceof Box || shapeB instanceof Polygon){
collisionDetected = testIntersectionPolygonPolygon(centerA, shapeA, centerB, shapeB, collisionData);
}else if(shapeB instanceof Circle){
collisionDetected = testIntersectionCirclePolygon(centerB, shapeB, centerA, shapeA, collisionData);
//Invert the normal if there's collision, since the test should be done from shapeA's perspective.
if(collisionDetected){
collisionData._collisionNormal.invert();
}
}
}else if(shapeA instanceof Circle){
if(shapeB instanceof Box || shapeB instanceof Polygon){
collisionDetected = testIntersectionCirclePolygon(centerA, shapeA, centerB, shapeB, collisionData);
}else if(shapeB instanceof Circle){
collisionDetected = testIntersectionCircleCircle(centerA, shapeA, centerB, shapeB, collisionData);
}
}
return collisionDetected;
};
})();
//=====Collision resolution algorithms=====
var testDynamicEntityForCollisions = (function(){
//Reusable vectors.
var totalImpulse = new Vector2D(),
separationVector = new Vector2D(),
relativeVelocity = new Vector2D(),
collisionTangent = new Vector2D(),
temporary = new Vector2D();
function resolveDynamicDynamicCollision(positionA, movableA, collidableA, positionB, movableB, collidableB, collisionData){
//The collision should have less of an effect on the heavier object.
//To achieve this, each object is affected based on a coefficient,
//which is inversely propotional to the object's mass.
var totalInverseMass = movableA._inverseMass + movableB._inverseMass;
//Calculate the vector needed to completely separate both bodies.
collisionData._collisionNormal.multiplied(collisionData._penetration, separationVector);
//If both bodies are infinitely heavy, no impulses will be applied and the bodies will be separated by equal amount.
if(totalInverseMass === 0){
//Separate the bodies by equal amount.
separationVector.multiply(0.5);
positionA._position.add(separationVector);
positionB._position.subtract(separationVector);
//No impulses will be applied.
collisionData._normalImpulse.x = 0;
collisionData._normalImpulse.y = 0;
collisionData._tangentImpulse.x = 0;
collisionData._tangentImpulse.y = 0;
//The collision has been resolved.
return;
}
//=====Resolve interpenetration=====
//Separate the two objects based on their mass.
positionA._position.add( separationVector.multiplied(movableA._inverseMass / totalInverseMass, temporary) );
positionB._position.subtract( separationVector.multiplied(movableB._inverseMass / totalInverseMass, temporary) );
//=====Resolve velocities=====
movableA._velocity.subtracted(movableB._velocity, relativeVelocity);
var separatingVelocity = relativeVelocity.dot(collisionData._collisionNormal);
//If the separating velocity is positive then the two objects are moving away from each other.
if(separatingVelocity > 0){
//No impulses will be applied.
collisionData._normalImpulse.x = 0;
collisionData._normalImpulse.y = 0;
collisionData._tangentImpulse.x = 0;
collisionData._tangentImpulse.y = 0;
//The collision has been resolved.
return;
}
//Calculate the coefficients of restitution and friction for the contact.
var restitution = Math.min(collidableA._restitution, collidableB._restitution),
friction = collidableA._friction * collidableB._friction,
//Calculate the total change in velocity along the contact normal.
//The reaction velocity is required to calculate the friction, so it's calculated separately.
//The calculations involving last frame's acceleration are solving resting contacts.
reactionVelocity = -separatingVelocity,
normalVelocityChange = reactionVelocity - restitution * (separatingVelocity - collisionData._collisionNormal.dot(movableA._lastFrameAcceleration) + collisionData._collisionNormal.dot(movableB._lastFrameAcceleration));
//Calculate the vector that's pararell to the contact surface.
collisionTangent.x = collisionData._collisionNormal.y;
collisionTangent.y = -collisionData._collisionNormal.x;
//Calculate the component of relative velocity that's pararell to the contact surface.
var tangentVelocity = relativeVelocity.dot(collisionTangent),
//Calculate the velocity lost due to friction.
tangentVelocityChange = -tangentVelocity;
//Make sure that the velocity lost isn't higher than the dynamic friction.
if(Math.abs(tangentVelocity) > reactionVelocity * friction){
//This division by the absolute value of the planar velocity is just a way of preserving the sign.
tangentVelocityChange /= Math.abs(tangentVelocity);
tangentVelocityChange *= reactionVelocity * friction;
}
//Turn the scalars into impulses.
collisionData._collisionNormal.multiplied(normalVelocityChange, collisionData._normalImpulse);
collisionData._normalImpulse.divide(totalInverseMass);
collisionTangent.multiplied(tangentVelocityChange, collisionData._tangentImpulse);
collisionData._tangentImpulse.divide(totalInverseMass);
//The total impulse is a vector that's a sum of the planar impulse caused by friction and the impulse along the contact normal.
collisionData._normalImpulse.added(collisionData._tangentImpulse, totalImpulse);
movableA._velocity.add(totalImpulse.multiplied(movableA._inverseMass, temporary));
movableB._velocity.subtract(totalImpulse.multiplied(movableB._inverseMass, temporary));
}
function resolveDynamicStaticCollision(positionA, movableA, collidableA, collidableB, collisionData, deltaTime){
//=====Resolve interpenetration=====
//Calculate the vector needed to completely separate both bodies and apply it.
positionA._position.add( collisionData._collisionNormal.multiplied(collisionData._penetration, temporary) );
//Calculate the vector that's pararell to the contact surface.
collisionTangent.x = collisionData._collisionNormal.y;
collisionTangent.y = -collisionData._collisionNormal.x;
/*
*This code prevents the above collision resolution algorithm from "pushing" entities down slopes
*that would otherwise stay still due to friction. It'll forcefully move the entity back along the
*contact surface by the change in position caused by forces applied to the entity in the previous frame.
*It's disabled for fast moving entities, because they require a division of the frame time, which
*causes substantial inconsistency issues due to limited precision.
*/
if(!movableA._fastObject){
var previousFrameVelocityChange = movableA._lastFrameAcceleration,
speedAlongTheContactSurface = collisionTangent.dot(previousFrameVelocityChange);
//Calculate how much the entity should be moved back.
collisionTangent.multiplied(speedAlongTheContactSurface, temporary);
positionA._position.subtract(temporary.multiply(deltaTime));
}
//=====Resolve velocity=====
//If the dynamic body is infinitely heavy, no impulses will be applied.
if(movableA._inverseMass === 0){
collisionData._normalImpulse.x = 0;
collisionData._normalImpulse.y = 0;
collisionData._tangentImpulse.x = 0;
collisionData._tangentImpulse.y = 0;
//The collision has been resolved.
return;
}
var separatingVelocity = movableA._velocity.dot(collisionData._collisionNormal);
//If the separating velocity is positive then the two objects are moving away from each other.
if(separatingVelocity > 0){
//No impulses will be applied.
collisionData._normalImpulse.x = 0;
collisionData._normalImpulse.y = 0;
collisionData._tangentImpulse.x = 0;
collisionData._tangentImpulse.y = 0;
//The collision has been resolved.
return;
}
//Calculate the coefficients of restitution and friction for the contact.
var restitution = Math.min(collidableA._restitution, collidableB._restitution),
friction = collidableA._friction * collidableB._friction,
//Calculate the total change in velocity along the contact normal.
//The reaction velocity is required to calculate the friction, so it's calculated separately.
//The calculations involving last frame's acceleration are solving resting contacts.
reactionVelocity = -separatingVelocity,
normalVelocityChange = reactionVelocity - restitution * (separatingVelocity - collisionData._collisionNormal.dot(movableA._lastFrameAcceleration));
//Calculate the component of relative velocity that's pararell to the contact surface.
var tangentVelocity = movableA._velocity.dot(collisionTangent),
//Calculate the velocity lost due to friction.
tangentVelocityChange = -tangentVelocity;
//Make sure that the velocity lost isn't higher than the dynamic friction.
if(Math.abs(tangentVelocity) > reactionVelocity * friction){
//This division by the absolute value of the planar velocity is just a way of preserving the sign.
tangentVelocityChange /= Math.abs(tangentVelocity);
tangentVelocityChange *= reactionVelocity * friction;
}
//Turn the scalars into impulses.
collisionData._collisionNormal.multiplied(normalVelocityChange, collisionData._normalImpulse);
collisionData._normalImpulse.multiply(movableA._mass);
collisionTangent.multiplied(tangentVelocityChange, collisionData._tangentImpulse);
collisionData._tangentImpulse.multiply(movableA._mass);
//The total impulse is a vector that's a sum of the planar impulse caused by friction and the impulse along the contact normal.
collisionData._normalImpulse.added(collisionData._tangentImpulse, totalImpulse);
movableA._velocity.add(totalImpulse.multiplied(movableA._inverseMass, temporary));
}
/**
This is the contact object that's sent with all contact and collision events. Every event uses the same object, so the data contained in it is not persistent outside of the event callback functions.
@property contactData
@type Object
@final
*/
var contactData = {
_entityA : null,
_entityB : null,
_resolveCollision : true,
/**
Returns the first entity of the contact pair.
@method contactData.getEntityA
@return {Entity} The first entity of the contact pair.
*/
getEntityA : function(){
return this._entityA;
},
/**
Returns the second entity of the contact pair.
@method contactData.getEntityB
@return {Entity} The second entity of the contact pair.
*/
getEntityB : function(){
return this._entityB;
},
/**
This function can be used to prevent a collision from being solved. Its usage is only valid in the "collisionDetected" event callback.
@method contactData.preventCollisionSolving
*/
preventCollisionSolving : function(){
this._resolveCollision = false;
}
};
/**
This object contains the collision data for the entity pair.
@property contactData.collisionData
@type Object
@final
*/
Object.defineProperty(contactData, 'collisionData', {
configurable : true,
enumerable : true,
writable : false,
value : {
_collisionNormal : new Vector2D(),
_penetration : 0,
_normalImpulse : new Vector2D(),
_tangentImpulse : new Vector2D(),
/**
This method is valid in all event callbacks.
@method contactData.collisionData.getCollisionNormal
@param vector {Vector2D} Vector to which the normal will be copied.
*/
getCollisionNormal : function(vector){
vector.x = this._collisionNormal.x;
vector.y = this._collisionNormal.y;
},
/**
This method is valid in all event callbacks.
@method contactData.collisionData.getPenetration
@return {Number}
*/
getPenetration : function(){
return this._penetration;
},
/**
This method is valid only in the "collisionResolved" event callback. The method retrieves the total impulse along the contact normal for this collision. It describes how "hard" both entities "bounced off" of each other.
@method contactData.collisionData.getNormalImpulse
@param vector {Vector2D} Vector to which the impulse will be copied.
*/
getNormalImpulse : function(vector){
vector.x = this._normalImpulse.x;
vector.y = this._normalImpulse.y;
},
/**
This method is valid only in the "collisionResolved" event callback. The method retrieves the total impulse along the contact surface for this collision. It describes friction.
@method contactData.collisionData.getTangentImpulse
@param vector {Vector2D} Vector to which the impulse will be copied.
*/
getTangentImpulse : function(vector){
vector.x = this._tangentImpulse.x;
vector.y = this._tangentImpulse.y;
}
}
});
//Event names.
var contactDetected = 'contactDetected',
collisionDetected = 'collisionDetected',
collisionResolved = 'collisionResolved';
return function(entity, deltaTime){
//Get all the components of this entity that will be required in the tests.
var positionComponent = entity.get(Position),
movableComponent = entity.get(Movable),
collidableComponent = entity.get(Collidable),
shapeComponent = entity.get(Shape),
AABB = shapeComponent._AABB,
//Declare variables that will be used inside of the loops.
otherEntity,
otherPositionComponent,
otherMovableComponent,
otherCollidableComponent,
otherShapeComponent,
possibleCollisions;
//Query the entity quad tree for possible collisions.
possibleCollisions = entityQuadTree.queryEntities(AABB);
//Check for collision against every entity on that list.
for(var i=0; i<possibleCollisions.length; ++i){
otherEntity = possibleCollisions[i];
//Don't test the entity against itself.
if(entity === otherEntity){
continue;
}
otherPositionComponent = otherEntity.get(Position);
otherMovableComponent = otherEntity.get(Movable);
otherCollidableComponent = otherEntity.get(Collidable);
otherShapeComponent = otherEntity.get(Shape);
if(testIntersection(positionComponent, shapeComponent, otherPositionComponent, otherShapeComponent, contactData.collisionData)){
contactData._entityA = entity;
contactData._entityB = otherEntity;
thisPhysicsSystem.trigger(contactDetected, contactData);
//Resolve collision only if both entities are collidable.
if(collidableComponent && otherCollidableComponent){
//Compare the collision bit masks to see if these entities should collide.
if( (collidableComponent._collideWith & otherCollidableComponent._category) &&
(otherCollidableComponent._collideWith & collidableComponent._category) ){
//Reset the collision resolution flag before triggering the event.
//The user can set this flag to false in the collision callback. By doing so
//the collision will not be resolved.
contactData._resolveCollision = true;
thisPhysicsSystem.trigger(collisionDetected, contactData);
if(contactData._resolveCollision){
//Check if the other entity is dynamic or static.
if(otherMovableComponent){
resolveDynamicDynamicCollision(positionComponent, movableComponent, collidableComponent, otherPositionComponent, otherMovableComponent, otherCollidableComponent, contactData.collisionData);
}else{
resolveDynamicStaticCollision(positionComponent, movableComponent, collidableComponent, otherCollidableComponent, contactData.collisionData, deltaTime);
}
thisPhysicsSystem.trigger(collisionResolved, contactData);
}
}
}
}
}
};
})();
function calculateAABB(positionComponent, shapeComponent){
var position = positionComponent._position,
shape = shapeComponent._shape;
if(shape instanceof Box || shape instanceof Polygon){
var vertices = shape.getVertices(),
vertex = vertices[0];
shapeComponent._AABB = new AABB(vertex.x, vertex.y, vertex.x, vertex.y);
for(var i=1; i<vertices.length; ++i){
vertex = vertices[i];
if(vertex.x < shapeComponent._AABB.topLeft.x){
shapeComponent._AABB.topLeft.x = vertex.x;
}
if(vertex.y < shapeComponent._AABB.topLeft.y){
shapeComponent._AABB.topLeft.y = vertex.y;
}
if(vertex.x > shapeComponent._AABB.bottomRight.x){
shapeComponent._AABB.bottomRight.x = vertex.x;
}
if(vertex.y > shapeComponent._AABB.bottomRight.y){
shapeComponent._AABB.bottomRight.y = vertex.y;
}
}
}else if(shape instanceof Circle){
var radius = shape.getRadius();
shapeComponent._AABB = new AABB(-radius, -radius, radius, radius);
}
//Offset the AABB corners by the position.
shapeComponent._AABB.topLeft.add(position);
shapeComponent._AABB.bottomRight.add(position);
}
function updateAABB(positionComponent, shapeComponent){
var position = positionComponent._position,
shape = shapeComponent._shape,
topLeft = shapeComponent._AABB.topLeft,
bottomRight = shapeComponent._AABB.bottomRight;
if(shape instanceof Box || shape instanceof Polygon){
var vertices = shape.getVertices(),
vertex = vertices[0];
topLeft.x = bottomRight.x = vertex.x;
topLeft.y = bottomRight.y = vertex.y;
for(var i=1; i<vertices.length; ++i){
vertex = vertices[i];
if(vertex.x < topLeft.x){
topLeft.x = vertex.x;
}
if(vertex.y < topLeft.y){
topLeft.y = vertex.y;
}
if(vertex.x > bottomRight.x){
bottomRight.x = vertex.x;
}
if(vertex.y > bottomRight.y){
bottomRight.y = vertex.y;
}
}
}else if(shape instanceof Circle){
var radius = shape.getRadius();
topLeft.x = topLeft.y = -radius;
bottomRight.x = bottomRight.y = radius;
}
//Offset the AABB corners by the position.
topLeft.add(position);
bottomRight.add(position);
}
/**
Steps the world.
@method update
@param deltaTime {Number} The time that passed since last update.
*/
this.update = function(deltaTime){
//Vector that will be reused during dynamic entity position integration.
var positionChangeDueToVelocity = new Vector2D();
//Update dynamic entities.
dynamicEntities.iterate(function(entity){
var position = entity.get(Position),
movable = entity.get(Movable),
shape = entity.get(Shape),
actualDeltaTime, i;
//The velocity is integrated once at the beginning of the test, no matter whether the object is fast or not.
//Since the force and impulse accumulators are cleared after integration anyway, all operations can be performed on them directly.
movable._velocity.add(movable._forceAccumulator.multiply(movable._inverseMass).multiply(deltaTime));
movable._velocity.add(movable._impulseAccumulator.multiply(movable._inverseMass));
//Store the velocity gained during this frame due to acceleration.
movable._lastFrameAcceleration.x = movable._forceAccumulator.x;
movable._lastFrameAcceleration.y = movable._forceAccumulator.y;
//Reset the force and impulse accumulators.
movable.clearForces();
movable.clearImpulses();
if(movable._fastObject){
//If the object is a fast object, calculate the delta time for each test.
actualDeltaTime = deltaTime / TESTS_PER_FRAME_FOR_FAST_OBJECTS;
//Set i so that multiple tests will be performed.
i = 0;
}else{
//If the object is a regular object, the delta time is equal to the passed-in frame time.
actualDeltaTime = deltaTime;
//Set i so that only one test will be performed.
i = TESTS_PER_FRAME_FOR_FAST_OBJECTS - 1;
}
for(; i<TESTS_PER_FRAME_FOR_FAST_OBJECTS; ++i){
//Remove the entity from the quad tree.
entityQuadTree.remove(entity);
//Integrate the position.
position._position.add(movable._velocity.multiplied(actualDeltaTime, positionChangeDueToVelocity));
//Recalculate the AABB for this entity.
updateAABB(position, shape);
//Insert the entity back into the quad tree.
entityQuadTree.insert(entity);
//Run collision detection.
testDynamicEntityForCollisions(entity, actualDeltaTime);
}
});
};
//=====Debug drawing=====
/**
The debugging interface. It's used to render the physical representation of entities as well as the spatial partitioning object used by the engine.
@property debugDraw
@type Object
@final
*/
this.debugDraw = {
/**
This object is used to set the colors of the objects being drawn as well as line thickness.
@property debugDraw.configure
@type Object
@final
*/
configure : {
/**
Specifies the color used when rendering static physics bodies.
@property debugDraw.configure.staticEntitiesColor
@type String
@default 'red'
*/
staticEntitiesColor : 'red',
/**
Specifies the color used when rendering dynamic physics bodies.
@property debugDraw.configure.dynamicEntitiesColor
@type String
@default 'green'
*/
dynamicEntitiesColor : 'green',
/**
Specifies the color used when rendering the spatial partitioning object.
@property debugDraw.configure.quadTreeColor
@type String
@default 'blue'
*/
quadTreeColor : 'blue',
/**
Specifies line thickness for rendered objects.
@property debugDraw.configure.lineThickness
@type Number
@default 1
*/
lineThickness : 1
},
/**
Draws the physical representations of all entities.
@method debugDraw.entities
@param view {View} View to be rendered to.
@param drawAABBs {Boolean} Specifies whether Axis-Aligned Bounding Boxes should be rendered for the entities.
@param drawVelocities {Boolean} Specifies whether velocity vectors should be rendered for dynamic physics bodies.
*/
entities : (function(){
var viewAABB = new AABB();
return function(view, drawAABBs, drawVelocities){
var viewHalfWidth = view.getViewWidth() / 2,
viewHalfHeight = view.getViewHeight() / 2;
//Calculate the view's AABB.
view.getPosition(viewAABB.topLeft);
viewAABB.topLeft.x -= viewHalfWidth;
viewAABB.topLeft.y -= viewHalfHeight;
view.getPosition(viewAABB.bottomRight);
viewAABB.bottomRight.x += viewHalfWidth;
viewAABB.bottomRight.y += viewHalfHeight;
var entitiesToRender = entityQuadTree.queryEntities(viewAABB),
entity, positionComponent, movableComponent, shapeComponent, shape, entityAABB, vertices;
view.getContext().save();
//Set line width.
view.getContext().lineWidth = this.configure.lineThickness;
//Reset the view transform.
view.resetContextTransform();
view.applyViewTransform();
for(var i=0; i<entitiesToRender.length; ++i){
entity = entitiesToRender[i];
positionComponent = entity.get(Position);
movableComponent = entity.get(Movable);
shapeComponent = entity.get(Shape);
shape = shapeComponent._shape;
entityAABB = shapeComponent._AABB;
//Set the color.
view.getContext().strokeStyle = movableComponent ? this.configure.dynamicEntitiesColor : this.configure.staticEntitiesColor;
//Draw the shape.
if(shape instanceof Box || shape instanceof Polygon){
vertices = shape.getVertices();
view.getContext().beginPath();
view.getContext().moveTo(vertices[0].x + positionComponent._position.x, vertices[0].y + positionComponent._position.y);
for(var j=1; j<vertices.length; ++j){
view.getContext().lineTo(vertices[j].x + positionComponent._position.x, vertices[j].y + positionComponent._position.y);
}
view.getContext().closePath();
view.getContext().stroke();
}else if(shape instanceof Circle){
view.getContext().beginPath();
view.getContext().arc(positionComponent._position.x, positionComponent._position.y, shape.getRadius(), 0, Math.PI * 2, true);
view.getContext().stroke();
}
//Draw the AABB.
if(drawAABBs){
view.getContext().strokeRect(entityAABB.topLeft.x, entityAABB.topLeft.y, entityAABB.bottomRight.x - entityAABB.topLeft.x, entityAABB.bottomRight.y - entityAABB.topLeft.y);
}
//Draw velocity.
if(drawVelocities && movableComponent){
view.getContext().beginPath();
view.getContext().moveTo(positionComponent._position.x, positionComponent._position.y);
view.getContext().lineTo(positionComponent._position.x + movableComponent._velocity.x, positionComponent._position.y + movableComponent._velocity.y);
view.getContext().stroke();
}
}
view.getContext().restore();
};
})(),
/**
Draws the spatial partitioning object.
@method debugDraw.quadTree
@param view {View} View to be rendered to.
*/
quadTree : (function(){
var viewAABB = new AABB();
return function(view){
var viewHalfWidth = view.getViewWidth() / 2,
viewHalfHeight = view.getViewHeight() / 2;
//Calculate the view's AABB.
view.getPosition(viewAABB.topLeft);
viewAABB.topLeft.x -= viewHalfWidth;
viewAABB.topLeft.y -= viewHalfHeight;
view.getPosition(viewAABB.bottomRight);
viewAABB.bottomRight.x += viewHalfWidth;
viewAABB.bottomRight.y += viewHalfHeight;
var leavesToRender = entityQuadTree.queryLeaves(viewAABB),
leaf;
view.getContext().save();
//Set the color.
view.getContext().strokeStyle = this.configure.quadTreeColor;
//Set line width.
view.getContext().lineWidth = this.configure.lineThickness;
//Reset the view transform.
view.resetContextTransform();
view.applyViewTransform();
//Draw the leaves.
for(var i=0; i<leavesToRender.length; ++i){
leaf = leavesToRender[i];
view.getContext().strokeRect(leaf.center.x - leaf.extents.halfWidth, leaf.center.y - leaf.extents.halfHeight, leaf.extents.halfWidth * 2, leaf.extents.halfHeight * 2);
}
view.getContext().restore();
};
})()
};
/**
@method destroy
*/
this.destroy = function(){
staticEntities.destroy();
dynamicEntities.destroy();
};
}
//Inherit from the event handling object.
PhysicsSystem.prototype = Object.create(EventHandler.prototype);
PhysicsSystem.prototype.constructor = PhysicsSystem;