UNPKG

lettuce

Version:

Lettuce JS, Mini Mobile Framework for Romantic with DSL.

814 lines (751 loc) 34.1 kB
/* * MelonJS Game Engine * Copyright (C) 2011 - 2014 Olivier Biot, Jason Oster, Aaron McLeod * http://www.melonjs.org * * Separating Axis Theorem implementation, based on the SAT.js library by Jim Riecken <jimr@jimr.ca> * Available under the MIT License - https://github.com/jriecken/sat-js */ (function () { /** * Constants for Vornoi regions * @ignore */ var LEFT_VORNOI_REGION = -1; /** * Constants for Vornoi regions * @ignore */ var MIDDLE_VORNOI_REGION = 0; /** * Constants for Vornoi regions * @ignore */ var RIGHT_VORNOI_REGION = 1; /** * A pool of `Vector` objects that are used in calculations to avoid allocating memory. * @type {Array.<Vector>} */ var T_VECTORS = []; for (var v = 0; v < 10; v++) { T_VECTORS.push(new me.Vector2d()); } /** * A pool of arrays of numbers used in calculations to avoid allocating memory. * @type {Array.<Array.<number>>} */ var T_ARRAYS = []; for (var a = 0; a < 5; a++) { T_ARRAYS.push([]); } /** * Flattens the specified array of points onto a unit vector axis, * resulting in a one dimensional range of the minimum and * maximum value on that axis. * @param {Array.<Vector>} points The points to flatten. * @param {Vector} normal The unit vector axis to flatten on. * @param {Array.<number>} result An array. After calling this function, * result[0] will be the minimum value, * result[1] will be the maximum value. */ function flattenPointsOn(points, normal, result) { var min = Number.MAX_VALUE; var max = -Number.MAX_VALUE; var len = points.length; for (var i = 0; i < len; i++) { // The magnitude of the projection of the point onto the normal var dot = points[i].dotProduct(normal); if (dot < min) { min = dot; } if (dot > max) { max = dot; } } result[0] = min; result[1] = max; } /** * Check whether two convex polygons are separated by the specified * axis (must be a unit vector). * @param {Vector} aPos The position of the first polygon. * @param {Vector} bPos The position of the second polygon. * @param {Array.<Vector>} aPoints The points in the first polygon. * @param {Array.<Vector>} bPoints The points in the second polygon. * @param {Vector} axis The axis (unit sized) to test against. The points of both polygons * will be projected onto this axis. * @param {Response=} response A Response object (optional) which will be populated * if the axis is not a separating axis. * @return {boolean} true if it is a separating axis, false otherwise. If false, * and a response is passed in, information about how much overlap and * the direction of the overlap will be populated. */ function isSeparatingAxis(aPos, bPos, aPoints, bPoints, axis, response) { var rangeA = T_ARRAYS.pop(); var rangeB = T_ARRAYS.pop(); // The magnitude of the offset between the two polygons var offsetV = T_VECTORS.pop().copy(bPos).sub(aPos); var projectedOffset = offsetV.dotProduct(axis); // Project the polygons onto the axis. flattenPointsOn(aPoints, axis, rangeA); flattenPointsOn(bPoints, axis, rangeB); // Move B's range to its position relative to A. rangeB[0] += projectedOffset; rangeB[1] += projectedOffset; // Check if there is a gap. If there is, this is a separating axis and we can stop if (rangeA[0] > rangeB[1] || rangeB[0] > rangeA[1]) { T_VECTORS.push(offsetV); T_ARRAYS.push(rangeA); T_ARRAYS.push(rangeB); return true; } // This is not a separating axis. If we're calculating a response, calculate the overlap. if (response) { var overlap = 0; // A starts further left than B if (rangeA[0] < rangeB[0]) { response.aInB = false; // A ends before B does. We have to pull A out of B if (rangeA[1] < rangeB[1]) { overlap = rangeA[1] - rangeB[0]; response.bInA = false; // B is fully inside A. Pick the shortest way out. } else { var option1 = rangeA[1] - rangeB[0]; var option2 = rangeB[1] - rangeA[0]; overlap = option1 < option2 ? option1 : -option2; } // B starts further left than A } else { response.bInA = false; // B ends before A ends. We have to push A out of B if (rangeA[1] > rangeB[1]) { overlap = rangeA[0] - rangeB[1]; response.aInB = false; // A is fully inside B. Pick the shortest way out. } else { var option11 = rangeA[1] - rangeB[0]; var option22 = rangeB[1] - rangeA[0]; overlap = option11 < option22 ? option11 : -option22; } } // If this is the smallest amount of overlap we've seen so far, set it as the minimum overlap. var absOverlap = Math.abs(overlap); if (absOverlap < response.overlap) { response.overlap = absOverlap; response.overlapN.copy(axis); if (overlap < 0) { response.overlapN.reverse(); } } } T_VECTORS.push(offsetV); T_ARRAYS.push(rangeA); T_ARRAYS.push(rangeB); return false; } /** * Calculates which Vornoi region a point is on a line segment. <br> * It is assumed that both the line and the point are relative to `(0,0)`<br> * <br> * | (0) |<br> * (-1) [S]--------------[E] (1)<br> * | (0) |<br> * * @ignore * @param {Vector} line The line segment. * @param {Vector} point The point. * @return {number} LEFT_VORNOI_REGION (-1) if it is the left region, * MIDDLE_VORNOI_REGION (0) if it is the middle region, * RIGHT_VORNOI_REGION (1) if it is the right region. */ function vornoiRegion(line, point) { var len2 = line.length2(); var dp = point.dotProduct(line); if (dp < 0) { // If the point is beyond the start of the line, it is in the // left vornoi region. return LEFT_VORNOI_REGION; } else if (dp > len2) { // If the point is beyond the end of the line, it is in the // right vornoi region. return RIGHT_VORNOI_REGION; } else { // Otherwise, it's in the middle one. return MIDDLE_VORNOI_REGION; } } /** * A singleton for managing collision detection (and projection-based collision response) of 2D shapes.<br> * Based on the Separating Axis Theorem and supports detecting collisions between simple Axis-Aligned Boxes, convex polygons and circles based shapes. * @namespace * @property {Singleton} collision * @memberOf me */ me.collision = (function () { // hold public stuff in our singleton var api = {}; /* * PUBLIC STUFF */ /** * the world quadtree used for the collision broadphase * @name quadTree * @memberOf me.collision * @public * @type {me.QuadTree} */ api.quadTree = null; /** * The maximum number of levels that the quadtree will create. Default is 4. * @name maxDepth * @memberOf me.collision * @public * @type {number} * @see me.collision.quadTree * */ api.maxDepth = 4; /** * The maximum number of children that a quadtree node can contain before it is split into sub-nodes. Default is 8. * @name maxChildren * @memberOf me.collision * @public * @type {boolean} * @see me.collision.quadTree */ api.maxChildren = 8; /** * bounds of the physic world. * @name bounds * @memberOf me.collision * @public * @type {me.Rect} */ api.bounds = null; /** * Enum for collision type values. <br> * Possible values are : <br> * - <b>`NO_OBJECT`</b> (to disable collision check) <br> * - <b>`PLAYER_OBJECT`</b> <br> * - <b>`NPC_OBJECT`</b> <br> * - <b>`ENEMY_OBJECT`</b> <br> * - <b>`COLLECTABLE_OBJECT`</b> <br> * - <b>`ACTION_OBJECT`</b> <br> * - <b>`PROJECTILE_OBJECT`</b> <br> * - <b>`WORLD_SHAPE`</b> (for collision check with collision shapes/tiles) <br> * - <b>`ALL_OBJECT`</b> <br> * @readonly * @enum {number} * @name types * @memberOf me.collision * @see me.body.setCollisionMask * @see me.body.collisionType * @example * // set the entity body collision type * myEntity.body.setCollisionType = me.collision.types.PLAYER_OBJECT; * // filter collision detection with collision shapes, enemies and collectables * myEntity.body.setCollisionMask(me.collision.types.WORLD_SHAPE | me.collision.types.ENEMY_OBJECT | me.collision.types.COLLECTABLE_OBJECT); */ api.types = { /** to disable collision check */ NO_OBJECT : 0, /** * Default object type constant for collision filtering * @constant * @name PLAYER_OBJECT * @memberOf me.collision.types */ PLAYER_OBJECT : 1, /** * Default object type constant for collision filtering * @constant * @name NPC_OBJECT * @memberOf me.collision.types */ NPC_OBJECT : 2, /** * Default object type constant for collision filtering * @constant * @name ENEMY_OBJECT * @memberOf me.collision.types */ ENEMY_OBJECT : 4, /** * Default object type constant for collision filtering * @constant * @name COLLECTABLE_OBJECT * @memberOf me.collision.types */ COLLECTABLE_OBJECT : 8, /** * Default object type constant for collision filtering * @constant * @name ACTION_OBJECT * @memberOf me.collision.types */ ACTION_OBJECT : 16, // door, etc... /** * Default object type constant for collision filtering * @constant * @name PROJECTILE_OBJECT * @memberOf me.collision.types */ PROJECTILE_OBJECT : 32, // missiles, etc... /** * Default object type constant for collision filtering * @constant * @name WORLD_SHAPE * @memberOf me.collision.types */ WORLD_SHAPE : 64, // walls, etc... /** * Default object type constant for collision filtering * @constant * @name ALL_OBJECT * @memberOf me.collision.types */ ALL_OBJECT : 0xFFFFFFFF // all objects }; /** * Initialize the collision/physic world * @ignore */ api.init = function () { // default bounds to the game viewport api.bounds = me.game.viewport.clone(); // initializa the quadtree api.quadTree = new me.QuadTree(api.bounds, api.maxChildren, api.maxDepth); // reset the collision detection engine if a TMX level is loaded me.event.subscribe(me.event.LEVEL_LOADED, function () { // default bounds to game world me.collision.bounds = me.game.world.clone(); // reset the quadtree me.collision.quadTree.clear(me.collision.bounds); }); }; /** * An object representing the result of an intersection, contains: <br> * - <b>`a`</b> and <b>`b`</b> {me.Entity} : The two objects participating in the intersection <br> * - <b>`overlap`</b> {number} : Magnitude of the overlap on the shortest colliding axis. <br> * - <b>`overlapV`</b> {me.vector2d}: The overlap vector (i.e. `overlapN.scale(overlap, overlap)`). If this vector is subtracted from the position of a, a and b will no longer be colliding <br> * - <b>`overlapN`</b> {me.vector2d}: The shortest colliding axis (unit-vector) <br> * - <b>`aInB`</b>, <b>`bInA`</b> {boolean} : Whether the first object is entirely inside the second, and vice versa. <br> * - <b>`indexShapeA</b> {number} : the index of the colliding shape for the object a body. <br> * - <b>`indexShapeB</b> {number} : the index of the colliding shape for the object b body. <br> * - <b>`clear()`</b> {function} : Set some values of the response back to their defaults. Call this between tests if you are going to reuse a single Response object for multiple intersection tests <br> * @name ResponseObject * @memberOf me.collision * @public * @type {external:Object} * @see me.collision.check */ api.ResponseObject = function () { this.a = null; this.b = null; this.overlapN = new me.Vector2d(); this.overlapV = new me.Vector2d(); this.aInB = true; this.bInA = true; this.indexShapeA = -1; this.indexShapeB = -1; this.overlap = Number.MAX_VALUE; }; /** * Set some values of the response back to their defaults. <br> * Call this between tests if you are going to reuse a single <br> * Response object for multiple intersection tests <br> * (recommended as it will avoid allocating extra memory) <br> * @name clear * @memberOf me.collision.ResponseObject * @public * @function */ api.ResponseObject.prototype.clear = function () { this.aInB = true; this.bInA = true; this.overlap = Number.MAX_VALUE; this.indexShapeA = -1; this.indexShapeB = -1; return this; }; /** * a global instance of a response object used for collision detection <br> * this object will be reused amongst collision detection call if not user-defined response is specified * @name response * @memberOf me.collision * @public * @type {me.collision.ResponseObject} */ api.response = new api.ResponseObject(); /** * a callback used to determine if two objects should collide (based on both respective objects collision mask and type).<br> * you can redefine this function if you need any specific rules over what should collide with what. * @name shouldCollide * @memberOf me.collision * @public * @function * @param {me.Entity} a a reference to the object A. * @param {me.Entity} b a reference to the object B. * @return {Boolean} true if they should collide, false otherwise */ api.shouldCollide = function (a, b) { return ( a.body && b.body && (a.body.collisionMask & b.body.collisionType) !== 0 && (a.body.collisionType & b.body.collisionMask) !== 0 ); }; /** * Checks if the specified entity collides with others entities * @name check * @memberOf me.collision * @public * @function * @param {me.Entity} obj entity to be tested for collision * @param {me.collision.ResponseObject} [respObj=me.collision.response] a user defined response object that will be populated if they intersect. * @return {Boolean} in case of collision, false otherwise * @example * update : function (dt) { * ... * // handle collisions against other shapes * me.collision.check(this); * ... * }; * * // colision handler * onCollision : function (response) { * if (response.b.body.collisionType === me.collision.types.ENEMY_OBJECT) { * // makes the other entity solid, by substracting the overlap vector to the current position * this.pos.sub(response.overlapV); * this.hurt(); * // not solid * return false; * } * // Make the object solid * return true; * }; */ api.check = function (objA, responseObject) { var collision = 0; var response = responseObject || api.response; // retreive a list of potential colliding objects var candidates = api.quadTree.retrieve(objA); for (var i = candidates.length, objB; i--, (objB = candidates[i]);) { // check if both objects "should" collide if ((objB !== objA) && api.shouldCollide(objA, objB) && // fast AABB check if both bounding boxes are overlaping objA.getBounds().overlaps(objB.getBounds())) { // go trough all defined shapes in A var aLen = objA.body.shapes.length; var bLen = objB.body.shapes.length; if (aLen === 0 || bLen === 0) { continue; } var indexA = 0; do { var shapeA = objA.body.getShape(indexA); // go through all defined shapes in B var indexB = 0; do { var shapeB = objB.body.getShape(indexB); // full SAT collision check if (api["test" + shapeA.shapeType + shapeB.shapeType] .call( this, objA, // a reference to the object A shapeA, objB, // a reference to the object B shapeB, // clear response object before reusing response.clear()) === true ) { // we touched something ! collision++; // set the shape index response.indexShapeA = indexA; response.indexShapeB = indexB; // execute the onCollision callback if (objA.onCollision(response, objB) !== false) { objA.body.respondToCollision.call(objA.body, response); } if (objB.onCollision(response, objA) !== false) { objB.body.respondToCollision.call(objB.body, response); } } indexB++; } while (indexB < bLen); indexA++; } while (indexA < aLen); } } // we could return the amount of objects we collided with ? return collision > 0; }; /** * Checks whether polygons collide. * @ignore * @param {me.Entity} a a reference to the object A. * @param {me.Polygon} polyA a reference to the object A Polygon to be tested * @param {me.Entity} b a reference to the object B. * @param {me.Polygon} polyB a reference to the object B Polygon to be tested * @param {Response=} response Response object (optional) that will be populated if they intersect. * @return {boolean} true if they intersect, false if they don't. */ api.testPolygonPolygon = function (a, polyA, b, polyB, response) { // specific point for var aPoints = polyA.points; var aNormals = polyA.normals; var aLen = aNormals.length; var bPoints = polyB.points; var bNormals = polyB.normals; var bLen = bNormals.length; // aboslute shape position var posA = T_VECTORS.pop().copy(a.pos).add(polyA.pos); var posB = T_VECTORS.pop().copy(b.pos).add(polyB.pos); var i; // If any of the edge normals of A is a separating axis, no intersection. for (i = 0; i < aLen; i++) { if (isSeparatingAxis(posA, posB, aPoints, bPoints, aNormals[i], response)) { T_VECTORS.push(posA); T_VECTORS.push(posB); return false; } } // If any of the edge normals of B is a separating axis, no intersection. for (i = 0;i < bLen; i++) { if (isSeparatingAxis(posA, posB, aPoints, bPoints, bNormals[i], response)) { T_VECTORS.push(posA); T_VECTORS.push(posB); return false; } } // Since none of the edge normals of A or B are a separating axis, there is an intersection // and we've already calculated the smallest overlap (in isSeparatingAxis). Calculate the // final overlap vector. if (response) { response.a = a; response.b = b; response.overlapV.copy(response.overlapN).scale(response.overlap); } T_VECTORS.push(posA); T_VECTORS.push(posB); return true; }; /** * Check if two Ellipse collide. * @ignore * @param {me.Entity} a a reference to the object A. * @param {me.Ellipse} ellipseA a reference to the object A Ellipse to be tested * @param {me.Entity} b a reference to the object B. * @param {me.Ellipse} ellipseB a reference to the object B Ellipse to be tested * @param {Response=} response Response object (optional) that will be populated if * the circles intersect. * @return {boolean} true if the circles intersect, false if they don't. */ api.testEllipseEllipse = function (a, ellipseA, b, ellipseB, response) { // Check if the distance between the centers of the two // circles is greater than their combined radius. var differenceV = T_VECTORS.pop().copy(b.pos).add(ellipseB.pos).sub(a.pos).sub(ellipseA.pos); var radiusA = ellipseA.radius; var radiusB = ellipseB.radius; var totalRadius = radiusA + radiusB; var totalRadiusSq = totalRadius * totalRadius; var distanceSq = differenceV.length2(); // If the distance is bigger than the combined radius, they don't intersect. if (distanceSq > totalRadiusSq) { T_VECTORS.push(differenceV); return false; } // They intersect. If we're calculating a response, calculate the overlap. if (response) { var dist = Math.sqrt(distanceSq); response.a = a; response.b = b; response.overlap = totalRadius - dist; response.overlapN.copy(differenceV.normalize()); response.overlapV.copy(differenceV).scale(response.overlap); response.aInB = radiusA <= radiusB && dist <= radiusB - radiusA; response.bInA = radiusB <= radiusA && dist <= radiusA - radiusB; } T_VECTORS.push(differenceV); return true; }; /** * Check if a polygon and an ellipse collide. * @ignore * @param {me.Entity} a a reference to the object A. * @param {me.Polygon} polyA a reference to the object A Polygon to be tested * @param {me.Entity} b a reference to the object B. * @param {me.Ellipse} ellipseB a reference to the object B Ellipse to be tested * @param {Response=} response Response object (optional) that will be populated if they intersect. * @return {boolean} true if they intersect, false if they don't. */ api.testPolygonEllipse = function (a, polyA, b, ellipseB, response) { // Get the position of the circle relative to the polygon. var circlePos = T_VECTORS.pop().copy(b.pos).add(ellipseB.pos).sub(a.pos).sub(polyA.pos); var radius = ellipseB.radius; var radius2 = radius * radius; var points = polyA.points; var edges = polyA.edges; var len = edges.length; var edge = T_VECTORS.pop(); var normal = T_VECTORS.pop(); var point = T_VECTORS.pop(); var dist = 0; // For each edge in the polygon: for (var i = 0; i < len; i++) { var next = i === len - 1 ? 0 : i + 1; var prev = i === 0 ? len - 1 : i - 1; var overlap = 0; var overlapN = null; // Get the edge. edge.copy(edges[i]); // Calculate the center of the circle relative to the starting point of the edge. point.copy(circlePos).sub(points[i]); // If the distance between the center of the circle and the point // is bigger than the radius, the polygon is definitely not fully in // the circle. if (response && point.length2() > radius2) { response.aInB = false; } // Calculate which Vornoi region the center of the circle is in. var region = vornoiRegion(edge, point); var inRegion = true; // If it's the left region: if (region === LEFT_VORNOI_REGION) { var point2 = null; if (len > 1) { // We need to make sure we're in the RIGHT_VORNOI_REGION of the previous edge. edge.copy(edges[prev]); // Calculate the center of the circle relative the starting point of the previous edge point2 = T_VECTORS.pop().copy(circlePos).sub(points[prev]); region = vornoiRegion(edge, point2); if (region !== RIGHT_VORNOI_REGION) { inRegion = false; } } if (inRegion) { // It's in the region we want. Check if the circle intersects the point. dist = point.length(); if (dist > radius) { // No intersection T_VECTORS.push(circlePos); T_VECTORS.push(edge); T_VECTORS.push(normal); T_VECTORS.push(point); if (point2) { T_VECTORS.push(point2); } return false; } else if (response) { // It intersects, calculate the overlap. response.bInA = false; overlapN = point.normalize(); overlap = radius - dist; } } if (point2) { T_VECTORS.push(point2); } // If it's the right region: } else if (region === RIGHT_VORNOI_REGION) { if (len > 1) { // We need to make sure we're in the left region on the next edge edge.copy(edges[next]); // Calculate the center of the circle relative to the starting point of the next edge. point.copy(circlePos).sub(points[next]); region = vornoiRegion(edge, point); if (region !== LEFT_VORNOI_REGION) { inRegion = false; } } if (inRegion) { // It's in the region we want. Check if the circle intersects the point. dist = point.length(); if (dist > radius) { // No intersection T_VECTORS.push(circlePos); T_VECTORS.push(edge); T_VECTORS.push(normal); T_VECTORS.push(point); return false; } else if (response) { // It intersects, calculate the overlap. response.bInA = false; overlapN = point.normalize(); overlap = radius - dist; } } // Otherwise, it's the middle region: } else { // Need to check if the circle is intersecting the edge, // Get the normal. normal.copy(polyA.normals[i]); // Find the perpendicular distance between the center of the // circle and the edge. dist = point.dotProduct(normal); var distAbs = Math.abs(dist); // If the circle is on the outside of the edge, there is no intersection. if ((len === 1 || dist > 0) && distAbs > radius) { // No intersection T_VECTORS.push(circlePos); T_VECTORS.push(edge); T_VECTORS.push(normal); T_VECTORS.push(point); return false; } else if (response) { // It intersects, calculate the overlap. overlapN = normal; overlap = radius - dist; // If the center of the circle is on the outside of the edge, or part of the // circle is on the outside, the circle is not fully inside the polygon. if (dist >= 0 || overlap < 2 * radius) { response.bInA = false; } } } // If this is the smallest overlap we've seen, keep it. // (overlapN may be null if the circle was in the wrong Vornoi region). if (overlapN && response && Math.abs(overlap) < Math.abs(response.overlap)) { response.overlap = overlap; response.overlapN.copy(overlapN); } } // Calculate the final overlap vector - based on the smallest overlap. if (response) { response.a = a; response.b = b; response.overlapV.copy(response.overlapN).scale(response.overlap); } T_VECTORS.push(circlePos); T_VECTORS.push(edge); T_VECTORS.push(normal); T_VECTORS.push(point); return true; }; /** * Check if an ellipse and a polygon collide. <br> * **NOTE:** This is slightly less efficient than testPolygonEllipse as it just * runs testPolygonEllipse and reverses the response at the end. * @ignore * @param {me.Entity} a a reference to the object A. * @param {me.Ellipse} ellipseA a reference to the object A Ellipse to be tested * @param {me.Entity} a a reference to the object B. * @param {me.Polygon} polyB a reference to the object B Polygon to be tested * @param {Response=} response Response object (optional) that will be populated if * they intersect. * @return {boolean} true if they intersect, false if they don't. */ api.testEllipsePolygon = function (a, ellipseA, b, polyB, response) { // Test the polygon against the circle. var result = api.testPolygonEllipse(b, polyB, a, ellipseA, response); if (result && response) { // Swap A and B in the response. var resa = response.a; var aInB = response.aInB; response.overlapN.reverse(); response.overlapV.reverse(); response.a = response.b; response.b = resa; response.aInB = response.bInA; response.bInA = aInB; } return result; }; // return our object return api; })(); })();