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