Skip to content

Commit

Permalink
Merge 61c91c3 into 4b68719
Browse files Browse the repository at this point in the history
  • Loading branch information
eonarheim committed Sep 6, 2021
2 parents 4b68719 + 61c91c3 commit 0919206
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 58 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Breaking Changes

- Collision `Pair`'s are now between Collider's and not bodies
- `PerlinNoise` has been removed from the core repo will now be offered as a [plugin](https://github.com/excaliburjs/excalibur-perlin)
- Legacy drawing implementations are moved behind `ex.LegacyDrawing` new Graphics implemenations of `Sprite`, `SpriteSheet`, `Animation` are now the default import.
- To use any of the `ex.LegacyDrawing.*` implementations you must opt-in with the `ex.Flags.useLegacyDrawing()` note: new graphics do not work in this egacy mode
Expand Down Expand Up @@ -55,16 +56,18 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added

- Turn on WebGL support with `ex.Flags.useWebGL()`
- Added new helpers to `CollisionGroup` to define groups that collide with specified groups `CollisionGroup.collidesWith([groupA, groupB])`
- Combine groups with `const groupAandB = CollisionGroup.combine([groupA, groupB])`
- Invert a group instance `const everthingButGroupA = groupA.invert()`
- Added new helpers to `CollisionGroup` to define groups that collide with specified groups `CollisionGroup.collidesWith([groupA, groupB])`
- Combine groups with `const groupAandB = CollisionGroup.combine([groupA, groupB])`
- Invert a group instance `const everthingButGroupA = groupA.invert()`
- Improved Collision Simulation
- New ECS based `CollisionSystem` and `MotionSystem`
- Rigid body's can now sleep for improved performance
- Multiple contacts now supported which improves stability
- Iterative solver for improved stability
- Added `ColliderComponent` to hold individual `Collider` implementations like `Circle`, `Box`, or `CompositeCollider`
- New `CompositeCollider` type to combine multiple colliders together into one for an entity
- Composite colliders flatten into their individual colliders in the collision system
- Composite collider keeps it's internal colliders in a DynamicTree for fast `.collide` checks
- New `TransformComponent` to encapsulate Entity transform, that is to say position, rotation, and scale
- New `MotionComponent` to encapsulate Entity transform values changing over time like velocity and acceleration
- Added multi-line support to `Text` graphics ([#1866](https://github.com/excaliburjs/Excalibur/issues/1866))
Expand Down Expand Up @@ -102,6 +105,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Add `KeyEvent.originalEvent?: KeyboardEvent` which exposes the raw keyboard event handled from the browser.

### Changed

- `Algebra.ts` refactored into separate files in `Math/`
- Engine/Scene refactored to make use of the new ECS world which simplifies their logic
- `TileMap` now uses the built in `Collider` component instead of custom collision code.
Expand Down
9 changes: 7 additions & 2 deletions src/engine/Collision/CollisionSystem.ts
Expand Up @@ -16,6 +16,7 @@ import { DynamicTreeCollisionProcessor } from './Detection/DynamicTreeCollisionP
import { RealisticSolver } from './Solver/RealisticSolver';
import { CollisionSolver } from './Solver/Solver';
import { ColliderComponent } from './ColliderComponent';
import { CompositeCollider } from './Shapes/CompositeCollider';

export class CollisionSystem extends System<TransformComponent | MotionComponent | ColliderComponent> {
public readonly types = ['ex.transform', 'ex.motion', 'ex.collider'] as const;
Expand Down Expand Up @@ -60,12 +61,16 @@ export class CollisionSystem extends System<TransformComponent | MotionComponent
}

// Collect up all the colliders
const colliders: Collider[] = [];
let colliders: Collider[] = [];
for (const entity of _entities) {
const collider = entity.get(ColliderComponent);
if (collider.collider && collider.owner?.active) {
collider.update();
colliders.push(collider.collider);
if (collider.collider instanceof CompositeCollider) {
colliders = colliders.concat(collider.collider.getColliders());
} else {
colliders.push(collider.collider);
}
}
}

Expand Down
48 changes: 36 additions & 12 deletions src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts
Expand Up @@ -14,6 +14,7 @@ import { Color } from '../../Color';
import { ConvexPolygon } from '../Shapes/ConvexPolygon';
import { DrawUtil } from '../../Util/Index';
import { BodyComponent } from '../BodyComponent';
import { CompositeCollider } from '../Shapes/CompositeCollider';

/**
* Responsible for performing the collision broadphase (locating potential colllisions) and
Expand All @@ -26,6 +27,10 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
private _collisionPairCache: Pair[] = [];
private _colliders: Collider[] = [];

public getColliders(): readonly Collider[] {
return this._colliders;
}

/**
* Tracks a physics body for collisions
*/
Expand All @@ -34,8 +39,17 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
Logger.getInstance().warn('Cannot track null collider');
return;
}
this._colliders.push(target);
this._dynamicCollisionTree.trackCollider(target);
if (target instanceof CompositeCollider) {
const colliders = target.getColliders();
for (const c of colliders) {
c.owner = target.owner;
this._colliders.push(c);
this._dynamicCollisionTree.trackCollider(c);
}
} else {
this._colliders.push(target);
this._dynamicCollisionTree.trackCollider(target);
}
}

/**
Expand All @@ -46,11 +60,23 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
Logger.getInstance().warn('Cannot untrack a null collider');
return;
}
const index = this._colliders.indexOf(target);
if (index !== -1) {
this._colliders.splice(index, 1);

if (target instanceof CompositeCollider) {
const colliders = target.getColliders();
for (const c of colliders) {
const index = this._colliders.indexOf(c);
if (index !== -1) {
this._colliders.splice(index, 1);
}
this._dynamicCollisionTree.untrackCollider(c);
}
} else {
const index = this._colliders.indexOf(target);
if (index !== -1) {
this._colliders.splice(index, 1);
}
this._dynamicCollisionTree.untrackCollider(target);
}
this._dynamicCollisionTree.untrackCollider(target);
}

private _shouldGenerateCollisionPair(colliderA: Collider, colliderB: Collider) {
Expand All @@ -70,7 +96,7 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
return false;
}

return Pair.canCollide(colliderA.owner?.get(BodyComponent), colliderB.owner?.get(BodyComponent));
return Pair.canCollide(colliderA, colliderB);
}

/**
Expand All @@ -93,12 +119,10 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
let collider: Collider;
for (let j = 0, l = potentialColliders.length; j < l; j++) {
collider = potentialColliders[j];
const body = collider.owner?.get(BodyComponent);

// Query the collision tree for potential colliders
this._dynamicCollisionTree.query(collider, (other: Collider) => {
if (this._shouldGenerateCollisionPair(collider, other)) {
const pair = new Pair(body, other.owner?.get(BodyComponent));
const pair = new Pair(collider, other);
this._collisions.add(pair.id);
this._collisionPairCache.push(pair);
}
Expand Down Expand Up @@ -146,7 +170,7 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
let minCollider: Collider;
let minTranslate: Vector = new Vector(Infinity, Infinity);
this._dynamicCollisionTree.rayCastQuery(ray, updateDistance + Physics.surfaceEpsilon * 2, (other: Collider) => {
if (collider !== other && Pair.canCollide(body, other.owner?.get(BodyComponent))) {
if (collider !== other && Pair.canCollide(collider, other)) {
const hitPoint = other.rayCast(ray, updateDistance + Physics.surfaceEpsilon * 10);
if (hitPoint) {
const translate = hitPoint.sub(origin);
Expand All @@ -160,7 +184,7 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
});

if (minCollider && Vector.isValid(minTranslate)) {
const pair = new Pair(body, minCollider.owner?.get(BodyComponent));
const pair = new Pair(collider, minCollider);
if (!this._collisions.has(pair.id)) {
this._collisions.add(pair.id);
this._collisionPairCache.push(pair);
Expand Down
37 changes: 22 additions & 15 deletions src/engine/Collision/Detection/Pair.ts
Expand Up @@ -2,22 +2,22 @@ import { CollisionContact } from './CollisionContact';
import { CollisionType } from '../CollisionType';
import { BodyComponent } from '../BodyComponent';
import { Id } from '../../Id';
import { ColliderComponent } from '../ColliderComponent';
import { Collider } from '../Shapes/Collider';

/**
* Models a potential collision between 2 bodies
* Models a potential collision between 2 colliders
*/
export class Pair {
public id: string = null;

constructor(public bodyA: BodyComponent, public bodyB: BodyComponent) {
const colliderA = bodyA.owner.get(ColliderComponent);
const colliderB = bodyB.owner.get(ColliderComponent);
this.id = Pair.calculatePairHash(colliderA.collider.id, colliderB.collider.id);
constructor(public colliderA: Collider, public colliderB: Collider) {
this.id = Pair.calculatePairHash(colliderA.id, colliderB.id);
}

public static canCollide(bodyA: BodyComponent, bodyB: BodyComponent) {
// Body's needed for collision
public static canCollide(colliderA: Collider, colliderB: Collider) {
const bodyA = colliderA?.owner?.get(BodyComponent);
const bodyB = colliderB?.owner?.get(BodyComponent);

// Body's needed for collision in the current state
// TODO can we collide without a body?
if (!bodyA || !bodyB) {
return false;
Expand Down Expand Up @@ -50,18 +50,25 @@ export class Pair {
* Returns whether or not it is possible for the pairs to collide
*/
public get canCollide(): boolean {
const bodyA = this.bodyA;
const bodyB = this.bodyB;
return Pair.canCollide(bodyA, bodyB);
const colliderA = this.colliderA;
const colliderB = this.colliderB;
return Pair.canCollide(colliderA, colliderB);
}

/**
* Runs the collision intersection logic on the members of this pair
*/
public collide(): CollisionContact[] {
const colliderA = this.bodyA.owner.get(ColliderComponent);
const colliderB = this.bodyB.owner.get(ColliderComponent);
return colliderA.collide(colliderB);
return this.colliderA.collide(this.colliderB);
}

/**
* Check if the collider is part of the pair
* @param collider
* @returns
*/
public hasCollider(collider: Collider) {
return collider === this.colliderA || collider === this.colliderB;
}

/**
Expand Down
60 changes: 35 additions & 25 deletions src/engine/Collision/Shapes/CompositeCollider.ts
@@ -1,3 +1,5 @@
import { Util } from '../..';
import { Pair } from '../Detection/Pair';
import { Color } from '../../Color';
import { Transform } from '../../EntityComponentSystem';
import { Line } from '../../Math/line';
Expand All @@ -6,25 +8,39 @@ import { Ray } from '../../Math/ray';
import { Vector } from '../../Math/vector';
import { BoundingBox } from '../BoundingBox';
import { CollisionContact } from '../Detection/CollisionContact';
import { DynamicTree } from '../Detection/DynamicTree';
import { DynamicTreeCollisionProcessor } from '../Detection/DynamicTreeCollisionProcessor';
import { Collider } from './Collider';

export class CompositeCollider extends Collider {
private _transform: Transform;
private _collisionProcessor = new DynamicTreeCollisionProcessor();
private _dynamicAABBTree = new DynamicTree();
private _colliders: Collider[] = [];

constructor(colliders: Collider[]) {
super();
this._colliders = colliders;
for (const c of colliders) {
this.addCollider(c);
}
}

private _colliders: Collider[];

clearColliders() {
this._colliders = [];
}

addCollider(collider: Collider) {
this._colliders.push(collider);
this._collisionProcessor.track(collider);
this._dynamicAABBTree.trackCollider(collider);
}

removeCollider(collider: Collider) {
Util.removeItemFromArray(collider, this._colliders);
this._collisionProcessor.untrack(collider);
this._dynamicAABBTree.untrackCollider(collider);
}

getColliders(): Collider[] {
return this._colliders;
}
Expand Down Expand Up @@ -96,31 +112,24 @@ export class CompositeCollider extends Collider {
}

collide(other: Collider): CollisionContact[] {
const colliders = this.getColliders();
let contacts: CollisionContact[] = [];
let otherColliders = [other];
if (other instanceof CompositeCollider) {
const otherColliders = other.getColliders();
for (const colliderA of colliders) {
for (const colliderB of otherColliders) {
const maybeContact = colliderA.collide(colliderB);
if (maybeContact) {
contacts = contacts.concat(maybeContact);
}
}
}
} else {
for (const collider of colliders) {
const maybeContact = collider.collide(other);
if (maybeContact) {
contacts = contacts.concat(maybeContact);
}
}
otherColliders = other.getColliders();
}

const pairs: Pair[] = [];
for (const c of otherColliders) {
this._dynamicAABBTree.query(c, (potentialCollider: Collider) => {
pairs.push(new Pair(c, potentialCollider));
return false;
});
}
// Return all the contacts
if (contacts.length) {
return contacts;

let contacts: CollisionContact[] = [];
for (const p of pairs) {
contacts = contacts.concat(p.collide());
}
return [];
return contacts;
}

getClosestLineBetween(other: Collider): Line {
Expand Down Expand Up @@ -216,6 +225,7 @@ export class CompositeCollider extends Collider {
if (transform) {
const colliders = this.getColliders();
for (const collider of colliders) {
collider.owner = this.owner;
collider.update(transform);
}
}
Expand Down
25 changes: 24 additions & 1 deletion src/spec/CompositeColliderSpec.ts
Expand Up @@ -119,7 +119,7 @@ describe('A CompositeCollider', () => {
expect(contacts.length).toBe(1);
expect(contacts[0].points)
.withContext('Right edge of comp1 poly')
.toEqual([vec(100, -5), vec(100, 5)]);
.toEqual([vec(100, 5), vec(100, -5)]);
});

it('returns empty on no contacts', () => {
Expand Down Expand Up @@ -184,4 +184,27 @@ describe('A CompositeCollider', () => {
expect(compCollider.contains(vec(-99.9, 0))).toBe(true);
expect(compCollider.contains(vec(-101, 0))).toBe(false);
});

it('is separated into a series of colliders in the dynamic tree', () => {
const compCollider = new ex.CompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(200, 10, Vector.Half)]);

const dynamicTreeProcessor = new ex.DynamicTreeCollisionProcessor();
dynamicTreeProcessor.track(compCollider);

expect(dynamicTreeProcessor.getColliders().length).toBe(2);
expect(dynamicTreeProcessor.getColliders()[0] instanceof ex.CompositeCollider).toBe(false);
expect(dynamicTreeProcessor.getColliders()[1] instanceof ex.CompositeCollider).toBe(false);
});

it('removes all colliders in the dynamic tree', () => {
const compCollider = new ex.CompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(200, 10, Vector.Half)]);

const dynamicTreeProcessor = new ex.DynamicTreeCollisionProcessor();
dynamicTreeProcessor.track(compCollider);

expect(dynamicTreeProcessor.getColliders().length).toBe(2);

dynamicTreeProcessor.untrack(compCollider);
expect(dynamicTreeProcessor.getColliders().length).toBe(0);
});
});

0 comments on commit 0919206

Please sign in to comment.