diff options
Diffstat (limited to 'engine')
-rw-r--r-- | engine/Game.ts | 25 | ||||
-rw-r--r-- | engine/components/BoundingBox.ts | 57 | ||||
-rw-r--r-- | engine/components/Control.ts | 8 | ||||
-rw-r--r-- | engine/components/Forces.ts | 2 | ||||
-rw-r--r-- | engine/components/Sprite.ts | 6 | ||||
-rw-r--r-- | engine/components/Velocity.ts | 10 | ||||
-rw-r--r-- | engine/config/assets.ts | 10 | ||||
-rw-r--r-- | engine/config/constants.ts | 8 | ||||
-rw-r--r-- | engine/config/sprites.ts | 35 | ||||
-rw-r--r-- | engine/entities/Entity.ts | 1 | ||||
-rw-r--r-- | engine/entities/Floor.ts | 14 | ||||
-rw-r--r-- | engine/entities/Player.ts | 15 | ||||
-rw-r--r-- | engine/interfaces/LeaderBoardEntry.ts | 5 | ||||
-rw-r--r-- | engine/interfaces/index.ts | 1 | ||||
-rw-r--r-- | engine/structures/QuadTree.ts | 91 | ||||
-rw-r--r-- | engine/systems/Collision.ts | 130 | ||||
-rw-r--r-- | engine/systems/FacingDirection.ts | 25 | ||||
-rw-r--r-- | engine/systems/Input.ts | 82 | ||||
-rw-r--r-- | engine/systems/Physics.ts | 34 | ||||
-rw-r--r-- | engine/systems/Render.ts | 57 | ||||
-rw-r--r-- | engine/systems/System.ts | 1 | ||||
-rw-r--r-- | engine/systems/WallBounds.ts | 27 | ||||
-rw-r--r-- | engine/utils/index.ts | 1 | ||||
-rw-r--r-- | engine/utils/normalizeVector.ts | 8 |
24 files changed, 338 insertions, 315 deletions
diff --git a/engine/Game.ts b/engine/Game.ts index 3682fbd..07d06e8 100644 --- a/engine/Game.ts +++ b/engine/Game.ts @@ -12,6 +12,7 @@ export class Game { public componentEntities: Map<string, Set<number>>; constructor() { + this.lastTimeStamp = performance.now(); this.running = false; this.systemOrder = []; this.systems = new Map(); @@ -28,7 +29,7 @@ export class Game { this.entities.set(entity.id, entity); } - public getEntity(id: number): Entity { + public getEntity(id: number): Entity | undefined { return this.entities.get(id); } @@ -36,6 +37,18 @@ export class Game { this.entities.delete(id); } + public forEachEntityWithComponent( + componentName: string, + callback: (entity: Entity) => void, + ) { + this.componentEntities.get(componentName)?.forEach((entityId) => { + const entity = this.getEntity(entityId); + if (!entity) return; + + callback(entity); + }); + } + public addSystem(system: System) { if (!this.systemOrder.includes(system.name)) { this.systemOrder.push(system.name); @@ -43,7 +56,7 @@ export class Game { this.systems.set(system.name, system); } - public getSystem(name: string): System { + public getSystem(name: string): System | undefined { return this.systems.get(name); } @@ -62,16 +75,16 @@ export class Game { if (!this.componentEntities.has(component.name)) { this.componentEntities.set( component.name, - new Set<number>([entity.id]) + new Set<number>([entity.id]), ); return; } - this.componentEntities.get(component.name).add(entity.id); - }) + this.componentEntities.get(component.name)?.add(entity.id); + }), ); this.systemOrder.forEach((systemName) => { - this.systems.get(systemName).update(dt, this); + this.systems.get(systemName)?.update(dt, this); }); }; } diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index 2b1d648..5e21b2f 100644 --- a/engine/components/BoundingBox.ts +++ b/engine/components/BoundingBox.ts @@ -1,6 +1,6 @@ import { Component, ComponentNames } from "."; import type { Coord2D, Dimension2D } from "../interfaces"; -import { dotProduct, rotateVector, normalizeVector } from "../utils"; +import { dotProduct, rotateVector } from "../utils"; export class BoundingBox extends Component { public center: Coord2D; @@ -15,10 +15,11 @@ export class BoundingBox extends Component { this.rotation = rotation ?? 0; } + // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem public isCollidingWith(box: BoundingBox): boolean { const boxes = [this.getVertices(), box.getVertices()]; for (const poly of boxes) { - for (let i = 0; i < poly.length; ++i) { + for (let i = 0; i < poly.length; i++) { const [A, B] = [poly[i], poly[(i + 1) % poly.length]]; const normal: Coord2D = { x: B.y - A.y, y: A.x - B.x }; @@ -28,8 +29,8 @@ export class BoundingBox extends Component { const projection = dotProduct(normal, vertex); return [Math.min(min, projection), Math.max(max, projection)]; }, - [Infinity, -Infinity] - ) + [Infinity, -Infinity], + ), ); if (maxThis < minBox || maxBox < minThis) return false; @@ -55,43 +56,29 @@ export class BoundingBox extends Component { }); } - private getAxes() { - const corners: Coord2D[] = this.getVerticesRelativeToCenter(); - const axes: Coord2D[] = []; - - for (let i = 0; i < corners.length; ++i) { - const [cornerA, cornerB] = [ - corners[i], - corners[(i + 1) % corners.length], - ].map((corner) => rotateVector(corner, this.rotation)); - - axes.push( - normalizeVector({ - x: cornerB.y - cornerA.y, - y: -(cornerB.x - cornerA.x), - }) - ); + public getRotationInPiOfUnitCircle() { + let rads = this.rotation * (Math.PI / 180); + if (rads >= Math.PI) { + rads -= Math.PI; } - - return axes; + return rads; } - private project(axis: Coord2D): [number, number] { - const corners = this.getCornersRelativeToCenter(); - let [min, max] = [Infinity, -Infinity]; + public getOutscribedBoxDims(): Dimension2D { + let rads = this.getRotationInPiOfUnitCircle(); + const { width, height } = this.dimension; - for (const corner of corners) { - const rotated = rotateVector(corner, this.rotation); - const translated = { - x: rotated.x + this.center.x, - y: rotated.y + this.center.y, + if (rads <= Math.PI / 2) { + return { + width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)), + height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads)), }; - const projection = dotProduct(translated, axis); - - min = Math.min(projection, min); - max = Math.max(projection, max); } - return [min, max]; + rads -= Math.PI / 2; + return { + width: Math.abs(height * Math.cos(rads) + width * Math.sin(rads)), + height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads)), + }; } } diff --git a/engine/components/Control.ts b/engine/components/Control.ts index 094ef1c..1e782ee 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -1,7 +1,11 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames, Velocity } from "."; export class Control extends Component { - constructor() { + public controlVelocity: Velocity; + + constructor(controlVelocity: Velocity = new Velocity()) { super(ComponentNames.Control); + + this.controlVelocity = controlVelocity; } } diff --git a/engine/components/Forces.ts b/engine/components/Forces.ts index bf540a1..91ae1c1 100644 --- a/engine/components/Forces.ts +++ b/engine/components/Forces.ts @@ -1,4 +1,4 @@ -import type { Accel2D, Force2D } from "../interfaces"; +import type { Force2D } from "../interfaces"; import { Component } from "./Component"; import { ComponentNames } from "."; diff --git a/engine/components/Sprite.ts b/engine/components/Sprite.ts index 90e1389..bdb4982 100644 --- a/engine/components/Sprite.ts +++ b/engine/components/Sprite.ts @@ -17,7 +17,7 @@ export class Sprite extends Component { spriteImgPos: Coord2D, spriteImgDimensions: Dimension2D, msPerFrame: number, - numFrames: number + numFrames: number, ) { super(ComponentNames.Sprite); @@ -44,7 +44,7 @@ export class Sprite extends Component { ctx.save(); ctx.translate(center.x, center.y); - if (rotation != 0) { + if (rotation != undefined && rotation != 0) { ctx.rotate(rotation * (Math.PI / 180)); } ctx.translate(-center.x, -center.y); @@ -56,7 +56,7 @@ export class Sprite extends Component { ctx.drawImage( this.sheet, ...this.getSpriteArgs(), - ...this.getDrawArgs(drawArgs) + ...this.getDrawArgs(drawArgs), ); if (tint) { diff --git a/engine/components/Velocity.ts b/engine/components/Velocity.ts index 119427d..068d8cd 100644 --- a/engine/components/Velocity.ts +++ b/engine/components/Velocity.ts @@ -6,10 +6,18 @@ export class Velocity extends Component { public dCartesian: Velocity2D; public dTheta: number; - constructor(dCartesian: Velocity2D, dTheta: number) { + constructor(dCartesian: Velocity2D = { dx: 0, dy: 0 }, dTheta: number = 0) { super(ComponentNames.Velocity); this.dCartesian = dCartesian; this.dTheta = dTheta; } + + public add(velocity?: Velocity) { + if (velocity) { + this.dCartesian.dx += velocity.dCartesian.dx; + this.dCartesian.dy += velocity.dCartesian.dy; + this.dTheta += velocity.dTheta; + } + } } diff --git a/engine/config/assets.ts b/engine/config/assets.ts index 51a5303..173bab3 100644 --- a/engine/config/assets.ts +++ b/engine/config/assets.ts @@ -4,7 +4,7 @@ import { SPRITE_SPECS } from "./sprites"; export const IMAGES = new Map<string, HTMLImageElement>(); export const loadSpritesIntoImageElements = ( - spriteSpecs: Partial<SpriteSpec>[] + spriteSpecs: Partial<SpriteSpec>[], ): Promise<void>[] => { const spritePromises: Promise<void>[] = []; @@ -17,13 +17,13 @@ export const loadSpritesIntoImageElements = ( spritePromises.push( new Promise((resolve) => { img.onload = () => resolve(); - }) + }), ); } if (spriteSpec.states) { spritePromises.push( - ...loadSpritesIntoImageElements(Object.values(spriteSpec.states)) + ...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values())), ); } } @@ -34,7 +34,9 @@ export const loadSpritesIntoImageElements = ( export const loadAssets = () => Promise.all([ ...loadSpritesIntoImageElements( - Array.from(SPRITE_SPECS.keys()).map((key) => SPRITE_SPECS.get(key)) + Array.from(SPRITE_SPECS.keys()).map( + (key) => SPRITE_SPECS.get(key) as SpriteSpec, + ), ), // TODO: Sound ]); diff --git a/engine/config/constants.ts b/engine/config/constants.ts index 9a3169b..3d536d3 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -11,12 +11,12 @@ export namespace KeyConstants { }; export const ActionKeys: Map<Action, string[]> = Object.keys( - KeyActions + KeyActions, ).reduce((acc: Map<Action, string[]>, key) => { const action = KeyActions[key]; if (acc.has(action)) { - acc.get(action).push(key); + acc.get(action)?.push(key); return acc; } @@ -29,8 +29,8 @@ export namespace PhysicsConstants { export const MAX_JUMP_TIME_MS = 150; export const GRAVITY = 0.0075; export const PLAYER_MOVE_VEL = 1; - export const PLAYER_JUMP_ACC = -0.01; - export const PLAYER_JUMP_INITIAL_VEL = -0.9; + export const PLAYER_JUMP_ACC = -0.008; + export const PLAYER_JUMP_INITIAL_VEL = -1; } export namespace Miscellaneous { diff --git a/engine/config/sprites.ts b/engine/config/sprites.ts index 18bec73..1f65c18 100644 --- a/engine/config/sprites.ts +++ b/engine/config/sprites.ts @@ -10,7 +10,7 @@ export interface SpriteSpec { height: number; frames: number; msPerFrame: number; - states?: Record<string | number, Partial<SpriteSpec>>; + states?: Map<string | number, Partial<SpriteSpec>>; } export const SPRITE_SPECS: Map<Sprites, Partial<SpriteSpec>> = new Map< @@ -22,28 +22,27 @@ const floorSpriteSpec = { height: 40, frames: 3, msPerFrame: 125, - states: {}, + states: new Map<number, Partial<SpriteSpec>>(), }; -floorSpriteSpec.states = [40, 80, 120, 160].reduce((acc, cur) => { - acc[cur] = { - width: cur, - sheet: `/assets/floor_tile_${cur}.png`, - }; - return acc; -}, {}); +[40, 80, 120, 160].forEach((width) => { + floorSpriteSpec.states.set(width, { + width, + sheet: `/assets/floor_tile_${width}.png`, + }); +}); SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec); -SPRITE_SPECS.set(Sprites.COFFEE, { +const coffeeSpriteSpec = { msPerFrame: 100, width: 60, height: 45, frames: 3, - states: { - LEFT: { - sheet: "/assets/coffee_left.png", - }, - RIGHT: { - sheet: "/assets/coffee_right.png", - }, - }, + states: new Map<string, Partial<SpriteSpec>>(), +}; +coffeeSpriteSpec.states.set("LEFT", { + sheet: "/assets/coffee_left.png", +}); +coffeeSpriteSpec.states.set("RIGHT", { + sheet: "/assets/coffee_right.png", }); +SPRITE_SPECS.set(Sprites.COFFEE, coffeeSpriteSpec); diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts index e57ccde..ca8d314 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -1,5 +1,4 @@ import type { Component } from "../components"; -import { ComponentNotFoundError } from "../exceptions"; export abstract class Entity { private static ID = 0; diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index d51badc..44587e6 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -4,26 +4,28 @@ import { TopCollidable } from "../components/TopCollidable"; import { Entity } from "../entities"; export class Floor extends Entity { - private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(Sprites.FLOOR); + private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( + Sprites.FLOOR, + ) as SpriteSpec; constructor(width: number) { super(); this.addComponent( new Sprite( - IMAGES.get(Floor.spriteSpec.states[width].sheet), + IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet), { x: 0, y: 0 }, { width, height: Floor.spriteSpec.height }, Floor.spriteSpec.msPerFrame, - Floor.spriteSpec.frames - ) + Floor.spriteSpec.frames, + ), ); this.addComponent( new BoundingBox( { x: 300, y: 300 }, - { width, height: Floor.spriteSpec.height } - ) + { width, height: Floor.spriteSpec.height }, + ), ); this.addComponent(new TopCollidable()); diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index 0ba5a41..45d7500 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -14,14 +14,15 @@ import { Mass, Moment, } from "../components"; -import { PhysicsConstants } from "../config"; import { Direction } from "../interfaces"; export class Player extends Entity { private static MASS: number = 10; private static MOI: number = 1000; - private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(Sprites.COFFEE); + private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( + Sprites.COFFEE, + ) as SpriteSpec; constructor() { super(); @@ -30,8 +31,8 @@ export class Player extends Entity { new BoundingBox( { x: 300, y: 100 }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, - 0 - ) + 0, + ), ); this.addComponent(new Velocity({ dx: 0, dy: 0 }, 0)); @@ -54,12 +55,12 @@ export class Player extends Entity { const [leftSprite, rightSprite] = [Direction.LEFT, Direction.RIGHT].map( (direction) => new Sprite( - IMAGES.get(Player.spriteSpec.states[direction].sheet), + IMAGES.get(Player.spriteSpec.states?.get(direction)?.sheet as string), { x: 0, y: 0 }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, Player.spriteSpec.msPerFrame, - Player.spriteSpec.frames - ) + Player.spriteSpec.frames, + ), ); this.addComponent(new FacingDirection(leftSprite, rightSprite)); diff --git a/engine/interfaces/LeaderBoardEntry.ts b/engine/interfaces/LeaderBoardEntry.ts deleted file mode 100644 index 1b1e7b3..0000000 --- a/engine/interfaces/LeaderBoardEntry.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface LeaderBoardEntry { - name: string; - score: number; - avatar: string; -} diff --git a/engine/interfaces/index.ts b/engine/interfaces/index.ts index 0398abd..8cdf4d8 100644 --- a/engine/interfaces/index.ts +++ b/engine/interfaces/index.ts @@ -1,4 +1,3 @@ -export * from "./LeaderBoardEntry"; export * from "./Vec2"; export * from "./Draw"; export * from "./Direction"; diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index 7913e59..d1ff3b1 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -1,6 +1,4 @@ import type { Coord2D, Dimension2D } from "../interfaces"; -import { ComponentNames, BoundingBox } from "../components"; -import { Entity } from "../entities"; interface BoxedEntry { id: number; @@ -30,21 +28,26 @@ export class QuadTree { dimension: Dimension2D, maxLevels: number, splitThreshold: number, - level?: number + level?: number, ) { - this.children = []; + this.children = new Map<Quadrant, QuadTree>(); this.objects = []; this.maxLevels = maxLevels; this.splitThreshold = splitThreshold; this.level = level ?? 0; + + this.topLeft = topLeft; + this.dimension = dimension; } public insert(id: number, dimension: Dimension2D, center: Coord2D): void { + const box: BoxedEntry = { id, center, dimension }; if (this.hasChildren()) { - this.getIndices(boundingBox).forEach((i) => - this.children[i].insert(id, dimension, center) - ); + this.getQuadrants(box).forEach((quadrant) => { + const quadrantBox = this.children.get(quadrant); + quadrantBox?.insert(id, dimension, center); + }); return; } @@ -74,9 +77,10 @@ export class QuadTree { if (this.hasChildren()) { this.getQuadrants(boxedEntry).forEach((quadrant) => { - this.children - .get(quadrant) - .getNeighborIds(boxedEntry) + const quadrantBox = this.children.get(quadrant); + + quadrantBox + ?.getNeighborIds(boxedEntry) .forEach((id) => neighbors.push(id)); }); } @@ -88,15 +92,17 @@ export class QuadTree { const halfWidth = this.dimension.width / 2; const halfHeight = this.dimension.height / 2; - [ - [Quadrant.I, { x: this.topLeft.x + halfWidth, y: this.topLeft.y }], - [Quadrant.II, { ...this.topLeft }], - [Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }], + ( [ - Quadrant.IV, - { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }, - ], - ].forEach(([quadrant, pos]) => { + [Quadrant.I, { x: this.topLeft.x + halfWidth, y: this.topLeft.y }], + [Quadrant.II, { ...this.topLeft }], + [Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }], + [ + Quadrant.IV, + { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }, + ], + ] as [[Quadrant, Coord2D]] + ).forEach(([quadrant, pos]) => { this.children.set( quadrant, new QuadTree( @@ -104,34 +110,48 @@ export class QuadTree { { width: halfWidth, height: halfHeight }, this.maxLevels, this.splitThreshold, - this.level + 1 - ) + this.level + 1, + ), ); }); } - private getQuandrants(boxedEntry: BoxedEntry): Quadrant[] { + private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] { const treeCenter: Coord2D = { x: this.topLeft.x + this.dimension.width / 2, y: this.topLeft.y + this.dimension.height / 2, }; - return [ - [Quadrant.I, (x, y) => x >= treeCenter.x && y < treeCenter.y], - [Quadrant.II, (x, y) => x < treeCenter.x && y < treeCenter.y], - [Quadrant.III, (x, y) => x < treeCenter.x && y >= treeCenter.y], - [Quadrant.IV, (x, y) => x >= treeCenter.x && y >= treeCenter.y], - ] + return ( + [ + [ + Quadrant.I, + (x: number, y: number) => x >= treeCenter.x && y < treeCenter.y, + ], + [ + Quadrant.II, + (x: number, y: number) => x < treeCenter.x && y < treeCenter.y, + ], + [ + Quadrant.III, + (x: number, y: number) => x < treeCenter.x && y >= treeCenter.y, + ], + [ + Quadrant.IV, + (x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y, + ], + ] as [[Quadrant, (x: number, y: number) => boolean]] + ) .filter( ([_quadrant, condition]) => condition( boxedEntry.center.x + boxedEntry.dimension.width / 2, - boxedEntry.center.y + boxedEntry.dimension.height / 2 + boxedEntry.center.y + boxedEntry.dimension.height / 2, ) || condition( boxedEntry.center.x - boxedEntry.dimension.width / 2, - boxedEntry.center.y - boxedEntry.dimension.height / 2 - ) + boxedEntry.center.y - boxedEntry.dimension.height / 2, + ), ) .map(([quadrant]) => quadrant); } @@ -139,9 +159,12 @@ export class QuadTree { private realignObjects(): void { this.objects.forEach((boxedEntry) => { this.getQuadrants(boxedEntry).forEach((direction) => { - this.children - .get(direction) - .insert(boxedEntry.id, boxedEntry.dimension, boxedEntry.center); + const quadrant = this.children.get(direction); + quadrant?.insert( + boxedEntry.id, + boxedEntry.dimension, + boxedEntry.center, + ); }); }); @@ -149,6 +172,6 @@ export class QuadTree { } private hasChildren() { - return this.children && this.children.length > 0; + return this.children && this.children.size > 0; } } diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts index 846a95a..2bba03b 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -5,7 +5,7 @@ import { ComponentNames, Jump, Velocity, - Moment, + Forces, } from "../components"; import { Game } from "../Game"; import { PhysicsConstants } from "../config"; @@ -30,60 +30,64 @@ export class Collision extends System { { x: 0, y: 0 }, screenDimensions, Collision.QUADTREE_MAX_LEVELS, - Collision.QUADTREE_SPLIT_THRESHOLD + Collision.QUADTREE_SPLIT_THRESHOLD, ); } - public update(dt: number, game: Game) { + 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); - }) + game.componentEntities.get(componentName), + ).forEach( + (entityIds?: Set<number>) => + entityIds?.forEach((id) => { + const entity = game.entities.get(id); + if (!entity || !entity.hasComponent(ComponentNames.BoundingBox)) { + return; + } + entitiesToAddToQuadtree.push(entity); + }), ); entitiesToAddToQuadtree.forEach((entity) => { const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox + ComponentNames.BoundingBox, ); - this.quadTree.insert( - entity.id, - boundingBox.dimension, - boundingBox.center - ); + let dimension = { ...boundingBox.dimension }; + if (boundingBox.rotation != 0) { + dimension = boundingBox.getOutscribedBoxDims(); + } + + this.quadTree.insert(entity.id, dimension, boundingBox.center); }); // find colliding entities and perform collisions const collidingEntities = this.getCollidingEntities( entitiesToAddToQuadtree, - game.entities + game, ); collidingEntities.forEach(([entityAId, entityBId]) => { const [entityA, entityB] = [entityAId, entityBId].map((id) => - game.entities.get(id) + game.entities.get(id), ); - this.performCollision(entityA, entityB); + if (entityA && entityB) { + this.performCollision(entityA, entityB); + } }); } private performCollision(entityA: Entity, entityB: Entity) { const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map( - (entity) => entity.getComponent<BoundingBox>(ComponentNames.BoundingBox) + (entity) => entity.getComponent<BoundingBox>(ComponentNames.BoundingBox), ); - let velocity: Velocity; + let velocity = new Velocity(); if (entityA.hasComponent(ComponentNames.Velocity)) { velocity = entityA.getComponent<Velocity>(ComponentNames.Velocity); } @@ -92,17 +96,16 @@ export class Collision extends System { 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.` + `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; + if (velocity) velocity.dCartesian.dy = 0; // apply normal force if (entityA.hasComponent(ComponentNames.Gravity)) { @@ -110,7 +113,8 @@ export class Collision extends System { const F_n = -mass * PhysicsConstants.GRAVITY; entityA.getComponent<Forces>(ComponentNames.Forces).forces.push({ - fCartesian: { fy: F_n }, + fCartesian: { fy: F_n, fx: 0 }, + torque: 0, }); } @@ -128,31 +132,35 @@ export class Collision extends System { private getCollidingEntities( collidableEntities: Entity[], - entityMap: Map<number, Entity> + game: Game, ): [number, number][] { - const collidingEntityIds: [number, number] = []; + const collidingEntityIds: [number, number][] = []; for (const entity of collidableEntities) { const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox + ComponentNames.BoundingBox, ); - this.quadTree + const neighborIds = 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]); - } - }); + .filter((neighborId) => neighborId != entity.id); + + neighborIds.forEach((neighborId) => { + const neighbor = game.getEntity(neighborId); + if (!neighbor) return; + + const neighborBoundingBox = neighbor.getComponent<BoundingBox>( + ComponentNames.BoundingBox, + ); + + if (boundingBox.isCollidingWith(neighborBoundingBox)) { + collidingEntityIds.push([entity.id, neighborId]); + } + }); } return collidingEntityIds; @@ -161,55 +169,45 @@ export class Collision extends System { // ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ private getDyToPushOutOfFloor( entityBoundingBox: BoundingBox, - floorBoundingBox: BoundingBox + floorBoundingBox: BoundingBox, ): number { const { - rotation, - center: { x, y }, dimension: { width, height }, + center: { x }, } = entityBoundingBox; - let rads = rotation * (Math.PI / 180); - if (rads >= Math.PI) { - rads -= Math.PI; // we have symmetry so we can skip two cases - } + const outScribedRectangle = entityBoundingBox.getOutscribedBoxDims(); - 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) { + let rads = entityBoundingBox.getRotationInPiOfUnitCircle(); + let dx = (width * Math.cos(rads) - height * Math.sin(rads)) / 2; + + if (rads >= Math.PI / 2) { rads -= Math.PI / 2; dx = (height * Math.cos(rads) - width * Math.sin(rads)) / 2; - outScribedRectangleHeight = - width * Math.cos(rads) + height * Math.sin(rads); } + const clippedX = x + dx; // x coordinate of the vertex below the surface (if existant) + let boundedCollisionX = 0; // bounded x on the surface from width + if (x >= floorBoundingBox.center.x) { - clippedX = x + dx; boundedCollisionX = Math.min( floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2, - clippedX + clippedX, ); return ( - outScribedRectangleHeight / 2 - + outScribedRectangle.height / 2 - Math.max((clippedX - boundedCollisionX) * Math.tan(rads), 0) ); } - clippedX = x - dx; boundedCollisionX = Math.max( floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2, - clippedX + clippedX, ); return ( - outScribedRectangleHeight / 2 - - Math.max((boundedCollisionX - clippedX) * Math.tan(rads), 0) + outScribedRectangle.height / 2 - + Math.max((boundedCollisionX - clippedX) * Math.tan(Math.PI / 2 - rads), 0) ); } } diff --git a/engine/systems/FacingDirection.ts b/engine/systems/FacingDirection.ts index c6513ac..4426ab6 100644 --- a/engine/systems/FacingDirection.ts +++ b/engine/systems/FacingDirection.ts @@ -2,9 +2,9 @@ import { ComponentNames, Velocity, FacingDirection as FacingDirectionComponent, + Control, } from "../components"; import { Game } from "../Game"; -import type { Entity } from "../entities"; import { System, SystemNames } from "./"; export class FacingDirection extends System { @@ -13,24 +13,31 @@ export class FacingDirection extends System { } public update(_dt: number, game: Game) { - game.componentEntities - .get(ComponentNames.FacingDirection) - ?.forEach((entityId) => { - const entity = game.entities.get(entityId); + game.forEachEntityWithComponent( + ComponentNames.FacingDirection, + (entity) => { if (!entity.hasComponent(ComponentNames.Velocity)) { return; } + const totalVelocity: Velocity = new Velocity(); + const control = entity.getComponent<Control>(ComponentNames.Control); const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); + totalVelocity.add(velocity); + if (control) { + totalVelocity.add(control.controlVelocity); + } + const facingDirection = entity.getComponent<FacingDirectionComponent>( - ComponentNames.FacingDirection + ComponentNames.FacingDirection, ); - if (velocity.dCartesian.dx > 0) { + if (totalVelocity.dCartesian.dx > 0) { entity.addComponent(facingDirection.facingRightSprite); - } else if (velocity.dCartesian.dx < 0) { + } else if (totalVelocity.dCartesian.dx < 0) { entity.addComponent(facingDirection.facingLeftSprite); } - }); + }, + ); } } diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts index 4272ec8..4aa9844 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -1,21 +1,16 @@ import { Jump, Forces, - Acceleration, ComponentNames, Velocity, Mass, + Control, } from "../components"; import { Game } from "../Game"; import { KeyConstants, PhysicsConstants } from "../config"; -import type { Entity } from "../entities"; import { Action } from "../interfaces"; import { System, SystemNames } from "./"; -/** - * TODO: Make velocities reset on each game loop (as similar to acceleration) - * - Then, we can add / remove velocity on update instead of just setting it and praying it's not modified externally - */ export class Input extends System { private keys: Set<string>; private actionTimeStamps: Map<Action, number>; @@ -23,7 +18,7 @@ export class Input extends System { constructor() { super(SystemNames.Input); - this.keys = new Set<number>(); + this.keys = new Set<string>(); this.actionTimeStamps = new Map<Action, number>(); } @@ -35,51 +30,52 @@ export class Input extends System { this.keys.delete(key); } - private hasSomeKey(keys: string[]): boolean { - return keys.some((key) => this.keys.has(key)); + private hasSomeKey(keys?: string[]): boolean { + if (keys) { + return keys.some((key) => this.keys.has(key)); + } + return false; } - public update(dt: number, game: Game) { - game.componentEntities.get(ComponentNames.Control)?.forEach((entityId) => { - const entity = game.entities.get(entityId); - if (!entity.hasComponent(ComponentNames.Velocity)) { - return; - } - - const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); + public update(_dt: number, game: Game) { + game.forEachEntityWithComponent(ComponentNames.Control, (entity) => { + const control = entity.getComponent<Control>(ComponentNames.Control); if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) { - velocity.dCartesian.dx = PhysicsConstants.PLAYER_MOVE_VEL; - } else if ( - this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT)) - ) { - velocity.dCartesian.dx = -PhysicsConstants.PLAYER_MOVE_VEL; - } else { - velocity.dCartesian.dx = 0; + control.controlVelocity.dCartesian.dx += + PhysicsConstants.PLAYER_MOVE_VEL; } - }); - game.componentEntities.get(ComponentNames.Jump)?.forEach((entityId) => { - const entity = game.entities.get(entityId); - const jump = entity.getComponent<Jump>(ComponentNames.Jump); - const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); + if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) { + control.controlVelocity.dCartesian.dx += + -PhysicsConstants.PLAYER_MOVE_VEL; + } - if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) { - if (jump.canJump) { - this.actionTimeStamps.set(Action.JUMP, performance.now()); + if (entity.hasComponent(ComponentNames.Jump)) { + const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); + const jump = entity.getComponent<Jump>(ComponentNames.Jump); - velocity.dCartesian.dy = PhysicsConstants.PLAYER_JUMP_INITIAL_VEL; - jump.canJump = false; - } + if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) { + if (jump.canJump) { + this.actionTimeStamps.set(Action.JUMP, performance.now()); + + velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL; + jump.canJump = false; + } - if ( - performance.now() - this.actionTimeStamps.get(Action.JUMP) < - PhysicsConstants.MAX_JUMP_TIME_MS - ) { - const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; - entity.getComponent<Forces>(ComponentNames.Forces)?.forces.push({ - fCartesian: { fy: mass * PhysicsConstants.PLAYER_JUMP_ACC }, - }); + if ( + performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) < + PhysicsConstants.MAX_JUMP_TIME_MS + ) { + const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; + entity.getComponent<Forces>(ComponentNames.Forces)?.forces.push({ + fCartesian: { + fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, + fx: 0, + }, + torque: 0, + }); + } } } }); diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts index 7edeb41..38962a6 100644 --- a/engine/systems/Physics.ts +++ b/engine/systems/Physics.ts @@ -1,6 +1,5 @@ import { System, SystemNames } from "."; import { - Acceleration, BoundingBox, ComponentNames, Forces, @@ -8,9 +7,10 @@ import { Velocity, Mass, Jump, + Moment, + Control, } from "../components"; import { PhysicsConstants } from "../config"; -import type { Entity } from "../entities"; import type { Force2D } from "../interfaces"; import { Game } from "../Game"; @@ -20,14 +20,12 @@ export class Physics extends System { } public update(dt: number, game: Game): void { - game.componentEntities.get(ComponentNames.Forces)?.forEach((entityId) => { - const entity = game.entities.get(entityId); - + game.forEachEntityWithComponent(ComponentNames.Forces, (entity) => { const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; const forces = entity.getComponent<Forces>(ComponentNames.Forces).forces; const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); const inertia = entity.getComponent<Moment>( - ComponentNames.Moment + ComponentNames.Moment, ).inertia; // F_g = mg, applied only until terminal velocity is reached @@ -37,7 +35,9 @@ export class Physics extends System { forces.push({ fCartesian: { fy: mass * PhysicsConstants.GRAVITY, + fx: 0, }, + torque: 0, }); } } @@ -51,7 +51,7 @@ export class Physics extends System { }, torque: accum.torque + (torque ?? 0), }), - { fCartesian: { fx: 0, fy: 0 }, torque: 0 } + { fCartesian: { fx: 0, fy: 0 }, torque: 0 }, ); // integrate accelerations @@ -62,6 +62,7 @@ export class Physics extends System { velocity.dCartesian.dx += ddx * dt; velocity.dCartesian.dy += ddy * dt; velocity.dTheta += (sumOfForces.torque * dt) / inertia; + // clear the forces entity.getComponent<Forces>(ComponentNames.Forces).forces = []; @@ -71,11 +72,17 @@ export class Physics extends System { } }); - game.componentEntities.get(ComponentNames.Velocity)?.forEach((entityId) => { - const entity = game.entities.get(entityId); - const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); + game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => { + const velocity: Velocity = new Velocity(); + const control = entity.getComponent<Control>(ComponentNames.Control); + + velocity.add(entity.getComponent<Velocity>(ComponentNames.Velocity)); + if (control) { + velocity.add(control.controlVelocity); + } + const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox + ComponentNames.BoundingBox, ); // integrate velocity @@ -86,6 +93,11 @@ export class Physics extends System { (boundingBox.rotation < 0 ? 360 + boundingBox.rotation : boundingBox.rotation) % 360; + + // clear the control velocity + if (control) { + control.controlVelocity = new Velocity(); + } }); } } diff --git a/engine/systems/Render.ts b/engine/systems/Render.ts index b5479e1..9bb4091 100644 --- a/engine/systems/Render.ts +++ b/engine/systems/Render.ts @@ -1,8 +1,6 @@ import { System, SystemNames } from "."; import { BoundingBox, ComponentNames, Sprite } from "../components"; -import type { Entity } from "../entities"; import { Game } from "../Game"; -import type { DrawArgs } from "../interfaces"; import { clamp } from "../utils"; export class Render extends System { @@ -16,39 +14,36 @@ export class Render extends System { public update(dt: number, game: Game) { this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); - game.componentEntities.get(ComponentNames.Sprite)?.forEach((entityId) => { - const entity = game.entities.get(entityId); + game.forEachEntityWithComponent(ComponentNames.Sprite, (entity) => { const sprite = entity.getComponent<Sprite>(ComponentNames.Sprite); sprite.update(dt); - let drawArgs: DrawArgs; - if (entity.hasComponent(ComponentNames.BoundingBox)) { - const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox - ); - - // don't render if we're outside the screen - if ( - clamp( - boundingBox.center.y, - -boundingBox.dimension.height / 2, - this.ctx.canvas.height + boundingBox.dimension.height / 2 - ) != boundingBox.center.y || - clamp( - boundingBox.center.x, - -boundingBox.dimension.width / 2, - this.ctx.canvas.width + boundingBox.dimension.width / 2 - ) != boundingBox.center.x - ) { - return; - } - - drawArgs = { - center: boundingBox.center, - dimension: boundingBox.dimension, - rotation: boundingBox.rotation, - }; + const boundingBox = entity.getComponent<BoundingBox>( + ComponentNames.BoundingBox, + ); + + // don't render if we're outside the screen + if ( + clamp( + boundingBox.center.y, + -boundingBox.dimension.height / 2, + this.ctx.canvas.height + boundingBox.dimension.height / 2, + ) != boundingBox.center.y || + clamp( + boundingBox.center.x, + -boundingBox.dimension.width / 2, + this.ctx.canvas.width + boundingBox.dimension.width / 2, + ) != boundingBox.center.x + ) { + return; } + + const drawArgs = { + center: boundingBox.center, + dimension: boundingBox.dimension, + rotation: boundingBox.rotation, + }; + sprite.draw(this.ctx, drawArgs); }); } diff --git a/engine/systems/System.ts b/engine/systems/System.ts index 1aad285..8b00dc5 100644 --- a/engine/systems/System.ts +++ b/engine/systems/System.ts @@ -1,4 +1,3 @@ -import { Entity } from "../entities"; import { Game } from "../Game"; export abstract class System { diff --git a/engine/systems/WallBounds.ts b/engine/systems/WallBounds.ts index 6ea2267..a0d4a9c 100644 --- a/engine/systems/WallBounds.ts +++ b/engine/systems/WallBounds.ts @@ -14,23 +14,16 @@ export class WallBounds extends System { } public update(_dt: number, game: Game) { - game.componentEntities - .get(ComponentNames.WallBounded) - ?.forEach((entityId) => { - const entity = game.entities.get(entityId); - if (!entity.hasComponent(ComponentNames.BoundingBox)) { - return; - } + game.forEachEntityWithComponent(ComponentNames.WallBounded, (entity) => { + const boundingBox = entity.getComponent<BoundingBox>( + ComponentNames.BoundingBox, + ); - const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox - ); - - boundingBox.center.x = clamp( - boundingBox.center.x, - boundingBox.dimension.width / 2, - this.screenWidth - boundingBox.dimension.width / 2 - ); - }); + boundingBox.center.x = clamp( + boundingBox.center.x, + boundingBox.dimension.width / 2, + this.screenWidth - boundingBox.dimension.width / 2, + ); + }); } } diff --git a/engine/utils/index.ts b/engine/utils/index.ts index 4e465c2..82a0d05 100644 --- a/engine/utils/index.ts +++ b/engine/utils/index.ts @@ -1,4 +1,3 @@ export * from "./rotateVector"; -export * from "./normalizeVector"; export * from "./dotProduct"; export * from "./clamp"; diff --git a/engine/utils/normalizeVector.ts b/engine/utils/normalizeVector.ts deleted file mode 100644 index e6dfd7f..0000000 --- a/engine/utils/normalizeVector.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Coord2D } from "../interfaces"; - -export const normalizeVector = (vector: Coord2D): Coord2D => { - const { x, y } = vector; - const length = Math.sqrt(x * x + y * y); - - return { x: x / length, y: y / length }; -}; |