From 432ce5428f357f31ae090d55c5183b4eccd5a37c Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Thu, 17 Aug 2023 22:42:09 -0600 Subject: [PATCH] increase collision performance _heavily_ --- client/src/JumpStorm.ts | 9 +- engine/components/BoundingBox.ts | 33 +++++++ engine/entities/Floor.ts | 5 +- engine/entities/Player.ts | 7 +- engine/structures/Grid.ts | 97 +++++++++++++++++++ engine/structures/QuadTree.ts | 41 ++++---- .../RefreshingCollisionFinderBehavior.ts | 14 +++ engine/structures/index.ts | 2 + engine/systems/Collision.ts | 73 ++++++-------- 9 files changed, 213 insertions(+), 68 deletions(-) create mode 100644 engine/structures/Grid.ts create mode 100644 engine/structures/RefreshingCollisionFinderBehavior.ts diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index 8075cc8..008ba13 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -1,5 +1,7 @@ import { Floor, Player } from "@engine/entities"; import { Game } from "@engine/Game"; +import { Grid } from "@engine/structures"; +import { Miscellaneous } from "@engine/config"; import { WallBounds, FacingDirection, @@ -65,11 +67,16 @@ export class JumpStorm { socket, ); + const grid = new Grid( + { width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }, + { width: 30, height: 30 }, + ); + [ this.createInputSystem(), new FacingDirection(), new Physics(), - new Collision(), + new Collision(grid), new WallBounds(ctx.canvas.width), new NetworkUpdate( clientSocketMessageQueueProvider, diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index 19967f7..26b404d 100644 --- a/engine/components/BoundingBox.ts +++ b/engine/components/BoundingBox.ts @@ -17,6 +17,25 @@ export class BoundingBox extends Component { // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem public isCollidingWith(box: BoundingBox): boolean { + if (this.rotation == 0 && box.rotation == 0) { + const thisTopLeft = this.getTopLeft(); + const thisBottomRight = this.getBottomRight(); + + const thatTopLeft = box.getTopLeft(); + const thatBottomRight = box.getBottomRight(); + + if ( + thisBottomRight.x <= thatTopLeft.x || + thisTopLeft.x >= thatBottomRight.x || + thisBottomRight.y <= thatTopLeft.y || + thisTopLeft.y >= thatBottomRight.y + ) { + return false; + } + + return true; + } + const boxes = [this.getVertices(), box.getVertices()]; for (const poly of boxes) { for (let i = 0; i < poly.length; i++) { @@ -83,4 +102,18 @@ export class BoundingBox extends Component { height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads)), }; } + + public getTopLeft(): Coord2D { + return { + x: this.center.x - this.dimension.width / 2, + y: this.center.y - this.dimension.height / 2, + }; + } + + public getBottomRight(): Coord2D { + return { + x: this.center.x + this.dimension.width / 2, + y: this.center.y + this.dimension.height / 2, + }; + } } diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index 44587e6..b204ce0 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -23,7 +23,10 @@ export class Floor extends Entity { this.addComponent( new BoundingBox( - { x: 300, y: 300 }, + { + x: 300, + y: 300, + }, { width, height: Floor.spriteSpec.height }, ), ); diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index eeddd69..377e0ca 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -18,7 +18,7 @@ import { Direction } from "../interfaces"; export class Player extends Entity { private static MASS: number = 10; - private static MOI: number = 1000; + private static MOI: number = 100; private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( Sprites.COFFEE, @@ -29,7 +29,10 @@ export class Player extends Entity { this.addComponent( new BoundingBox( - { x: 300, y: 100 }, + { + x: 300, + y: 100, + }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, 0, ), diff --git a/engine/structures/Grid.ts b/engine/structures/Grid.ts new file mode 100644 index 0000000..d359909 --- /dev/null +++ b/engine/structures/Grid.ts @@ -0,0 +1,97 @@ +import type { Coord2D, Dimension2D } from "../interfaces"; +import type { RefreshingCollisionFinderBehavior } from "."; + +export class Grid implements RefreshingCollisionFinderBehavior { + private cellEntities: Map; + + private gridDimension: Dimension2D; + private cellDimension: Dimension2D; + private topLeft: Coord2D; + + constructor( + gridDimension: Dimension2D, + cellDimension: Dimension2D, + topLeft = { x: 0, y: 0 }, + ) { + this.gridDimension = gridDimension; + this.cellDimension = cellDimension; + this.topLeft = topLeft; + + this.cellEntities = new Map(); + } + + public insert(boxedEntry: BoxedEntry) { + this.getOverlappingCells(boxedEntry).forEach((gridIdx) => { + if (!this.cellEntities.has(gridIdx)) { + this.cellEntities.set(gridIdx, []); + } + this.cellEntities.get(gridIdx).push(boxedEntry.id); + }); + } + + public getNeighborIds(boxedEntry: BoxedEntry): Set { + const neighborIds: Set = new Set(); + this.getOverlappingCells(boxedEntry).forEach((gridIdx) => { + if (this.cellEntities.has(gridIdx)) { + this.cellEntities.get(gridIdx).forEach((id) => neighborIds.add(id)); + } + }); + return neighborIds; + } + + public clear() { + this.cellEntities.clear(); + } + + public setTopLeft(topLeft: Coord2D) { + this.topLeft = topLeft; + } + + public setDimension(dimension: Dimension2D) { + this.gridDimension = dimension; + } + + public setCellDimension(cellDimension: Dimension2D) { + this.cellDimension = cellDimension; + } + + private getOverlappingCells(boxedEntry: BoxedEntry): number[] { + const { center, dimension } = boxedEntry; + const yBoxes = Math.ceil( + this.gridDimension.height / this.cellDimension.height, + ); + const xBoxes = Math.ceil( + this.gridDimension.width / this.cellDimension.width, + ); + + const translated: Coord2D = { + y: center.y - this.topLeft.y, + x: center.x - this.topLeft.x, + }; + + const topLeftBox = { + x: Math.floor( + (translated.x - dimension.width / 2) / this.cellDimension.width, + ), + y: Math.floor( + (translated.y - dimension.height / 2) / this.cellDimension.height, + ), + }; + const bottomRightBox = { + x: Math.floor( + (translated.x + dimension.width / 2) / this.cellDimension.width, + ), + y: Math.floor( + (translated.y + dimension.height / 2) / this.cellDimension.height, + ), + }; + + const cells: number[] = []; + + for (let y = topLeftBox.y; y <= bottomRightBox.y; ++y) + for (let x = topLeftBox.x; x <= bottomRightBox.x; ++x) + cells.push(yBoxes * y + x); + + return cells; + } +} diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index e6e29fa..90227a0 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -1,10 +1,5 @@ import type { Coord2D, Dimension2D } from "../interfaces"; - -export interface BoxedEntry { - id: string; - dimension: Dimension2D; - center: Coord2D; -} +import type { BoxedEntry, RefreshingCollisionFinderBehavior } from "."; enum Quadrant { I, @@ -13,7 +8,14 @@ enum Quadrant { IV, } -export class QuadTree { +/* + unused due to performance problems. here anyways, in case it _really_ is necessary at some point + (and to justify the amount of time i spent here). +*/ +export class QuadTree implements RefreshingCollisionFinderBehavior { + private static readonly QUADTREE_MAX_LEVELS = 3; + private static readonly QUADTREE_SPLIT_THRESHOLD = 2000; + private maxLevels: number; private splitThreshold: number; private level: number; @@ -24,18 +26,18 @@ export class QuadTree { private objects: BoxedEntry[]; constructor( - topLeft: Coord2D, + topLeft: Coord2D = { x: 0, y: 0 }, dimension: Dimension2D, - maxLevels: number, - splitThreshold: number, - level?: number, + maxLevels: number = QuadTree.QUADTREE_MAX_LEVELS, + splitThreshold: number = QuadTree.QUADTREE_SPLIT_THRESHOLD, + level: number = 0, ) { this.children = new Map(); this.objects = []; this.maxLevels = maxLevels; this.splitThreshold = splitThreshold; - this.level = level ?? 0; + this.level = level; this.topLeft = topLeft; this.dimension = dimension; @@ -45,7 +47,7 @@ export class QuadTree { if (this.hasChildren()) { this.getQuadrants(boxedEntry).forEach((quadrant) => { const quadrantBox = this.children.get(quadrant); - quadrantBox?.insert(boxedEntry); + quadrantBox!.insert(boxedEntry); }); return; } @@ -73,15 +75,16 @@ export class QuadTree { } public getNeighborIds(boxedEntry: BoxedEntry): string[] { - const neighbors: string[] = this.objects.map(({ id }) => id); + const neighbors = new Set( + this.objects.map(({ id }) => id).filter((id) => id != boxedEntry.id), + ); if (this.hasChildren()) { this.getQuadrants(boxedEntry).forEach((quadrant) => { const quadrantBox = this.children.get(quadrant); - quadrantBox ?.getNeighborIds(boxedEntry) - .forEach((id) => neighbors.push(id)); + .forEach((id) => neighbors.add(id)); }); } @@ -158,9 +161,9 @@ export class QuadTree { private realignObjects(): void { this.objects.forEach((boxedEntry) => { - this.getQuadrants(boxedEntry).forEach((direction) => { - const quadrant = this.children.get(direction); - quadrant?.insert(boxedEntry); + this.getQuadrants(boxedEntry).forEach((quadrant) => { + const quadrantBox = this.children.get(quadrant); + quadrantBox!.insert(boxedEntry); }); }); diff --git a/engine/structures/RefreshingCollisionFinderBehavior.ts b/engine/structures/RefreshingCollisionFinderBehavior.ts new file mode 100644 index 0000000..21d690d --- /dev/null +++ b/engine/structures/RefreshingCollisionFinderBehavior.ts @@ -0,0 +1,14 @@ +import type { Coord2D, Dimension2D } from "../interfaces"; + +export interface BoxedEntry { + id: string; + dimension: Dimension2D; + center: Coord2D; +} + +export interface RefreshingCollisionFinderBehavior { + public clear(): void; + public insert(boxedEntry: BoxedEntry): void; + public getNeighborIds(boxedEntry: BoxedEntry): Set; + public setTopLeft(topLeft: Coord2d): void; +} diff --git a/engine/structures/index.ts b/engine/structures/index.ts index 605a82a..49a84af 100644 --- a/engine/structures/index.ts +++ b/engine/structures/index.ts @@ -1 +1,3 @@ +export * from "./RefreshingCollisionFinderBehavior"; export * from "./QuadTree"; +export * from "./Grid"; diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts index e05aba0..2dd920e 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -8,58 +8,49 @@ import { Forces, } from "../components"; import { Game } from "../Game"; -import { PhysicsConstants } from "../config"; +import { Miscellaneous, PhysicsConstants } from "../config"; import { Entity } from "../entities"; import type { Coord2D, Dimension2D, Velocity2D } from "../interfaces"; -import { QuadTree, BoxedEntry } from "../structures"; +import { BoxedEntry, RefreshingCollisionFinderBehavior } from "../structures"; export class Collision extends System { private static readonly COLLIDABLE_COMPONENT_NAMES = [ ComponentNames.Collide, ComponentNames.TopCollidable, ]; - private static readonly QUADTREE_MAX_LEVELS = 10; - private static readonly QUADTREE_SPLIT_THRESHOLD = 10; - private quadTree: QuadTree; + private collisionFinder: RefreshingCollisionFinderBehavior; - constructor(screenDimensions: Dimension2D) { + constructor(refreshingCollisionFinder: RefreshingCollisionFinderBehavior) { super(SystemNames.Collision); - this.quadTree = new QuadTree( - { x: 0, y: 0 }, - screenDimensions, - Collision.QUADTREE_MAX_LEVELS, - Collision.QUADTREE_SPLIT_THRESHOLD, - ); + this.collisionFinder = refreshingCollisionFinder; } public update(_dt: number, game: Game) { - // rebuild the quadtree - this.quadTree.clear(); + this.collisionFinder.clear(); - const entitiesToAddToQuadtree: Entity[] = []; + const entitiesToAddToCollisionFinder: Entity[] = []; Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) => game.forEachEntityWithComponent(componentName, (entity) => { if (!entity.hasComponent(ComponentNames.BoundingBox)) { return; } - entitiesToAddToQuadtree.push(entity); + entitiesToAddToCollisionFinder.push(entity); }), ); - this.insertEntitiesInQuadTreeAndUpdateBounds(entitiesToAddToQuadtree); - - this.findCollidingEntitiesAndCollide(entitiesToAddToQuadtree, game); + this.insertEntitiesAndUpdateBounds(entitiesToAddToCollisionFinder); + this.findCollidingEntitiesAndCollide(entitiesToAddToCollisionFinder, game); } - private insertEntitiesInQuadTreeAndUpdateBounds(entities: Entity[]) { + private insertEntitiesAndUpdateBounds(entities: Entity[]) { + const collisionFinderInsertions: BoxedEntry[] = []; + const topLeft: Coord2D = { x: Infinity, y: Infinity }; const bottomRight: Coord2D = { x: -Infinity, y: -Infinity }; - const quadTreeInsertions: BoxedEntry[] = []; - entities.forEach((entity) => { const boundingBox = entity.getComponent( ComponentNames.BoundingBox, @@ -71,21 +62,15 @@ export class Collision extends System { } const { center } = boundingBox; - const topLeftBoundingBox = { - x: center.x - dimension.width / 2, - y: center.y - dimension.height / 2, - }; - const bottomRightBoundingBox = { - x: center.x + dimension.width / 2, - y: center.y + dimension.height / 2, - }; + const topLeftBoundingBox = boundingBox.getTopLeft(); + const bottomRightBoundingBox = boundingBox.getBottomRight(); topLeft.x = Math.min(topLeftBoundingBox.x, topLeft.x); topLeft.y = Math.min(topLeftBoundingBox.y, topLeft.y); bottomRight.x = Math.max(bottomRightBoundingBox.x, bottomRight.x); - bottomRight.y = Math.min(bottomRightBoundingBox.y, bottomRight.y); + bottomRight.y = Math.max(bottomRightBoundingBox.y, bottomRight.y); - quadTreeInsertions.push({ + collisionFinderInsertions.push({ id: entity.id, dimension, center, @@ -94,16 +79,16 @@ export class Collision extends System { // set bounds first if (entities.length > 0) { - this.quadTree.setTopLeft(topLeft); - this.quadTree.setDimension({ + this.collisionFinder.setTopLeft(topLeft); + this.collisionFinder.setDimension({ width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y, }); } // then, begin insertions - quadTreeInsertions.forEach((boxedEntry: BoxedEntry) => - this.quadTree.insert(boxedEntry), + collisionFinderInsertions.forEach((boxedEntry: BoxedEntry) => + this.collisionFinder.insert(boxedEntry), ); } @@ -181,15 +166,13 @@ export class Collision extends System { ComponentNames.BoundingBox, ); - const neighborIds = this.quadTree - .getNeighborIds({ - id: entity.id, - dimension: boundingBox.dimension, - center: boundingBox.center, - }) - .filter((neighborId) => neighborId != entity.id); + const neighborIds = this.collisionFinder.getNeighborIds({ + id: entity.id, + dimension: boundingBox.dimension, + center: boundingBox.center, + }); - neighborIds.forEach((neighborId) => { + for (const neighborId of neighborIds) { const neighbor = game.getEntity(neighborId); if (!neighbor) return; @@ -200,7 +183,7 @@ export class Collision extends System { if (boundingBox.isCollidingWith(neighborBoundingBox)) { collidingEntityIds.push([entity.id, neighborId]); } - }); + } } return collidingEntityIds;