Skip to content

Commit

Permalink
perf: Hash Grid Broadphase + Hot Path Allocation Reduction (#3071)
Browse files Browse the repository at this point in the history
This PR explores a different data structure for handling large numbers of colliders in the scene at a time efficiently.

* New features
  - Query colliders directly on the `PhysicsWorld`
      ```typescript
      const scene = ...;
      const colliders = scene.physics.query(ex.BoundingBox.fromDimensions(...));
      ```

* New `SparseHashGrid` data structure
  - Used as the new default for the collision broadphase which is faster performing than the dynamic try
  - Used in the `PointerSystem` to improve pointer dispatch when there are a lot of entities

* More allocation reduction
  - New pool type `RentalPool`
  - `Transform` refactoring to remove 
  - `AffineMatrix`
  - Graphics context state/Transform stack hot path allocations pooling 

* 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
  • Loading branch information
eonarheim authored Jun 29, 2024
1 parent 2fbb29c commit 6db28e1
Show file tree
Hide file tree
Showing 32 changed files with 1,655 additions and 193 deletions.
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

0 comments on commit 6db28e1

Please sign in to comment.