summaryrefslogtreecommitdiff
path: root/engine/systems/Collision.ts
diff options
context:
space:
mode:
Diffstat (limited to 'engine/systems/Collision.ts')
-rw-r--r--engine/systems/Collision.ts215
1 files changed, 215 insertions, 0 deletions
diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts
new file mode 100644
index 0000000..4fcb906
--- /dev/null
+++ b/engine/systems/Collision.ts
@@ -0,0 +1,215 @@
+import { SystemNames, System } from ".";
+import {
+ Mass,
+ BoundingBox,
+ ComponentNames,
+ Jump,
+ Velocity,
+ Moment,
+} from "../components";
+import { Game } from "../Game";
+import { PhysicsConstants } from "../config";
+import { Entity } from "../entities";
+import type { Dimension2D } from "../interfaces";
+import { QuadTree } 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;
+
+ constructor(screenDimensions: Dimension2D) {
+ super(SystemNames.Collision);
+
+ this.quadTree = new QuadTree(
+ { x: 0, y: 0 },
+ screenDimensions,
+ Collision.QUADTREE_MAX_LEVELS,
+ Collision.QUADTREE_SPLIT_THRESHOLD
+ );
+ }
+
+ public update(dt: number, game: Game) {
+ // rebuild the quadtree
+ this.quadTree.clear();
+
+ const entitiesToAddToQuadtree: Entity[] = [];
+
+ Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) =>
+ game.componentEntities.get(componentName)
+ ).forEach((entityIds: Set<number>) =>
+ entityIds.forEach((id) => {
+ const entity = game.entities.get(id);
+ if (!entity.hasComponent(ComponentNames.BoundingBox)) {
+ return;
+ }
+ entitiesToAddToQuadtree.push(entity);
+ })
+ );
+
+ entitiesToAddToQuadtree.forEach((entity) => {
+ const boundingBox = entity.getComponent<BoundingBox>(
+ ComponentNames.BoundingBox
+ );
+
+ this.quadTree.insert(
+ entity.id,
+ boundingBox.dimension,
+ boundingBox.center
+ );
+ });
+
+ // find colliding entities and perform collisions
+ const collidingEntities = this.getCollidingEntities(
+ entitiesToAddToQuadtree,
+ game.entities
+ );
+
+ collidingEntities.forEach(([entityAId, entityBId]) => {
+ const [entityA, entityB] = [entityAId, entityBId].map((id) =>
+ game.entities.get(id)
+ );
+ this.performCollision(entityA, entityB);
+ });
+ }
+
+ private performCollision(entityA: Entity, entityB: Entity) {
+ const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map(
+ (entity) => entity.getComponent<BoundingBox>(ComponentNames.BoundingBox)
+ );
+
+ let velocity: Velocity;
+ if (entityA.hasComponent(ComponentNames.Velocity)) {
+ velocity = entityA.getComponent<Velocity>(ComponentNames.Velocity);
+ }
+
+ if (
+ entityA.hasComponent(ComponentNames.Collide) &&
+ entityB.hasComponent(ComponentNames.TopCollidable) &&
+ entityABoundingBox.center.y <= entityBBoundingBox.center.y &&
+ velocity &&
+ velocity.dCartesian.dy >= 0 // don't apply "floor" logic when coming through the bottom
+ ) {
+ if (entityBBoundingBox.rotation != 0) {
+ throw new Error(
+ `entity with id ${entityB.id} has TopCollidable component and a non-zero rotation. that is not (yet) supported.`
+ );
+ }
+
+ // remove previous velocity in the y axis
+ velocity.dCartesian.dy = 0;
+
+ // apply normal force
+ if (entityA.hasComponent(ComponentNames.Gravity)) {
+ const mass = entityA.getComponent<Mass>(ComponentNames.Mass).mass;
+ const F_n = -mass * PhysicsConstants.GRAVITY;
+
+ entityA.getComponent<Forces>(ComponentNames.Forces).forces.push({
+ fCartesian: { fy: F_n },
+ });
+ }
+
+ // reset the entities' jump
+ if (entityA.hasComponent(ComponentNames.Jump)) {
+ entityA.getComponent<Jump>(ComponentNames.Jump).canJump = true;
+ }
+
+ entityABoundingBox.center.y =
+ entityBBoundingBox.center.y -
+ entityBBoundingBox.dimension.height / 2 -
+ this.getDyToPushOutOfFloor(entityABoundingBox, entityBBoundingBox);
+ }
+ }
+
+ private getCollidingEntities(
+ collidableEntities: Entity[],
+ entityMap: Map<number, Entity>
+ ): [number, number][] {
+ const collidingEntityIds: [number, number] = [];
+
+ for (const entity of collidableEntities) {
+ const boundingBox = entity.getComponent<BoundingBox>(
+ ComponentNames.BoundingBox
+ );
+
+ this.quadTree
+ .getNeighborIds({
+ id: entity.id,
+ dimension: boundingBox.dimension,
+ center: boundingBox.center,
+ })
+ .filter((neighborId) => neighborId != entity.id)
+ .forEach((neighborId) => {
+ const neighborBoundingBox = entityMap
+ .get(neighborId)
+ .getComponent<BoundingBox>(ComponentNames.BoundingBox);
+
+ if (boundingBox.isCollidingWith(neighborBoundingBox)) {
+ collidingEntityIds.push([entity.id, neighborId]);
+ }
+ });
+ }
+
+ return collidingEntityIds;
+ }
+
+ // ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ
+ private getDyToPushOutOfFloor(
+ entityBoundingBox: BoundingBox,
+ floorBoundingBox: BoundingBox
+ ): number {
+ const {
+ rotation,
+ center: { x, y },
+ dimension: { width, height },
+ } = entityBoundingBox;
+
+ let rads = rotation * (Math.PI / 180);
+ if (rads >= Math.PI) {
+ rads -= Math.PI; // we have symmetry so we can skip two cases
+ }
+
+ let boundedCollisionX = 0; // bounded x on the surface from width
+ let clippedX = 0; // x coordinate of the vertex below the surface
+ let outScribedRectangleHeight, dy, dx;
+
+ if (rads <= Math.PI / 2) {
+ dx = (width * Math.cos(rads) - height * Math.sin(rads)) / 2;
+ outScribedRectangleHeight =
+ width * Math.sin(rads) + height * Math.cos(rads);
+ } else if (rads <= Math.PI) {
+ rads -= Math.PI / 2;
+ dx = (height * Math.cos(rads) - width * Math.sin(rads)) / 2;
+ outScribedRectangleHeight =
+ width * Math.cos(rads) + height * Math.sin(rads);
+ }
+
+ if (x >= floorBoundingBox.center.x) {
+ clippedX = x + dx;
+ boundedCollisionX = Math.min(
+ floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2,
+ clippedX
+ );
+ return (
+ outScribedRectangleHeight / 2 -
+ Math.max((clippedX - boundedCollisionX) * Math.tan(rads), 0)
+ );
+ }
+
+ clippedX = x - dx;
+ boundedCollisionX = Math.max(
+ floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2,
+ clippedX
+ );
+
+ return (
+ outScribedRectangleHeight / 2 -
+ Math.max((boundedCollisionX - clippedX) * Math.tan(rads), 0)
+ );
+ }
+}