diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b84046..7bcae900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed issue where zero mtv collisions cause erroneous precollision events to be fired in the `ArcadeSolver` and `RealisticSolver` - Fixed issue where calling `.kill()` on a child entity would not remove it from the parent `Entity` - Fixed issue where calling `.removeAllChildren()` would not remove all the children from the parent `Entity` - Fixed issue where world origin was inconsistent when the using `ex.DisplayMode.FitScreenAndFill` when the screen was resized. diff --git a/sandbox/tests/incorrectside/index.html b/sandbox/tests/incorrectside/index.html new file mode 100644 index 00000000..714ba6ae --- /dev/null +++ b/sandbox/tests/incorrectside/index.html @@ -0,0 +1,13 @@ + + + + + + + Incorrect Side + + + + + + \ No newline at end of file diff --git a/sandbox/tests/incorrectside/index.ts b/sandbox/tests/incorrectside/index.ts new file mode 100644 index 00000000..1d5f3802 --- /dev/null +++ b/sandbox/tests/incorrectside/index.ts @@ -0,0 +1,71 @@ +var engine = new ex.Engine({ + width: 400, + height: 400 +}); + +ex.Physics.gravity = ex.vec(0, 800); + +class Player4 extends ex.Actor { + collisions = []; + + constructor() { + super({ + name: "player", + x: 200, + y: 200, + width: 40, + height: 40, + color: ex.Color.Red, + collisionType: ex.CollisionType.Active + }); + + this.on("precollision", (ev) => this.onPreCollision(ev)); + } + + onPreCollision(ev) { + this.collisions.push(ev.side); + } + + onPreUpdate() { + console.log(this.collisions); + this.collisions = []; + + if (engine.input.keyboard.wasPressed(ex.Input.Keys.ArrowUp)) { + this.vel.y = -400; + } + + if (engine.input.keyboard.isHeld(ex.Input.Keys.ArrowLeft)) { + this.vel.x = -100; + } else if (engine.input.keyboard.isHeld(ex.Input.Keys.ArrowRight)) { + this.vel.x = 100; + } else { + this.vel.x = 0; + } + } +} + +engine.start().then(() => { + engine.add(new Player4()); + + engine.add( + new ex.Actor({ + y: 300, + anchor: ex.vec(0, 0), + width: 400, + height: 100, + color: ex.Color.Green, + collisionType: ex.CollisionType.Fixed + }) + ); + engine.add( + new ex.Actor({ + x: 300, + y: 220, + anchor: ex.vec(0, 0), + width: 40, + height: 80, + color: ex.Color.Green, + collisionType: ex.CollisionType.Fixed + }) + ); +}); diff --git a/src/engine/Collision/Solver/ArcadeSolver.ts b/src/engine/Collision/Solver/ArcadeSolver.ts index 0bdf4661..b95a22ab 100644 --- a/src/engine/Collision/Solver/ArcadeSolver.ts +++ b/src/engine/Collision/Solver/ArcadeSolver.ts @@ -46,8 +46,13 @@ export class ArcadeSolver implements CollisionSolver { } public preSolve(contacts: CollisionContact[]) { - + const epsilon = .0001; for (const contact of contacts) { + if (Math.abs(contact.mtv.x) < epsilon && Math.abs(contact.mtv.y) < epsilon) { + // Cancel near 0 mtv collisions + contact.cancel(); + continue; + } const side = Side.fromDirection(contact.mtv); const mtv = contact.mtv.negate(); diff --git a/src/engine/Collision/Solver/RealisticSolver.ts b/src/engine/Collision/Solver/RealisticSolver.ts index 0a2d84b0..e25cf079 100644 --- a/src/engine/Collision/Solver/RealisticSolver.ts +++ b/src/engine/Collision/Solver/RealisticSolver.ts @@ -39,7 +39,13 @@ export class RealisticSolver implements CollisionSolver { } preSolve(contacts: CollisionContact[]) { + const epsilon = .0001; for (const contact of contacts) { + if (Math.abs(contact.mtv.x) < epsilon && Math.abs(contact.mtv.y) < epsilon) { + // Cancel near 0 mtv collisions + contact.cancel(); + continue; + } // Publish collision events on both participants const side = Side.fromDirection(contact.mtv); contact.colliderA.events.emit('precollision', new PreCollisionEvent(contact.colliderA, contact.colliderB, side, contact.mtv)); diff --git a/src/spec/ArcadeSolverSpec.ts b/src/spec/ArcadeSolverSpec.ts index d0c1406e..2737fb99 100644 --- a/src/spec/ArcadeSolverSpec.ts +++ b/src/spec/ArcadeSolverSpec.ts @@ -230,4 +230,35 @@ describe('An ArcadeSolver', () => { // Considers infinitesimally overlapping to no longer be overlapping and thus cancels the contact expect(contact.isCanceled()).toBe(true); }); + + it('should cancel zero overlap collisions during presolve', () => { + const arcadeSolver = new ex.ArcadeSolver(); + + const player = new ex.Actor({ + x: 0, + y: 0, + width: 40, + height: 40, + collisionType: ex.CollisionType.Active, + color: ex.Color.Red + }); + + const block = new ex.Actor({ + x: 40, + y: 0, + width: 40 + .00005, + height: 40, + collisionType: ex.CollisionType.Fixed, + color: ex.Color.Green + }); + + const contact = new ex.CollisionContact( + player.collider.get(), block.collider.get(), ex.Vector.Down, ex.Vector.Down, ex.Vector.Up.perpendicular(), [], [], null); + + contact.mtv = ex.vec(-0, 0); + + arcadeSolver.preSolve([contact]); + // Considers infinitesimally overlapping to no longer be overlapping and thus cancels the contact + expect(contact.isCanceled()).toBe(true); + }); }); \ No newline at end of file diff --git a/src/spec/RealisticSolverSpec.ts b/src/spec/RealisticSolverSpec.ts new file mode 100644 index 00000000..f490b041 --- /dev/null +++ b/src/spec/RealisticSolverSpec.ts @@ -0,0 +1,44 @@ +import { ExcaliburMatchers } from 'excalibur-jasmine'; +import * as ex from '@excalibur'; + +describe('An ArcadeSolver', () => { + beforeAll(() => { + jasmine.addMatchers(ExcaliburMatchers); + }); + + it('should exist', () => { + expect(ex.RealisticSolver).toBeDefined(); + }); + + it('should cancel zero overlap collisions during presolve', () => { + const realisticSolver = new ex.RealisticSolver(); + + const player = new ex.Actor({ + x: 0, + y: 0, + width: 40, + height: 40, + collisionType: ex.CollisionType.Active, + color: ex.Color.Red + }); + + const block = new ex.Actor({ + x: 40, + y: 0, + width: 40 + .00005, + height: 40, + collisionType: ex.CollisionType.Fixed, + color: ex.Color.Green + }); + + const contact = new ex.CollisionContact( + player.collider.get(), block.collider.get(), ex.Vector.Down, ex.Vector.Down, ex.Vector.Up.perpendicular(), [], [], null); + + contact.mtv = ex.vec(-0, 0); + + realisticSolver.preSolve([contact]); + // Considers infinitesimally overlapping to no longer be overlapping and thus cancels the contact + expect(contact.isCanceled()).toBe(true); + }); + +}); \ No newline at end of file