// Phaser.Physics.Arcade.World var Body = require('./Body'); var Class = require('../../utils/Class'); var Rectangle = require('../../geom/rectangle/Rectangle'); var Vector2 = require('../../math/Vector2'); var DistanceBetween = require('../../math/distance/DistanceBetween'); var Clamp = require('../../math/Clamp'); var CONST = require('./const'); var GetValue = require('../../utils/object/GetValue'); var Set = require('../../structs/Set'); var World = new Class({ initialize: function World (scene, config) { this.scene = scene; this.events = scene.sys.events; this.bodies = new Set(); this.gravity = new Vector2(GetValue(config, 'gravity.x', 0), GetValue(config, 'gravity.y', 0)); this.bounds = new Rectangle( GetValue(config, 'x', 0), GetValue(config, 'y', 0), GetValue(config, 'width', scene.sys.game.config.width), GetValue(config, 'height', scene.sys.game.config.height) ); this.checkCollision = { up: GetValue(config, 'checkCollision.up', true), down: GetValue(config, 'checkCollision.down', true), left: GetValue(config, 'checkCollision.left', true), right: GetValue(config, 'checkCollision.right', true) }; this.maxObjects = GetValue(config, 'maxObjects', 10); this.maxLevels = GetValue(config, 'maxLevels',40); this.OVERLAP_BIAS = GetValue(config, 'overlapBias', 4); this.forceX = GetValue(config, 'forceX', false); this.sortDirection = CONST.LEFT_RIGHT; this.skipQuadTree = GetValue(config, 'skipQuadTree', true); this.isPaused = GetValue(config, 'isPaused', false); // this.quadTree = new Phaser.QuadTree(this.game.world.bounds.x, this.game.world.bounds.y, this.game.world.bounds.width, this.game.world.bounds.height, this.maxObjects, this.maxLevels); this._total = 0; this.drawDebug = GetValue(config, 'debug', false); this.debugGraphic; // this.setBoundsToWorld(); this.defaults = { debugShowBody: GetValue(config, 'debugShowBody', true), debugShowVelocity: GetValue(config, 'debugShowVelocity', true), bodyDebugColor: GetValue(config, 'debugBodyColor', 0xff00ff), velocityDebugColor: GetValue(config, 'debugVelocityColor', 0x00ff00) }; if (this.drawDebug) { this.createDebugGraphic(); } }, createDebugGraphic: function () { var graphic = this.scene.sys.add.graphics({ x: 0, y: 0 }); graphic.setZ(Number.MAX_SAFE_INTEGER); this.debugGraphic = graphic; this.drawDebug = true; return graphic; }, setBounds: function (x, y, width, height) { this.bounds.setTo(x, y, width, height); return this; }, pause: function () { this.isPaused = true; return this; }, resume: function () { this.isPaused = false; return this; }, update: function (time, delta) { if (this.isPaused || this.bodies.size === 0) { return; } // this.delta = Math.min(delta / 1000, this.maxStep) * this.timeScale; delta /= 1000; this.delta = delta; // Update all active bodies var i; var body; var bodies = this.bodies.entries; var len = bodies.length; for (i = 0; i < len; i++) { body = bodies[i]; if (body.enable) { body.update(delta); } } }, postUpdate: function () { var i; var body; var bodies = this.bodies.entries; var len = bodies.length; for (i = 0; i < len; i++) { body = bodies[i]; if (body.enable) { body.postUpdate(); } } if (this.drawDebug) { var graphics = this.debugGraphic; graphics.clear(); for (i = 0; i < len; i++) { body = bodies[i]; if (body.willDrawDebug()) { body.drawDebug(graphics); } } } }, updateMotion: function (body) { if (body.allowRotation) { var velocityDelta = this.computeVelocity(0, body, body.angularVelocity, body.angularAcceleration, body.angularDrag, body.maxAngular) - body.angularVelocity; body.angularVelocity += velocityDelta; body.rotation += (body.angularVelocity * this.delta); } body.velocity.x = this.computeVelocity(1, body, body.velocity.x, body.acceleration.x, body.drag.x, body.maxVelocity.x); body.velocity.y = this.computeVelocity(2, body, body.velocity.y, body.acceleration.y, body.drag.y, body.maxVelocity.y); }, computeVelocity: function (axis, body, velocity, acceleration, drag, max) { if (max === undefined) { max = 10000; } if (axis === 1 && body.allowGravity) { velocity += (this.gravity.x + body.gravity.x) * this.delta; } else if (axis === 2 && body.allowGravity) { velocity += (this.gravity.y + body.gravity.y) * this.delta; } if (acceleration) { velocity += acceleration * this.delta; } else if (drag && body.allowDrag) { drag *= this.delta; if (velocity - drag > 0) { velocity -= drag; } else if (velocity + drag < 0) { velocity += drag; } else { velocity = 0; } } if (velocity > max) { velocity = max; } else if (velocity < -max) { velocity = -max; } return velocity; }, overlap: function (object1, object2, overlapCallback, processCallback, callbackContext) { overlapCallback = overlapCallback || null; processCallback = processCallback || null; callbackContext = callbackContext || overlapCallback; this._total = 0; this.collideObjects(object1, object2, overlapCallback, processCallback, callbackContext, true); return (this._total > 0); }, collide: function (object1, object2, collideCallback, processCallback, callbackContext) { collideCallback = collideCallback || null; processCallback = processCallback || null; callbackContext = callbackContext || collideCallback; this._total = 0; this.collideObjects(object1, object2, collideCallback, processCallback, callbackContext, false); return (this._total > 0); }, sortLeftRight: function (a, b) { if (!a.body || !b.body) { return 0; } return a.body.x - b.body.x; }, sortRightLeft: function (a, b) { if (!a.body || !b.body) { return 0; } return b.body.x - a.body.x; }, sortTopBottom: function (a, b) { if (!a.body || !b.body) { return 0; } return a.body.y - b.body.y; }, sortBottomTop: function (a, b) { if (!a.body || !b.body) { return 0; } return b.body.y - a.body.y; }, /* sort: function (group, sortDirection) { if (group.physicsSortDirection !== null) { sortDirection = group.physicsSortDirection; } else { if (sortDirection === undefined) { sortDirection = this.sortDirection; } } if (sortDirection === Phaser.Physics.Arcade.LEFT_RIGHT) { // Game world is say 2000x600 and you start at 0 group.hash.sort(this.sortLeftRight); } else if (sortDirection === Phaser.Physics.Arcade.RIGHT_LEFT) { // Game world is say 2000x600 and you start at 2000 group.hash.sort(this.sortRightLeft); } else if (sortDirection === Phaser.Physics.Arcade.TOP_BOTTOM) { // Game world is say 800x2000 and you start at 0 group.hash.sort(this.sortTopBottom); } else if (sortDirection === Phaser.Physics.Arcade.BOTTOM_TOP) { // Game world is say 800x2000 and you start at 2000 group.hash.sort(this.sortBottomTop); } }, */ collideObjects: function (object1, object2, collideCallback, processCallback, callbackContext, overlapOnly) { var i; if (!Array.isArray(object1) && Array.isArray(object2)) { for (i = 0; i < object2.length; i++) { if (!object2[i]) { continue; } this.collideHandler(object1, object2[i], collideCallback, processCallback, callbackContext, overlapOnly); } } else if (Array.isArray(object1) && !Array.isArray(object2)) { for (i = 0; i < object1.length; i++) { if (!object1[i]) { continue; } this.collideHandler(object1[i], object2, collideCallback, processCallback, callbackContext, overlapOnly); } } else if (Array.isArray(object1) && Array.isArray(object2)) { for (i = 0; i < object1.length; i++) { if (!object1[i]) { continue; } for (var j = 0; j < object2.length; j++) { if (!object2[j]) { continue; } this.collideHandler(object1[i], object2[j], collideCallback, processCallback, callbackContext, overlapOnly); } } } else { this.collideHandler(object1, object2, collideCallback, processCallback, callbackContext, overlapOnly); } }, collideHandler: function (object1, object2, collideCallback, processCallback, callbackContext, overlapOnly) { // Only collide valid objects // if (object2 === undefined && object1.physicsType === Phaser.GROUP) // { // this.sort(object1); // this.collideGroupVsSelf(object1, collideCallback, processCallback, callbackContext, overlapOnly); // return; // } // If neither of the objects are set or exist then bail out if (!object1 || !object2 || !object1.enable || !object2.enable) { return; } this.collideSpriteVsSprite(object1, object2, collideCallback, processCallback, callbackContext, overlapOnly); }, collideSpriteVsSprite: function (sprite1, sprite2, collideCallback, processCallback, callbackContext, overlapOnly) { if (!sprite1.body || !sprite2.body) { return false; } if (this.separate(sprite1.body, sprite2.body, processCallback, callbackContext, overlapOnly)) { if (collideCallback) { collideCallback.call(callbackContext, sprite1, sprite2); } this._total++; } return true; }, separate: function (body1, body2, processCallback, callbackContext, overlapOnly) { if ( !body1.enable || !body2.enable || body1.checkCollision.none || body2.checkCollision.none || !this.intersects(body1, body2)) { return false; } // They overlap. Is there a custom process callback? If it returns true then we can carry on, otherwise we should abort. if (processCallback && processCallback.call(callbackContext, body1.gameObject, body2.gameObject) === false) { return false; } // Circle vs. Circle quick bail out if (body1.isCircle && body2.isCircle) { return this.separateCircle(body1, body2, overlapOnly); } // We define the behavior of bodies in a collision circle and rectangle // If a collision occurs in the corner points of the rectangle, the body behave like circles // Either body1 or body2 is a circle if (body1.isCircle !== body2.isCircle) { var bodyRect = (body1.isCircle) ? body2 : body1; var bodyCircle = (body1.isCircle) ? body1 : body2; var rect = { x: bodyRect.x, y: bodyRect.y, right: bodyRect.right, bottom: bodyRect.bottom }; var circle = bodyCircle.center; if (circle.y < rect.y || circle.y > rect.bottom) { if (circle.x < rect.x || circle.x > rect.right) { return this.separateCircle(body1, body2, overlapOnly); } } } var resultX = false; var resultY = false; // Do we separate on x or y first? if (this.forceX || Math.abs(this.gravity.y + body1.gravity.y) < Math.abs(this.gravity.x + body1.gravity.x)) { resultX = this.separateX(body1, body2, overlapOnly); // Are they still intersecting? Let's do the other axis then if (this.intersects(body1, body2)) { resultY = this.separateY(body1, body2, overlapOnly); } } else { resultY = this.separateY(body1, body2, overlapOnly); // Are they still intersecting? Let's do the other axis then if (this.intersects(body1, body2)) { resultX = this.separateX(body1, body2, overlapOnly); } } var result = (resultX || resultY); if (result) { if (overlapOnly) { if (body1.onOverlap) { body1.onOverlap.dispatch(body1.sprite, body2.sprite); } if (body2.onOverlap) { body2.onOverlap.dispatch(body2.sprite, body1.sprite); } } else { if (body1.onCollide) { body1.onCollide.dispatch(body1.sprite, body2.sprite); } if (body2.onCollide) { body2.onCollide.dispatch(body2.sprite, body1.sprite); } } } return result; }, intersects: function (body1, body2) { if (body1 === body2) { return false; } if (body1.isCircle) { if (body2.isCircle) { // Circle vs. Circle return DistanceBetween(body1.center.x, body1.center.y, body2.center.x, body2.center.y) <= (body1.halfWidth + body2.halfWidth); } else { // Circle vs. Rect return this.circleBodyIntersects(body1, body2); } } else if (body2.isCircle) { // Rect vs. Circle return this.circleBodyIntersects(body2, body1); } else { // Rect vs. Rect if (body1.right <= body2.position.x) { return false; } if (body1.bottom <= body2.position.y) { return false; } if (body1.position.x >= body2.right) { return false; } if (body1.position.y >= body2.bottom) { return false; } return true; } }, circleBodyIntersects: function (circle, body) { var x = Clamp(circle.center.x, body.left, body.right); var y = Clamp(circle.center.y, body.top, body.bottom); var dx = (circle.center.x - x) * (circle.center.x - x); var dy = (circle.center.y - y) * (circle.center.y - y); return (dx + dy) <= (circle.halfWidth * circle.halfWidth); }, separateCircle: function (body1, body2, overlapOnly) { // Set the bounding box overlap values this.getOverlapX(body1, body2); this.getOverlapY(body1, body2); var dx = body2.center.x - body1.center.x; var dy = body2.center.y - body1.center.y; var angleCollision = Math.atan2(dy, dx); var overlap = 0; if (body1.isCircle !== body2.isCircle) { var rect = { x: (body2.isCircle) ? body1.position.x : body2.position.x, y: (body2.isCircle) ? body1.position.y : body2.position.y, right: (body2.isCircle) ? body1.right : body2.right, bottom: (body2.isCircle) ? body1.bottom : body2.bottom }; var circle = { x: (body1.isCircle) ? body1.center.x : body2.center.x, y: (body1.isCircle) ? body1.center.y : body2.center.y, radius: (body1.isCircle) ? body1.halfWidth : body2.halfWidth }; if (circle.y < rect.y) { if (circle.x < rect.x) { overlap = DistanceBetween(circle.x, circle.y, rect.x, rect.y) - circle.radius; } else if (circle.x > rect.right) { overlap = DistanceBetween(circle.x, circle.y, rect.right, rect.y) - circle.radius; } } else if (circle.y > rect.bottom) { if (circle.x < rect.x) { overlap = DistanceBetween(circle.x, circle.y, rect.x, rect.bottom) - circle.radius; } else if (circle.x > rect.right) { overlap = DistanceBetween(circle.x, circle.y, rect.right, rect.bottom) - circle.radius; } } overlap *= -1; } else { overlap = (body1.halfWidth + body2.halfWidth) - DistanceBetween(body1.center.x, body1.center.y, body2.center.x, body2.center.y); } // Can't separate two immovable bodies, or a body with its own custom separation logic if (overlapOnly || overlap === 0 || (body1.immovable && body2.immovable) || body1.customSeparateX || body2.customSeparateX) { if (overlap !== 0) { if (body1.onOverlap) { body1.onOverlap.dispatch(body1.sprite, body2.sprite); } if (body2.onOverlap) { body2.onOverlap.dispatch(body2.sprite, body1.sprite); } } // return true if there was some overlap, otherwise false return (overlap !== 0); } // Transform the velocity vector to the coordinate system oriented along the direction of impact. // This is done to eliminate the vertical component of the velocity var v1 = { x: body1.velocity.x * Math.cos(angleCollision) + body1.velocity.y * Math.sin(angleCollision), y: body1.velocity.x * Math.sin(angleCollision) - body1.velocity.y * Math.cos(angleCollision) }; var v2 = { x: body2.velocity.x * Math.cos(angleCollision) + body2.velocity.y * Math.sin(angleCollision), y: body2.velocity.x * Math.sin(angleCollision) - body2.velocity.y * Math.cos(angleCollision) }; // We expect the new velocity after impact var tempVel1 = ((body1.mass - body2.mass) * v1.x + 2 * body2.mass * v2.x) / (body1.mass + body2.mass); var tempVel2 = (2 * body1.mass * v1.x + (body2.mass - body1.mass) * v2.x) / (body1.mass + body2.mass); // We convert the vector to the original coordinate system and multiplied by factor of rebound if (!body1.immovable) { body1.velocity.x = (tempVel1 * Math.cos(angleCollision) - v1.y * Math.sin(angleCollision)) * body1.bounce.x; body1.velocity.y = (v1.y * Math.cos(angleCollision) + tempVel1 * Math.sin(angleCollision)) * body1.bounce.y; } if (!body2.immovable) { body2.velocity.x = (tempVel2 * Math.cos(angleCollision) - v2.y * Math.sin(angleCollision)) * body2.bounce.x; body2.velocity.y = (v2.y * Math.cos(angleCollision) + tempVel2 * Math.sin(angleCollision)) * body2.bounce.y; } // When the collision angle is almost perpendicular to the total initial velocity vector // (collision on a tangent) vector direction can be determined incorrectly. // This code fixes the problem if (Math.abs(angleCollision) < Math.PI / 2) { if ((body1.velocity.x > 0) && !body1.immovable && (body2.velocity.x > body1.velocity.x)) { body1.velocity.x *= -1; } else if ((body2.velocity.x < 0) && !body2.immovable && (body1.velocity.x < body2.velocity.x)) { body2.velocity.x *= -1; } else if ((body1.velocity.y > 0) && !body1.immovable && (body2.velocity.y > body1.velocity.y)) { body1.velocity.y *= -1; } else if ((body2.velocity.y < 0) && !body2.immovable && (body1.velocity.y < body2.velocity.y)) { body2.velocity.y *= -1; } } else if (Math.abs(angleCollision) > Math.PI / 2) { if ((body1.velocity.x < 0) && !body1.immovable && (body2.velocity.x < body1.velocity.x)) { body1.velocity.x *= -1; } else if ((body2.velocity.x > 0) && !body2.immovable && (body1.velocity.x > body2.velocity.x)) { body2.velocity.x *= -1; } else if ((body1.velocity.y < 0) && !body1.immovable && (body2.velocity.y < body1.velocity.y)) { body1.velocity.y *= -1; } else if ((body2.velocity.y > 0) && !body2.immovable && (body1.velocity.x > body2.velocity.y)) { body2.velocity.y *= -1; } } if (!body1.immovable) { body1.x += (body1.velocity.x * this.game.time.physicsElapsed) - overlap * Math.cos(angleCollision); body1.y += (body1.velocity.y * this.game.time.physicsElapsed) - overlap * Math.sin(angleCollision); } if (!body2.immovable) { body2.x += (body2.velocity.x * this.game.time.physicsElapsed) + overlap * Math.cos(angleCollision); body2.y += (body2.velocity.y * this.game.time.physicsElapsed) + overlap * Math.sin(angleCollision); } if (body1.onCollide) { body1.onCollide.dispatch(body1.sprite, body2.sprite); } if (body2.onCollide) { body2.onCollide.dispatch(body2.sprite, body1.sprite); } return true; }, getOverlapX: function (body1, body2, overlapOnly) { var overlap = 0; var maxOverlap = body1.deltaAbsX() + body2.deltaAbsX() + this.OVERLAP_BIAS; if (body1.deltaX() === 0 && body2.deltaX() === 0) { // They overlap but neither of them are moving body1.embedded = true; body2.embedded = true; } else if (body1.deltaX() > body2.deltaX()) { // Body1 is moving right and / or Body2 is moving left overlap = body1.right - body2.x; if ((overlap > maxOverlap && !overlapOnly) || body1.checkCollision.right === false || body2.checkCollision.left === false) { overlap = 0; } else { body1.touching.none = false; body1.touching.right = true; body2.touching.none = false; body2.touching.left = true; } } else if (body1.deltaX() < body2.deltaX()) { // Body1 is moving left and/or Body2 is moving right overlap = body1.x - body2.width - body2.x; if ((-overlap > maxOverlap && !overlapOnly) || body1.checkCollision.left === false || body2.checkCollision.right === false) { overlap = 0; } else { body1.touching.none = false; body1.touching.left = true; body2.touching.none = false; body2.touching.right = true; } } // Resets the overlapX to zero if there is no overlap, or to the actual pixel value if there is body1.overlapX = overlap; body2.overlapX = overlap; return overlap; }, getOverlapY: function (body1, body2, overlapOnly) { var overlap = 0; var maxOverlap = body1.deltaAbsY() + body2.deltaAbsY() + this.OVERLAP_BIAS; if (body1.deltaY() === 0 && body2.deltaY() === 0) { // They overlap but neither of them are moving body1.embedded = true; body2.embedded = true; } else if (body1.deltaY() > body2.deltaY()) { // Body1 is moving down and/or Body2 is moving up overlap = body1.bottom - body2.y; if ((overlap > maxOverlap && !overlapOnly) || body1.checkCollision.down === false || body2.checkCollision.up === false) { overlap = 0; } else { body1.touching.none = false; body1.touching.down = true; body2.touching.none = false; body2.touching.up = true; } } else if (body1.deltaY() < body2.deltaY()) { // Body1 is moving up and/or Body2 is moving down overlap = body1.y - body2.bottom; if ((-overlap > maxOverlap && !overlapOnly) || body1.checkCollision.up === false || body2.checkCollision.down === false) { overlap = 0; } else { body1.touching.none = false; body1.touching.up = true; body2.touching.none = false; body2.touching.down = true; } } // Resets the overlapY to zero if there is no overlap, or to the actual pixel value if there is body1.overlapY = overlap; body2.overlapY = overlap; return overlap; }, separateX: function (body1, body2, overlapOnly) { var overlap = this.getOverlapX(body1, body2, overlapOnly); // Can't separate two immovable bodies, or a body with its own custom separation logic if (overlapOnly || overlap === 0 || (body1.immovable && body2.immovable) || body1.customSeparateX || body2.customSeparateX) { // return true if there was some overlap, otherwise false return (overlap !== 0) || (body1.embedded && body2.embedded); } // Adjust their positions and velocities accordingly (if there was any overlap) var v1 = body1.velocity.x; var v2 = body2.velocity.x; if (!body1.immovable && !body2.immovable) { overlap *= 0.5; body1.x -= overlap; body2.x += overlap; var nv1 = Math.sqrt((v2 * v2 * body2.mass) / body1.mass) * ((v2 > 0) ? 1 : -1); var nv2 = Math.sqrt((v1 * v1 * body1.mass) / body2.mass) * ((v1 > 0) ? 1 : -1); var avg = (nv1 + nv2) * 0.5; nv1 -= avg; nv2 -= avg; body1.velocity.x = avg + nv1 * body1.bounce.x; body2.velocity.x = avg + nv2 * body2.bounce.x; } else if (!body1.immovable) { body1.x -= overlap; body1.velocity.x = v2 - v1 * body1.bounce.x; // This is special case code that handles things like vertically moving platforms you can ride if (body2.moves) { body1.y += (body2.y - body2.prev.y) * body2.friction.y; } } else { body2.x += overlap; body2.velocity.x = v1 - v2 * body2.bounce.x; // This is special case code that handles things like vertically moving platforms you can ride if (body1.moves) { body2.y += (body1.y - body1.prev.y) * body1.friction.y; } } // If we got this far then there WAS overlap, and separation is complete, so return true return true; }, separateY: function (body1, body2, overlapOnly) { var overlap = this.getOverlapY(body1, body2, overlapOnly); // Can't separate two immovable bodies, or a body with its own custom separation logic if (overlapOnly || overlap === 0 || (body1.immovable && body2.immovable) || body1.customSeparateY || body2.customSeparateY) { // return true if there was some overlap, otherwise false return (overlap !== 0) || (body1.embedded && body2.embedded); } // Adjust their positions and velocities accordingly (if there was any overlap) var v1 = body1.velocity.y; var v2 = body2.velocity.y; if (!body1.immovable && !body2.immovable) { overlap *= 0.5; body1.y -= overlap; body2.y += overlap; var nv1 = Math.sqrt((v2 * v2 * body2.mass) / body1.mass) * ((v2 > 0) ? 1 : -1); var nv2 = Math.sqrt((v1 * v1 * body1.mass) / body2.mass) * ((v1 > 0) ? 1 : -1); var avg = (nv1 + nv2) * 0.5; nv1 -= avg; nv2 -= avg; body1.velocity.y = avg + nv1 * body1.bounce.y; body2.velocity.y = avg + nv2 * body2.bounce.y; } else if (!body1.immovable) { body1.y -= overlap; body1.velocity.y = v2 - v1 * body1.bounce.y; // This is special case code that handles things like horizontal moving platforms you can ride if (body2.moves) { body1.x += (body2.x - body2.prev.x) * body2.friction.x; } } else { body2.y += overlap; body2.velocity.y = v1 - v2 * body2.bounce.y; // This is special case code that handles things like horizontal moving platforms you can ride if (body1.moves) { body2.x += (body1.x - body1.prev.x) * body1.friction.x; } } // If we got this far then there WAS overlap, and separation is complete, so return true return true; }, enable: function (object) { var i = 1; if (Array.isArray(object)) { i = object.length; while (i--) { if (object[i].hasOwnProperty('children')) { // If it's a Group then we do it on the children regardless this.enable(object[i].children.entries); } else { this.enableBody(object[i]); } } } else if (object.hasOwnProperty('children')) { // If it's a Group then we do it on the children regardless this.enable(object.children.entries); } else { this.enableBody(object); } }, enableBody: function (object) { if (object.body === null) { object.body = new Body(this, object); this.bodies.set(object.body); } return object; } }); module.exports = World;