From 9117b909a8853622a801f1c611ce8f9d26c19db6 Mon Sep 17 00:00:00 2001 From: Richard Davey Date: Wed, 8 Nov 2017 17:18:41 +0000 Subject: [PATCH] Ported over Arcade Physics into V3 and started working through the classes and functions. --- v3/src/physics/arcade/Arcade.js | 20 + v3/src/physics/arcade/Body.js | 660 ++++++++++++++++++++ v3/src/physics/arcade/Factory.js | 47 ++ v3/src/physics/arcade/World.js | 820 ++++++++++++++++++++++++- v3/src/physics/arcade/const.js | 8 +- v3/src/physics/arcade/index.js | 17 + v3/src/physics/index.js | 1 + v3/src/scene/plugins/PhysicsManager.js | 12 +- 8 files changed, 1565 insertions(+), 20 deletions(-) create mode 100644 v3/src/physics/arcade/Arcade.js create mode 100644 v3/src/physics/arcade/Body.js create mode 100644 v3/src/physics/arcade/Factory.js create mode 100644 v3/src/physics/arcade/index.js diff --git a/v3/src/physics/arcade/Arcade.js b/v3/src/physics/arcade/Arcade.js new file mode 100644 index 000000000..14d65171b --- /dev/null +++ b/v3/src/physics/arcade/Arcade.js @@ -0,0 +1,20 @@ +var Class = require('../../utils/Class'); +var Factory = require('./Factory'); +var World = require('./World'); + +var Arcade = new Class({ + + initialize: + + function Arcade (physicsManager, config) + { + this.config = config; + + physicsManager.world = new World(physicsManager.scene, config); + + physicsManager.add = new Factory(physicsManager.world); + } + +}); + +module.exports = Arcade; diff --git a/v3/src/physics/arcade/Body.js b/v3/src/physics/arcade/Body.js new file mode 100644 index 000000000..c35727388 --- /dev/null +++ b/v3/src/physics/arcade/Body.js @@ -0,0 +1,660 @@ +// Phaser.Physics.Arcade.Body + +var CircleContains = require('../../geom/circle/Contains'); +var Class = require('../../utils/Class'); +var CONST = require('./const'); +// var DegToRad = require('../../math/DegToRad'); +// var RadToDeg = require('../../math/RadToDeg'); +var Rectangle = require('../../geom/rectangle/Rectangle'); +var RectangleContains = require('../../geom/rectangle/Contains'); +var Vector2 = require('../../math/Vector2'); + +var Body = new Class({ + + initialize: + + function Body (world, gameObject) + { + this.world = world; + + this.gameObject = gameObject; + + this.debugShowBody = world.defaults.debugShowBody; + this.debugShowVelocity = world.defaults.debugShowVelocity; + this.debugBodyColor = world.defaults.bodyDebugColor; + + this.enable = true; + + this.isCircle = false; + + this.radius = 0; + + this.offset = new Vector2(); + + this.position = new Vector2(gameObject.x, gameObject.y); + + this.prev = new Vector2(this.position.x, this.position.y); + + this.allowRotation = true; + + this.rotation = gameObject.angle; + + this.preRotation = gameObject.angle; + + this.width = gameObject.width; + + this.height = gameObject.height; + + this.sourceWidth = gameObject.width; + + this.sourceHeight = gameObject.height; + + if (gameObject.frame) + { + this.sourceWidth = gameObject.frame.realWidth; + this.sourceHeight = gameObject.frame.realHeight; + } + + this.halfWidth = Math.abs(gameObject.width / 2); + + this.halfHeight = Math.abs(gameObject.height / 2); + + this.center = new Vector2(gameObject.x + this.halfWidth, gameObject.y + this.halfHeight); + + this.velocity = new Vector2(); + + this.newVelocity = new Vector2(); + + this.deltaMax = new Vector2(); + + this.acceleration = new Vector2(); + + this.allowDrag = true; + + this.drag = new Vector2(); + + this.allowGravity = true; + + this.gravity = new Vector2(); + + this.bounce = new Vector2(); + + this.worldBounce = null; + + // this.onWorldBounds = null; + // this.onCollide = null; + // this.onOverlap = null; + + this.maxVelocity = new Vector2(10000, 10000); + + this.friction = new Vector2(1, 0); + + this.angularVelocity = 0; + + this.angularAcceleration = 0; + + this.angularDrag = 0; + + this.maxAngular = 1000; + + this.mass = 1; + + this.angle = 0; + + this.speed = 0; + + this.facing = CONST.FACING_NONE; + + this.immovable = false; + + this.moves = true; + + this.customSeparateX = false; + + this.customSeparateY = false; + + this.overlapX = 0; + + this.overlapY = 0; + + this.overlapR = 0; + + this.embedded = false; + + this.collideWorldBounds = false; + + this.checkCollision = { none: false, up: true, down: true, left: true, right: true }; + + this.touching = { none: true, up: false, down: false, left: false, right: false }; + + this.wasTouching = { none: true, up: false, down: false, left: false, right: false }; + + this.blocked = { none: true, up: false, down: false, left: false, right: false }; + + this.tilePadding = new Vector2(); + + this.dirty = false; + + this.skipQuadTree = false; + + this.syncBounds = false; + + this.isMoving = false; + + this.stopVelocityOnCollide = true; + + // this.moveTimer = 0; + // this.moveDistance = 0; + // this.moveDuration = 0; + // this.moveTarget = null; + // this.moveEnd = null; + // this.onMoveComplete = new Phaser.Signal(); + // this.movementCallback = null; + // this.movementCallbackContext = null; + + this._reset = true; + + this._sx = gameObject.scaleX; + this._sy = gameObject.scaleY; + + this._dx = 0; + this._dy = 0; + + this._bounds = new Rectangle(); + }, + + updateBounds: function () + { + var sprite = this.gameObject; + + if (this.syncBounds) + { + var b = sprite.getBounds(this._bounds); + + // b.ceilAll(); + + if (b.width !== this.width || b.height !== this.height) + { + this.width = b.width; + this.height = b.height; + this._reset = true; + } + } + else + { + var asx = Math.abs(sprite.scaleX); + var asy = Math.abs(sprite.scaleY); + + if (asx !== this._sx || asy !== this._sy) + { + this.width = this.sourceWidth * asx; + this.height = this.sourceHeight * asy; + this._sx = asx; + this._sy = asy; + this._reset = true; + } + } + + if (this._reset) + { + this.halfWidth = Math.floor(this.width / 2); + this.halfHeight = Math.floor(this.height / 2); + this.updateCenter(); + } + }, + + updateCenter: function () + { + this.center.set(this.position.x + this.halfWidth, this.position.y + this.halfHeight); + }, + + update: function (delta) + { + this.dirty = true; + + // Store and reset collision flags + this.wasTouching.none = this.touching.none; + this.wasTouching.up = this.touching.up; + this.wasTouching.down = this.touching.down; + this.wasTouching.left = this.touching.left; + this.wasTouching.right = this.touching.right; + + this.touching.none = true; + this.touching.up = false; + this.touching.down = false; + this.touching.left = false; + this.touching.right = false; + + this.blocked.none = true; + this.blocked.up = false; + this.blocked.down = false; + this.blocked.left = false; + this.blocked.right = false; + + this.overlapR = 0; + this.overlapX = 0; + this.overlapY = 0; + + this.embedded = false; + + this.updateBounds(); + + var sprite = this.gameObject; + + this.position.x = sprite.x - sprite.displayOriginX + (sprite.scaleX * this.offset.x); + this.position.y = sprite.y - sprite.displayOriginY + (sprite.scaleY * this.offset.y); + + // this.position.x -= this.sprite.scale.x < 0 ? this.width : 0; + // this.position.y -= this.sprite.scale.y < 0 ? this.height : 0; + + // this.position.x = sprite.x + (sprite.scaleX * this.offset.x); + // this.position.y = sprite.y + (sprite.scaleY * this.offset.y); + + this.updateCenter(); + + this.rotation = sprite.angle; + + this.preRotation = this.rotation; + + if (this._reset) + { + this.prev.x = this.position.x; + this.prev.y = this.position.y; + } + + if (this.moves) + { + this.world.updateMotion(this); + + this.newVelocity.set(this.velocity.x * delta, this.velocity.y * delta); + + this.position.x += this.newVelocity.x; + this.position.y += this.newVelocity.y; + + this.updateCenter(); + + if (this.position.x !== this.prev.x || this.position.y !== this.prev.y) + { + this.angle = Math.atan2(this.velocity.y, this.velocity.x); + } + + this.speed = Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y); + + // Now the State update will throw collision checks at the Body + // And finally we'll integrate the new position back to the Sprite in postUpdate + + if (this.collideWorldBounds) + { + this.checkWorldBounds(); + + // if (this.checkWorldBounds() && this.onWorldBounds) + // { + // this.onWorldBounds.dispatch(this.sprite, this.blocked.up, this.blocked.down, this.blocked.left, this.blocked.right); + // } + } + } + + this._dx = this.deltaX(); + this._dy = this.deltaY(); + + this._reset = false; + }, + + // Feeds the body results back into the parent gameobject (if there is one) + postUpdate: function () + { + // Only allow postUpdate to be called once per frame + if (!this.enable || !this.dirty) + { + return; + } + + this.dirty = false; + + if (this.deltaX() < 0) + { + this.facing = CONST.FACING_LEFT; + } + else if (this.deltaX() > 0) + { + this.facing = CONST.FACING_RIGHT; + } + + if (this.deltaY() < 0) + { + this.facing = CONST.FACING_UP; + } + else if (this.deltaY() > 0) + { + this.facing = CONST.FACING_DOWN; + } + + if (this.moves) + { + this._dx = this.deltaX(); + this._dy = this.deltaY(); + + if (this.deltaMax.x !== 0 && this._dx !== 0) + { + if (this._dx < 0 && this._dx < -this.deltaMax.x) + { + this._dx = -this.deltaMax.x; + } + else if (this._dx > 0 && this._dx > this.deltaMax.x) + { + this._dx = this.deltaMax.x; + } + } + + if (this.deltaMax.y !== 0 && this._dy !== 0) + { + if (this._dy < 0 && this._dy < -this.deltaMax.y) + { + this._dy = -this.deltaMax.y; + } + else if (this._dy > 0 && this._dy > this.deltaMax.y) + { + this._dy = this.deltaMax.y; + } + } + + this.gameObject.x += this._dx; + this.gameObject.y += this._dy; + + this._reset = true; + } + + this.updateCenter(); + + if (this.allowRotation) + { + this.gameObject.angle += this.deltaZ(); + } + + this.prev.x = this.position.x; + this.prev.y = this.position.y; + }, + + checkWorldBounds: function () + { + var pos = this.position; + var bounds = this.world.bounds; + var check = this.world.checkCollision; + + var bx = (this.worldBounce) ? -this.worldBounce.x : -this.bounce.x; + var by = (this.worldBounce) ? -this.worldBounce.y : -this.bounce.y; + + if (pos.x < bounds.x && check.left) + { + pos.x = bounds.x; + this.velocity.x *= bx; + this.blocked.left = true; + this.blocked.none = false; + } + else if (this.right > bounds.right && check.right) + { + pos.x = bounds.right - this.width; + this.velocity.x *= bx; + this.blocked.right = true; + this.blocked.none = false; + } + + if (pos.y < bounds.y && check.up) + { + pos.y = bounds.y; + this.velocity.y *= by; + this.blocked.up = true; + this.blocked.none = false; + } + else if (this.bottom > bounds.bottom && check.down) + { + pos.y = bounds.bottom - this.height; + this.velocity.y *= by; + this.blocked.down = true; + this.blocked.none = false; + } + + return !this.blocked.none; + }, + + setSize: function (width, height, offsetX, offsetY) + { + if (offsetX === undefined) { offsetX = this.offset.x; } + if (offsetY === undefined) { offsetY = this.offset.y; } + + this.sourceWidth = width; + this.sourceHeight = height; + this.width = this.sourceWidth * this._sx; + this.height = this.sourceHeight * this._sy; + this.halfWidth = Math.floor(this.width / 2); + this.halfHeight = Math.floor(this.height / 2); + this.offset.set(offsetX, offsetY); + + this.updateCenter(); + + this.isCircle = false; + this.radius = 0; + + return this; + }, + + setCircle: function (radius, offsetX, offsetY) + { + if (offsetX === undefined) { offsetX = this.offset.x; } + if (offsetY === undefined) { offsetY = this.offset.y; } + + if (radius > 0) + { + this.isCircle = true; + this.radius = radius; + + this.sourceWidth = radius * 2; + this.sourceHeight = radius * 2; + + this.width = this.sourceWidth * this._sx; + this.height = this.sourceHeight * this._sy; + + this.halfWidth = Math.floor(this.width / 2); + this.halfHeight = Math.floor(this.height / 2); + + this.offset.set(offsetX, offsetY); + + this.updateCenter(); + } + else + { + this.isCircle = false; + } + + return this; + }, + + reset: function (x, y) + { + this.stop(); + + var sprite = this.gameObject; + + this.position.x = x + (sprite.scaleX * this.offset.x); + this.position.y = y + (sprite.scaleY * this.offset.y); + + // this.position.x = (x - (this.sprite.anchor.x * this.sprite.width)) + this.sprite.scale.x * this.offset.x; + // this.position.x -= this.sprite.scale.x < 0 ? this.width : 0; + + // this.position.y = (y - (this.sprite.anchor.y * this.sprite.height)) + this.sprite.scale.y * this.offset.y; + // this.position.y -= this.sprite.scale.y < 0 ? this.height : 0; + + this.prev.x = this.position.x; + this.prev.y = this.position.y; + + this.rotation = this.gameObject.angle; + this.preRotation = this.rotation; + + this.updateBounds(); + this.updateCenter(); + }, + + stop: function () + { + this.velocity.set(0); + this.acceleration.set(0); + this.speed = 0; + this.angularVelocity = 0; + this.angularAcceleration = 0; + + return this; + }, + + getBounds: function (obj) + { + obj.x = this.x; + obj.y = this.y; + obj.right = this.right; + obj.bottom = this.bottom; + + return obj; + }, + + hitTest: function (x, y) + { + return (this.isCircle) ? CircleContains(this, x, y) : RectangleContains(this, x, y); + }, + + onFloor: function () + { + return this.blocked.down; + }, + + onCeiling: function () + { + return this.blocked.up; + }, + + onWall: function () + { + return (this.blocked.left || this.blocked.right); + }, + + deltaAbsX: function () + { + return (this.deltaX() > 0) ? this.deltaX() : -this.deltaX(); + }, + + deltaAbsY: function () + { + return (this.deltaY() > 0) ? this.deltaY() : -this.deltaY(); + }, + + deltaX: function () + { + return this.position.x - this.prev.x; + }, + + deltaY: function () + { + return this.position.y - this.prev.y; + }, + + deltaZ: function () + { + return this.rotation - this.preRotation; + }, + + destroy: function () + { + this.gameObject.body = null; + this.gameObject = null; + }, + + drawDebug: function (graphic) + { + var pos = this.position; + + if (this.debugShowBody) + { + graphic.lineStyle(1, this.debugBodyColor, 1); + graphic.strokeRect(pos.x, pos.y, this.width, this.height); + } + + if (this.debugShowVelocity) + { + var x = pos.x + this.halfWidth; + var y = pos.y + this.halfHeight; + + graphic.lineStyle(1, this.world.defaults.velocityDebugColor, 1); + graphic.lineBetween(x, y, x + this.velocity.x, y + this.velocity.y); + } + }, + + willDrawDebug: function () + { + return (this.debugShowBody || this.debugShowVelocity); + }, + + x: { + + get: function () + { + return this.position.x; + }, + + set: function (value) + { + this.position.x = value; + } + + }, + + y: { + + get: function () + { + return this.position.y; + }, + + set: function (value) + { + this.position.y = value; + } + + }, + + left: { + + get: function () + { + return this.position.x; + } + + }, + + right: { + + get: function () + { + return this.position.x + this.width; + } + + }, + + top: { + + get: function () + { + return this.position.y; + } + + }, + + bottom: { + + get: function () + { + return this.position.y + this.height; + } + + } + +}); + +module.exports = Body; diff --git a/v3/src/physics/arcade/Factory.js b/v3/src/physics/arcade/Factory.js new file mode 100644 index 000000000..61874f688 --- /dev/null +++ b/v3/src/physics/arcade/Factory.js @@ -0,0 +1,47 @@ +var Body = require('./Body'); +var Class = require('../../utils/Class'); + +// var ImpactBody = require('./ImpactBody'); +// var ImpactImage = require('./ImpactImage'); +// var ImpactSprite = require('./ImpactSprite'); + +var Factory = new Class({ + + initialize: + + function Factory (world) + { + this.world = world; + + this.sys = world.scene.sys; + }, + + body: function (gameObject) + { + return new Body(this.world, gameObject); + } + + /* + image: function (x, y, key, frame) + { + var image = new ImpactImage(this.world, x, y, key, frame); + + this.sys.displayList.add(image); + + return image; + }, + + sprite: function (x, y, key, frame) + { + var sprite = new ImpactSprite(this.world, x, y, key, frame); + + this.sys.displayList.add(sprite); + this.sys.updateList.add(sprite); + + return sprite; + } + */ + +}); + +module.exports = Factory; diff --git a/v3/src/physics/arcade/World.js b/v3/src/physics/arcade/World.js index 2e7088b05..bed8363be 100644 --- a/v3/src/physics/arcade/World.js +++ b/v3/src/physics/arcade/World.js @@ -1,41 +1,90 @@ // 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 (width, height) + function World (scene, config) { - this.gravity = new Vector2(); + this.scene = scene; - this.bounds = new Rectangle(0, 0, width, height); + this.events = scene.sys.events; - this.checkCollision = { up: true, down: true, left: true, right: true }; + this.bodies = new Set(); - this.maxObjects = 10; + this.gravity = new Vector2(GetValue(config, 'gravity.x', 0), GetValue(config, 'gravity.y', 0)); - this.maxLevels = 4; + 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.OVERLAP_BIAS = 4; + 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.forceX = false; + 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 = true; + this.skipQuadTree = GetValue(config, 'skipQuadTree', true); - this.isPaused = false; + 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) @@ -45,6 +94,85 @@ var World = new Class({ 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) @@ -52,7 +180,7 @@ var World = new Class({ var velocityDelta = this.computeVelocity(0, body, body.angularVelocity, body.angularAcceleration, body.angularDrag, body.maxAngular) - body.angularVelocity; body.angularVelocity += velocityDelta; - body.rotation += (body.angularVelocity * this.game.time.physicsElapsed); + 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); @@ -65,20 +193,20 @@ var World = new Class({ if (axis === 1 && body.allowGravity) { - velocity += (this.gravity.x + body.gravity.x) * this.game.time.physicsElapsed; + velocity += (this.gravity.x + body.gravity.x) * this.delta; } else if (axis === 2 && body.allowGravity) { - velocity += (this.gravity.y + body.gravity.y) * this.game.time.physicsElapsed; + velocity += (this.gravity.y + body.gravity.y) * this.delta; } if (acceleration) { - velocity += acceleration * this.game.time.physicsElapsed; + velocity += acceleration * this.delta; } else if (drag && body.allowDrag) { - drag *= this.game.time.physicsElapsed; + drag *= this.delta; if (velocity - drag > 0) { @@ -209,9 +337,11 @@ var World = new Class({ collideObjects: function (object1, object2, collideCallback, processCallback, callbackContext, overlapOnly) { + var i; + if (!Array.isArray(object1) && Array.isArray(object2)) { - for (var i = 0; i < object2.length; i++) + for (i = 0; i < object2.length; i++) { if (!object2[i]) { continue; } @@ -220,7 +350,7 @@ var World = new Class({ } else if (Array.isArray(object1) && !Array.isArray(object2)) { - for (var i = 0; i < object1.length; i++) + for (i = 0; i < object1.length; i++) { if (!object1[i]) { continue; } @@ -229,7 +359,7 @@ var World = new Class({ } else if (Array.isArray(object1) && Array.isArray(object2)) { - for (var i = 0; i < object1.length; i++) + for (i = 0; i < object1.length; i++) { if (!object1[i]) { continue; } @@ -247,6 +377,660 @@ var World = new Class({ } }, + 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; diff --git a/v3/src/physics/arcade/const.js b/v3/src/physics/arcade/const.js index c97e5c17c..766f19cbc 100644 --- a/v3/src/physics/arcade/const.js +++ b/v3/src/physics/arcade/const.js @@ -38,6 +38,12 @@ module.exports = { * @constant * @type {number} */ - BOTTOM_TOP: 4 + BOTTOM_TOP: 4, + + FACING_NONE: 5, + FACING_UP: 6, + FACING_DOWN: 7, + FACING_LEFT: 8, + FACING_RIGHT: 9 }; diff --git a/v3/src/physics/arcade/index.js b/v3/src/physics/arcade/index.js new file mode 100644 index 000000000..3c449781a --- /dev/null +++ b/v3/src/physics/arcade/index.js @@ -0,0 +1,17 @@ +// Phaser.Physics.Arcade + +// World updated to run off the Phaser main loop. +// Body extended to support additional setter functions. + +module.exports = { + + Body: require('./Body'), + + // COLLIDES: require('./COLLIDES'), + // CollisionMap: require('./CollisionMap'), + // TYPE: require('./TYPE'), + + World: require('./World'), + Factory: require('./Factory') + +}; diff --git a/v3/src/physics/index.js b/v3/src/physics/index.js index 064b52314..09dcef722 100644 --- a/v3/src/physics/index.js +++ b/v3/src/physics/index.js @@ -1,6 +1,7 @@ // Phaser.Physics module.exports = { + Arcade: require('./arcade'), Impact: require('./impact'), PolyDecomp: require('./poly-decomp/') }; diff --git a/v3/src/scene/plugins/PhysicsManager.js b/v3/src/scene/plugins/PhysicsManager.js index 9c4f60d7e..7b3d3eba5 100644 --- a/v3/src/scene/plugins/PhysicsManager.js +++ b/v3/src/scene/plugins/PhysicsManager.js @@ -3,7 +3,8 @@ var GetValue = require('../../utils/object/GetValue'); var Merge = require('../../utils/object/Merge'); var NOOP = require('../../utils/NOOP'); -// Physics Systems +// Physics Systems (TODO: Remove from here) +var Arcade = require('../../physics/arcade/Arcade'); var Impact = require('../../physics/impact/Impact'); var PhysicsManager = new Class({ @@ -46,6 +47,10 @@ var PhysicsManager = new Class({ switch (system) { + case 'arcade': + this.system = new Arcade(this, config); + break; + case 'impact': this.system = new Impact(this, config); break; @@ -55,6 +60,11 @@ var PhysicsManager = new Class({ update: function (time, delta) { this.world.update(time, delta); + }, + + postUpdate: function () + { + this.world.postUpdate(); } });