const Vector = require('engine/Vector'); const { clamp } = require('engine/utils/math'); const { UP, DOWN, LEFT, RIGHT, OVERLAP, BOX, CIRC } = require('./const'); /** * AABB collision solver. This collision solver only supports * Box vs Box, Box vs Circle collisions. * * @class AABBSolver */ class AABBSolver { /** * @constructor */ constructor() { this.response = [ new Vector(), new Vector(), ]; } /** * Hit test a versus b. * @memberof AABBSolver# * @method hitTest * @param {Collider} a First collider * @param {Collider} b Second collider * @return {boolean} return true if bodies hit. */ hitTest(a, b) { // Skip when shape is not available if (!a.shape || !b.shape) {return false;} if (a.shape.type === BOX && b.shape.type === BOX) { return !( a.bottom <= b.top || a.top >= b.bottom || a.left >= b.right || a.right <= b.left ); } if (a.shape.type === CIRC && b.shape.type === CIRC) { // AABB overlap if (!( a.bottom <= b.top || a.top >= b.bottom || a.left >= b.right || a.right <= b.left) ) { return a.position.squaredDistance(b.position) < (a.shape.radius + b.shape.radius) * (a.shape.radius + b.shape.radius); } return false; } if ((a.shape.type === BOX && b.shape.type === CIRC) || (a.shape.type === CIRC && b.shape.type === BOX)) { let box = (a.shape.type === BOX) ? a : b; let circle = (a.shape.type === CIRC) ? a : b; // AABB overlap if (!( a.bottom <= b.top || a.top >= b.bottom || a.left >= b.right || a.right <= b.left) ) { let distX = circle.position.x - clamp(circle.position.x, box.left, box.right); let distY = circle.position.y - clamp(circle.position.y, box.top, box.bottom); return (distX * distX + distY * distY) < (circle.shape.radius * circle.shape.radius); } } return false; } /** * Hit response a versus b. * @memberof AABBSolver# * @method hitResponse * @param {Collider} a First collider * @param {Collider} b Second collider * @param {boolean} a2b Whether first collider receives hit response * @param {boolean} b2a Whether second collider receives hit response */ hitResponse(a, b, a2b, b2a) { let pushA = false, pushB = false; let resA = this.response[0].set(0), resB = this.response[1].set(0); let angle, dist; if (a.shape.type === BOX && b.shape.type === BOX) { // a.bottom <-> b.top if (a.lastBottom <= b.lastTop) { pushA = (a2b && a.collide(b, DOWN)); pushB = (b2a && b.collide(a, UP)); if (pushA && pushB) { resA.y = (b.top - a.bottom) * 0.5; resB.y = (a.bottom - b.top) * 0.5; } else if (pushA) { resA.y = (b.top - a.bottom); } else if (pushB) { resB.y = (a.bottom - b.top); } } // a.top <-> b.bottom else if (a.lastTop >= b.lastBottom) { pushA = (a2b && a.collide(b, UP)); pushB = (b2a && b.collide(a, DOWN)); if (pushA && pushB) { resA.y = (b.bottom - a.top) * 0.5; resB.y = (a.top - b.bottom) * 0.5; } else if (pushA) { resA.y = (b.bottom - a.top); } else if (pushB) { resB.y = (a.top - b.bottom); } } else if (a.lastRight <= b.lastLeft) { pushA = (a2b && a.collide(b, RIGHT)); pushB = (b2a && b.collide(a, LEFT)); if (pushA && pushB) { resA.x = (b.left - a.right) * 0.5; resB.x = (a.right - b.left) * 0.5; } else if (pushA) { resA.x = (b.left - a.right); } else if (pushB) { resB.x = (a.right - b.left); } } else if (a.lastLeft >= b.lastRight) { pushA = (a2b && a.collide(b, LEFT)); pushB = (b2a && b.collide(a, RIGHT)); if (pushA && pushB) { resA.x = (b.right - a.left) * 0.5; resB.x = (a.left - b.right) * 0.5; } else if (pushA) { resA.x = (b.right - a.left); } else if (pushB) { resB.x = (a.left - b.right); } } else { // Overlap does not push at all a2b && a.collide(b, OVERLAP); b2a && b.collide(a, OVERLAP); } } else if (a.shape.type === CIRC && b.shape.type === CIRC) { angle = b.position.angle(a.position); dist = a.shape.radius + b.shape.radius; pushA = (a2b && a.collide(b, angle)); pushB = (b2a && b.collide(a, angle)); if (pushA && pushB) { resA.x = Math.cos(angle) * dist * 0.5; resA.y = Math.sin(angle) * dist * 0.5; resB.x = -resA.x; resB.y = -resA.y; } else if (pushA) { resA.x = Math.cos(angle) * dist; resA.y = Math.sin(angle) * dist; } else if (pushB) { resB.x = -Math.cos(angle) * dist; resB.y = -Math.sin(angle) * dist; } } else { let box, circle; if (a.shape.type === BOX && b.shape.type === CIRC) { box = a; circle = b; } else { box = b; circle = a; } let closeX = clamp(circle.position.x, box.left, box.right); let closeY = clamp(circle.position.y, box.top, box.bottom); let radiusSq = circle.shape.radius * circle.shape.radius; let overlapX = Math.sqrt(radiusSq - (closeY - circle.position.y) * (closeY - circle.position.y)) - Math.abs(closeX - circle.position.x); let overlapY = Math.sqrt(radiusSq - (closeX - circle.position.x) * (closeX - circle.position.x)) - Math.abs(closeY - circle.position.y); overlapX = Math.max(0, overlapX); overlapY = Math.max(0, overlapY); angle = Math.atan2(b.velocity.y - a.velocity.y, b.velocity.x - a.velocity.x); pushA = (a2b && a.collide(b, -angle)); pushB = (b2a && b.collide(a, angle)); if (pushA && pushB) { resA.x = overlapX * Math.cos(angle) * 0.5; resA.y = overlapY * Math.sin(angle) * 0.5; resB.x = -resA.x; resB.y = -resA.y; } else if (pushA) { resA.x = overlapX * Math.cos(angle); resA.y = overlapY * Math.sin(angle); } else if (pushB) { resB.x = -overlapX * Math.cos(angle); resB.y = -overlapY * Math.sin(angle); } } // Apply response to colliders a.position.x = a.position.x + resA.x; a.position.y = a.position.y + resA.y; b.position.x = b.position.x + resB.x; b.position.y = b.position.y + resB.y; } } /** * AABBSolver factory * @return {AABBSolver} solver instance. */ module.exports = function() { return new AABBSolver(); };