diff options
author | Elizabeth (Lizzy) Hunt <elizabeth.hunt@simponic.xyz> | 2023-08-26 17:57:05 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-26 17:57:05 -0600 |
commit | 8a4ab8d79b5ce1dabb431168398b5d5111fe326c (patch) | |
tree | e60767dc5295edf379cf421e20171dc418e548b7 /engine | |
parent | c6e9baa0009f7cce0f6ff156a3957ef04a8cb684 (diff) | |
parent | 6ce6946a4401d2ee6fa5cb747fab7d4c658a63c8 (diff) | |
download | jumpstorm-8a4ab8d79b5ce1dabb431168398b5d5111fe326c.tar.gz jumpstorm-8a4ab8d79b5ce1dabb431168398b5d5111fe326c.zip |
Merge pull request #1 from Simponic/network
Network
Diffstat (limited to 'engine')
50 files changed, 893 insertions, 405 deletions
diff --git a/engine/Game.ts b/engine/Game.ts index 07d06e8..cdd3507 100644 --- a/engine/Game.ts +++ b/engine/Game.ts @@ -1,5 +1,5 @@ -import { Entity } from "./entities"; -import { System } from "./systems"; +import { Entity } from './entities'; +import { System } from './systems'; export class Game { private systemOrder: string[]; @@ -7,9 +7,9 @@ export class Game { private running: boolean; private lastTimeStamp: number; - public entities: Map<number, Entity>; + public entities: Map<string, Entity>; public systems: Map<string, System>; - public componentEntities: Map<string, Set<number>>; + public componentEntities: Map<string, Set<string>>; constructor() { this.lastTimeStamp = performance.now(); @@ -29,17 +29,17 @@ export class Game { this.entities.set(entity.id, entity); } - public getEntity(id: number): Entity | undefined { + public getEntity(id: string): Entity | undefined { return this.entities.get(id); } - public removeEntity(id: number) { + public removeEntity(id: string) { this.entities.delete(id); } public forEachEntityWithComponent( componentName: string, - callback: (entity: Entity) => void, + callback: (entity: Entity) => void ) { this.componentEntities.get(componentName)?.forEach((entityId) => { const entity = this.getEntity(entityId); @@ -60,7 +60,7 @@ export class Game { return this.systems.get(name); } - public doGameLoop = (timeStamp: number) => { + public doGameLoop(timeStamp: number) { if (!this.running) { return; } @@ -75,16 +75,16 @@ export class Game { if (!this.componentEntities.has(component.name)) { this.componentEntities.set( component.name, - new Set<number>([entity.id]), + new Set<string>([entity.id]) ); return; } this.componentEntities.get(component.name)?.add(entity.id); - }), + }) ); this.systemOrder.forEach((systemName) => { this.systems.get(systemName)?.update(dt, this); }); - }; + } } diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index 5e21b2f..921feb9 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 } from "../utils"; +import { Component, ComponentNames } from '.'; +import type { Coord2D, Dimension2D } from '../interfaces'; +import { dotProduct, rotateVector } from '../utils'; export class BoundingBox extends Component { public center: Coord2D; @@ -15,8 +15,27 @@ export class BoundingBox extends Component { this.rotation = rotation ?? 0; } - // 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; + } + + // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem const boxes = [this.getVertices(), box.getVertices()]; for (const poly of boxes) { for (let i = 0; i < poly.length; i++) { @@ -29,8 +48,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; @@ -45,20 +64,22 @@ export class BoundingBox extends Component { { x: -this.dimension.width / 2, y: -this.dimension.height / 2 }, { x: -this.dimension.width / 2, y: this.dimension.height / 2 }, { x: this.dimension.width / 2, y: this.dimension.height / 2 }, - { x: this.dimension.width / 2, y: -this.dimension.height / 2 }, + { x: this.dimension.width / 2, y: -this.dimension.height / 2 } ] - .map((vertex) => rotateVector(vertex, this.rotation)) + .map((vertex) => rotateVector(vertex, this.rotation)) // rotate .map((vertex) => { + // translate return { x: vertex.x + this.center.x, - y: vertex.y + this.center.y, + y: vertex.y + this.center.y }; }); } - public getRotationInPiOfUnitCircle() { + public getRotationInPiOfUnitCircle(): number { let rads = this.rotation * (Math.PI / 180); if (rads >= Math.PI) { + // Physics system guarantees rotation \in [0, 360) rads -= Math.PI; } return rads; @@ -68,17 +89,33 @@ export class BoundingBox extends Component { let rads = this.getRotationInPiOfUnitCircle(); const { width, height } = this.dimension; + if (rads == 0) return this.dimension; + 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)), + height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads)) }; } 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)), + 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/components/Collide.ts b/engine/components/Collide.ts index 889ecf8..ed72b92 100644 --- a/engine/components/Collide.ts +++ b/engine/components/Collide.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames } from '.'; export class Collide extends Component { constructor() { diff --git a/engine/components/Control.ts b/engine/components/Control.ts index 1e782ee..d3987d7 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -1,11 +1,18 @@ -import { Component, ComponentNames, Velocity } from "."; +import { Component, ComponentNames, Velocity } from '.'; export class Control extends Component { - public controlVelocity: Velocity; + public controlVelocityComponent: Velocity; + public controllableBy: string; + public isControllable: boolean; // computed each update in the input system - constructor(controlVelocity: Velocity = new Velocity()) { + constructor( + controllableBy: string, + controlVelocityComponent: Velocity = new Velocity() + ) { super(ComponentNames.Control); - this.controlVelocity = controlVelocity; + this.controllableBy = controllableBy; + this.controlVelocityComponent = controlVelocityComponent; + this.isControllable = false; } } diff --git a/engine/components/FacingDirection.ts b/engine/components/FacingDirection.ts index 1c701a3..8c2a9d2 100644 --- a/engine/components/FacingDirection.ts +++ b/engine/components/FacingDirection.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames, Sprite } from "."; +import { Component, ComponentNames, Sprite } from '.'; export class FacingDirection extends Component { public readonly facingLeftSprite: Sprite; diff --git a/engine/components/Forces.ts b/engine/components/Forces.ts index 91ae1c1..e397985 100644 --- a/engine/components/Forces.ts +++ b/engine/components/Forces.ts @@ -1,6 +1,6 @@ -import type { Force2D } from "../interfaces"; -import { Component } from "./Component"; -import { ComponentNames } from "."; +import type { Force2D } from '../interfaces'; +import { Component } from './Component'; +import { ComponentNames } from '.'; /** * A list of forces and torque, (in newtons, and newton-meters respectively) diff --git a/engine/components/Gravity.ts b/engine/components/Gravity.ts index 89fcb67..dd6dd2e 100644 --- a/engine/components/Gravity.ts +++ b/engine/components/Gravity.ts @@ -1,7 +1,7 @@ -import { ComponentNames, Component } from "."; +import { ComponentNames, Component } from '.'; export class Gravity extends Component { - private static DEFAULT_TERMINAL_VELOCITY = 5; + private static DEFAULT_TERMINAL_VELOCITY = 4.5; public terminalVelocity: number; diff --git a/engine/components/Jump.ts b/engine/components/Jump.ts index 0b40767..6cbfb08 100644 --- a/engine/components/Jump.ts +++ b/engine/components/Jump.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames } from '.'; export class Jump extends Component { public canJump: boolean; diff --git a/engine/components/Mass.ts b/engine/components/Mass.ts index daa2d71..a7f98fd 100644 --- a/engine/components/Mass.ts +++ b/engine/components/Mass.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames } from '.'; export class Mass extends Component { public mass: number; diff --git a/engine/components/Moment.ts b/engine/components/Moment.ts index 3d0dd2f..cd76294 100644 --- a/engine/components/Moment.ts +++ b/engine/components/Moment.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames } from '.'; export class Moment extends Component { public inertia: number; diff --git a/engine/components/NetworkUpdateable.ts b/engine/components/NetworkUpdateable.ts new file mode 100644 index 0000000..014270c --- /dev/null +++ b/engine/components/NetworkUpdateable.ts @@ -0,0 +1,7 @@ +import { Component, ComponentNames } from '.'; + +export class NetworkUpdateable extends Component { + constructor() { + super(ComponentNames.NetworkUpdateable); + } +} diff --git a/engine/components/Sprite.ts b/engine/components/Sprite.ts index bdb4982..36b944e 100644 --- a/engine/components/Sprite.ts +++ b/engine/components/Sprite.ts @@ -1,5 +1,5 @@ -import { Component, ComponentNames } from "."; -import type { Dimension2D, DrawArgs, Coord2D } from "../interfaces"; +import { Component, ComponentNames } from '.'; +import type { Dimension2D, DrawArgs, Coord2D } from '../interfaces'; export class Sprite extends Component { private sheet: HTMLImageElement; @@ -17,7 +17,7 @@ export class Sprite extends Component { spriteImgPos: Coord2D, spriteImgDimensions: Dimension2D, msPerFrame: number, - numFrames: number, + numFrames: number ) { super(ComponentNames.Sprite); @@ -56,12 +56,12 @@ export class Sprite extends Component { ctx.drawImage( this.sheet, ...this.getSpriteArgs(), - ...this.getDrawArgs(drawArgs), + ...this.getDrawArgs(drawArgs) ); if (tint) { ctx.globalAlpha = 0.5; - ctx.globalCompositeOperation = "source-atop"; + ctx.globalCompositeOperation = 'source-atop'; ctx.fillStyle = tint; ctx.fillRect(...this.getDrawArgs(drawArgs)); } @@ -74,19 +74,23 @@ export class Sprite extends Component { this.spriteImgPos.x + this.currentFrame * this.spriteImgDimensions.width, this.spriteImgPos.y, this.spriteImgDimensions.width, - this.spriteImgDimensions.height, + this.spriteImgDimensions.height ]; } private getDrawArgs({ center, - dimension, + dimension }: DrawArgs): [dx: number, dy: number, dw: number, dh: number] { return [ center.x - dimension.width / 2, center.y - dimension.height / 2, dimension.width, - dimension.height, + dimension.height ]; } + + public getSpriteDimensions() { + return this.spriteImgDimensions; + } } diff --git a/engine/components/TopCollidable.ts b/engine/components/TopCollidable.ts index 7fb147d..05ce484 100644 --- a/engine/components/TopCollidable.ts +++ b/engine/components/TopCollidable.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames } from '.'; export class TopCollidable extends Component { constructor() { diff --git a/engine/components/Velocity.ts b/engine/components/Velocity.ts index 068d8cd..0071891 100644 --- a/engine/components/Velocity.ts +++ b/engine/components/Velocity.ts @@ -1,23 +1,23 @@ -import type { Velocity2D } from "../interfaces"; -import { Component } from "./Component"; -import { ComponentNames } from "."; +import type { Velocity2D } from '../interfaces'; +import { Component } from './Component'; +import { ComponentNames } from '.'; export class Velocity extends Component { - public dCartesian: Velocity2D; - public dTheta: number; + public velocity: Velocity2D; - constructor(dCartesian: Velocity2D = { dx: 0, dy: 0 }, dTheta: number = 0) { + constructor( + velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 } + ) { super(ComponentNames.Velocity); - this.dCartesian = dCartesian; - this.dTheta = dTheta; + this.velocity = velocity; } - public add(velocity?: Velocity) { + public add(velocity?: Velocity2D) { if (velocity) { - this.dCartesian.dx += velocity.dCartesian.dx; - this.dCartesian.dy += velocity.dCartesian.dy; - this.dTheta += velocity.dTheta; + this.velocity.dCartesian.dx += velocity.dCartesian.dx; + this.velocity.dCartesian.dy += velocity.dCartesian.dy; + this.velocity.dTheta += velocity.dTheta; } } } diff --git a/engine/components/WallBounded.ts b/engine/components/WallBounded.ts index 5f787e1..c1745a8 100644 --- a/engine/components/WallBounded.ts +++ b/engine/components/WallBounded.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames } from '.'; export class WallBounded extends Component { constructor() { diff --git a/engine/components/index.ts b/engine/components/index.ts index 67f1259..6d7c1e5 100644 --- a/engine/components/index.ts +++ b/engine/components/index.ts @@ -1,15 +1,16 @@ -export * from "./Component"; -export * from "./BoundingBox"; -export * from "./Velocity"; -export * from "./Forces"; -export * from "./Sprite"; -export * from "./FacingDirection"; -export * from "./Jump"; -export * from "./TopCollidable"; -export * from "./Collide"; -export * from "./Control"; -export * from "./WallBounded"; -export * from "./Gravity"; -export * from "./Mass"; -export * from "./Moment"; -export * from "./names"; +export * from './Component'; +export * from './BoundingBox'; +export * from './Velocity'; +export * from './Forces'; +export * from './Sprite'; +export * from './FacingDirection'; +export * from './Jump'; +export * from './TopCollidable'; +export * from './Collide'; +export * from './Control'; +export * from './WallBounded'; +export * from './Gravity'; +export * from './Mass'; +export * from './Moment'; +export * from './NetworkUpdateable'; +export * from './names'; diff --git a/engine/components/names.ts b/engine/components/names.ts index e2ee3d3..97b4edd 100644 --- a/engine/components/names.ts +++ b/engine/components/names.ts @@ -1,15 +1,16 @@ export namespace ComponentNames { - export const Sprite = "Sprite"; - export const BoundingBox = "BoundingBox"; - export const Velocity = "Velocity"; - export const FacingDirection = "FacingDirection"; - export const Control = "Control"; - export const Jump = "Jump"; - export const TopCollidable = "TopCollidable"; - export const Collide = "Collide"; - export const WallBounded = "WallBounded"; - export const Gravity = "Gravity"; - export const Forces = "Forces"; - export const Mass = "Mass"; - export const Moment = "Moment"; + export const Sprite = 'Sprite'; + export const BoundingBox = 'BoundingBox'; + export const Velocity = 'Velocity'; + export const FacingDirection = 'FacingDirection'; + export const Control = 'Control'; + export const Jump = 'Jump'; + export const TopCollidable = 'TopCollidable'; + export const Collide = 'Collide'; + export const WallBounded = 'WallBounded'; + export const Gravity = 'Gravity'; + export const Forces = 'Forces'; + export const Mass = 'Mass'; + export const Moment = 'Moment'; + export const NetworkUpdateable = 'NetworkUpdateable'; } diff --git a/engine/config/assets.ts b/engine/config/assets.ts index 173bab3..289f181 100644 --- a/engine/config/assets.ts +++ b/engine/config/assets.ts @@ -1,10 +1,10 @@ -import type { SpriteSpec } from "./sprites"; -import { SPRITE_SPECS } from "./sprites"; +import type { SpriteSpec } from './sprites'; +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(Array.from(spriteSpec.states.values())), + ...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values())) ); } } @@ -35,8 +35,8 @@ export const loadAssets = () => Promise.all([ ...loadSpritesIntoImageElements( Array.from(SPRITE_SPECS.keys()).map( - (key) => SPRITE_SPECS.get(key) as SpriteSpec, - ), - ), + (key) => SPRITE_SPECS.get(key) as SpriteSpec + ) + ) // TODO: Sound ]); diff --git a/engine/config/constants.ts b/engine/config/constants.ts index 3d536d3..dc98ad0 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -1,34 +1,39 @@ -import { Action } from "../interfaces"; +import { Action } from '../interfaces'; export namespace KeyConstants { export const KeyActions: Record<string, Action> = { a: Action.MOVE_LEFT, - ArrowLeft: Action.MOVE_LEFT, + arrowleft: Action.MOVE_LEFT, + d: Action.MOVE_RIGHT, - ArrowRight: Action.MOVE_RIGHT, + arrowright: Action.MOVE_RIGHT, + w: Action.JUMP, - ArrowUp: Action.JUMP, + arrowup: Action.JUMP, + + ' ': Action.JUMP }; + // value -> [key] from KeyActions export const ActionKeys: Map<Action, string[]> = Object.keys( - KeyActions, + KeyActions ).reduce((acc: Map<Action, string[]>, key) => { - const action = KeyActions[key]; + const action = KeyActions[key.toLowerCase()]; if (acc.has(action)) { - acc.get(action)?.push(key); + acc.get(action)!.push(key); return acc; } acc.set(action, [key]); return acc; - }, new Map<Action, string[]>()); + }, new Map()); } export namespace PhysicsConstants { export const MAX_JUMP_TIME_MS = 150; export const GRAVITY = 0.0075; - export const PLAYER_MOVE_VEL = 1; + export const PLAYER_MOVE_VEL = 0.8; export const PLAYER_JUMP_ACC = -0.008; export const PLAYER_JUMP_INITIAL_VEL = -1; } @@ -36,4 +41,7 @@ export namespace PhysicsConstants { export namespace Miscellaneous { export const WIDTH = 600; export const HEIGHT = 800; + + export const DEFAULT_GRID_WIDTH = 30; + export const DEFAULT_GRID_HEIGHT = 30; } diff --git a/engine/config/index.ts b/engine/config/index.ts index 7a1052a..03b2246 100644 --- a/engine/config/index.ts +++ b/engine/config/index.ts @@ -1,3 +1,3 @@ -export * from "./constants"; -export * from "./assets.ts"; -export * from "./sprites.ts"; +export * from './constants'; +export * from './assets.ts'; +export * from './sprites.ts'; diff --git a/engine/config/sprites.ts b/engine/config/sprites.ts index 1f65c18..e5fcd31 100644 --- a/engine/config/sprites.ts +++ b/engine/config/sprites.ts @@ -1,7 +1,7 @@ export enum Sprites { FLOOR, TRAMPOLINE, - COFFEE, + COFFEE } export interface SpriteSpec { @@ -22,12 +22,12 @@ const floorSpriteSpec = { height: 40, frames: 3, msPerFrame: 125, - states: new Map<number, Partial<SpriteSpec>>(), + states: new Map<number, Partial<SpriteSpec>>() }; [40, 80, 120, 160].forEach((width) => { floorSpriteSpec.states.set(width, { width, - sheet: `/assets/floor_tile_${width}.png`, + sheet: `/assets/floor_tile_${width}.png` }); }); SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec); @@ -37,12 +37,12 @@ const coffeeSpriteSpec = { width: 60, height: 45, frames: 3, - states: new Map<string, Partial<SpriteSpec>>(), + states: new Map<string, Partial<SpriteSpec>>() }; -coffeeSpriteSpec.states.set("LEFT", { - sheet: "/assets/coffee_left.png", +coffeeSpriteSpec.states.set('LEFT', { + sheet: '/assets/coffee_left.png' }); -coffeeSpriteSpec.states.set("RIGHT", { - sheet: "/assets/coffee_right.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 ca8d314..63fb370 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -1,13 +1,17 @@ -import type { Component } from "../components"; +import { EntityNames, Floor, Player } from '.'; +import { type Component } from '../components'; -export abstract class Entity { - private static ID = 0; +const randomId = () => + (performance.now() + Math.random() * 10_000_000).toString(); - public readonly id: number; - public readonly components: Map<string, Component>; +export abstract class Entity { + public id: string; + public components: Map<string, Component>; + public name: string; - constructor() { - this.id = Entity.ID++; + constructor(name: string, id: string = randomId()) { + this.name = name; + this.id = id; this.components = new Map(); } @@ -17,7 +21,7 @@ export abstract class Entity { public getComponent<T extends Component>(name: string): T { if (!this.hasComponent(name)) { - throw new Error("Entity does not have component " + name); + throw new Error('Entity does not have component ' + name); } return this.components.get(name) as T; } @@ -29,4 +33,30 @@ export abstract class Entity { public hasComponent(name: string): boolean { return this.components.has(name); } + + public static from(entityName: string, id: string, args: any): Entity { + let entity: Entity; + + switch (entityName) { + case EntityNames.Player: + const player = new Player(); + player.setFrom(args); + entity = player; + break; + case EntityNames.Floor: + const floor = new Floor(args.floorWidth); + floor.setFrom(args); + entity = floor; + break; + default: + throw new Error('.from() Entity type not implemented: ' + entityName); + } + + entity.id = id; + return entity; + } + + public abstract setFrom(args: Record<string, any>): void; + + public abstract serialize(): Record<string, any>; } diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index 44587e6..b4f48e5 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -1,15 +1,19 @@ -import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config"; -import { BoundingBox, Sprite } from "../components"; -import { TopCollidable } from "../components/TopCollidable"; -import { Entity } from "../entities"; +import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config'; +import { BoundingBox, ComponentNames, Sprite } from '../components'; +import { TopCollidable } from '../components/TopCollidable'; +import { Entity, EntityNames } from '../entities'; export class Floor extends Entity { private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( - Sprites.FLOOR, + Sprites.FLOOR ) as SpriteSpec; + private width: number; + constructor(width: number) { - super(); + super(EntityNames.Floor); + + this.width = width; this.addComponent( new Sprite( @@ -17,17 +21,28 @@ export class Floor extends Entity { { x: 0, y: 0 }, { width, height: Floor.spriteSpec.height }, Floor.spriteSpec.msPerFrame, - Floor.spriteSpec.frames, - ), + Floor.spriteSpec.frames + ) ); + this.addComponent(new TopCollidable()); + } + + public serialize() { + return { + floorWidth: this.width, + boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox) + }; + } + + public setFrom(args: any) { + const { boundingBox } = args; this.addComponent( new BoundingBox( - { x: 300, y: 300 }, - { width, height: Floor.spriteSpec.height }, - ), + boundingBox.center, + boundingBox.dimension, + boundingBox.rotation + ) ); - - this.addComponent(new TopCollidable()); } } diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index 45d7500..4d91c6f 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -1,5 +1,5 @@ -import { Entity } from "."; -import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config"; +import { Entity, EntityNames } from '.'; +import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config'; import { Jump, FacingDirection, @@ -10,32 +10,38 @@ import { WallBounded, Forces, Collide, - Control, Mass, Moment, -} from "../components"; -import { Direction } from "../interfaces"; + ComponentNames, + Control +} from '../components'; +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, + Sprites.COFFEE ) as SpriteSpec; constructor() { - super(); + super(EntityNames.Player); this.addComponent( new BoundingBox( - { x: 300, y: 100 }, + { + x: 0, + y: 0 + }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, - 0, - ), + 0 + ) ); - this.addComponent(new Velocity({ dx: 0, dy: 0 }, 0)); + this.addComponent( + new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }) + ); this.addComponent(new Mass(Player.MASS)); this.addComponent(new Moment(Player.MOI)); @@ -43,7 +49,6 @@ export class Player extends Entity { this.addComponent(new Gravity()); this.addComponent(new Jump()); - this.addComponent(new Control()); this.addComponent(new Collide()); this.addComponent(new WallBounded()); @@ -59,11 +64,41 @@ export class Player extends Entity { { 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)); - this.addComponent(leftSprite); // face Left by default + this.addComponent(leftSprite); // face left by default + } + + public serialize(): Record<string, any> { + return { + control: this.getComponent<Control>(ComponentNames.Control), + boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox), + velocity: this.getComponent<Velocity>(ComponentNames.Velocity), + forces: this.getComponent<Forces>(ComponentNames.Forces) + }; + } + + public setFrom(args: Record<string, any>) { + const { control, velocity, forces, boundingBox } = args; + + let center = boundingBox.center; + + const myCenter = this.getComponent<BoundingBox>( + ComponentNames.BoundingBox + ).center; + const distance = Math.sqrt( + Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2) + ); + if (distance < 30) center = myCenter; + + [ + Object.assign(new Control(control.controllableBy), control), + new Velocity(velocity.velocity), + new Forces(forces.forces), + new BoundingBox(center, boundingBox.dimension, boundingBox.rotation) + ].forEach((component) => this.addComponent(component)); } } diff --git a/engine/entities/index.ts b/engine/entities/index.ts index a921512..8aee83c 100644 --- a/engine/entities/index.ts +++ b/engine/entities/index.ts @@ -1,3 +1,4 @@ -export * from "./Entity"; -export * from "./Floor"; -export * from "./Player"; +export * from './Entity'; +export * from './Floor'; +export * from './Player'; +export * from './names'; diff --git a/engine/entities/names.ts b/engine/entities/names.ts new file mode 100644 index 0000000..cf65f9f --- /dev/null +++ b/engine/entities/names.ts @@ -0,0 +1,4 @@ +export namespace EntityNames { + export const Player = 'Player'; + export const Floor = 'Floor'; +} diff --git a/engine/interfaces/Action.ts b/engine/interfaces/Action.ts index 61c89e1..f0e6a66 100644 --- a/engine/interfaces/Action.ts +++ b/engine/interfaces/Action.ts @@ -1,5 +1,5 @@ export enum Action { MOVE_LEFT, MOVE_RIGHT, - JUMP, + JUMP } diff --git a/engine/interfaces/Direction.ts b/engine/interfaces/Direction.ts index 0bc6ef3..af1c7ac 100644 --- a/engine/interfaces/Direction.ts +++ b/engine/interfaces/Direction.ts @@ -1,6 +1,6 @@ export enum Direction { - UP = "UP", - DOWN = "DOWN", - LEFT = "LEFT", - RIGHT = "RIGHT", + UP = 'UP', + DOWN = 'DOWN', + LEFT = 'LEFT', + RIGHT = 'RIGHT' } diff --git a/engine/interfaces/Draw.ts b/engine/interfaces/Draw.ts index 6561a01..8479fe4 100644 --- a/engine/interfaces/Draw.ts +++ b/engine/interfaces/Draw.ts @@ -1,4 +1,4 @@ -import type { Coord2D, Dimension2D } from "./"; +import type { Coord2D, Dimension2D } from './'; export interface DrawArgs { center: Coord2D; diff --git a/engine/interfaces/Vec2.ts b/engine/interfaces/Vec2.ts index b2bae37..04be4be 100644 --- a/engine/interfaces/Vec2.ts +++ b/engine/interfaces/Vec2.ts @@ -9,8 +9,11 @@ export interface Dimension2D { } export interface Velocity2D { - dx: number; - dy: number; + dCartesian: { + dx: number; + dy: number; + }; + dTheta: number; } export interface Force2D { diff --git a/engine/interfaces/index.ts b/engine/interfaces/index.ts index 8cdf4d8..c2f6896 100644 --- a/engine/interfaces/index.ts +++ b/engine/interfaces/index.ts @@ -1,4 +1,4 @@ -export * from "./Vec2"; -export * from "./Draw"; -export * from "./Direction"; -export * from "./Action"; +export * from './Vec2'; +export * from './Draw'; +export * from './Direction'; +export * from './Action'; diff --git a/engine/network/index.ts b/engine/network/index.ts new file mode 100644 index 0000000..5dc7ece --- /dev/null +++ b/engine/network/index.ts @@ -0,0 +1,37 @@ +export enum MessageType { + NEW_ENTITIES = 'NEW_ENTITIES', + REMOVE_ENTITIES = 'REMOVE_ENTITIES', + UPDATE_ENTITIES = 'UPDATE_ENTITIES', + NEW_INPUT = 'NEW_INPUT', + REMOVE_INPUT = 'REMOVE_INPUT' +} + +export type EntityAddBody = { + entityName: string; + id: string; + args: Record<string, any>; +}; + +export type EntityUpdateBody = { + id: string; + args: Record<string, any>; +}; + +export type Message = { + type: MessageType; + body: any; +}; + +export interface MessageQueueProvider { + getNewMessages(): Message[]; + clearMessages(): void; +} + +export interface MessagePublisher { + addMessage(message: Message): void; + publish(): void; +} + +export interface MessageProcessor { + process(message: Message): void; +} diff --git a/engine/structures/Grid.ts b/engine/structures/Grid.ts new file mode 100644 index 0000000..5f0e053 --- /dev/null +++ b/engine/structures/Grid.ts @@ -0,0 +1,104 @@ +import type { Coord2D, Dimension2D } from '../interfaces'; +import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.'; +import { Miscellaneous } from '../config/constants'; + +export class Grid implements RefreshingCollisionFinderBehavior { + private cellEntities: Map<number, string[]>; + + private gridDimension: Dimension2D; + private cellDimension: Dimension2D; + private topLeft: Coord2D; + + constructor( + gridDimension: Dimension2D = { + width: Miscellaneous.WIDTH, + height: Miscellaneous.HEIGHT + }, + cellDimension: Dimension2D = { + width: Miscellaneous.DEFAULT_GRID_WIDTH, + height: Miscellaneous.DEFAULT_GRID_HEIGHT + }, + 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<string> { + const neighborIds: Set<string> = 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 d1ff3b1..93702d0 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -1,19 +1,21 @@ -import type { Coord2D, Dimension2D } from "../interfaces"; - -interface BoxedEntry { - id: number; - dimension: Dimension2D; - center: Coord2D; -} +import type { Coord2D, Dimension2D } from '../interfaces'; +import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.'; enum Quadrant { I, II, III, - IV, + 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,34 +26,33 @@ 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<Quadrant, QuadTree>(); this.objects = []; this.maxLevels = maxLevels; this.splitThreshold = splitThreshold; - this.level = level ?? 0; + this.level = level; this.topLeft = topLeft; this.dimension = dimension; } - public insert(id: number, dimension: Dimension2D, center: Coord2D): void { - const box: BoxedEntry = { id, center, dimension }; + public insert(boxedEntry: BoxedEntry): void { if (this.hasChildren()) { - this.getQuadrants(box).forEach((quadrant) => { + this.getQuadrants(boxedEntry).forEach((quadrant) => { const quadrantBox = this.children.get(quadrant); - quadrantBox?.insert(id, dimension, center); + quadrantBox!.insert(boxedEntry); }); return; } - this.objects.push({ id, dimension, center }); + this.objects.push(boxedEntry); if ( this.objects.length > this.splitThreshold && @@ -66,22 +67,24 @@ export class QuadTree { public clear(): void { this.objects = []; + if (this.hasChildren()) { this.children.forEach((child) => child.clear()); this.children.clear(); } } - public getNeighborIds(boxedEntry: BoxedEntry): number[] { - const neighbors: number[] = this.objects.map(({ id }) => id); + public getNeighborIds(boxedEntry: BoxedEntry): Set<string> { + const neighbors = new Set<string>( + 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)); }); } @@ -99,9 +102,9 @@ export class QuadTree { [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]] + { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight } + ] + ] as [Quadrant, Coord2D][] ).forEach(([quadrant, pos]) => { this.children.set( quadrant, @@ -110,8 +113,8 @@ export class QuadTree { { width: halfWidth, height: halfHeight }, this.maxLevels, this.splitThreshold, - this.level + 1, - ), + this.level + 1 + ) ); }); } @@ -119,52 +122,48 @@ export class QuadTree { private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] { const treeCenter: Coord2D = { x: this.topLeft.x + this.dimension.width / 2, - y: this.topLeft.y + this.dimension.height / 2, + y: this.topLeft.y + this.dimension.height / 2 }; return ( [ [ Quadrant.I, - (x: number, y: number) => x >= treeCenter.x && y < treeCenter.y, + (x: number, y: number) => x >= treeCenter.x && y < treeCenter.y ], [ Quadrant.II, - (x: number, y: number) => x < treeCenter.x && y < treeCenter.y, + (x: number, y: number) => x < treeCenter.x && y < treeCenter.y ], [ Quadrant.III, - (x: number, y: number) => x < treeCenter.x && y >= treeCenter.y, + (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]] + (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); } private realignObjects(): void { this.objects.forEach((boxedEntry) => { - this.getQuadrants(boxedEntry).forEach((direction) => { - const quadrant = this.children.get(direction); - quadrant?.insert( - boxedEntry.id, - boxedEntry.dimension, - boxedEntry.center, - ); + this.getQuadrants(boxedEntry).forEach((quadrant) => { + const quadrantBox = this.children.get(quadrant); + quadrantBox!.insert(boxedEntry); }); }); @@ -174,4 +173,12 @@ export class QuadTree { private hasChildren() { return this.children && this.children.size > 0; } + + public setTopLeft(topLeft: Coord2D) { + this.topLeft = topLeft; + } + + public setDimension(dimension: Dimension2D) { + this.dimension = dimension; + } } diff --git a/engine/structures/RefreshingCollisionFinderBehavior.ts b/engine/structures/RefreshingCollisionFinderBehavior.ts new file mode 100644 index 0000000..573ddd8 --- /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 { + clear(): void; + insert(boxedEntry: BoxedEntry): void; + getNeighborIds(boxedEntry: BoxedEntry): Set<string>; + setTopLeft(topLeft: Coord2D): void; +} diff --git a/engine/structures/index.ts b/engine/structures/index.ts index 605a82a..679dbd4 100644 --- a/engine/structures/index.ts +++ b/engine/structures/index.ts @@ -1 +1,3 @@ -export * from "./QuadTree"; +export * from './RefreshingCollisionFinderBehavior'; +export * from './QuadTree'; +export * from './Grid'; diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts index 2bba03b..4a838dd 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -1,61 +1,59 @@ -import { SystemNames, System } from "."; +import { SystemNames, System } from '.'; import { Mass, BoundingBox, ComponentNames, Jump, Velocity, - Forces, -} from "../components"; -import { Game } from "../Game"; -import { PhysicsConstants } from "../config"; -import { Entity } from "../entities"; -import type { Dimension2D } from "../interfaces"; -import { QuadTree } from "../structures"; + Forces +} from '../components'; +import { Game } from '../Game'; +import { Miscellaneous, PhysicsConstants } from '../config'; +import { Entity } from '../entities'; +import type { Coord2D, Dimension2D, Velocity2D } from '../interfaces'; +import { BoxedEntry, RefreshingCollisionFinderBehavior } from '../structures'; export class Collision extends System { private static readonly COLLIDABLE_COMPONENT_NAMES = [ ComponentNames.Collide, - ComponentNames.TopCollidable, + 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.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); - }), + game.forEachEntityWithComponent(componentName, (entity) => { + if (!entity.hasComponent(ComponentNames.BoundingBox)) { + return; + } + entitiesToAddToCollisionFinder.push(entity); + }) ); - entitiesToAddToQuadtree.forEach((entity) => { + this.insertEntitiesAndUpdateBounds(entitiesToAddToCollisionFinder); + this.findCollidingEntitiesAndCollide(entitiesToAddToCollisionFinder, game); + } + + private insertEntitiesAndUpdateBounds(entities: Entity[]) { + const collisionFinderInsertions: BoxedEntry[] = []; + + const topLeft: Coord2D = { x: Infinity, y: Infinity }; + const bottomRight: Coord2D = { x: -Infinity, y: -Infinity }; + + entities.forEach((entity) => { const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); let dimension = { ...boundingBox.dimension }; @@ -63,18 +61,43 @@ export class Collision extends System { dimension = boundingBox.getOutscribedBoxDims(); } - this.quadTree.insert(entity.id, dimension, boundingBox.center); + const { center } = boundingBox; + 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.max(bottomRightBoundingBox.y, bottomRight.y); + + collisionFinderInsertions.push({ + id: entity.id, + dimension, + center + }); }); - // find colliding entities and perform collisions - const collidingEntities = this.getCollidingEntities( - entitiesToAddToQuadtree, - game, + // set bounds first + if (entities.length > 0) { + this.collisionFinder.setTopLeft(topLeft); + this.collisionFinder.setDimension({ + width: bottomRight.x - topLeft.x, + height: bottomRight.y - topLeft.y + }); + } + + // then, begin insertions + collisionFinderInsertions.forEach((boxedEntry: BoxedEntry) => + this.collisionFinder.insert(boxedEntry) ); + } + + private findCollidingEntitiesAndCollide(entities: Entity[], game: Game) { + const collidingEntities = this.getCollidingEntities(entities, game); collidingEntities.forEach(([entityAId, entityBId]) => { const [entityA, entityB] = [entityAId, entityBId].map((id) => - game.entities.get(id), + game.entities.get(id) ); if (entityA && entityB) { this.performCollision(entityA, entityB); @@ -84,12 +107,14 @@ export class Collision extends System { 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 = new Velocity(); + let velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }; if (entityA.hasComponent(ComponentNames.Velocity)) { - velocity = entityA.getComponent<Velocity>(ComponentNames.Velocity); + velocity = entityA.getComponent<Velocity>( + ComponentNames.Velocity + ).velocity; } if ( @@ -100,7 +125,7 @@ export class Collision extends System { ) { 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.` ); } @@ -114,7 +139,7 @@ export class Collision extends System { entityA.getComponent<Forces>(ComponentNames.Forces).forces.push({ fCartesian: { fy: F_n, fx: 0 }, - torque: 0, + torque: 0 }); } @@ -132,35 +157,33 @@ export class Collision extends System { private getCollidingEntities( collidableEntities: Entity[], - game: Game, - ): [number, number][] { - const collidingEntityIds: [number, number][] = []; + game: Game + ): [string, string][] { + const collidingEntityIds: [string, string][] = []; for (const entity of collidableEntities) { const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox, + 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; const neighborBoundingBox = neighbor.getComponent<BoundingBox>( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); if (boundingBox.isCollidingWith(neighborBoundingBox)) { collidingEntityIds.push([entity.id, neighborId]); } - }); + } } return collidingEntityIds; @@ -169,11 +192,11 @@ export class Collision extends System { // ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ private getDyToPushOutOfFloor( entityBoundingBox: BoundingBox, - floorBoundingBox: BoundingBox, + floorBoundingBox: BoundingBox ): number { const { dimension: { width, height }, - center: { x }, + center: { x } } = entityBoundingBox; const outScribedRectangle = entityBoundingBox.getOutscribedBoxDims(); @@ -192,7 +215,7 @@ export class Collision extends System { if (x >= floorBoundingBox.center.x) { boundedCollisionX = Math.min( floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2, - clippedX, + clippedX ); return ( outScribedRectangle.height / 2 - @@ -202,7 +225,7 @@ export class Collision extends System { boundedCollisionX = Math.max( floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2, - clippedX, + clippedX ); return ( diff --git a/engine/systems/FacingDirection.ts b/engine/systems/FacingDirection.ts index 4426ab6..01f32cf 100644 --- a/engine/systems/FacingDirection.ts +++ b/engine/systems/FacingDirection.ts @@ -2,10 +2,10 @@ import { ComponentNames, Velocity, FacingDirection as FacingDirectionComponent, - Control, -} from "../components"; -import { Game } from "../Game"; -import { System, SystemNames } from "./"; + Control +} from '../components'; +import { Game } from '../Game'; +import { System, SystemNames } from './'; export class FacingDirection extends System { constructor() { @@ -20,24 +20,27 @@ export class FacingDirection extends System { return; } - const totalVelocity: Velocity = new Velocity(); + const totalVelocityComponent = new Velocity(); const control = entity.getComponent<Control>(ComponentNames.Control); - const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); - totalVelocity.add(velocity); + const velocity = entity.getComponent<Velocity>( + ComponentNames.Velocity + ).velocity; + + totalVelocityComponent.add(velocity); if (control) { - totalVelocity.add(control.controlVelocity); + totalVelocityComponent.add(control.controlVelocityComponent.velocity); } const facingDirection = entity.getComponent<FacingDirectionComponent>( - ComponentNames.FacingDirection, + ComponentNames.FacingDirection ); - if (totalVelocity.dCartesian.dx > 0) { + if (totalVelocityComponent.velocity.dCartesian.dx > 0) { entity.addComponent(facingDirection.facingRightSprite); - } else if (totalVelocity.dCartesian.dx < 0) { + } else if (totalVelocityComponent.velocity.dCartesian.dx < 0) { entity.addComponent(facingDirection.facingLeftSprite); } - }, + } ); } } diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts index 4aa9844..9afd1ab 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -4,30 +4,117 @@ import { ComponentNames, Velocity, Mass, - Control, -} from "../components"; -import { Game } from "../Game"; -import { KeyConstants, PhysicsConstants } from "../config"; -import { Action } from "../interfaces"; -import { System, SystemNames } from "./"; + Control +} from '../components'; +import { Game } from '../Game'; +import { KeyConstants, PhysicsConstants } from '../config'; +import { Action } from '../interfaces'; +import { System, SystemNames } from '.'; +import { MessagePublisher, MessageType } from '../network'; +import { Entity } from '../entities'; export class Input extends System { + public clientId: string; + private keys: Set<string>; private actionTimeStamps: Map<Action, number>; + private messagePublisher?: MessagePublisher; - constructor() { + constructor(clientId: string, messagePublisher?: MessagePublisher) { super(SystemNames.Input); - this.keys = new Set<string>(); - this.actionTimeStamps = new Map<Action, number>(); + this.clientId = clientId; + this.keys = new Set(); + this.actionTimeStamps = new Map(); + + this.messagePublisher = messagePublisher; } public keyPressed(key: string) { this.keys.add(key); + + if (this.messagePublisher) { + this.messagePublisher.addMessage({ + type: MessageType.NEW_INPUT, + body: key + }); + } } public keyReleased(key: string) { this.keys.delete(key); + + if (this.messagePublisher) { + this.messagePublisher.addMessage({ + type: MessageType.REMOVE_INPUT, + body: key + }); + } + } + + public update(_dt: number, game: Game) { + game.forEachEntityWithComponent(ComponentNames.Control, (entity) => + this.handleInput(entity) + ); + } + + public handleInput(entity: Entity) { + const controlComponent = entity.getComponent<Control>( + ComponentNames.Control + ); + controlComponent.isControllable = + controlComponent.controllableBy === this.clientId; + + if (!controlComponent.isControllable) return; + + if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) { + controlComponent.controlVelocityComponent.velocity.dCartesian.dx += + PhysicsConstants.PLAYER_MOVE_VEL; + } + + if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) { + controlComponent.controlVelocityComponent.velocity.dCartesian.dx += + -PhysicsConstants.PLAYER_MOVE_VEL; + } + + if ( + entity.hasComponent(ComponentNames.Jump) && + this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP)) + ) { + this.performJump(entity); + } + } + + private performJump(entity: Entity) { + const velocity = entity.getComponent<Velocity>( + ComponentNames.Velocity + ).velocity; + const jump = entity.getComponent<Jump>(ComponentNames.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) || 0) < + PhysicsConstants.MAX_JUMP_TIME_MS + ) { + const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; + + const jumpForce = { + fCartesian: { + fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, + fx: 0 + }, + torque: 0 + }; + entity + .getComponent<Forces>(ComponentNames.Forces) + ?.forces.push(jumpForce); + } } private hasSomeKey(keys?: string[]): boolean { @@ -36,48 +123,4 @@ export class Input extends System { } return false; } - - 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))) { - control.controlVelocity.dCartesian.dx += - PhysicsConstants.PLAYER_MOVE_VEL; - } - - if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) { - control.controlVelocity.dCartesian.dx += - -PhysicsConstants.PLAYER_MOVE_VEL; - } - - if (entity.hasComponent(ComponentNames.Jump)) { - const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); - const jump = entity.getComponent<Jump>(ComponentNames.Jump); - - 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) || 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/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts new file mode 100644 index 0000000..6d13574 --- /dev/null +++ b/engine/systems/NetworkUpdate.ts @@ -0,0 +1,72 @@ +import { System, SystemNames } from '.'; +import { Game } from '../Game'; +import { ComponentNames } from '../components'; +import { + type MessageQueueProvider, + type MessagePublisher, + type MessageProcessor, + MessageType, + EntityUpdateBody +} from '../network'; + +export class NetworkUpdate extends System { + private queueProvider: MessageQueueProvider; + private publisher: MessagePublisher; + private messageProcessor: MessageProcessor; + + private entityUpdateTimers: Map<string, number>; + + constructor( + queueProvider: MessageQueueProvider, + publisher: MessagePublisher, + messageProcessor: MessageProcessor + ) { + super(SystemNames.NetworkUpdate); + + this.queueProvider = queueProvider; + this.publisher = publisher; + this.messageProcessor = messageProcessor; + + this.entityUpdateTimers = new Map(); + } + + public update(dt: number, game: Game) { + // 1. process new messages + this.queueProvider + .getNewMessages() + .forEach((message) => this.messageProcessor.process(message)); + this.queueProvider.clearMessages(); + + // 2. send entity updates + const updateMessages: EntityUpdateBody[] = []; + game.forEachEntityWithComponent( + ComponentNames.NetworkUpdateable, + (entity) => { + let timer = this.entityUpdateTimers.get(entity.id) ?? dt; + timer -= dt; + this.entityUpdateTimers.set(entity.id, timer); + + if (timer > 0) return; + this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs()); + + if (entity.hasComponent(ComponentNames.NetworkUpdateable)) { + updateMessages.push({ + id: entity.id, + args: entity.serialize() + }); + } + } + ); + this.publisher.addMessage({ + type: MessageType.UPDATE_ENTITIES, + body: updateMessages + }); + + // 3. publish changes + this.publisher.publish(); + } + + private getNextUpdateTimeMs() { + return Math.random() * 70 + 50; + } +} diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts index 38962a6..b5df459 100644 --- a/engine/systems/Physics.ts +++ b/engine/systems/Physics.ts @@ -1,4 +1,4 @@ -import { System, SystemNames } from "."; +import { System, SystemNames } from '.'; import { BoundingBox, ComponentNames, @@ -8,11 +8,11 @@ import { Mass, Jump, Moment, - Control, -} from "../components"; -import { PhysicsConstants } from "../config"; -import type { Force2D } from "../interfaces"; -import { Game } from "../Game"; + Control +} from '../components'; +import { PhysicsConstants } from '../config'; +import type { Force2D, Velocity2D } from '../interfaces'; +import { Game } from '../Game'; export class Physics extends System { constructor() { @@ -23,9 +23,11 @@ export class Physics extends System { 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 velocity = entity.getComponent<Velocity>( + ComponentNames.Velocity + ).velocity; const inertia = entity.getComponent<Moment>( - ComponentNames.Moment, + ComponentNames.Moment ).inertia; // F_g = mg, applied only until terminal velocity is reached @@ -35,9 +37,9 @@ export class Physics extends System { forces.push({ fCartesian: { fy: mass * PhysicsConstants.GRAVITY, - fx: 0, + fx: 0 }, - torque: 0, + torque: 0 }); } } @@ -47,17 +49,17 @@ export class Physics extends System { (accum: Force2D, { fCartesian, torque }: Force2D) => ({ fCartesian: { fx: accum.fCartesian.fx + (fCartesian?.fx ?? 0), - fy: accum.fCartesian.fy + (fCartesian?.fy ?? 0), + fy: accum.fCartesian.fy + (fCartesian?.fy ?? 0) }, - torque: accum.torque + (torque ?? 0), + torque: accum.torque + (torque ?? 0) }), - { fCartesian: { fx: 0, fy: 0 }, torque: 0 }, + { fCartesian: { fx: 0, fy: 0 }, torque: 0 } ); // integrate accelerations const [ddy, ddx] = [ sumOfForces.fCartesian.fy, - sumOfForces.fCartesian.fx, + sumOfForces.fCartesian.fx ].map((x) => x / mass); velocity.dCartesian.dx += ddx * dt; velocity.dCartesian.dy += ddy * dt; @@ -73,30 +75,32 @@ export class Physics extends System { }); game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => { - const velocity: Velocity = new Velocity(); + const velocityComponent: Velocity = new Velocity(); const control = entity.getComponent<Control>(ComponentNames.Control); - velocity.add(entity.getComponent<Velocity>(ComponentNames.Velocity)); + velocityComponent.add( + entity.getComponent<Velocity>(ComponentNames.Velocity).velocity + ); if (control) { - velocity.add(control.controlVelocity); + velocityComponent.add(control.controlVelocityComponent.velocity); } const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); // integrate velocity - boundingBox.center.x += velocity.dCartesian.dx * dt; - boundingBox.center.y += velocity.dCartesian.dy * dt; - boundingBox.rotation += velocity.dTheta * dt; + boundingBox.center.x += velocityComponent.velocity.dCartesian.dx * dt; + boundingBox.center.y += velocityComponent.velocity.dCartesian.dy * dt; + boundingBox.rotation += velocityComponent.velocity.dTheta * dt; boundingBox.rotation = (boundingBox.rotation < 0 ? 360 + boundingBox.rotation : boundingBox.rotation) % 360; // clear the control velocity - if (control) { - control.controlVelocity = new Velocity(); + if (control && control.isControllable) { + control.controlVelocityComponent = new Velocity(); } }); } diff --git a/engine/systems/Render.ts b/engine/systems/Render.ts index 9bb4091..4a4500d 100644 --- a/engine/systems/Render.ts +++ b/engine/systems/Render.ts @@ -1,7 +1,7 @@ -import { System, SystemNames } from "."; -import { BoundingBox, ComponentNames, Sprite } from "../components"; -import { Game } from "../Game"; -import { clamp } from "../utils"; +import { System, SystemNames } from '.'; +import { BoundingBox, ComponentNames, Sprite } from '../components'; +import { Game } from '../Game'; +import { clamp } from '../utils'; export class Render extends System { private ctx: CanvasRenderingContext2D; @@ -19,7 +19,7 @@ export class Render extends System { sprite.update(dt); const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); // don't render if we're outside the screen @@ -27,12 +27,12 @@ export class Render extends System { clamp( boundingBox.center.y, -boundingBox.dimension.height / 2, - this.ctx.canvas.height + 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, + this.ctx.canvas.width + boundingBox.dimension.width / 2 ) != boundingBox.center.x ) { return; @@ -41,7 +41,7 @@ export class Render extends System { const drawArgs = { center: boundingBox.center, dimension: boundingBox.dimension, - rotation: boundingBox.rotation, + rotation: boundingBox.rotation }; sprite.draw(this.ctx, drawArgs); diff --git a/engine/systems/System.ts b/engine/systems/System.ts index 8b00dc5..de41988 100644 --- a/engine/systems/System.ts +++ b/engine/systems/System.ts @@ -1,4 +1,4 @@ -import { Game } from "../Game"; +import { Game } from '../Game'; export abstract class System { public readonly name: string; diff --git a/engine/systems/WallBounds.ts b/engine/systems/WallBounds.ts index a0d4a9c..7da84e4 100644 --- a/engine/systems/WallBounds.ts +++ b/engine/systems/WallBounds.ts @@ -1,28 +1,24 @@ -import { System, SystemNames } from "."; -import { BoundingBox, ComponentNames } from "../components"; -import { Game } from "../Game"; -import type { Entity } from "../entities"; -import { clamp } from "../utils"; +import { System, SystemNames } from '.'; +import { BoundingBox, ComponentNames } from '../components'; +import { Game } from '../Game'; +import { clamp } from '../utils'; +import { Miscellaneous } from '../config'; export class WallBounds extends System { - private screenWidth: number; - - constructor(screenWidth: number) { + constructor() { super(SystemNames.WallBounds); - - this.screenWidth = screenWidth; } public update(_dt: number, game: Game) { game.forEachEntityWithComponent(ComponentNames.WallBounded, (entity) => { const boundingBox = entity.getComponent<BoundingBox>( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); boundingBox.center.x = clamp( boundingBox.center.x, boundingBox.dimension.width / 2, - this.screenWidth - boundingBox.dimension.width / 2, + Miscellaneous.WIDTH - boundingBox.dimension.width / 2 ); }); } diff --git a/engine/systems/index.ts b/engine/systems/index.ts index 6cb6f35..43181e9 100644 --- a/engine/systems/index.ts +++ b/engine/systems/index.ts @@ -1,8 +1,9 @@ -export * from "./names"; -export * from "./System"; -export * from "./Render"; -export * from "./Physics"; -export * from "./Input"; -export * from "./FacingDirection"; -export * from "./Collision"; -export * from "./WallBounds"; +export * from './names'; +export * from './System'; +export * from './Render'; +export * from './Physics'; +export * from './Input'; +export * from './FacingDirection'; +export * from './Collision'; +export * from './WallBounds'; +export * from './NetworkUpdate'; diff --git a/engine/systems/names.ts b/engine/systems/names.ts index 23f31fc..ddf6f19 100644 --- a/engine/systems/names.ts +++ b/engine/systems/names.ts @@ -1,8 +1,9 @@ export namespace SystemNames { - export const Render = "Render"; - export const Physics = "Physics"; - export const FacingDirection = "FacingDirection"; - export const Input = "Input"; - export const Collision = "Collision"; - export const WallBounds = "WallBounds"; + export const Render = 'Render'; + export const Physics = 'Physics'; + export const FacingDirection = 'FacingDirection'; + export const Input = 'Input'; + export const Collision = 'Collision'; + export const WallBounds = 'WallBounds'; + export const NetworkUpdate = 'NetworkUpdate'; } diff --git a/engine/utils/coding.ts b/engine/utils/coding.ts new file mode 100644 index 0000000..3f78889 --- /dev/null +++ b/engine/utils/coding.ts @@ -0,0 +1,27 @@ +const replacer = (_key: any, value: any) => { + if (value instanceof Map) { + return { + dataType: 'Map', + value: Array.from(value.entries()) + }; + } else { + return value; + } +}; + +const reviver = (_key: any, value: any) => { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { + return new Map(value.value); + } + } + return value; +}; + +export const stringify = (obj: any) => { + return JSON.stringify(obj, replacer); +}; + +export const parse = <T>(str: string) => { + return JSON.parse(str, reviver) as unknown as T; +}; diff --git a/engine/utils/dotProduct.ts b/engine/utils/dotProduct.ts index 59f8857..82bcdea 100644 --- a/engine/utils/dotProduct.ts +++ b/engine/utils/dotProduct.ts @@ -1,4 +1,4 @@ -import type { Coord2D } from "../interfaces"; +import type { Coord2D } from '../interfaces'; export const dotProduct = (vector1: Coord2D, vector2: Coord2D): number => vector1.x * vector2.x + vector1.y * vector2.y; diff --git a/engine/utils/index.ts b/engine/utils/index.ts index 82a0d05..65446d1 100644 --- a/engine/utils/index.ts +++ b/engine/utils/index.ts @@ -1,3 +1,4 @@ -export * from "./rotateVector"; -export * from "./dotProduct"; -export * from "./clamp"; +export * from './rotateVector'; +export * from './dotProduct'; +export * from './clamp'; +export * from './coding'; diff --git a/engine/utils/rotateVector.ts b/engine/utils/rotateVector.ts index 82bb54d..221ffb2 100644 --- a/engine/utils/rotateVector.ts +++ b/engine/utils/rotateVector.ts @@ -1,4 +1,4 @@ -import type { Coord2D } from "../interfaces"; +import type { Coord2D } from '../interfaces'; /** * ([[cos(θ), -sin(θ),]) ([x,) @@ -10,6 +10,6 @@ export const rotateVector = (vector: Coord2D, theta: number): Coord2D => { return { x: vector.x * cos - vector.y * sin, - y: vector.x * sin + vector.y * cos, + y: vector.x * sin + vector.y * cos }; }; |