Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Hash Grid Broadphase + Allocation Reduction #3071

Merged
merged 45 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6b7acf2
perf: Hash Grid Broadphase
eonarheim May 24, 2024
836bde6
wip broadphase/narrowphase
eonarheim May 24, 2024
55cf77e
Wire in sparse hash
eonarheim May 24, 2024
bfbd0a1
fix odd formatting
eonarheim May 24, 2024
04c897a
fix query bug
eonarheim May 24, 2024
c4557f2
Add some object pooling
eonarheim May 24, 2024
641669a
adjust grid size
eonarheim May 24, 2024
bd419cb
cache circle bounds
eonarheim May 24, 2024
05351e1
retrieve body lazily
eonarheim May 24, 2024
a001e04
Tweak spatial hash
eonarheim May 24, 2024
6aa0bfa
quickly skip non-pairs
eonarheim May 24, 2024
d1fc822
reduce pair generation
eonarheim May 24, 2024
bdb318c
Improve some broadphase perf
eonarheim May 25, 2024
5488979
More speed
eonarheim May 25, 2024
6b8b972
Squeeze low hanging loop perf
eonarheim May 25, 2024
fb3189c
Update graphics loop
eonarheim May 25, 2024
ed49333
Optimize loop
eonarheim May 25, 2024
390f36c
Optimize pointer entity
eonarheim May 25, 2024
dc5e78c
deleteme maybe attempt to speed up component accessor
eonarheim May 25, 2024
b7dcaa9
implement raycast
eonarheim May 26, 2024
a25333f
fix more tests
eonarheim May 26, 2024
d745117
fix circle test
eonarheim May 26, 2024
ed8db01
fix all the tests
eonarheim May 26, 2024
78e0d59
tweak broadphase
eonarheim May 26, 2024
1b65344
Merge branch 'main' of https://github.com/excaliburjs/Excalibur into …
eonarheim Jun 27, 2024
385d620
add configuration
eonarheim Jun 27, 2024
fbf89bb
WIP needs rental pool
eonarheim Jun 27, 2024
2c65157
Merge branch 'main' of https://github.com/excaliburjs/Excalibur into …
eonarheim Jun 27, 2024
1ff5aa3
Add rental pool
eonarheim Jun 27, 2024
0c81cbc
add todo
eonarheim Jun 27, 2024
a9c10d8
fix tests
eonarheim Jun 28, 2024
979574d
small perf changes
eonarheim Jun 28, 2024
8f5e76f
fix tests
eonarheim Jun 28, 2024
ad9a1b9
speed up action loop
eonarheim Jun 28, 2024
a0155ad
Add changelog
eonarheim Jun 28, 2024
1425add
Add raycast bounds check, remove
eonarheim Jun 28, 2024
f7f485d
BB updates + tests
eonarheim Jun 28, 2024
1b24f70
memoize radius
eonarheim Jun 28, 2024
072da4f
refactor hash grid out of collision logic
eonarheim Jun 28, 2024
d543f83
cleanup
eonarheim Jun 28, 2024
e4bd4d3
Add physics world query
eonarheim Jun 28, 2024
c6ee0a8
perf improvements to pointer system
eonarheim Jun 28, 2024
933d363
tweak matrix gen
eonarheim Jun 29, 2024
d1b5d35
switch to c-style loop
eonarheim Jun 29, 2024
8de93ce
fix pointer system bug
eonarheim Jun 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- You can now query for colliders on the physics world
```typescript
const scene = ...;
const colliders = scene.physics.query(ex.BoundingBox.fromDimensions(...));
```
- `actor.oldGlobalPos` returns the globalPosition from the previous frame
- create development builds of excalibur that bundlers can use in dev mode
- show warning in development when Entity hasn't been added to a scene after a few seconds
- New `RentalPool` type for sparse object pooling
- New `ex.SparseHashGridCollisionProcessor` which is a simpler (and faster) implementation for broadphase pair generation. This works by bucketing colliders into uniform sized square buckets and using that to generate pairs.

### Fixed

Expand All @@ -33,11 +39,20 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Updates

- Perf improvements to PointerSystem by using new spatial hash grid data structure
- Perf improvements: Hot path allocations
* Reduce State/Transform stack hot path allocations in graphics context
* Reduce Transform allocations
* Reduce AffineMatrix allocations

- Perf improvements to `CircleCollider` bounds calculations
- Switch from iterators to c-style loops which bring more speed
* `Entity` component iteration
* `EntityManager` iteration
* `EventEmitter`s
* `GraphicsSystem` entity iteration
* `PointerSystem` entity iteration

### Changed


Expand Down
5 changes: 3 additions & 2 deletions src/engine/Actions/ActionsSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ export class ActionsSystem extends System {
});
}
update(delta: number): void {
for (const actions of this._actions) {
actions.update(delta);
for (let i = 0; i < this._actions.length; i++) {
const action = this._actions[i];
action.update(delta);
}
}
}
36 changes: 27 additions & 9 deletions src/engine/Collision/BoundingBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,23 @@ export class BoundingBox {
/**
* Returns a new instance of [[BoundingBox]] that is a copy of the current instance
*/
public clone(): BoundingBox {
return new BoundingBox(this.left, this.top, this.right, this.bottom);
public clone(dest?: BoundingBox): BoundingBox {
const result = dest || new BoundingBox(0, 0, 0, 0);
result.left = this.left;
result.right = this.right;
result.top = this.top;
result.bottom = this.bottom;
return result;
}

/**
* Resets the bounds to a zero width/height box
*/
public reset(): void {
this.left = 0;
this.top = 0;
this.bottom = 0;
this.right = 0;
}

/**
Expand Down Expand Up @@ -326,13 +341,16 @@ export class BoundingBox {
* Combines this bounding box and another together returning a new bounding box
* @param other The bounding box to combine
*/
public combine(other: BoundingBox): BoundingBox {
const compositeBB = new BoundingBox(
Math.min(this.left, other.left),
Math.min(this.top, other.top),
Math.max(this.right, other.right),
Math.max(this.bottom, other.bottom)
);
public combine(other: BoundingBox, dest?: BoundingBox): BoundingBox {
const compositeBB = dest || new BoundingBox(0, 0, 0, 0);
const left = Math.min(this.left, other.left);
const top = Math.min(this.top, other.top);
const right = Math.max(this.right, other.right);
const bottom = Math.max(this.bottom, other.bottom);
compositeBB.left = left;
compositeBB.top = top;
compositeBB.right = right;
compositeBB.bottom = bottom;
return compositeBB;
}

Expand Down
37 changes: 17 additions & 20 deletions src/engine/Collision/Colliders/CircleCollider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,19 @@ export class CircleCollider extends Collider {
}

private _naturalRadius: number;

private _radius: number | undefined;
/**
* Get the radius of the circle
*/
public get radius(): number {
if (this._radius) {
return this._radius;
}
const tx = this._transform;
const scale = tx?.globalScale ?? Vector.One;
// This is a trade off, the alternative is retooling circles to support ellipse collisions
return this._naturalRadius * Math.min(scale.x, scale.y);
return (this._radius = this._naturalRadius * Math.min(scale.x, scale.y));
}

/**
Expand All @@ -63,6 +68,8 @@ export class CircleCollider extends Collider {
const scale = tx?.globalScale ?? Vector.One;
// This is a trade off, the alternative is retooling circles to support ellipse collisions
this._naturalRadius = val / Math.min(scale.x, scale.y);
this._localBoundsDirty = true;
this._radius = val;
}

private _transform: Transform;
Expand Down Expand Up @@ -211,31 +218,20 @@ export class CircleCollider extends Collider {
* Get the axis aligned bounding box for the circle collider in world coordinates
*/
public get bounds(): BoundingBox {
const tx = this._transform;
const scale = tx?.globalScale ?? Vector.One;
const rotation = tx?.globalRotation ?? 0;
const pos = tx?.globalPos ?? Vector.Zero;
return new BoundingBox(
this.offset.x - this._naturalRadius,
this.offset.y - this._naturalRadius,
this.offset.x + this._naturalRadius,
this.offset.y + this._naturalRadius
)
.rotate(rotation)
.scale(scale)
.translate(pos);
return this.localBounds.transform(this._globalMatrix);
}

private _localBoundsDirty = true;
private _localBounds: BoundingBox;
/**
* Get the axis aligned bounding box for the circle collider in local coordinates
*/
public get localBounds(): BoundingBox {
return new BoundingBox(
this.offset.x - this._naturalRadius,
this.offset.y - this._naturalRadius,
this.offset.x + this._naturalRadius,
this.offset.y + this._naturalRadius
);
if (this._localBoundsDirty) {
this._localBounds = new BoundingBox(-this._naturalRadius, -this._naturalRadius, +this._naturalRadius, +this._naturalRadius);
this._localBoundsDirty = false;
}
return this._localBounds;
}

/**
Expand All @@ -259,6 +255,7 @@ export class CircleCollider extends Collider {
const globalMat = transform.matrix ?? this._globalMatrix;
globalMat.clone(this._globalMatrix);
this._globalMatrix.translate(this.offset.x, this.offset.y);
this._radius = undefined;
}

/**
Expand Down
17 changes: 15 additions & 2 deletions src/engine/Collision/Colliders/CompositeCollider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,21 @@ import { DefaultPhysicsConfig } from '../PhysicsConfig';

export class CompositeCollider extends Collider {
private _transform: Transform;
private _collisionProcessor = new DynamicTreeCollisionProcessor(DefaultPhysicsConfig);
private _dynamicAABBTree = new DynamicTree(DefaultPhysicsConfig.dynamicTree);
private _collisionProcessor = new DynamicTreeCollisionProcessor({
...DefaultPhysicsConfig,
...{
spatialPartition: {
type: 'dynamic-tree',
boundsPadding: 5,
velocityMultiplier: 2
}
}
});
private _dynamicAABBTree = new DynamicTree({
type: 'dynamic-tree',
boundsPadding: 5,
velocityMultiplier: 2
});
private _colliders: Collider[] = [];

private _compositeStrategy?: 'separate' | 'together';
Expand Down
14 changes: 9 additions & 5 deletions src/engine/Collision/CollisionSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { Engine } from '../Engine';
import { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext';
import { Scene } from '../Scene';
import { Side } from '../Collision/Side';
import { DynamicTreeCollisionProcessor } from './Detection/DynamicTreeCollisionProcessor';
import { PhysicsWorld } from './PhysicsWorld';
import { CollisionProcessor } from './Detection/CollisionProcessor';
export class CollisionSystem extends System {
public systemType = SystemType.Update;
public priority = SystemPriority.Higher;
Expand All @@ -28,7 +28,7 @@ export class CollisionSystem extends System {
private _arcadeSolver: ArcadeSolver;
private _lastFrameContacts = new Map<string, CollisionContact>();
private _currentFrameContacts = new Map<string, CollisionContact>();
private get _processor(): DynamicTreeCollisionProcessor {
private get _processor(): CollisionProcessor {
return this._physics.collisionProcessor;
}

Expand Down Expand Up @@ -73,13 +73,17 @@ export class CollisionSystem extends System {
return;
}

// TODO do we need to do this every frame?
// Collect up all the colliders and update them
let colliders: Collider[] = [];
for (const entity of this.query.entities) {
for (let entityIndex = 0; entityIndex < this.query.entities.length; entityIndex++) {
const entity = this.query.entities[entityIndex];
const colliderComp = entity.get(ColliderComponent);
const collider = colliderComp?.get();
if (colliderComp && colliderComp.owner?.active && collider) {
colliderComp.update();

// Flatten composite colliders
if (collider instanceof CompositeCollider) {
const compositeColliders = collider.getColliders();
if (!collider.compositeStrategy) {
Expand All @@ -95,7 +99,7 @@ export class CollisionSystem extends System {
// Update the spatial partitioning data structures
// TODO if collider invalid it will break the processor
// TODO rename "update" to something more specific
this._processor.update(colliders);
this._processor.update(colliders, elapsedMs);

// Run broadphase on all colliders and locates potential collisions
const pairs = this._processor.broadphase(colliders, elapsedMs);
Expand Down Expand Up @@ -153,7 +157,7 @@ export class CollisionSystem extends System {
}

debug(ex: ExcaliburGraphicsContext) {
this._processor.debug(ex);
this._processor.debug(ex, 0);
}

public runContactStartEnd() {
Expand Down
39 changes: 38 additions & 1 deletion src/engine/Collision/Detection/CollisionProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,51 @@
import { Pair } from './Pair';
import { Collider } from '../Colliders/Collider';
import { CollisionContact } from './CollisionContact';
import { ExcaliburGraphicsContext } from '../..';
import { RayCastOptions } from './RayCastOptions';
import { Ray } from '../../Math/ray';
import { RayCastHit } from './RayCastHit';
import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext';
import { BoundingBox } from '../BoundingBox';
import { Vector } from '../../Math/vector';

/**
* Definition for collision processor
*
* Collision processors are responsible for tracking colliders and identifying contacts between them
*/
export interface CollisionProcessor {
/**
*
*/
rayCast(ray: Ray, options?: RayCastOptions): RayCastHit[];

/**
* Query the collision processor for colliders that contain the point
* @param point
*/
query(point: Vector): Collider[];

/**
* Query the collision processor for colliders that overlap with the bounds
* @param bounds
*/
query(bounds: BoundingBox): Collider[];

/**
* Get all tracked colliders
*/
getColliders(): readonly Collider[];

/**
* Track collider in collision processor
*/
track(target: Collider): void;

/**
* Untrack collider in collision processor
*/
untrack(target: Collider): void;

/**
* Detect potential collision pairs given a list of colliders
*/
Expand Down
Loading