From 98e795029bcc404463ed151ff5255a72498bc641 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 13 Aug 2023 16:47:58 -0600 Subject: [PATCH 01/13] Create network component and system --- engine/components/BoundingBox.ts | 6 ++++-- engine/components/NetworkUpdateable.ts | 7 +++++++ engine/components/index.ts | 1 + engine/components/names.ts | 1 + engine/structures/QuadTree.ts | 10 +++++----- engine/systems/Collision.ts | 6 +++++- engine/systems/Input.ts | 2 +- engine/systems/NetworkUpdate.ts | 10 ++++++++++ engine/systems/index.ts | 1 + engine/systems/names.ts | 1 + 10 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 engine/components/NetworkUpdateable.ts create mode 100644 engine/systems/NetworkUpdate.ts diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index 5e21b2f..19967f7 100644 --- a/engine/components/BoundingBox.ts +++ b/engine/components/BoundingBox.ts @@ -47,8 +47,9 @@ 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 }, ] - .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, @@ -56,9 +57,10 @@ export class BoundingBox extends Component { }); } - 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; diff --git a/engine/components/NetworkUpdateable.ts b/engine/components/NetworkUpdateable.ts new file mode 100644 index 0000000..73ceeba --- /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/index.ts b/engine/components/index.ts index 67f1259..90f4965 100644 --- a/engine/components/index.ts +++ b/engine/components/index.ts @@ -12,4 +12,5 @@ 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..02ee064 100644 --- a/engine/components/names.ts +++ b/engine/components/names.ts @@ -12,4 +12,5 @@ export namespace ComponentNames { export const Forces = "Forces"; export const Mass = "Mass"; export const Moment = "Moment"; + export const NetworkUpdateable = "NetworkUpdateable"; } diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index d1ff3b1..a57c6e7 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -41,17 +41,16 @@ export class QuadTree { 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,6 +65,7 @@ export class QuadTree { public clear(): void { this.objects = []; + if (this.hasChildren()) { this.children.forEach((child) => child.clear()); this.children.clear(); diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts index 2bba03b..1366ef4 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -63,7 +63,11 @@ export class Collision extends System { dimension = boundingBox.getOutscribedBoxDims(); } - this.quadTree.insert(entity.id, dimension, boundingBox.center); + this.quadTree.insert({ + id: entity.id, + dimension, + center: boundingBox.center, + }); }); // find colliding entities and perform collisions diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts index 4aa9844..35d2e1d 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -9,7 +9,7 @@ import { import { Game } from "../Game"; import { KeyConstants, PhysicsConstants } from "../config"; import { Action } from "../interfaces"; -import { System, SystemNames } from "./"; +import { System, SystemNames } from "."; export class Input extends System { private keys: Set; diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts new file mode 100644 index 0000000..dc7be20 --- /dev/null +++ b/engine/systems/NetworkUpdate.ts @@ -0,0 +1,10 @@ +import { System, SystemNames } from "."; +import { Game } from "../Game"; + +export class NetworkUpdate extends System { + constructor() { + super(SystemNames.NetworkUpdate); + } + + public update(_dt: number, _game: Game) {} +} diff --git a/engine/systems/index.ts b/engine/systems/index.ts index 6cb6f35..075fc4e 100644 --- a/engine/systems/index.ts +++ b/engine/systems/index.ts @@ -6,3 +6,4 @@ 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..cf66422 100644 --- a/engine/systems/names.ts +++ b/engine/systems/names.ts @@ -5,4 +5,5 @@ export namespace SystemNames { export const Input = "Input"; export const Collision = "Collision"; export const WallBounds = "WallBounds"; + export const NetworkUpdate = "NetworkUpdate"; } From 2dc3120831fbcd03b635bbad59213ff0bf1f8879 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 13 Aug 2023 17:09:12 -0600 Subject: [PATCH 02/13] refactor velocity a bit for no real reason besides verbosity --- engine/components/Control.ts | 6 +++--- engine/components/Velocity.ts | 18 +++++++++--------- engine/entities/Player.ts | 4 +++- engine/interfaces/Vec2.ts | 7 +++++-- engine/systems/Collision.ts | 8 +++++--- engine/systems/FacingDirection.ts | 15 +++++++++------ engine/systems/Input.ts | 12 ++++++++---- engine/systems/Physics.ts | 22 +++++++++++++--------- 8 files changed, 55 insertions(+), 37 deletions(-) diff --git a/engine/components/Control.ts b/engine/components/Control.ts index 1e782ee..fb7b916 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -1,11 +1,11 @@ import { Component, ComponentNames, Velocity } from "."; export class Control extends Component { - public controlVelocity: Velocity; + public controlVelocityComponent: Velocity; - constructor(controlVelocity: Velocity = new Velocity()) { + constructor(controlVelocityComponent: Velocity = new Velocity()) { super(ComponentNames.Control); - this.controlVelocity = controlVelocity; + this.controlVelocityComponent = controlVelocityComponent; } } diff --git a/engine/components/Velocity.ts b/engine/components/Velocity.ts index 068d8cd..aec0c03 100644 --- a/engine/components/Velocity.ts +++ b/engine/components/Velocity.ts @@ -3,21 +3,21 @@ 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/entities/Player.ts b/engine/entities/Player.ts index 45d7500..eeddd69 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -35,7 +35,9 @@ export class Player extends Entity { ), ); - 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)); 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/systems/Collision.ts b/engine/systems/Collision.ts index 1366ef4..889f85e 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -10,7 +10,7 @@ import { import { Game } from "../Game"; import { PhysicsConstants } from "../config"; import { Entity } from "../entities"; -import type { Dimension2D } from "../interfaces"; +import type { Dimension2D, Velocity2D } from "../interfaces"; import { QuadTree } from "../structures"; export class Collision extends System { @@ -91,9 +91,11 @@ export class Collision extends System { (entity) => entity.getComponent(ComponentNames.BoundingBox), ); - let velocity = new Velocity(); + let velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }; if (entityA.hasComponent(ComponentNames.Velocity)) { - velocity = entityA.getComponent(ComponentNames.Velocity); + velocity = entityA.getComponent( + ComponentNames.Velocity, + ).velocity; } if ( diff --git a/engine/systems/FacingDirection.ts b/engine/systems/FacingDirection.ts index 4426ab6..daf639f 100644 --- a/engine/systems/FacingDirection.ts +++ b/engine/systems/FacingDirection.ts @@ -20,21 +20,24 @@ export class FacingDirection extends System { return; } - const totalVelocity: Velocity = new Velocity(); + const totalVelocityComponent = new Velocity(); const control = entity.getComponent(ComponentNames.Control); - const velocity = entity.getComponent(ComponentNames.Velocity); - totalVelocity.add(velocity); + const velocity = entity.getComponent( + ComponentNames.Velocity, + ).velocity; + + totalVelocityComponent.add(velocity); if (control) { - totalVelocity.add(control.controlVelocity); + totalVelocityComponent.add(control.controlVelocityComponent.velocity); } const facingDirection = entity.getComponent( 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 35d2e1d..d9b7133 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -39,20 +39,24 @@ export class Input extends System { public update(_dt: number, game: Game) { game.forEachEntityWithComponent(ComponentNames.Control, (entity) => { - const control = entity.getComponent(ComponentNames.Control); + const controlComponent = entity.getComponent( + ComponentNames.Control, + ); if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) { - control.controlVelocity.dCartesian.dx += + controlComponent.controlVelocityComponent.velocity.dCartesian.dx += PhysicsConstants.PLAYER_MOVE_VEL; } if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) { - control.controlVelocity.dCartesian.dx += + controlComponent.controlVelocityComponent.velocity.dCartesian.dx += -PhysicsConstants.PLAYER_MOVE_VEL; } if (entity.hasComponent(ComponentNames.Jump)) { - const velocity = entity.getComponent(ComponentNames.Velocity); + const velocity = entity.getComponent( + ComponentNames.Velocity, + ).velocity; const jump = entity.getComponent(ComponentNames.Jump); if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) { diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts index 38962a6..e324c97 100644 --- a/engine/systems/Physics.ts +++ b/engine/systems/Physics.ts @@ -11,7 +11,7 @@ import { Control, } from "../components"; import { PhysicsConstants } from "../config"; -import type { Force2D } from "../interfaces"; +import type { Force2D, Velocity2D } from "../interfaces"; import { Game } from "../Game"; export class Physics extends System { @@ -23,7 +23,9 @@ export class Physics extends System { game.forEachEntityWithComponent(ComponentNames.Forces, (entity) => { const mass = entity.getComponent(ComponentNames.Mass).mass; const forces = entity.getComponent(ComponentNames.Forces).forces; - const velocity = entity.getComponent(ComponentNames.Velocity); + const velocity = entity.getComponent( + ComponentNames.Velocity, + ).velocity; const inertia = entity.getComponent( ComponentNames.Moment, ).inertia; @@ -73,12 +75,14 @@ export class Physics extends System { }); game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => { - const velocity: Velocity = new Velocity(); + const velocityComponent: Velocity = new Velocity(); const control = entity.getComponent(ComponentNames.Control); - velocity.add(entity.getComponent(ComponentNames.Velocity)); + velocityComponent.add( + entity.getComponent(ComponentNames.Velocity).velocity, + ); if (control) { - velocity.add(control.controlVelocity); + velocityComponent.add(control.controlVelocityComponent.velocity); } const boundingBox = entity.getComponent( @@ -86,9 +90,9 @@ export class Physics extends System { ); // 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 @@ -96,7 +100,7 @@ export class Physics extends System { // clear the control velocity if (control) { - control.controlVelocity = new Velocity(); + control.controlVelocityComponent = new Velocity(); } }); } From 732fe6f4811cc082bf938fed2d28c1f9c8bbd1f6 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Tue, 15 Aug 2023 18:30:19 -0600 Subject: [PATCH 03/13] generate uuids for entities; scaffolding for a server --- client/src/JumpStorm.ts | 57 +++++++++++++++++++- engine/Game.ts | 10 ++-- engine/components/NetworkUpdateable.ts | 8 ++- engine/entities/Entity.ts | 6 +-- engine/structures/QuadTree.ts | 12 ++--- engine/systems/NetworkUpdate.ts | 36 ++++++++++++- server/src/server.ts | 75 +++++++++++++++++--------- server/tsconfig.json | 33 +++++++----- 8 files changed, 176 insertions(+), 61 deletions(-) diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index bd48483..8075cc8 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -7,15 +7,63 @@ import { Physics, Input, Collision, + MessageQueueProvider, + MessagePublisher, + NetworkUpdate, } from "@engine/systems"; +class ClientSocketMessageQueueProvider implements MessageQueueProvider { + private socket: WebSocket; + private messages: any[]; + + constructor(socket: WebSocket) { + this.socket = socket; + this.messages = []; + + this.socket.addEventListener("message", (e) => { + console.log(e); + }); + } + + getNewMessages() { + return this.messages; + } + + clearMessages() { + this.messages = []; + } +} + +class ClientSocketMessagePublisher implements MessagePublisher { + private socket: WebSocket; + private messages: any[]; + + constructor(socket: WebSocket) { + this.socket = socket; + this.messages = []; + + this.socket.addEventListener("message", (e) => { + console.log(e); + }); + } + + addMessage(_message: any) {} + + publish() {} +} + export class JumpStorm { private game: Game; - private socket: WebSocket; constructor(ctx: CanvasRenderingContext2D) { this.game = new Game(); - this.socket = new WebSocket("ws://localhost:8080"); + + const socket = new WebSocket("ws://localhost:8080"); + const clientSocketMessageQueueProvider = + new ClientSocketMessageQueueProvider(socket); + const clientSocketMessagePublisher = new ClientSocketMessagePublisher( + socket, + ); [ this.createInputSystem(), @@ -23,6 +71,10 @@ export class JumpStorm { new Physics(), new Collision(), new WallBounds(ctx.canvas.width), + new NetworkUpdate( + clientSocketMessageQueueProvider, + clientSocketMessagePublisher, + ), new Render(ctx), ].forEach((system) => this.game.addSystem(system)); @@ -49,6 +101,7 @@ export class JumpStorm { inputSystem.keyPressed(e.key); } }); + window.addEventListener("keyup", (e) => inputSystem.keyReleased(e.key)); return inputSystem; diff --git a/engine/Game.ts b/engine/Game.ts index 07d06e8..8dc5db7 100644 --- a/engine/Game.ts +++ b/engine/Game.ts @@ -7,9 +7,9 @@ export class Game { private running: boolean; private lastTimeStamp: number; - public entities: Map; + public entities: Map; public systems: Map; - public componentEntities: Map>; + public componentEntities: Map>; constructor() { this.lastTimeStamp = performance.now(); @@ -29,11 +29,11 @@ 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); } @@ -75,7 +75,7 @@ export class Game { if (!this.componentEntities.has(component.name)) { this.componentEntities.set( component.name, - new Set([entity.id]), + new Set([entity.id]), ); return; } diff --git a/engine/components/NetworkUpdateable.ts b/engine/components/NetworkUpdateable.ts index 73ceeba..980b064 100644 --- a/engine/components/NetworkUpdateable.ts +++ b/engine/components/NetworkUpdateable.ts @@ -1,7 +1,13 @@ import { Component, ComponentNames } from "."; export class NetworkUpdateable extends Component { - constructor() { + public isPublish: boolean; + public isSubscribe: boolean; + + constructor(isPublish: boolean, isSubscribe: boolean) { super(ComponentNames.NetworkUpdateable); + + this.isPublish = isPublish; + this.isSubscribe = isSubscribe; } } diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts index ca8d314..b2d875d 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -1,13 +1,11 @@ import type { Component } from "../components"; export abstract class Entity { - private static ID = 0; - - public readonly id: number; + public readonly id: string; public readonly components: Map; constructor() { - this.id = Entity.ID++; + this.id = crypto.randomUUID(); this.components = new Map(); } diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index a57c6e7..49afdad 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -1,7 +1,7 @@ import type { Coord2D, Dimension2D } from "../interfaces"; interface BoxedEntry { - id: number; + id: string; dimension: Dimension2D; center: Coord2D; } @@ -72,8 +72,8 @@ export class QuadTree { } } - public getNeighborIds(boxedEntry: BoxedEntry): number[] { - const neighbors: number[] = this.objects.map(({ id }) => id); + public getNeighborIds(boxedEntry: BoxedEntry): string[] { + const neighbors: string[] = this.objects.map(({ id }) => id); if (this.hasChildren()) { this.getQuadrants(boxedEntry).forEach((quadrant) => { @@ -160,11 +160,7 @@ export class QuadTree { this.objects.forEach((boxedEntry) => { this.getQuadrants(boxedEntry).forEach((direction) => { const quadrant = this.children.get(direction); - quadrant?.insert( - boxedEntry.id, - boxedEntry.dimension, - boxedEntry.center, - ); + quadrant?.insert(boxedEntry); }); }); diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts index dc7be20..6f8acb9 100644 --- a/engine/systems/NetworkUpdate.ts +++ b/engine/systems/NetworkUpdate.ts @@ -1,10 +1,42 @@ import { System, SystemNames } from "."; import { Game } from "../Game"; +import { ComponentNames, NetworkUpdateable } from "../components"; + +export interface MessageQueueProvider { + getNewMessages(): any[]; + clearMessages(): void; +} + +export interface MessagePublisher { + addMessage(message: any): void; + publish(): void; +} export class NetworkUpdate extends System { - constructor() { + private queueProvider: MessageQueueProvider; + private publisher: MessagePublisher; + + constructor( + queueProvider: MessageQueueProvider, + publisher: MessagePublisher, + ) { super(SystemNames.NetworkUpdate); + + this.queueProvider = queueProvider; + this.publisher = publisher; } - public update(_dt: number, _game: Game) {} + public update(_dt: number, game: Game) { + const messages = this.queueProvider.getNewMessages(); + this.queueProvider.clearMessages(); + + game.forEachEntityWithComponent( + ComponentNames.NetworkUpdateable, + (entity) => { + const networkUpdateComponent = entity.getComponent( + ComponentNames.NetworkUpdateable, + ); + }, + ); + } } diff --git a/server/src/server.ts b/server/src/server.ts index 74d901b..9a73f11 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,37 +1,60 @@ import { Game } from "../../engine/Game"; import { Floor, Player } from "../../engine/entities"; -import { WallBounds, Physics, Collision } from "../../engine/systems"; +import { + WallBounds, + Physics, + Collision, + MessageQueueProvider, + MessagePublisher, +} from "../../engine/systems"; import { Miscellaneous } from "../../engine/config"; const TICK_RATE = 60 / 1000; -const game = new Game(); +class Server { + private server: any; + private game: Game; -[ - new Physics(), - new Collision({ width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }), - new WallBounds(Miscellaneous.WIDTH), -].forEach((system) => game.addSystem(system)); + constructor() { + this.game = new Game(); -[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); + [ + new Physics(), + new Collision({ + width: Miscellaneous.WIDTH, + height: Miscellaneous.HEIGHT, + }), + new WallBounds(Miscellaneous.WIDTH), + ].forEach((system) => this.game.addSystem(system)); -game.start(); -setInterval(() => { - game.doGameLoop(performance.now()); -}, TICK_RATE); + [new Floor(160), new Player()].forEach((entity) => + this.game.addEntity(entity), + ); -const server = Bun.serve<>({ - port: 8080, - fetch(req, server) { - server.upgrade(req, { - data: {}, + this.game.start(); + setInterval(() => { + this.game.doGameLoop(performance.now()); + }, TICK_RATE); + + this.server = Bun.serve({ + websocket: { + open(ws) { + ws.subscribe("the-group-chat"); + ws.publish("the-group-chat", msg); + }, + message(ws, message) { + // this is a group chat + // so the server re-broadcasts incoming message to everyone + ws.publish("the-group-chat", `${ws.data.username}: ${message}`); + }, + close(ws) { + const msg = `${ws.data.username} has left the chat`; + ws.unsubscribe("the-group-chat"); + ws.publish("the-group-chat", msg); + }, + }, }); - }, - websocket: { - // handler called when a message is received - async message(ws, message) { - console.log(`Received ${message}`); - }, - }, -}); -console.log(`Listening on localhost:${server.port}`); + } +} + +new Server(); diff --git a/server/tsconfig.json b/server/tsconfig.json index 29f8aa0..2567512 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,21 +1,28 @@ { "compilerOptions": { - "lib": ["ESNext"], + // add Bun type definitions + "types": ["bun-types"], + + // enable latest features + "lib": ["esnext"], "module": "esnext", "target": "esnext", + + // if TS 5.x+ "moduleResolution": "bundler", - "moduleDetection": "force", - "allowImportingTsExtensions": true, - "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "preserve", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, "noEmit": true, - "types": [ - "bun-types" // add Bun global - ] + "allowImportingTsExtensions": true, + "moduleDetection": "force", + // if TS 4.x or earlier + "moduleResolution": "nodenext", + + "jsx": "react-jsx", // support JSX + "allowJs": true, // allow importing `.js` from `.ts` + "esModuleInterop": true, // allow default imports for CommonJS modules + + // best practices + "strict": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true } } From 1c28e10b860056d85cc07e5a834c4a54eac14563 Mon Sep 17 00:00:00 2001 From: Lizzy Hunt Date: Wed, 16 Aug 2023 15:41:35 -0600 Subject: [PATCH 04/13] refactor collision methods, rescaffold server --- engine/structures/QuadTree.ts | 10 ++++- engine/systems/Collision.ts | 76 +++++++++++++++++++++++++---------- server/src/server.ts | 66 +++++++++++++----------------- server/tsconfig.json | 2 - 4 files changed, 91 insertions(+), 63 deletions(-) diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index 49afdad..e6e29fa 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -1,6 +1,6 @@ import type { Coord2D, Dimension2D } from "../interfaces"; -interface BoxedEntry { +export interface BoxedEntry { id: string; dimension: Dimension2D; center: Coord2D; @@ -170,4 +170,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/systems/Collision.ts b/engine/systems/Collision.ts index 889f85e..e05aba0 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -10,8 +10,8 @@ import { import { Game } from "../Game"; import { PhysicsConstants } from "../config"; import { Entity } from "../entities"; -import type { Dimension2D, Velocity2D } from "../interfaces"; -import { QuadTree } from "../structures"; +import type { Coord2D, Dimension2D, Velocity2D } from "../interfaces"; +import { QuadTree, BoxedEntry } from "../structures"; export class Collision extends System { private static readonly COLLIDABLE_COMPONENT_NAMES = [ @@ -41,19 +41,26 @@ export class Collision extends System { const entitiesToAddToQuadtree: Entity[] = []; Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) => - game.componentEntities.get(componentName), - ).forEach( - (entityIds?: Set) => - 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; + } + entitiesToAddToQuadtree.push(entity); + }), ); - entitiesToAddToQuadtree.forEach((entity) => { + this.insertEntitiesInQuadTreeAndUpdateBounds(entitiesToAddToQuadtree); + + this.findCollidingEntitiesAndCollide(entitiesToAddToQuadtree, game); + } + + private insertEntitiesInQuadTreeAndUpdateBounds(entities: Entity[]) { + const topLeft: Coord2D = { x: Infinity, y: Infinity }; + const bottomRight: Coord2D = { x: -Infinity, y: -Infinity }; + + const quadTreeInsertions: BoxedEntry[] = []; + + entities.forEach((entity) => { const boundingBox = entity.getComponent( ComponentNames.BoundingBox, ); @@ -63,18 +70,45 @@ export class Collision extends System { dimension = boundingBox.getOutscribedBoxDims(); } - this.quadTree.insert({ + const { center } = boundingBox; + const topLeftBoundingBox = { + x: center.x - dimension.width / 2, + y: center.y - dimension.height / 2, + }; + const bottomRightBoundingBox = { + x: center.x + dimension.width / 2, + y: center.y + dimension.height / 2, + }; + + topLeft.x = Math.min(topLeftBoundingBox.x, topLeft.x); + topLeft.y = Math.min(topLeftBoundingBox.y, topLeft.y); + bottomRight.x = Math.max(bottomRightBoundingBox.x, bottomRight.x); + bottomRight.y = Math.min(bottomRightBoundingBox.y, bottomRight.y); + + quadTreeInsertions.push({ id: entity.id, dimension, - center: boundingBox.center, + center, }); }); - // find colliding entities and perform collisions - const collidingEntities = this.getCollidingEntities( - entitiesToAddToQuadtree, - game, + // set bounds first + if (entities.length > 0) { + this.quadTree.setTopLeft(topLeft); + this.quadTree.setDimension({ + width: bottomRight.x - topLeft.x, + height: bottomRight.y - topLeft.y, + }); + } + + // then, begin insertions + quadTreeInsertions.forEach((boxedEntry: BoxedEntry) => + this.quadTree.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) => @@ -139,8 +173,8 @@ export class Collision extends System { private getCollidingEntities( collidableEntities: Entity[], game: Game, - ): [number, number][] { - const collidingEntityIds: [number, number][] = []; + ): [string, string][] { + const collidingEntityIds: [string, string][] = []; for (const entity of collidableEntities) { const boundingBox = entity.getComponent( diff --git a/server/src/server.ts b/server/src/server.ts index 9a73f11..d169f7d 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -11,50 +11,38 @@ import { Miscellaneous } from "../../engine/config"; const TICK_RATE = 60 / 1000; -class Server { - private server: any; - private game: Game; +const game = new Game(); - constructor() { - this.game = new Game(); +[new Physics(), new Collision(), new WallBounds(Miscellaneous.WIDTH)].forEach( + (system) => game.addSystem(system), +); - [ - new Physics(), - new Collision({ - width: Miscellaneous.WIDTH, - height: Miscellaneous.HEIGHT, - }), - new WallBounds(Miscellaneous.WIDTH), - ].forEach((system) => this.game.addSystem(system)); +[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); - [new Floor(160), new Player()].forEach((entity) => - this.game.addEntity(entity), - ); +game.start(); - this.game.start(); - setInterval(() => { - this.game.doGameLoop(performance.now()); - }, TICK_RATE); +setInterval(() => { + game.doGameLoop(performance.now()); +}, TICK_RATE); - this.server = Bun.serve({ - websocket: { - open(ws) { - ws.subscribe("the-group-chat"); - ws.publish("the-group-chat", msg); - }, - message(ws, message) { - // this is a group chat - // so the server re-broadcasts incoming message to everyone - ws.publish("the-group-chat", `${ws.data.username}: ${message}`); - }, - close(ws) { - const msg = `${ws.data.username} has left the chat`; - ws.unsubscribe("the-group-chat"); - ws.publish("the-group-chat", msg); - }, +const server = Bun.serve({ + port: 8080, + fetch(req, server) { + const sessionId = Math.floor(Math.random() * 1e10).toString(); + + server.upgrade(req, { + headers: { + "Set-Cookie": `SessionId=${sessionId}`, }, }); - } -} + }, + websocket: { + open(ws) {}, + message(ws, message) { + console.log(message); + }, + close(ws) {}, + }, +}); -new Server(); +console.log(`Listening on ${server.hostname}:${server.port}`); diff --git a/server/tsconfig.json b/server/tsconfig.json index 2567512..e39b364 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -13,8 +13,6 @@ "noEmit": true, "allowImportingTsExtensions": true, "moduleDetection": "force", - // if TS 4.x or earlier - "moduleResolution": "nodenext", "jsx": "react-jsx", // support JSX "allowJs": true, // allow importing `.js` from `.ts` From 432ce5428f357f31ae090d55c5183b4eccd5a37c Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Thu, 17 Aug 2023 22:42:09 -0600 Subject: [PATCH 05/13] increase collision performance _heavily_ --- client/src/JumpStorm.ts | 9 +- engine/components/BoundingBox.ts | 33 +++++++ engine/entities/Floor.ts | 5 +- engine/entities/Player.ts | 7 +- engine/structures/Grid.ts | 97 +++++++++++++++++++ engine/structures/QuadTree.ts | 41 ++++---- .../RefreshingCollisionFinderBehavior.ts | 14 +++ engine/structures/index.ts | 2 + engine/systems/Collision.ts | 73 ++++++-------- 9 files changed, 213 insertions(+), 68 deletions(-) create mode 100644 engine/structures/Grid.ts create mode 100644 engine/structures/RefreshingCollisionFinderBehavior.ts diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index 8075cc8..008ba13 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -1,5 +1,7 @@ import { Floor, Player } from "@engine/entities"; import { Game } from "@engine/Game"; +import { Grid } from "@engine/structures"; +import { Miscellaneous } from "@engine/config"; import { WallBounds, FacingDirection, @@ -65,11 +67,16 @@ export class JumpStorm { socket, ); + const grid = new Grid( + { width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }, + { width: 30, height: 30 }, + ); + [ this.createInputSystem(), new FacingDirection(), new Physics(), - new Collision(), + new Collision(grid), new WallBounds(ctx.canvas.width), new NetworkUpdate( clientSocketMessageQueueProvider, diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index 19967f7..26b404d 100644 --- a/engine/components/BoundingBox.ts +++ b/engine/components/BoundingBox.ts @@ -17,6 +17,25 @@ export class BoundingBox extends Component { // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem public isCollidingWith(box: BoundingBox): boolean { + if (this.rotation == 0 && box.rotation == 0) { + const thisTopLeft = this.getTopLeft(); + const thisBottomRight = this.getBottomRight(); + + const thatTopLeft = box.getTopLeft(); + const thatBottomRight = box.getBottomRight(); + + if ( + thisBottomRight.x <= thatTopLeft.x || + thisTopLeft.x >= thatBottomRight.x || + thisBottomRight.y <= thatTopLeft.y || + thisTopLeft.y >= thatBottomRight.y + ) { + return false; + } + + return true; + } + const boxes = [this.getVertices(), box.getVertices()]; for (const poly of boxes) { for (let i = 0; i < poly.length; i++) { @@ -83,4 +102,18 @@ export class BoundingBox extends Component { height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads)), }; } + + public getTopLeft(): Coord2D { + return { + x: this.center.x - this.dimension.width / 2, + y: this.center.y - this.dimension.height / 2, + }; + } + + public getBottomRight(): Coord2D { + return { + x: this.center.x + this.dimension.width / 2, + y: this.center.y + this.dimension.height / 2, + }; + } } diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index 44587e6..b204ce0 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -23,7 +23,10 @@ export class Floor extends Entity { this.addComponent( new BoundingBox( - { x: 300, y: 300 }, + { + x: 300, + y: 300, + }, { width, height: Floor.spriteSpec.height }, ), ); diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index eeddd69..377e0ca 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -18,7 +18,7 @@ import { Direction } from "../interfaces"; export class Player extends Entity { private static MASS: number = 10; - private static MOI: number = 1000; + private static MOI: number = 100; private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( Sprites.COFFEE, @@ -29,7 +29,10 @@ export class Player extends Entity { this.addComponent( new BoundingBox( - { x: 300, y: 100 }, + { + x: 300, + y: 100, + }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, 0, ), diff --git a/engine/structures/Grid.ts b/engine/structures/Grid.ts new file mode 100644 index 0000000..d359909 --- /dev/null +++ b/engine/structures/Grid.ts @@ -0,0 +1,97 @@ +import type { Coord2D, Dimension2D } from "../interfaces"; +import type { RefreshingCollisionFinderBehavior } from "."; + +export class Grid implements RefreshingCollisionFinderBehavior { + private cellEntities: Map; + + private gridDimension: Dimension2D; + private cellDimension: Dimension2D; + private topLeft: Coord2D; + + constructor( + gridDimension: Dimension2D, + cellDimension: Dimension2D, + topLeft = { x: 0, y: 0 }, + ) { + this.gridDimension = gridDimension; + this.cellDimension = cellDimension; + this.topLeft = topLeft; + + this.cellEntities = new Map(); + } + + public insert(boxedEntry: BoxedEntry) { + this.getOverlappingCells(boxedEntry).forEach((gridIdx) => { + if (!this.cellEntities.has(gridIdx)) { + this.cellEntities.set(gridIdx, []); + } + this.cellEntities.get(gridIdx).push(boxedEntry.id); + }); + } + + public getNeighborIds(boxedEntry: BoxedEntry): Set { + const neighborIds: Set = new Set(); + this.getOverlappingCells(boxedEntry).forEach((gridIdx) => { + if (this.cellEntities.has(gridIdx)) { + this.cellEntities.get(gridIdx).forEach((id) => neighborIds.add(id)); + } + }); + return neighborIds; + } + + public clear() { + this.cellEntities.clear(); + } + + public setTopLeft(topLeft: Coord2D) { + this.topLeft = topLeft; + } + + public setDimension(dimension: Dimension2D) { + this.gridDimension = dimension; + } + + public setCellDimension(cellDimension: Dimension2D) { + this.cellDimension = cellDimension; + } + + private getOverlappingCells(boxedEntry: BoxedEntry): number[] { + const { center, dimension } = boxedEntry; + const yBoxes = Math.ceil( + this.gridDimension.height / this.cellDimension.height, + ); + const xBoxes = Math.ceil( + this.gridDimension.width / this.cellDimension.width, + ); + + const translated: Coord2D = { + y: center.y - this.topLeft.y, + x: center.x - this.topLeft.x, + }; + + const topLeftBox = { + x: Math.floor( + (translated.x - dimension.width / 2) / this.cellDimension.width, + ), + y: Math.floor( + (translated.y - dimension.height / 2) / this.cellDimension.height, + ), + }; + const bottomRightBox = { + x: Math.floor( + (translated.x + dimension.width / 2) / this.cellDimension.width, + ), + y: Math.floor( + (translated.y + dimension.height / 2) / this.cellDimension.height, + ), + }; + + const cells: number[] = []; + + for (let y = topLeftBox.y; y <= bottomRightBox.y; ++y) + for (let x = topLeftBox.x; x <= bottomRightBox.x; ++x) + cells.push(yBoxes * y + x); + + return cells; + } +} diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index e6e29fa..90227a0 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -1,10 +1,5 @@ import type { Coord2D, Dimension2D } from "../interfaces"; - -export interface BoxedEntry { - id: string; - dimension: Dimension2D; - center: Coord2D; -} +import type { BoxedEntry, RefreshingCollisionFinderBehavior } from "."; enum Quadrant { I, @@ -13,7 +8,14 @@ enum Quadrant { IV, } -export class QuadTree { +/* + unused due to performance problems. here anyways, in case it _really_ is necessary at some point + (and to justify the amount of time i spent here). +*/ +export class QuadTree implements RefreshingCollisionFinderBehavior { + private static readonly QUADTREE_MAX_LEVELS = 3; + private static readonly QUADTREE_SPLIT_THRESHOLD = 2000; + private maxLevels: number; private splitThreshold: number; private level: number; @@ -24,18 +26,18 @@ export class QuadTree { private objects: BoxedEntry[]; constructor( - topLeft: Coord2D, + topLeft: Coord2D = { x: 0, y: 0 }, dimension: Dimension2D, - maxLevels: number, - splitThreshold: number, - level?: number, + maxLevels: number = QuadTree.QUADTREE_MAX_LEVELS, + splitThreshold: number = QuadTree.QUADTREE_SPLIT_THRESHOLD, + level: number = 0, ) { this.children = new Map(); this.objects = []; this.maxLevels = maxLevels; this.splitThreshold = splitThreshold; - this.level = level ?? 0; + this.level = level; this.topLeft = topLeft; this.dimension = dimension; @@ -45,7 +47,7 @@ export class QuadTree { if (this.hasChildren()) { this.getQuadrants(boxedEntry).forEach((quadrant) => { const quadrantBox = this.children.get(quadrant); - quadrantBox?.insert(boxedEntry); + quadrantBox!.insert(boxedEntry); }); return; } @@ -73,15 +75,16 @@ export class QuadTree { } public getNeighborIds(boxedEntry: BoxedEntry): string[] { - const neighbors: string[] = this.objects.map(({ id }) => id); + const neighbors = new Set( + this.objects.map(({ id }) => id).filter((id) => id != boxedEntry.id), + ); if (this.hasChildren()) { this.getQuadrants(boxedEntry).forEach((quadrant) => { const quadrantBox = this.children.get(quadrant); - quadrantBox ?.getNeighborIds(boxedEntry) - .forEach((id) => neighbors.push(id)); + .forEach((id) => neighbors.add(id)); }); } @@ -158,9 +161,9 @@ export class QuadTree { private realignObjects(): void { this.objects.forEach((boxedEntry) => { - this.getQuadrants(boxedEntry).forEach((direction) => { - const quadrant = this.children.get(direction); - quadrant?.insert(boxedEntry); + this.getQuadrants(boxedEntry).forEach((quadrant) => { + const quadrantBox = this.children.get(quadrant); + quadrantBox!.insert(boxedEntry); }); }); diff --git a/engine/structures/RefreshingCollisionFinderBehavior.ts b/engine/structures/RefreshingCollisionFinderBehavior.ts new file mode 100644 index 0000000..21d690d --- /dev/null +++ b/engine/structures/RefreshingCollisionFinderBehavior.ts @@ -0,0 +1,14 @@ +import type { Coord2D, Dimension2D } from "../interfaces"; + +export interface BoxedEntry { + id: string; + dimension: Dimension2D; + center: Coord2D; +} + +export interface RefreshingCollisionFinderBehavior { + public clear(): void; + public insert(boxedEntry: BoxedEntry): void; + public getNeighborIds(boxedEntry: BoxedEntry): Set; + public setTopLeft(topLeft: Coord2d): void; +} diff --git a/engine/structures/index.ts b/engine/structures/index.ts index 605a82a..49a84af 100644 --- a/engine/structures/index.ts +++ b/engine/structures/index.ts @@ -1 +1,3 @@ +export * from "./RefreshingCollisionFinderBehavior"; export * from "./QuadTree"; +export * from "./Grid"; diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts index e05aba0..2dd920e 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -8,58 +8,49 @@ import { Forces, } from "../components"; import { Game } from "../Game"; -import { PhysicsConstants } from "../config"; +import { Miscellaneous, PhysicsConstants } from "../config"; import { Entity } from "../entities"; import type { Coord2D, Dimension2D, Velocity2D } from "../interfaces"; -import { QuadTree, BoxedEntry } from "../structures"; +import { BoxedEntry, RefreshingCollisionFinderBehavior } from "../structures"; export class Collision extends System { private static readonly COLLIDABLE_COMPONENT_NAMES = [ ComponentNames.Collide, ComponentNames.TopCollidable, ]; - private static readonly QUADTREE_MAX_LEVELS = 10; - private static readonly QUADTREE_SPLIT_THRESHOLD = 10; - private quadTree: QuadTree; + private collisionFinder: RefreshingCollisionFinderBehavior; - constructor(screenDimensions: Dimension2D) { + constructor(refreshingCollisionFinder: RefreshingCollisionFinderBehavior) { super(SystemNames.Collision); - this.quadTree = new QuadTree( - { x: 0, y: 0 }, - screenDimensions, - Collision.QUADTREE_MAX_LEVELS, - Collision.QUADTREE_SPLIT_THRESHOLD, - ); + this.collisionFinder = refreshingCollisionFinder; } public update(_dt: number, game: Game) { - // rebuild the quadtree - this.quadTree.clear(); + this.collisionFinder.clear(); - const entitiesToAddToQuadtree: Entity[] = []; + const entitiesToAddToCollisionFinder: Entity[] = []; Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) => game.forEachEntityWithComponent(componentName, (entity) => { if (!entity.hasComponent(ComponentNames.BoundingBox)) { return; } - entitiesToAddToQuadtree.push(entity); + entitiesToAddToCollisionFinder.push(entity); }), ); - this.insertEntitiesInQuadTreeAndUpdateBounds(entitiesToAddToQuadtree); - - this.findCollidingEntitiesAndCollide(entitiesToAddToQuadtree, game); + this.insertEntitiesAndUpdateBounds(entitiesToAddToCollisionFinder); + this.findCollidingEntitiesAndCollide(entitiesToAddToCollisionFinder, game); } - private insertEntitiesInQuadTreeAndUpdateBounds(entities: Entity[]) { + private insertEntitiesAndUpdateBounds(entities: Entity[]) { + const collisionFinderInsertions: BoxedEntry[] = []; + const topLeft: Coord2D = { x: Infinity, y: Infinity }; const bottomRight: Coord2D = { x: -Infinity, y: -Infinity }; - const quadTreeInsertions: BoxedEntry[] = []; - entities.forEach((entity) => { const boundingBox = entity.getComponent( ComponentNames.BoundingBox, @@ -71,21 +62,15 @@ export class Collision extends System { } const { center } = boundingBox; - const topLeftBoundingBox = { - x: center.x - dimension.width / 2, - y: center.y - dimension.height / 2, - }; - const bottomRightBoundingBox = { - x: center.x + dimension.width / 2, - y: center.y + dimension.height / 2, - }; + const topLeftBoundingBox = boundingBox.getTopLeft(); + const bottomRightBoundingBox = boundingBox.getBottomRight(); topLeft.x = Math.min(topLeftBoundingBox.x, topLeft.x); topLeft.y = Math.min(topLeftBoundingBox.y, topLeft.y); bottomRight.x = Math.max(bottomRightBoundingBox.x, bottomRight.x); - bottomRight.y = Math.min(bottomRightBoundingBox.y, bottomRight.y); + bottomRight.y = Math.max(bottomRightBoundingBox.y, bottomRight.y); - quadTreeInsertions.push({ + collisionFinderInsertions.push({ id: entity.id, dimension, center, @@ -94,16 +79,16 @@ export class Collision extends System { // set bounds first if (entities.length > 0) { - this.quadTree.setTopLeft(topLeft); - this.quadTree.setDimension({ + this.collisionFinder.setTopLeft(topLeft); + this.collisionFinder.setDimension({ width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y, }); } // then, begin insertions - quadTreeInsertions.forEach((boxedEntry: BoxedEntry) => - this.quadTree.insert(boxedEntry), + collisionFinderInsertions.forEach((boxedEntry: BoxedEntry) => + this.collisionFinder.insert(boxedEntry), ); } @@ -181,15 +166,13 @@ export class Collision extends System { ComponentNames.BoundingBox, ); - const neighborIds = this.quadTree - .getNeighborIds({ - id: entity.id, - dimension: boundingBox.dimension, - center: boundingBox.center, - }) - .filter((neighborId) => neighborId != entity.id); + const neighborIds = this.collisionFinder.getNeighborIds({ + id: entity.id, + dimension: boundingBox.dimension, + center: boundingBox.center, + }); - neighborIds.forEach((neighborId) => { + for (const neighborId of neighborIds) { const neighbor = game.getEntity(neighborId); if (!neighbor) return; @@ -200,7 +183,7 @@ export class Collision extends System { if (boundingBox.isCollidingWith(neighborBoundingBox)) { collidingEntityIds.push([entity.id, neighborId]); } - }); + } } return collidingEntityIds; From 8fce5a5f2530496e1390763364c01392a1a63640 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Mon, 21 Aug 2023 14:19:02 -0600 Subject: [PATCH 06/13] fix some ts errors --- engine/structures/Grid.ts | 20 ++++++++++---------- engine/structures/QuadTree.ts | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/engine/structures/Grid.ts b/engine/structures/Grid.ts index d359909..836aaf4 100644 --- a/engine/structures/Grid.ts +++ b/engine/structures/Grid.ts @@ -1,5 +1,5 @@ import type { Coord2D, Dimension2D } from "../interfaces"; -import type { RefreshingCollisionFinderBehavior } from "."; +import type { BoxedEntry, RefreshingCollisionFinderBehavior } from "."; export class Grid implements RefreshingCollisionFinderBehavior { private cellEntities: Map; @@ -11,7 +11,7 @@ export class Grid implements RefreshingCollisionFinderBehavior { constructor( gridDimension: Dimension2D, cellDimension: Dimension2D, - topLeft = { x: 0, y: 0 }, + topLeft = { x: 0, y: 0 } ) { this.gridDimension = gridDimension; this.cellDimension = cellDimension; @@ -25,7 +25,7 @@ export class Grid implements RefreshingCollisionFinderBehavior { if (!this.cellEntities.has(gridIdx)) { this.cellEntities.set(gridIdx, []); } - this.cellEntities.get(gridIdx).push(boxedEntry.id); + this.cellEntities.get(gridIdx)!.push(boxedEntry.id); }); } @@ -33,7 +33,7 @@ export class Grid implements RefreshingCollisionFinderBehavior { const neighborIds: Set = new Set(); this.getOverlappingCells(boxedEntry).forEach((gridIdx) => { if (this.cellEntities.has(gridIdx)) { - this.cellEntities.get(gridIdx).forEach((id) => neighborIds.add(id)); + this.cellEntities.get(gridIdx)!.forEach((id) => neighborIds.add(id)); } }); return neighborIds; @@ -58,10 +58,10 @@ export class Grid implements RefreshingCollisionFinderBehavior { private getOverlappingCells(boxedEntry: BoxedEntry): number[] { const { center, dimension } = boxedEntry; const yBoxes = Math.ceil( - this.gridDimension.height / this.cellDimension.height, + this.gridDimension.height / this.cellDimension.height ); const xBoxes = Math.ceil( - this.gridDimension.width / this.cellDimension.width, + this.gridDimension.width / this.cellDimension.width ); const translated: Coord2D = { @@ -71,18 +71,18 @@ export class Grid implements RefreshingCollisionFinderBehavior { const topLeftBox = { x: Math.floor( - (translated.x - dimension.width / 2) / this.cellDimension.width, + (translated.x - dimension.width / 2) / this.cellDimension.width ), y: Math.floor( - (translated.y - dimension.height / 2) / this.cellDimension.height, + (translated.y - dimension.height / 2) / this.cellDimension.height ), }; const bottomRightBox = { x: Math.floor( - (translated.x + dimension.width / 2) / this.cellDimension.width, + (translated.x + dimension.width / 2) / this.cellDimension.width ), y: Math.floor( - (translated.y + dimension.height / 2) / this.cellDimension.height, + (translated.y + dimension.height / 2) / this.cellDimension.height ), }; diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index 90227a0..1ab2d1d 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -30,7 +30,7 @@ export class QuadTree implements RefreshingCollisionFinderBehavior { dimension: Dimension2D, maxLevels: number = QuadTree.QUADTREE_MAX_LEVELS, splitThreshold: number = QuadTree.QUADTREE_SPLIT_THRESHOLD, - level: number = 0, + level: number = 0 ) { this.children = new Map(); this.objects = []; @@ -74,9 +74,9 @@ export class QuadTree implements RefreshingCollisionFinderBehavior { } } - public getNeighborIds(boxedEntry: BoxedEntry): string[] { + public getNeighborIds(boxedEntry: BoxedEntry): Set { const neighbors = new Set( - this.objects.map(({ id }) => id).filter((id) => id != boxedEntry.id), + this.objects.map(({ id }) => id).filter((id) => id != boxedEntry.id) ); if (this.hasChildren()) { @@ -104,7 +104,7 @@ export class QuadTree implements RefreshingCollisionFinderBehavior { Quadrant.IV, { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }, ], - ] as [[Quadrant, Coord2D]] + ] as [Quadrant, Coord2D][] ).forEach(([quadrant, pos]) => { this.children.set( quadrant, @@ -113,8 +113,8 @@ export class QuadTree implements RefreshingCollisionFinderBehavior { { width: halfWidth, height: halfHeight }, this.maxLevels, this.splitThreshold, - this.level + 1, - ), + this.level + 1 + ) ); }); } @@ -143,18 +143,18 @@ export class QuadTree implements RefreshingCollisionFinderBehavior { Quadrant.IV, (x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y, ], - ] as [[Quadrant, (x: number, y: number) => boolean]] + ] 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); } From b786fe1e723b7cf905cdd7e525375dfe96241a21 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Mon, 21 Aug 2023 14:50:09 -0600 Subject: [PATCH 07/13] add default arguments to grid; add grid to server gl --- client/src/JumpStorm.ts | 8 ++++---- engine/config/constants.ts | 5 ++++- engine/structures/Grid.ts | 11 +++++++++-- server/bun.lockb | Bin 1650 -> 1270 bytes server/src/server.ts | 21 +++++++++++++-------- server/tsconfig.json | 14 +++++++++++++- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index 008ba13..e094a6f 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -64,12 +64,12 @@ export class JumpStorm { const clientSocketMessageQueueProvider = new ClientSocketMessageQueueProvider(socket); const clientSocketMessagePublisher = new ClientSocketMessagePublisher( - socket, + socket ); const grid = new Grid( { width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }, - { width: 30, height: 30 }, + { width: 30, height: 30 } ); [ @@ -80,13 +80,13 @@ export class JumpStorm { new WallBounds(ctx.canvas.width), new NetworkUpdate( clientSocketMessageQueueProvider, - clientSocketMessagePublisher, + clientSocketMessagePublisher ), new Render(ctx), ].forEach((system) => this.game.addSystem(system)); [new Floor(160), new Player()].forEach((entity) => - this.game.addEntity(entity), + this.game.addEntity(entity) ); } diff --git a/engine/config/constants.ts b/engine/config/constants.ts index 3d536d3..b3c3f62 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -11,7 +11,7 @@ export namespace KeyConstants { }; export const ActionKeys: Map = Object.keys( - KeyActions, + KeyActions ).reduce((acc: Map, key) => { const action = KeyActions[key]; @@ -36,4 +36,7 @@ export namespace PhysicsConstants { export namespace Miscellaneous { export const WIDTH = 600; export const HEIGHT = 800; + + export const DEFAULT_GRID_WIDTH = 40; + export const DEFAULT_GRID_HEIGHT = 40; } diff --git a/engine/structures/Grid.ts b/engine/structures/Grid.ts index 836aaf4..6e8c0cb 100644 --- a/engine/structures/Grid.ts +++ b/engine/structures/Grid.ts @@ -1,5 +1,6 @@ import type { Coord2D, Dimension2D } from "../interfaces"; import type { BoxedEntry, RefreshingCollisionFinderBehavior } from "."; +import { Miscellaneous } from "../config/constants"; export class Grid implements RefreshingCollisionFinderBehavior { private cellEntities: Map; @@ -9,8 +10,14 @@ export class Grid implements RefreshingCollisionFinderBehavior { private topLeft: Coord2D; constructor( - gridDimension: Dimension2D, - cellDimension: Dimension2D, + 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; diff --git a/server/bun.lockb b/server/bun.lockb index 7f8b5ce2234046c9a802d015751d375ab8402e83..28b67ce2c2466108dc6314e3bd59d4aace865af0 100755 GIT binary patch delta 328 zcmeyw^Nn+Yo~F%_aPPl4-)AL%$P=vpR)1*Uk1$n{TXE-fKd#7h+pTyrjfDXWm?nnH z8&@$wI1CKMsYPX}MGOpFKmkq$h6Zm7AJ#^fqHWc0($97Xy;fpm;QAsC)XFwlkWpT7 z1ymbIH5l-(eEzSVfdOWm@MKFy>&XU;EP^l*kSY+HY4S|QlF6w|-jm<3222)VHsU$} zRdR)CawD^Y)W81_0Mft+BA7u00|N{5rr=!#vrKMQd^m zivi~|sL->?4a}mGpRnj~2|&Fb$1+)pRYAC=@uIj2O4%=S_NNiJcU2LLZP@s^zz4jxcz>Y|szpLBy*Qt34qOgU?k(2IAD9*DK}It$lvEa^ z7AF^F7L+8F=IMfYx%nxjIjOpdIhl#Y86_nJ#a8S VuZzOf1v@~mB)tl^{>ip1I{^ZQv#9_8 diff --git a/server/src/server.ts b/server/src/server.ts index d169f7d..18829e4 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,21 +1,24 @@ -import { Game } from "../../engine/Game"; -import { Floor, Player } from "../../engine/entities"; +import { Game } from "@engine/Game"; +import { Floor, Player } from "@engine/entities"; import { WallBounds, Physics, Collision, MessageQueueProvider, MessagePublisher, -} from "../../engine/systems"; -import { Miscellaneous } from "../../engine/config"; +} from "@engine/systems"; +import { Grid } from "@engine/structures"; +import { Miscellaneous } from "@engine/config"; const TICK_RATE = 60 / 1000; const game = new Game(); -[new Physics(), new Collision(), new WallBounds(Miscellaneous.WIDTH)].forEach( - (system) => game.addSystem(system), -); +[ + new Physics(), + new Collision(new Grid()), + new WallBounds(Miscellaneous.WIDTH), +].forEach((system) => game.addSystem(system)); [new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); @@ -27,7 +30,7 @@ setInterval(() => { const server = Bun.serve({ port: 8080, - fetch(req, server) { + fetch: async (req, server): Promise => { const sessionId = Math.floor(Math.random() * 1e10).toString(); server.upgrade(req, { @@ -35,6 +38,8 @@ const server = Bun.serve({ "Set-Cookie": `SessionId=${sessionId}`, }, }); + + return "200 OK"; }, websocket: { open(ws) {}, diff --git a/server/tsconfig.json b/server/tsconfig.json index e39b364..8cc9ad3 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -21,6 +21,18 @@ // best practices "strict": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + "skipLibCheck": true, + + // engine path + "paths": { + "@engine/*": ["../engine/*"], + "@engine/components": ["../engine/components"], + "@engine/config": ["../engine/config"], + "@engine/entities": ["../engine/entities"], + "@engine/interfaces": ["../engine/interfaces"], + "@engine/structures": ["../engine/structures"], + "@engine/systems": ["../engine/systems"], + "@engine/utils": ["../engine/utils"], + } } } From d64ffb5016119e54f0e20d05ae8ac9c96955d9d5 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Mon, 21 Aug 2023 17:22:23 -0600 Subject: [PATCH 08/13] very basic server messages - need to figure shit out still --- client/src/JumpStorm.ts | 12 +--- engine/components/Control.ts | 5 +- engine/config/constants.ts | 13 ++-- engine/entities/Entity.ts | 4 +- engine/entities/Player.ts | 12 ++-- engine/systems/NetworkUpdate.ts | 7 ++- server/src/server.ts | 102 +++++++++++++++++++++++++------- 7 files changed, 106 insertions(+), 49 deletions(-) diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index e094a6f..ae99b8e 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -1,7 +1,5 @@ -import { Floor, Player } from "@engine/entities"; import { Game } from "@engine/Game"; import { Grid } from "@engine/structures"; -import { Miscellaneous } from "@engine/config"; import { WallBounds, FacingDirection, @@ -61,16 +59,14 @@ export class JumpStorm { this.game = new Game(); const socket = new WebSocket("ws://localhost:8080"); + setInterval(() => socket.send(JSON.stringify({ x: 1 })), 1_000); const clientSocketMessageQueueProvider = new ClientSocketMessageQueueProvider(socket); const clientSocketMessagePublisher = new ClientSocketMessagePublisher( socket ); - const grid = new Grid( - { width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }, - { width: 30, height: 30 } - ); + const grid = new Grid(); [ this.createInputSystem(), @@ -84,10 +80,6 @@ export class JumpStorm { ), new Render(ctx), ].forEach((system) => this.game.addSystem(system)); - - [new Floor(160), new Player()].forEach((entity) => - this.game.addEntity(entity) - ); } public play() { diff --git a/engine/components/Control.ts b/engine/components/Control.ts index fb7b916..a3621b0 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -3,7 +3,10 @@ import { Component, ComponentNames, Velocity } from "."; export class Control extends Component { public controlVelocityComponent: Velocity; - constructor(controlVelocityComponent: Velocity = new Velocity()) { + constructor( + controlVelocityComponent: Velocity = new Velocity(), + controllableBy: string + ) { super(ComponentNames.Control); this.controlVelocityComponent = controlVelocityComponent; diff --git a/engine/config/constants.ts b/engine/config/constants.ts index b3c3f62..fa3f81b 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -4,25 +4,28 @@ export namespace KeyConstants { export const KeyActions: Record = { a: Action.MOVE_LEFT, ArrowLeft: Action.MOVE_LEFT, + d: Action.MOVE_RIGHT, ArrowRight: Action.MOVE_RIGHT, + w: Action.JUMP, ArrowUp: Action.JUMP, }; + // value -> [key] from KeyActions export const ActionKeys: Map = Object.keys( KeyActions ).reduce((acc: Map, key) => { const action = KeyActions[key]; if (acc.has(action)) { - acc.get(action)?.push(key); + acc.get(action)!.push(key); return acc; } acc.set(action, [key]); return acc; - }, new Map()); + }, new Map()); } export namespace PhysicsConstants { @@ -37,6 +40,8 @@ export namespace Miscellaneous { export const WIDTH = 600; export const HEIGHT = 800; - export const DEFAULT_GRID_WIDTH = 40; - export const DEFAULT_GRID_HEIGHT = 40; + export const DEFAULT_GRID_WIDTH = 30; + export const DEFAULT_GRID_HEIGHT = 30; + + export const SERVER_TICK_RATE = 5 / 100; } diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts index b2d875d..4e9df78 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -4,8 +4,8 @@ export abstract class Entity { public readonly id: string; public readonly components: Map; - constructor() { - this.id = crypto.randomUUID(); + constructor(id: string = crypto.randomUUID()) { + this.id = id; this.components = new Map(); } diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index 377e0ca..03fa69b 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -21,7 +21,7 @@ export class Player extends Entity { private static MOI: number = 100; private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( - Sprites.COFFEE, + Sprites.COFFEE ) as SpriteSpec; constructor() { @@ -34,12 +34,12 @@ export class Player extends Entity { y: 100, }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, - 0, - ), + 0 + ) ); this.addComponent( - new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }), + new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }) ); this.addComponent(new Mass(Player.MASS)); @@ -64,8 +64,8 @@ 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)); diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts index 6f8acb9..cdd6de7 100644 --- a/engine/systems/NetworkUpdate.ts +++ b/engine/systems/NetworkUpdate.ts @@ -18,7 +18,7 @@ export class NetworkUpdate extends System { constructor( queueProvider: MessageQueueProvider, - publisher: MessagePublisher, + publisher: MessagePublisher ) { super(SystemNames.NetworkUpdate); @@ -28,15 +28,16 @@ export class NetworkUpdate extends System { public update(_dt: number, game: Game) { const messages = this.queueProvider.getNewMessages(); + if (messages.length) console.log(messages); this.queueProvider.clearMessages(); game.forEachEntityWithComponent( ComponentNames.NetworkUpdateable, (entity) => { const networkUpdateComponent = entity.getComponent( - ComponentNames.NetworkUpdateable, + ComponentNames.NetworkUpdateable ); - }, + } ); } } diff --git a/server/src/server.ts b/server/src/server.ts index 18829e4..713d3ed 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -4,20 +4,96 @@ import { WallBounds, Physics, Collision, + NetworkUpdate, MessageQueueProvider, MessagePublisher, } from "@engine/systems"; import { Grid } from "@engine/structures"; import { Miscellaneous } from "@engine/config"; +import { Server } from "bun"; -const TICK_RATE = 60 / 1000; +class ServerSocketMessageReceiver implements MessageQueueProvider { + private messages: any[]; + + constructor() { + this.messages = []; + } + + addMessage(message: any) { + this.messages.push(message); + } + + getNewMessages() { + return this.messages; + } + + clearMessages() { + this.messages = []; + } +} + +class ServerSocketMessagePublisher implements MessagePublisher { + private server: Server; + private messages: any[]; + + constructor(server: Server) { + this.server = server; + this.messages = []; + } + + addMessage(_message: any) {} + + publish() {} +} const game = new Game(); +const messageReceiver = new ServerSocketMessageReceiver(); + +const server = Bun.serve<{ sessionId: string }>({ + port: 8080, + fetch: async (req, server): Promise => { + const sessionId = crypto.randomUUID(); + + server.upgrade(req, { + headers: { + "Set-Cookie": `SessionId=${sessionId}`, + }, + data: { + sessionId, + }, + }); + + return sessionId; + }, + websocket: { + open(ws) { + const { sessionId } = ws.data; + + if (sessionControllableEntities.has(sessionId)) { + return; + } + + const player = new Player(); + game.addEntity(player); + + sessionControllableEntities.set(sessionId, new Set(player.id)); + }, + message(ws, message) { + console.log(JSON.parse(message)); + messageReceiver.addMessage(message); + }, + close(ws) {}, + }, +}); + +const messagePublisher = new ServerSocketMessagePublisher(server); + [ new Physics(), new Collision(new Grid()), new WallBounds(Miscellaneous.WIDTH), + new NetworkUpdate(messageReceiver, messagePublisher), ].forEach((system) => game.addSystem(system)); [new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); @@ -26,28 +102,8 @@ game.start(); setInterval(() => { game.doGameLoop(performance.now()); -}, TICK_RATE); +}, Miscellaneous.SERVER_TICK_RATE); -const server = Bun.serve({ - port: 8080, - fetch: async (req, server): Promise => { - const sessionId = Math.floor(Math.random() * 1e10).toString(); - - server.upgrade(req, { - headers: { - "Set-Cookie": `SessionId=${sessionId}`, - }, - }); - - return "200 OK"; - }, - websocket: { - open(ws) {}, - message(ws, message) { - console.log(message); - }, - close(ws) {}, - }, -}); +const sessionControllableEntities: Map> = new Map(); console.log(`Listening on ${server.hostname}:${server.port}`); From dec7b614d895a1b507137e4a96a8999ff63aa179 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Wed, 23 Aug 2023 19:44:59 -0600 Subject: [PATCH 09/13] holy fuck we actually got somewhere --- client/src/JumpStorm.ts | 99 +++++++++++---- client/src/components/GameCanvas.svelte | 17 +-- client/tsconfig.json | 5 +- client/vite.config.ts | 10 ++ engine/Game.ts | 4 +- engine/components/Control.ts | 4 +- engine/config/constants.ts | 4 +- engine/entities/Entity.ts | 18 ++- engine/entities/Floor.ts | 4 +- engine/entities/Player.ts | 20 +-- engine/entities/index.ts | 7 +- engine/entities/names.ts | 4 + engine/network/index.ts | 29 +++++ engine/systems/Input.ts | 5 +- engine/systems/NetworkUpdate.ts | 31 ++--- engine/utils/coding.ts | 27 ++++ engine/utils/index.ts | 1 + server/src/server.ts | 160 +++++++++++++++++------- server/tsconfig.json | 15 +-- tsconfig.engine.json | 15 +++ 20 files changed, 346 insertions(+), 133 deletions(-) create mode 100644 engine/entities/names.ts create mode 100644 engine/network/index.ts create mode 100644 engine/utils/coding.ts create mode 100644 tsconfig.engine.json diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index ae99b8e..01cc8d8 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -1,4 +1,5 @@ import { Game } from "@engine/Game"; +import { Entity } from "@engine/entities"; import { Grid } from "@engine/structures"; import { WallBounds, @@ -7,67 +8,116 @@ import { Physics, Input, Collision, - MessageQueueProvider, - MessagePublisher, NetworkUpdate, } from "@engine/systems"; +import { + type MessageQueueProvider, + type MessagePublisher, + type MessageProcessor, + type Message, + type EntityAddBody, + MessageType, +} from "@engine/network"; +import { stringify, parse } from "@engine/utils"; + +class ClientMessageProcessor implements MessageProcessor { + private game: Game; + + constructor(game: Game) { + this.game = game; + } + + public process(message: Message) { + switch (message.type) { + case MessageType.NEW_ENTITY: + const entityAddBody = message.body as unknown as EntityAddBody; + this.game.addEntity( + Entity.from(entityAddBody.entityName, entityAddBody.args), + ); + break; + } + + console.log(message); + } +} class ClientSocketMessageQueueProvider implements MessageQueueProvider { private socket: WebSocket; - private messages: any[]; + private messages: Message[]; constructor(socket: WebSocket) { this.socket = socket; this.messages = []; this.socket.addEventListener("message", (e) => { - console.log(e); + const message = parse(e.data); + this.messages.push(message); }); } - getNewMessages() { + public getNewMessages() { return this.messages; } - clearMessages() { + public clearMessages() { this.messages = []; } } class ClientSocketMessagePublisher implements MessagePublisher { private socket: WebSocket; - private messages: any[]; + private messages: Message[]; constructor(socket: WebSocket) { this.socket = socket; this.messages = []; - - this.socket.addEventListener("message", (e) => { - console.log(e); - }); } - addMessage(_message: any) {} + public addMessage(message: Message) { + this.messages.push(message); + } - publish() {} + public publish() { + this.messages.forEach((message: Message) => + this.socket.send(stringify(message)), + ); + } } export class JumpStorm { private game: Game; + private clientId: string; - constructor(ctx: CanvasRenderingContext2D) { - this.game = new Game(); + constructor(game: Game) { + this.game = game; + } - const socket = new WebSocket("ws://localhost:8080"); - setInterval(() => socket.send(JSON.stringify({ x: 1 })), 1_000); - const clientSocketMessageQueueProvider = - new ClientSocketMessageQueueProvider(socket); - const clientSocketMessagePublisher = new ClientSocketMessagePublisher( - socket - ); + public async init( + ctx: CanvasRenderingContext2D, + httpMethod: string, + wsMethod: string, + host: string, + ) { + await fetch(`${httpMethod}://${host}/assign`) + .then((resp) => { + if (resp.ok) { + return resp.text(); + } + throw resp; + }) + .then((cookie) => { + this.clientId = cookie; + }); const grid = new Grid(); + const socket = new WebSocket(`${wsMethod}://${host}/game`); + const clientSocketMessageQueueProvider = + new ClientSocketMessageQueueProvider(socket); + const clientSocketMessagePublisher = new ClientSocketMessagePublisher( + socket, + ); + const clientMessageProcessor = new ClientMessageProcessor(this.game); [ this.createInputSystem(), new FacingDirection(), @@ -76,7 +126,8 @@ export class JumpStorm { new WallBounds(ctx.canvas.width), new NetworkUpdate( clientSocketMessageQueueProvider, - clientSocketMessagePublisher + clientSocketMessagePublisher, + clientMessageProcessor, ), new Render(ctx), ].forEach((system) => this.game.addSystem(system)); @@ -93,7 +144,7 @@ export class JumpStorm { } private createInputSystem(): Input { - const inputSystem = new Input(); + const inputSystem = new Input(this.clientId); window.addEventListener("keydown", (e) => { if (!e.repeat) { diff --git a/client/src/components/GameCanvas.svelte b/client/src/components/GameCanvas.svelte index ae8c1b0..ed16f33 100644 --- a/client/src/components/GameCanvas.svelte +++ b/client/src/components/GameCanvas.svelte @@ -1,6 +1,7 @@ diff --git a/client/tsconfig.json b/client/tsconfig.json index 781d1b3..fadebb0 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@tsconfig/svelte/tsconfig.json", + "extends": ["@tsconfig/svelte/tsconfig.json", "../tsconfig.engine.json"], "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, @@ -24,8 +24,5 @@ "src/**/*.js", "src/**/*.svelte" ], - "paths": { - "@engine/*": ["../engine/*"] - }, "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/client/vite.config.ts b/client/vite.config.ts index 0307338..cdf1ab1 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -4,6 +4,16 @@ import { fileURLToPath, URL } from "node:url"; // https://vitejs.dev/config/ export default defineConfig({ + server: { + proxy: { + "/api": { + target: "http://localhost:8080", + ws: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, + cors: true, plugins: [svelte()], resolve: { alias: { diff --git a/engine/Game.ts b/engine/Game.ts index 8dc5db7..301c8df 100644 --- a/engine/Game.ts +++ b/engine/Game.ts @@ -60,7 +60,7 @@ export class Game { return this.systems.get(name); } - public doGameLoop = (timeStamp: number) => { + public doGameLoop(timeStamp: number) { if (!this.running) { return; } @@ -86,5 +86,5 @@ export class Game { this.systemOrder.forEach((systemName) => { this.systems.get(systemName)?.update(dt, this); }); - }; + } } diff --git a/engine/components/Control.ts b/engine/components/Control.ts index a3621b0..a8dae59 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -2,13 +2,15 @@ import { Component, ComponentNames, Velocity } from "."; export class Control extends Component { public controlVelocityComponent: Velocity; + public controllableBy: string; constructor( + controllableBy: string, controlVelocityComponent: Velocity = new Velocity(), - controllableBy: string ) { super(ComponentNames.Control); + this.controllableBy = controllableBy; this.controlVelocityComponent = controlVelocityComponent; } } diff --git a/engine/config/constants.ts b/engine/config/constants.ts index fa3f81b..e93986b 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -14,7 +14,7 @@ export namespace KeyConstants { // value -> [key] from KeyActions export const ActionKeys: Map = Object.keys( - KeyActions + KeyActions, ).reduce((acc: Map, key) => { const action = KeyActions[key]; @@ -42,6 +42,4 @@ export namespace Miscellaneous { export const DEFAULT_GRID_WIDTH = 30; export const DEFAULT_GRID_HEIGHT = 30; - - export const SERVER_TICK_RATE = 5 / 100; } diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts index 4e9df78..88982cb 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -1,10 +1,13 @@ +import { EntityNames, Player } from "."; import type { Component } from "../components"; export abstract class Entity { - public readonly id: string; - public readonly components: Map; + public id: string; + public components: Map; + public name: string; - constructor(id: string = crypto.randomUUID()) { + constructor(name: string, id: string = crypto.randomUUID()) { + this.name = name; this.id = id; this.components = new Map(); } @@ -27,4 +30,13 @@ export abstract class Entity { public hasComponent(name: string): boolean { return this.components.has(name); } + + static from(entityName: string, args: any): Entity { + switch (entityName) { + case EntityNames.Player: + return new Player(args.playerId); + default: + throw new Error(".from() Entity type not implemented: " + entityName); + } + } } diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index b204ce0..6cfc276 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -1,7 +1,7 @@ import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config"; import { BoundingBox, Sprite } from "../components"; import { TopCollidable } from "../components/TopCollidable"; -import { Entity } from "../entities"; +import { Entity, EntityNames } from "../entities"; export class Floor extends Entity { private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( @@ -9,7 +9,7 @@ export class Floor extends Entity { ) as SpriteSpec; constructor(width: number) { - super(); + super(EntityNames.Floor); this.addComponent( new Sprite( diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index 03fa69b..cfe4dd2 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -1,4 +1,4 @@ -import { Entity } from "."; +import { Entity, EntityNames } from "."; import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config"; import { Jump, @@ -21,11 +21,11 @@ export class Player extends Entity { private static MOI: number = 100; private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( - Sprites.COFFEE + Sprites.COFFEE, ) as SpriteSpec; - constructor() { - super(); + constructor(playerId: string) { + super(EntityNames.Player); this.addComponent( new BoundingBox( @@ -34,12 +34,12 @@ export class Player extends Entity { y: 100, }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, - 0 - ) + 0, + ), ); this.addComponent( - new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }) + new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }), ); this.addComponent(new Mass(Player.MASS)); @@ -48,7 +48,7 @@ export class Player extends Entity { this.addComponent(new Gravity()); this.addComponent(new Jump()); - this.addComponent(new Control()); + this.addComponent(new Control(playerId)); this.addComponent(new Collide()); this.addComponent(new WallBounded()); @@ -64,8 +64,8 @@ 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)); diff --git a/engine/entities/index.ts b/engine/entities/index.ts index a921512..dd3dba9 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..21594c8 --- /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/network/index.ts b/engine/network/index.ts new file mode 100644 index 0000000..1726ffc --- /dev/null +++ b/engine/network/index.ts @@ -0,0 +1,29 @@ +export enum MessageType { + NEW_ENTITY = "NEW_ENTITY", + REMOVE_ENTITY = "REMOVE_ENTITY", + UPDATE_ENTITY = "UPDATE_ENTITY", +} + +export type EntityAddBody = { + entityName: string; + args: 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/systems/Input.ts b/engine/systems/Input.ts index d9b7133..a32ba9a 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -12,12 +12,14 @@ import { Action } from "../interfaces"; import { System, SystemNames } from "."; export class Input extends System { + public clientId: string; private keys: Set; private actionTimeStamps: Map; - constructor() { + constructor(clientId: string) { super(SystemNames.Input); + this.clientId = clientId; this.keys = new Set(); this.actionTimeStamps = new Map(); } @@ -42,6 +44,7 @@ export class Input extends System { const controlComponent = entity.getComponent( ComponentNames.Control, ); + if (controlComponent.controllableBy != this.clientId) return; if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) { controlComponent.controlVelocityComponent.velocity.dCartesian.dx += diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts index cdd6de7..6c1d3e4 100644 --- a/engine/systems/NetworkUpdate.ts +++ b/engine/systems/NetworkUpdate.ts @@ -1,43 +1,44 @@ import { System, SystemNames } from "."; import { Game } from "../Game"; import { ComponentNames, NetworkUpdateable } from "../components"; - -export interface MessageQueueProvider { - getNewMessages(): any[]; - clearMessages(): void; -} - -export interface MessagePublisher { - addMessage(message: any): void; - publish(): void; -} +import { + type MessageQueueProvider, + type MessagePublisher, + type MessageProcessor, +} from "../network"; export class NetworkUpdate extends System { private queueProvider: MessageQueueProvider; private publisher: MessagePublisher; + private messageProcessor: MessageProcessor; constructor( queueProvider: MessageQueueProvider, - publisher: MessagePublisher + publisher: MessagePublisher, + messageProcessor: MessageProcessor, ) { super(SystemNames.NetworkUpdate); this.queueProvider = queueProvider; this.publisher = publisher; + this.messageProcessor = messageProcessor; } public update(_dt: number, game: Game) { - const messages = this.queueProvider.getNewMessages(); - if (messages.length) console.log(messages); + this.queueProvider + .getNewMessages() + .forEach((message) => this.messageProcessor.process(message)); this.queueProvider.clearMessages(); game.forEachEntityWithComponent( ComponentNames.NetworkUpdateable, (entity) => { const networkUpdateComponent = entity.getComponent( - ComponentNames.NetworkUpdateable + ComponentNames.NetworkUpdateable, ); - } + }, ); + + this.publisher.publish(); } } diff --git a/engine/utils/coding.ts b/engine/utils/coding.ts new file mode 100644 index 0000000..4c1b17f --- /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 = (str: string) => { + return JSON.parse(str, reviver) as unknown as T; +}; diff --git a/engine/utils/index.ts b/engine/utils/index.ts index 82a0d05..b70734f 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 "./coding"; diff --git a/server/src/server.ts b/server/src/server.ts index 713d3ed..b3eb1ea 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,109 +1,179 @@ import { Game } from "@engine/Game"; -import { Floor, Player } from "@engine/entities"; +import { EntityNames, Player } from "@engine/entities"; +import { WallBounds, Physics, Collision, NetworkUpdate } from "@engine/systems"; import { - WallBounds, - Physics, - Collision, - NetworkUpdate, - MessageQueueProvider, - MessagePublisher, -} from "@engine/systems"; + type MessageQueueProvider, + type MessagePublisher, + MessageType, + type MessageProcessor, + type Message, +} from "@engine/network"; +import { stringify, parse } from "@engine/utils"; import { Grid } from "@engine/structures"; import { Miscellaneous } from "@engine/config"; import { Server } from "bun"; +const SERVER_PORT = 8080; +const SERVER_TICK_RATE = (1 / 100) * 1000; +const GAME_TOPIC = "game"; + +type SessionData = { sessionId: string }; + +interface ServerMessage extends Message { + sessionData: SessionData; +} + class ServerSocketMessageReceiver implements MessageQueueProvider { - private messages: any[]; + private messages: ServerMessage[]; constructor() { this.messages = []; } - addMessage(message: any) { + public addMessage(message: ServerMessage) { this.messages.push(message); } - getNewMessages() { + public getNewMessages() { return this.messages; } - clearMessages() { + public clearMessages() { this.messages = []; } } -class ServerSocketMessagePublisher implements MessagePublisher { - private server: Server; - private messages: any[]; +class ServerMessageProcessor implements MessageProcessor { + constructor() {} + + public process(_message: ServerMessage) {} +} + +class ServerSocketMessagePublisher implements MessagePublisher { + private server?: Server; + private messages: Message[]; + + constructor(server?: Server) { + if (server) { + this.server = server; + } - constructor(server: Server) { - this.server = server; this.messages = []; } - addMessage(_message: any) {} + public setServer(server: Server) { + this.server = server; + } - publish() {} + public addMessage(message: Message) { + this.messages.push(message); + } + + public publish() { + this.messages.forEach( + (message) => this.server?.publish(GAME_TOPIC, stringify(message)), + ); + + this.messages = []; + } } const game = new Game(); const messageReceiver = new ServerSocketMessageReceiver(); +const messagePublisher = new ServerSocketMessagePublisher(); +const messageProcessor = new ServerMessageProcessor(); +const sessionControllableEntities: Map> = new Map(); -const server = Bun.serve<{ sessionId: string }>({ - port: 8080, - fetch: async (req, server): Promise => { - const sessionId = crypto.randomUUID(); +const server = Bun.serve({ + port: SERVER_PORT, + fetch: async (req, server): Promise => { + const url = new URL(req.url); - server.upgrade(req, { - headers: { - "Set-Cookie": `SessionId=${sessionId}`, - }, - data: { - sessionId, - }, - }); + const headers = new Headers(); + headers.set("Access-Control-Allow-Origin", "*"); - return sessionId; + if (url.pathname == "/assign") { + const sessionId = crypto.randomUUID(); + headers.set("Set-Cookie", `SessionId=${sessionId};`); + + return new Response(sessionId, { headers }); + } + + const cookie = req.headers.get("cookie"); + if (!cookie) { + return new Response("No session", { headers, status: 401 }); + } + + const sessionId = cookie.split(";").at(0)!.split("SessionId=").at(1); + + if (url.pathname == "/game") { + headers.set( + "Set-Cookie", + `SessionId=${sessionId}; HttpOnly; SameSite=Strict;`, + ); + server.upgrade(req, { + headers, + data: { + sessionId, + }, + }); + + return new Response("upgraded", { headers }); + } + if (url.pathname == "/me") { + return new Response(sessionId, { headers }); + } + + return new Response("Not found", { headers, status: 404 }); }, websocket: { open(ws) { const { sessionId } = ws.data; if (sessionControllableEntities.has(sessionId)) { + // no need to add player return; } - const player = new Player(); + const player = new Player(sessionId); game.addEntity(player); - sessionControllableEntities.set(sessionId, new Set(player.id)); + + messagePublisher.addMessage({ + type: MessageType.NEW_ENTITY, + body: { + entityName: EntityNames.Player, + args: { playerId: sessionId }, + }, + }); + + ws.subscribe(GAME_TOPIC); }, message(ws, message) { - console.log(JSON.parse(message)); - messageReceiver.addMessage(message); + if (typeof message == "string") { + const receivedMessage = parse(message); + receivedMessage.sessionData = ws.data; + + messageReceiver.addMessage(receivedMessage); + } }, - close(ws) {}, + close(_ws) {}, }, }); -const messagePublisher = new ServerSocketMessagePublisher(server); +messagePublisher.setServer(server); [ new Physics(), new Collision(new Grid()), new WallBounds(Miscellaneous.WIDTH), - new NetworkUpdate(messageReceiver, messagePublisher), + new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor), ].forEach((system) => game.addSystem(system)); -[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); - game.start(); - setInterval(() => { game.doGameLoop(performance.now()); -}, Miscellaneous.SERVER_TICK_RATE); - -const sessionControllableEntities: Map> = new Map(); +}, SERVER_TICK_RATE); console.log(`Listening on ${server.hostname}:${server.port}`); diff --git a/server/tsconfig.json b/server/tsconfig.json index 8cc9ad3..52f0ddc 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,4 +1,5 @@ { + "extends": "../tsconfig.engine.json", "compilerOptions": { // add Bun type definitions "types": ["bun-types"], @@ -21,18 +22,6 @@ // best practices "strict": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - - // engine path - "paths": { - "@engine/*": ["../engine/*"], - "@engine/components": ["../engine/components"], - "@engine/config": ["../engine/config"], - "@engine/entities": ["../engine/entities"], - "@engine/interfaces": ["../engine/interfaces"], - "@engine/structures": ["../engine/structures"], - "@engine/systems": ["../engine/systems"], - "@engine/utils": ["../engine/utils"], - } + "skipLibCheck": true } } diff --git a/tsconfig.engine.json b/tsconfig.engine.json new file mode 100644 index 0000000..52482a2 --- /dev/null +++ b/tsconfig.engine.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "paths": { + "@engine/*": ["./engine/*"], + "@engine/components": ["./engine/components"], + "@engine/config": ["./engine/config"], + "@engine/entities": ["./engine/entities"], + "@engine/interfaces": ["./engine/interfaces"], + "@engine/structures": ["./engine/structures"], + "@engine/systems": ["./engine/systems"], + "@engine/utils": ["./engine/utils"], + "@engine/network": ["./engine/network"] + } + } +} From 773ce84f4bf559337e132edd7fcce02a0a2598fd Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Fri, 25 Aug 2023 16:48:17 -0600 Subject: [PATCH 10/13] prettier formatting --- .prettierrc | 4 + client/.eslintrc.js | 6 +- client/index.html | 2 +- client/public/css/colors.css | 2 +- client/public/css/style.css | 10 +- client/public/css/tf.css | 2 +- client/public/css/theme.css | 2 +- client/src/JumpStorm.ts | 69 ++++++++---- client/src/components/GameCanvas.svelte | 5 +- client/src/components/LeaderBoard.svelte | 2 +- client/src/main.ts | 4 +- client/src/routes/Home.svelte | 3 +- client/svelte.config.js | 6 +- client/vite.config.ts | 22 ++-- engine/Game.ts | 10 +- engine/components/BoundingBox.ts | 22 ++-- engine/components/Collide.ts | 2 +- engine/components/Control.ts | 4 +- engine/components/FacingDirection.ts | 2 +- engine/components/Forces.ts | 6 +- engine/components/Gravity.ts | 4 +- engine/components/Jump.ts | 2 +- engine/components/Mass.ts | 2 +- engine/components/Moment.ts | 2 +- engine/components/NetworkUpdateable.ts | 2 +- engine/components/Sprite.ts | 20 ++-- engine/components/TopCollidable.ts | 2 +- engine/components/Velocity.ts | 8 +- engine/components/WallBounded.ts | 2 +- engine/components/index.ts | 32 +++--- engine/components/names.ts | 28 ++--- engine/config/assets.ts | 16 +-- engine/config/constants.ts | 6 +- engine/config/index.ts | 6 +- engine/config/sprites.ts | 16 +-- engine/entities/Entity.ts | 12 +- engine/entities/Floor.ts | 24 ++-- engine/entities/Player.ts | 24 ++-- engine/entities/index.ts | 8 +- engine/entities/names.ts | 4 +- engine/interfaces/Action.ts | 2 +- engine/interfaces/Direction.ts | 8 +- engine/interfaces/Draw.ts | 2 +- engine/interfaces/index.ts | 8 +- engine/network/index.ts | 6 +- engine/structures/Grid.ts | 16 +-- engine/structures/QuadTree.ts | 22 ++-- .../RefreshingCollisionFinderBehavior.ts | 10 +- engine/structures/index.ts | 6 +- engine/systems/Collision.ts | 54 ++++----- engine/systems/FacingDirection.ts | 14 +-- engine/systems/Input.ts | 20 ++-- engine/systems/NetworkUpdate.ts | 16 +-- engine/systems/Physics.ts | 32 +++--- engine/systems/Render.ts | 16 +-- engine/systems/System.ts | 2 +- engine/systems/WallBounds.ts | 20 ++-- engine/systems/index.ts | 18 +-- engine/systems/names.ts | 14 +-- engine/utils/coding.ts | 8 +- engine/utils/dotProduct.ts | 2 +- engine/utils/index.ts | 8 +- engine/utils/rotateVector.ts | 4 +- server/package.json | 3 +- server/src/server.ts | 104 +++++++++++------- 65 files changed, 429 insertions(+), 391 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..32ebab4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "none" +} diff --git a/client/.eslintrc.js b/client/.eslintrc.js index f200fbf..a9b1e2d 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,11 +1,11 @@ module.exports = { extends: [ // add more generic rule sets here, such as: - "eslint:recommended", - "plugin:svelte/recommended", + 'eslint:recommended', + 'plugin:svelte/recommended' ], rules: { // override/add rules settings here, such as: // 'svelte/rule-name': 'error' - }, + } }; diff --git a/client/index.html b/client/index.html index 00b94e7..892a3af 100644 --- a/client/index.html +++ b/client/index.html @@ -1,4 +1,4 @@ - + diff --git a/client/public/css/colors.css b/client/public/css/colors.css index 067ddcd..6108028 100644 --- a/client/public/css/colors.css +++ b/client/public/css/colors.css @@ -10,7 +10,7 @@ --orange: #af3a03; } -[data-theme="dark"] { +[data-theme='dark'] { --bg: #282828; --text: #f9f5d7; --red: #fb4934; diff --git a/client/public/css/style.css b/client/public/css/style.css index cdfef76..4dfe605 100644 --- a/client/public/css/style.css +++ b/client/public/css/style.css @@ -1,15 +1,15 @@ -@import url("./theme.css"); -@import url("./tf.css"); +@import url('./theme.css'); +@import url('./tf.css'); @font-face { - font-family: "scientifica"; - src: url("/fonts/scientifica.ttf"); + font-family: 'scientifica'; + src: url('/fonts/scientifica.ttf'); } * { padding: 0; margin: 0; - font-family: "scientifica", monospace; + font-family: 'scientifica', monospace; transition: background 0.2s ease-in-out; font-smooth: never; } diff --git a/client/public/css/tf.css b/client/public/css/tf.css index c1acd72..855fe0d 100644 --- a/client/public/css/tf.css +++ b/client/public/css/tf.css @@ -17,7 +17,7 @@ rgba(162, 254, 254, 1) 100% ); - content: ""; + content: ''; width: 100%; height: 100%; top: 0; diff --git a/client/public/css/theme.css b/client/public/css/theme.css index c65b2a8..eeb15ee 100644 --- a/client/public/css/theme.css +++ b/client/public/css/theme.css @@ -1,4 +1,4 @@ -@import url("./colors.css"); +@import url('./colors.css'); .primary { color: var(--aqua); diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index 01cc8d8..92bddcf 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -1,6 +1,6 @@ -import { Game } from "@engine/Game"; -import { Entity } from "@engine/entities"; -import { Grid } from "@engine/structures"; +import { Game } from '@engine/Game'; +import { Entity, Floor } from '@engine/entities'; +import { Grid } from '@engine/structures'; import { WallBounds, FacingDirection, @@ -8,17 +8,19 @@ import { Physics, Input, Collision, - NetworkUpdate, -} from "@engine/systems"; + NetworkUpdate +} from '@engine/systems'; import { type MessageQueueProvider, type MessagePublisher, type MessageProcessor, type Message, type EntityAddBody, - MessageType, -} from "@engine/network"; -import { stringify, parse } from "@engine/utils"; + MessageType +} from '@engine/network'; +import { stringify, parse } from '@engine/utils'; +import { BoundingBox, Sprite } from '@engine/components'; +import { Miscellaneous } from '@engine/config'; class ClientMessageProcessor implements MessageProcessor { private game: Game; @@ -29,14 +31,19 @@ class ClientMessageProcessor implements MessageProcessor { public process(message: Message) { switch (message.type) { - case MessageType.NEW_ENTITY: - const entityAddBody = message.body as unknown as EntityAddBody; - this.game.addEntity( - Entity.from(entityAddBody.entityName, entityAddBody.args), + case MessageType.NEW_ENTITIES: + const entityAdditions = message.body as unknown as EntityAddBody[]; + entityAdditions.forEach((addBody) => + this.game.addEntity(Entity.from(addBody.entityName, addBody.args)) ); break; + case MessageType.REMOVE_ENTITIES: + const ids = message.body as unknown as string[]; + ids.forEach((id) => this.game.removeEntity(id)); + break; + default: + break; } - console.log(message); } } @@ -49,9 +56,9 @@ class ClientSocketMessageQueueProvider implements MessageQueueProvider { this.socket = socket; this.messages = []; - this.socket.addEventListener("message", (e) => { - const message = parse(e.data); - this.messages.push(message); + this.socket.addEventListener('message', (e) => { + const messages = parse(e.data); + this.messages = this.messages.concat(messages); }); } @@ -79,7 +86,7 @@ class ClientSocketMessagePublisher implements MessagePublisher { public publish() { this.messages.forEach((message: Message) => - this.socket.send(stringify(message)), + this.socket.send(stringify(message)) ); } } @@ -96,7 +103,7 @@ export class JumpStorm { ctx: CanvasRenderingContext2D, httpMethod: string, wsMethod: string, - host: string, + host: string ) { await fetch(`${httpMethod}://${host}/assign`) .then((resp) => { @@ -115,7 +122,7 @@ export class JumpStorm { const clientSocketMessageQueueProvider = new ClientSocketMessageQueueProvider(socket); const clientSocketMessagePublisher = new ClientSocketMessagePublisher( - socket, + socket ); const clientMessageProcessor = new ClientMessageProcessor(this.game); [ @@ -123,14 +130,28 @@ export class JumpStorm { new FacingDirection(), new Physics(), new Collision(grid), - new WallBounds(ctx.canvas.width), + new WallBounds(), new NetworkUpdate( clientSocketMessageQueueProvider, clientSocketMessagePublisher, - clientMessageProcessor, + clientMessageProcessor ), - new Render(ctx), + new Render(ctx) ].forEach((system) => this.game.addSystem(system)); + + const floor = new Floor(160); + const floorHeight = 40; + + floor.addComponent( + new BoundingBox( + { + x: Miscellaneous.WIDTH / 2, + y: Miscellaneous.HEIGHT - floorHeight / 2 + }, + { width: Miscellaneous.WIDTH, height: floorHeight } + ) + ); + this.game.addEntity(floor); } public play() { @@ -146,13 +167,13 @@ export class JumpStorm { private createInputSystem(): Input { const inputSystem = new Input(this.clientId); - window.addEventListener("keydown", (e) => { + window.addEventListener('keydown', (e) => { if (!e.repeat) { inputSystem.keyPressed(e.key); } }); - window.addEventListener("keyup", (e) => inputSystem.keyReleased(e.key)); + window.addEventListener('keyup', (e) => inputSystem.keyReleased(e.key)); return inputSystem; } diff --git a/client/src/components/GameCanvas.svelte b/client/src/components/GameCanvas.svelte index ed16f33..ea7dd15 100644 --- a/client/src/components/GameCanvas.svelte +++ b/client/src/components/GameCanvas.svelte @@ -3,7 +3,7 @@ import { loadAssets } from "@engine/config"; import { Game } from "@engine/Game"; import { JumpStorm } from "../JumpStorm"; - + let canvas: HTMLCanvasElement; let ctx: CanvasRenderingContext2D; @@ -19,8 +19,7 @@ const game = new Game(); const jumpStorm = new JumpStorm(game); - const url = new URL(document.location); - await jumpStorm.init(ctx, "http", "ws", url.host + "/api"); + await jumpStorm.init(ctx, "http", "ws", document.location.host + "/api"); jumpStorm.play(); }); diff --git a/client/src/components/LeaderBoard.svelte b/client/src/components/LeaderBoard.svelte index 8343c56..2f3e411 100644 --- a/client/src/components/LeaderBoard.svelte +++ b/client/src/components/LeaderBoard.svelte @@ -3,7 +3,7 @@ const MAX_ENTRIES = 8; - export let entries: { name: string, score: number }[] = []; + export let entries: { name: string; score: number }[] = [];
diff --git a/client/src/main.ts b/client/src/main.ts index 5332616..aa7431f 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1,7 +1,7 @@ -import App from "./App.svelte"; +import App from './App.svelte'; const app = new App({ - target: document.getElementById("app"), + target: document.getElementById('app') }); export default app; diff --git a/client/src/routes/Home.svelte b/client/src/routes/Home.svelte index 9ada10e..71ad324 100644 --- a/client/src/routes/Home.svelte +++ b/client/src/routes/Home.svelte @@ -3,10 +3,9 @@ import LeaderBoard from "../components/LeaderBoard.svelte"; import { Miscellaneous } from "@engine/config"; - + let width: number = Miscellaneous.WIDTH; let height: number = Miscellaneous.HEIGHT; -
diff --git a/client/svelte.config.js b/client/svelte.config.js index b0683fd..db735be 100644 --- a/client/svelte.config.js +++ b/client/svelte.config.js @@ -1,7 +1,7 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors - preprocess: vitePreprocess(), -} + preprocess: vitePreprocess() +}; diff --git a/client/vite.config.ts b/client/vite.config.ts index cdf1ab1..d8b999c 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,23 +1,23 @@ -import { defineConfig } from "vite"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; -import { fileURLToPath, URL } from "node:url"; +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { fileURLToPath, URL } from 'node:url'; // https://vitejs.dev/config/ export default defineConfig({ server: { proxy: { - "/api": { - target: "http://localhost:8080", + '/api': { + target: 'http://localhost:8080', ws: true, - rewrite: (path) => path.replace(/^\/api/, ""), - }, - }, + rewrite: (path) => path.replace(/^\/api/, '') + } + } }, cors: true, plugins: [svelte()], resolve: { alias: { - "@engine": fileURLToPath(new URL("../engine", import.meta.url)), - }, - }, + '@engine': fileURLToPath(new URL('../engine', import.meta.url)) + } + } }); diff --git a/engine/Game.ts b/engine/Game.ts index 301c8df..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[]; @@ -39,7 +39,7 @@ export class Game { public forEachEntityWithComponent( componentName: string, - callback: (entity: Entity) => void, + callback: (entity: Entity) => void ) { this.componentEntities.get(componentName)?.forEach((entityId) => { const entity = this.getEntity(entityId); @@ -75,12 +75,12 @@ export class Game { if (!this.componentEntities.has(component.name)) { this.componentEntities.set( component.name, - new Set([entity.id]), + new Set([entity.id]) ); return; } this.componentEntities.get(component.name)?.add(entity.id); - }), + }) ); this.systemOrder.forEach((systemName) => { diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index 26b404d..dbe083e 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; @@ -48,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; @@ -64,14 +64,14 @@ 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)) // rotate .map((vertex) => { // translate return { x: vertex.x + this.center.x, - y: vertex.y + this.center.y, + y: vertex.y + this.center.y }; }); } @@ -92,28 +92,28 @@ export class BoundingBox extends Component { 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, + 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, + 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 a8dae59..beec82c 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames, Velocity } from "."; +import { Component, ComponentNames, Velocity } from '.'; export class Control extends Component { public controlVelocityComponent: Velocity; @@ -6,7 +6,7 @@ export class Control extends Component { constructor( controllableBy: string, - controlVelocityComponent: Velocity = new Velocity(), + controlVelocityComponent: Velocity = new Velocity() ) { super(ComponentNames.Control); 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 index 980b064..7fb6d18 100644 --- a/engine/components/NetworkUpdateable.ts +++ b/engine/components/NetworkUpdateable.ts @@ -1,4 +1,4 @@ -import { Component, ComponentNames } from "."; +import { Component, ComponentNames } from '.'; export class NetworkUpdateable extends Component { public isPublish: boolean; 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 aec0c03..0071891 100644 --- a/engine/components/Velocity.ts +++ b/engine/components/Velocity.ts @@ -1,12 +1,12 @@ -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 velocity: Velocity2D; constructor( - velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }, + velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 } ) { super(ComponentNames.Velocity); 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 90f4965..6d7c1e5 100644 --- a/engine/components/index.ts +++ b/engine/components/index.ts @@ -1,16 +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 "./NetworkUpdateable"; -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 02ee064..97b4edd 100644 --- a/engine/components/names.ts +++ b/engine/components/names.ts @@ -1,16 +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 NetworkUpdateable = "NetworkUpdateable"; + 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(); export const loadSpritesIntoImageElements = ( - spriteSpecs: Partial[], + spriteSpecs: Partial[] ): Promise[] => { const spritePromises: Promise[] = []; @@ -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 e93986b..45b0301 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -1,4 +1,4 @@ -import { Action } from "../interfaces"; +import { Action } from '../interfaces'; export namespace KeyConstants { export const KeyActions: Record = { @@ -10,11 +10,13 @@ export namespace KeyConstants { w: Action.JUMP, ArrowUp: Action.JUMP, + + ' ': Action.JUMP }; // value -> [key] from KeyActions export const ActionKeys: Map = Object.keys( - KeyActions, + KeyActions ).reduce((acc: Map, key) => { const action = KeyActions[key]; 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>(), + states: new Map>() }; [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>(), + states: new Map>() }; -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 88982cb..b016fc0 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -1,5 +1,5 @@ -import { EntityNames, Player } from "."; -import type { Component } from "../components"; +import { EntityNames, Player } from '.'; +import type { Component } from '../components'; export abstract class Entity { public id: string; @@ -18,7 +18,7 @@ export abstract class Entity { public getComponent(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; } @@ -34,9 +34,11 @@ export abstract class Entity { static from(entityName: string, args: any): Entity { switch (entityName) { case EntityNames.Player: - return new Player(args.playerId); + const player = new Player(args.playerId); + player.id = args.id; + return player; default: - throw new Error(".from() Entity type not implemented: " + entityName); + throw new Error('.from() Entity type not implemented: ' + entityName); } } } diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index 6cfc276..6f9b13b 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -1,11 +1,11 @@ -import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config"; -import { BoundingBox, Sprite } from "../components"; -import { TopCollidable } from "../components/TopCollidable"; -import { Entity, EntityNames } from "../entities"; +import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config'; +import { BoundingBox, 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; constructor(width: number) { @@ -17,18 +17,8 @@ export class Floor extends Entity { { x: 0, y: 0 }, { width, height: Floor.spriteSpec.height }, Floor.spriteSpec.msPerFrame, - Floor.spriteSpec.frames, - ), - ); - - this.addComponent( - new BoundingBox( - { - x: 300, - y: 300, - }, - { width, height: Floor.spriteSpec.height }, - ), + Floor.spriteSpec.frames + ) ); this.addComponent(new TopCollidable()); diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index cfe4dd2..947fbd6 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -1,5 +1,5 @@ -import { Entity, EntityNames } 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, @@ -12,16 +12,16 @@ import { Collide, Control, Mass, - Moment, -} from "../components"; -import { Direction } from "../interfaces"; + Moment +} from '../components'; +import { Direction } from '../interfaces'; export class Player extends Entity { private static MASS: number = 10; private static MOI: number = 100; private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( - Sprites.COFFEE, + Sprites.COFFEE ) as SpriteSpec; constructor(playerId: string) { @@ -31,15 +31,15 @@ export class Player extends Entity { new BoundingBox( { x: 300, - y: 100, + y: 100 }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, - 0, - ), + 0 + ) ); this.addComponent( - new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }), + new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }) ); this.addComponent(new Mass(Player.MASS)); @@ -64,8 +64,8 @@ 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)); diff --git a/engine/entities/index.ts b/engine/entities/index.ts index dd3dba9..8aee83c 100644 --- a/engine/entities/index.ts +++ b/engine/entities/index.ts @@ -1,4 +1,4 @@ -export * from "./Entity"; -export * from "./Floor"; -export * from "./Player"; -export * from "./names"; +export * from './Entity'; +export * from './Floor'; +export * from './Player'; +export * from './names'; diff --git a/engine/entities/names.ts b/engine/entities/names.ts index 21594c8..cf65f9f 100644 --- a/engine/entities/names.ts +++ b/engine/entities/names.ts @@ -1,4 +1,4 @@ export namespace EntityNames { - export const Player = "Player"; - export const Floor = "Floor"; + 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/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 index 1726ffc..1bf95fb 100644 --- a/engine/network/index.ts +++ b/engine/network/index.ts @@ -1,7 +1,7 @@ export enum MessageType { - NEW_ENTITY = "NEW_ENTITY", - REMOVE_ENTITY = "REMOVE_ENTITY", - UPDATE_ENTITY = "UPDATE_ENTITY", + NEW_ENTITIES = 'NEW_ENTITIES', + REMOVE_ENTITIES = 'REMOVE_ENTITIES', + UPDATE_ENTITY = 'UPDATE_ENTITY' } export type EntityAddBody = { diff --git a/engine/structures/Grid.ts b/engine/structures/Grid.ts index 6e8c0cb..5f0e053 100644 --- a/engine/structures/Grid.ts +++ b/engine/structures/Grid.ts @@ -1,6 +1,6 @@ -import type { Coord2D, Dimension2D } from "../interfaces"; -import type { BoxedEntry, RefreshingCollisionFinderBehavior } from "."; -import { Miscellaneous } from "../config/constants"; +import type { Coord2D, Dimension2D } from '../interfaces'; +import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.'; +import { Miscellaneous } from '../config/constants'; export class Grid implements RefreshingCollisionFinderBehavior { private cellEntities: Map; @@ -12,11 +12,11 @@ export class Grid implements RefreshingCollisionFinderBehavior { constructor( gridDimension: Dimension2D = { width: Miscellaneous.WIDTH, - height: Miscellaneous.HEIGHT, + height: Miscellaneous.HEIGHT }, cellDimension: Dimension2D = { width: Miscellaneous.DEFAULT_GRID_WIDTH, - height: Miscellaneous.DEFAULT_GRID_HEIGHT, + height: Miscellaneous.DEFAULT_GRID_HEIGHT }, topLeft = { x: 0, y: 0 } ) { @@ -73,7 +73,7 @@ export class Grid implements RefreshingCollisionFinderBehavior { const translated: Coord2D = { y: center.y - this.topLeft.y, - x: center.x - this.topLeft.x, + x: center.x - this.topLeft.x }; const topLeftBox = { @@ -82,7 +82,7 @@ export class Grid implements RefreshingCollisionFinderBehavior { ), y: Math.floor( (translated.y - dimension.height / 2) / this.cellDimension.height - ), + ) }; const bottomRightBox = { x: Math.floor( @@ -90,7 +90,7 @@ export class Grid implements RefreshingCollisionFinderBehavior { ), y: Math.floor( (translated.y + dimension.height / 2) / this.cellDimension.height - ), + ) }; const cells: number[] = []; diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts index 1ab2d1d..93702d0 100644 --- a/engine/structures/QuadTree.ts +++ b/engine/structures/QuadTree.ts @@ -1,11 +1,11 @@ -import type { Coord2D, Dimension2D } from "../interfaces"; -import type { BoxedEntry, RefreshingCollisionFinderBehavior } from "."; +import type { Coord2D, Dimension2D } from '../interfaces'; +import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.'; enum Quadrant { I, II, III, - IV, + IV } /* @@ -102,8 +102,8 @@ export class QuadTree implements RefreshingCollisionFinderBehavior { [Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }], [ Quadrant.IV, - { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }, - ], + { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight } + ] ] as [Quadrant, Coord2D][] ).forEach(([quadrant, pos]) => { this.children.set( @@ -122,27 +122,27 @@ export class QuadTree implements RefreshingCollisionFinderBehavior { 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, - ], + (x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y + ] ] as [Quadrant, (x: number, y: number) => boolean][] ) .filter( diff --git a/engine/structures/RefreshingCollisionFinderBehavior.ts b/engine/structures/RefreshingCollisionFinderBehavior.ts index 21d690d..573ddd8 100644 --- a/engine/structures/RefreshingCollisionFinderBehavior.ts +++ b/engine/structures/RefreshingCollisionFinderBehavior.ts @@ -1,4 +1,4 @@ -import type { Coord2D, Dimension2D } from "../interfaces"; +import type { Coord2D, Dimension2D } from '../interfaces'; export interface BoxedEntry { id: string; @@ -7,8 +7,8 @@ export interface BoxedEntry { } export interface RefreshingCollisionFinderBehavior { - public clear(): void; - public insert(boxedEntry: BoxedEntry): void; - public getNeighborIds(boxedEntry: BoxedEntry): Set; - public setTopLeft(topLeft: Coord2d): void; + clear(): void; + insert(boxedEntry: BoxedEntry): void; + getNeighborIds(boxedEntry: BoxedEntry): Set; + setTopLeft(topLeft: Coord2D): void; } diff --git a/engine/structures/index.ts b/engine/structures/index.ts index 49a84af..679dbd4 100644 --- a/engine/structures/index.ts +++ b/engine/structures/index.ts @@ -1,3 +1,3 @@ -export * from "./RefreshingCollisionFinderBehavior"; -export * from "./QuadTree"; -export * from "./Grid"; +export * from './RefreshingCollisionFinderBehavior'; +export * from './QuadTree'; +export * from './Grid'; diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts index 2dd920e..4a838dd 100644 --- a/engine/systems/Collision.ts +++ b/engine/systems/Collision.ts @@ -1,22 +1,22 @@ -import { SystemNames, System } from "."; +import { SystemNames, System } from '.'; import { Mass, BoundingBox, ComponentNames, Jump, Velocity, - 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"; + 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 collisionFinder: RefreshingCollisionFinderBehavior; @@ -38,7 +38,7 @@ export class Collision extends System { return; } entitiesToAddToCollisionFinder.push(entity); - }), + }) ); this.insertEntitiesAndUpdateBounds(entitiesToAddToCollisionFinder); @@ -53,7 +53,7 @@ export class Collision extends System { entities.forEach((entity) => { const boundingBox = entity.getComponent( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); let dimension = { ...boundingBox.dimension }; @@ -73,7 +73,7 @@ export class Collision extends System { collisionFinderInsertions.push({ id: entity.id, dimension, - center, + center }); }); @@ -82,13 +82,13 @@ export class Collision extends System { this.collisionFinder.setTopLeft(topLeft); this.collisionFinder.setDimension({ width: bottomRight.x - topLeft.x, - height: bottomRight.y - topLeft.y, + height: bottomRight.y - topLeft.y }); } // then, begin insertions collisionFinderInsertions.forEach((boxedEntry: BoxedEntry) => - this.collisionFinder.insert(boxedEntry), + this.collisionFinder.insert(boxedEntry) ); } @@ -97,7 +97,7 @@ export class Collision extends System { 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); @@ -107,13 +107,13 @@ export class Collision extends System { private performCollision(entityA: Entity, entityB: Entity) { const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map( - (entity) => entity.getComponent(ComponentNames.BoundingBox), + (entity) => entity.getComponent(ComponentNames.BoundingBox) ); let velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }; if (entityA.hasComponent(ComponentNames.Velocity)) { velocity = entityA.getComponent( - ComponentNames.Velocity, + ComponentNames.Velocity ).velocity; } @@ -125,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.` ); } @@ -139,7 +139,7 @@ export class Collision extends System { entityA.getComponent(ComponentNames.Forces).forces.push({ fCartesian: { fy: F_n, fx: 0 }, - torque: 0, + torque: 0 }); } @@ -157,19 +157,19 @@ export class Collision extends System { private getCollidingEntities( collidableEntities: Entity[], - game: Game, + game: Game ): [string, string][] { const collidingEntityIds: [string, string][] = []; for (const entity of collidableEntities) { const boundingBox = entity.getComponent( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); const neighborIds = this.collisionFinder.getNeighborIds({ id: entity.id, dimension: boundingBox.dimension, - center: boundingBox.center, + center: boundingBox.center }); for (const neighborId of neighborIds) { @@ -177,7 +177,7 @@ export class Collision extends System { if (!neighbor) return; const neighborBoundingBox = neighbor.getComponent( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); if (boundingBox.isCollidingWith(neighborBoundingBox)) { @@ -192,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(); @@ -215,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 - @@ -225,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 daf639f..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() { @@ -23,7 +23,7 @@ export class FacingDirection extends System { const totalVelocityComponent = new Velocity(); const control = entity.getComponent(ComponentNames.Control); const velocity = entity.getComponent( - ComponentNames.Velocity, + ComponentNames.Velocity ).velocity; totalVelocityComponent.add(velocity); @@ -32,7 +32,7 @@ export class FacingDirection extends System { } const facingDirection = entity.getComponent( - ComponentNames.FacingDirection, + ComponentNames.FacingDirection ); if (totalVelocityComponent.velocity.dCartesian.dx > 0) { @@ -40,7 +40,7 @@ export class FacingDirection extends System { } else if (totalVelocityComponent.velocity.dCartesian.dx < 0) { entity.addComponent(facingDirection.facingLeftSprite); } - }, + } ); } } diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts index a32ba9a..4a5a3c3 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -4,12 +4,12 @@ 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 '.'; export class Input extends System { public clientId: string; @@ -42,7 +42,7 @@ export class Input extends System { public update(_dt: number, game: Game) { game.forEachEntityWithComponent(ComponentNames.Control, (entity) => { const controlComponent = entity.getComponent( - ComponentNames.Control, + ComponentNames.Control ); if (controlComponent.controllableBy != this.clientId) return; @@ -58,7 +58,7 @@ export class Input extends System { if (entity.hasComponent(ComponentNames.Jump)) { const velocity = entity.getComponent( - ComponentNames.Velocity, + ComponentNames.Velocity ).velocity; const jump = entity.getComponent(ComponentNames.Jump); @@ -78,9 +78,9 @@ export class Input extends System { entity.getComponent(ComponentNames.Forces)?.forces.push({ fCartesian: { fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, - fx: 0, + fx: 0 }, - torque: 0, + torque: 0 }); } } diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts index 6c1d3e4..bcfb71e 100644 --- a/engine/systems/NetworkUpdate.ts +++ b/engine/systems/NetworkUpdate.ts @@ -1,11 +1,11 @@ -import { System, SystemNames } from "."; -import { Game } from "../Game"; -import { ComponentNames, NetworkUpdateable } from "../components"; +import { System, SystemNames } from '.'; +import { Game } from '../Game'; +import { ComponentNames, NetworkUpdateable } from '../components'; import { type MessageQueueProvider, type MessagePublisher, - type MessageProcessor, -} from "../network"; + type MessageProcessor +} from '../network'; export class NetworkUpdate extends System { private queueProvider: MessageQueueProvider; @@ -15,7 +15,7 @@ export class NetworkUpdate extends System { constructor( queueProvider: MessageQueueProvider, publisher: MessagePublisher, - messageProcessor: MessageProcessor, + messageProcessor: MessageProcessor ) { super(SystemNames.NetworkUpdate); @@ -34,9 +34,9 @@ export class NetworkUpdate extends System { ComponentNames.NetworkUpdateable, (entity) => { const networkUpdateComponent = entity.getComponent( - ComponentNames.NetworkUpdateable, + ComponentNames.NetworkUpdateable ); - }, + } ); this.publisher.publish(); diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts index e324c97..35afb3f 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, Velocity2D } 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() { @@ -24,10 +24,10 @@ export class Physics extends System { const mass = entity.getComponent(ComponentNames.Mass).mass; const forces = entity.getComponent(ComponentNames.Forces).forces; const velocity = entity.getComponent( - ComponentNames.Velocity, + ComponentNames.Velocity ).velocity; const inertia = entity.getComponent( - ComponentNames.Moment, + ComponentNames.Moment ).inertia; // F_g = mg, applied only until terminal velocity is reached @@ -37,9 +37,9 @@ export class Physics extends System { forces.push({ fCartesian: { fy: mass * PhysicsConstants.GRAVITY, - fx: 0, + fx: 0 }, - torque: 0, + torque: 0 }); } } @@ -49,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; @@ -79,14 +79,14 @@ export class Physics extends System { const control = entity.getComponent(ComponentNames.Control); velocityComponent.add( - entity.getComponent(ComponentNames.Velocity).velocity, + entity.getComponent(ComponentNames.Velocity).velocity ); if (control) { velocityComponent.add(control.controlVelocityComponent.velocity); } const boundingBox = entity.getComponent( - ComponentNames.BoundingBox, + ComponentNames.BoundingBox ); // integrate 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( - 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( - 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 075fc4e..43181e9 100644 --- a/engine/systems/index.ts +++ b/engine/systems/index.ts @@ -1,9 +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 "./NetworkUpdate"; +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 cf66422..ddf6f19 100644 --- a/engine/systems/names.ts +++ b/engine/systems/names.ts @@ -1,9 +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 NetworkUpdate = "NetworkUpdate"; + 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 index 4c1b17f..3f78889 100644 --- a/engine/utils/coding.ts +++ b/engine/utils/coding.ts @@ -1,8 +1,8 @@ const replacer = (_key: any, value: any) => { if (value instanceof Map) { return { - dataType: "Map", - value: Array.from(value.entries()), + dataType: 'Map', + value: Array.from(value.entries()) }; } else { return value; @@ -10,8 +10,8 @@ const replacer = (_key: any, value: any) => { }; const reviver = (_key: any, value: any) => { - if (typeof value === "object" && value !== null) { - if (value.dataType === "Map") { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { return new Map(value.value); } } 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 b70734f..65446d1 100644 --- a/engine/utils/index.ts +++ b/engine/utils/index.ts @@ -1,4 +1,4 @@ -export * from "./rotateVector"; -export * from "./dotProduct"; -export * from "./clamp"; -export * from "./coding"; +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 }; }; diff --git a/server/package.json b/server/package.json index 17d3c25..388cff2 100644 --- a/server/package.json +++ b/server/package.json @@ -8,6 +8,5 @@ "peerDependencies": { "typescript": "^5.0.0" }, - "dependencies": { - } + "dependencies": {} } diff --git a/server/src/server.ts b/server/src/server.ts index b3eb1ea..c77bfef 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,21 +1,22 @@ -import { Game } from "@engine/Game"; -import { EntityNames, Player } from "@engine/entities"; -import { WallBounds, Physics, Collision, NetworkUpdate } from "@engine/systems"; +import { Game } from '@engine/Game'; +import { EntityNames, Player } from '@engine/entities'; +import { WallBounds, Physics, Collision, NetworkUpdate } from '@engine/systems'; import { type MessageQueueProvider, type MessagePublisher, MessageType, type MessageProcessor, - type Message, -} from "@engine/network"; -import { stringify, parse } from "@engine/utils"; -import { Grid } from "@engine/structures"; -import { Miscellaneous } from "@engine/config"; -import { Server } from "bun"; + type Message +} from '@engine/network'; +import { stringify, parse } from '@engine/utils'; +import { Grid } from '@engine/structures'; +import { Miscellaneous } from '@engine/config'; +import { Server } from 'bun'; const SERVER_PORT = 8080; -const SERVER_TICK_RATE = (1 / 100) * 1000; -const GAME_TOPIC = "game"; +const SERVER_TICK_RATE = (1 / 60) * 1000; +const GAME_TOPIC = 'game'; +const MAX_PLAYERS = 8; type SessionData = { sessionId: string }; @@ -70,11 +71,11 @@ class ServerSocketMessagePublisher implements MessagePublisher { } public publish() { - this.messages.forEach( - (message) => this.server?.publish(GAME_TOPIC, stringify(message)), - ); + if (this.messages.length) { + this.server?.publish(GAME_TOPIC, stringify(this.messages)); - this.messages = []; + this.messages = []; + } } } @@ -85,81 +86,102 @@ const messagePublisher = new ServerSocketMessagePublisher(); const messageProcessor = new ServerMessageProcessor(); const sessionControllableEntities: Map> = new Map(); +const sessions = new Set(); + const server = Bun.serve({ port: SERVER_PORT, fetch: async (req, server): Promise => { const url = new URL(req.url); const headers = new Headers(); - headers.set("Access-Control-Allow-Origin", "*"); + headers.set('Access-Control-Allow-Origin', '*'); + + if (url.pathname == '/assign') { + if (sessions.size > MAX_PLAYERS) + return new Response('too many players', { headers, status: 400 }); - if (url.pathname == "/assign") { const sessionId = crypto.randomUUID(); - headers.set("Set-Cookie", `SessionId=${sessionId};`); + headers.set('Set-Cookie', `SessionId=${sessionId};`); + + sessions.add(sessionId); return new Response(sessionId, { headers }); } - const cookie = req.headers.get("cookie"); + const cookie = req.headers.get('cookie'); if (!cookie) { - return new Response("No session", { headers, status: 401 }); + return new Response('No session', { headers, status: 401 }); } - const sessionId = cookie.split(";").at(0)!.split("SessionId=").at(1); + const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1); - if (url.pathname == "/game") { + if (url.pathname == '/game') { headers.set( - "Set-Cookie", - `SessionId=${sessionId}; HttpOnly; SameSite=Strict;`, + 'Set-Cookie', + `SessionId=${sessionId}; HttpOnly; SameSite=Strict;` ); server.upgrade(req, { headers, data: { - sessionId, - }, + sessionId + } }); - return new Response("upgraded", { headers }); + return new Response('upgraded', { headers }); } - if (url.pathname == "/me") { + + if (url.pathname == '/me') { return new Response(sessionId, { headers }); } - return new Response("Not found", { headers, status: 404 }); + return new Response('Not found', { headers, status: 404 }); }, websocket: { open(ws) { const { sessionId } = ws.data; if (sessionControllableEntities.has(sessionId)) { - // no need to add player return; } const player = new Player(sessionId); game.addEntity(player); - sessionControllableEntities.set(sessionId, new Set(player.id)); + sessionControllableEntities.set(sessionId, new Set([player.id])); messagePublisher.addMessage({ - type: MessageType.NEW_ENTITY, - body: { - entityName: EntityNames.Player, - args: { playerId: sessionId }, - }, + type: MessageType.NEW_ENTITIES, + body: [ + { + entityName: EntityNames.Player, + args: { playerId: sessionId, id: player.id } + } + ] }); ws.subscribe(GAME_TOPIC); }, message(ws, message) { - if (typeof message == "string") { + if (typeof message == 'string') { const receivedMessage = parse(message); receivedMessage.sessionData = ws.data; messageReceiver.addMessage(receivedMessage); } }, - close(_ws) {}, - }, + close(ws) { + const { sessionId } = ws.data; + + sessions.delete(sessionId); + + const sessionEntities = sessionControllableEntities.get(sessionId); + if (!sessionEntities) return; + + messagePublisher.addMessage({ + type: MessageType.REMOVE_ENTITIES, + body: Array.from(sessionEntities) + }); + } + } }); messagePublisher.setServer(server); @@ -167,8 +189,8 @@ messagePublisher.setServer(server); [ new Physics(), new Collision(new Grid()), - new WallBounds(Miscellaneous.WIDTH), - new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor), + new WallBounds(), + new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor) ].forEach((system) => game.addSystem(system)); game.start(); From 2fbe0f0595d06800c1a648a4168b57471d395ee4 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Fri, 25 Aug 2023 18:10:09 -0600 Subject: [PATCH 11/13] refactor server structure --- server/src/constants.ts | 6 + server/src/main.ts | 32 ++++ server/src/network/MessageProcessor.ts | 8 + server/src/network/MessagePublisher.ts | 31 ++++ server/src/network/MessageReceiver.ts | 22 +++ server/src/network/index.ts | 16 ++ server/src/server.ts | 234 ++++++++++--------------- 7 files changed, 208 insertions(+), 141 deletions(-) create mode 100644 server/src/constants.ts create mode 100644 server/src/main.ts create mode 100644 server/src/network/MessageProcessor.ts create mode 100644 server/src/network/MessagePublisher.ts create mode 100644 server/src/network/MessageReceiver.ts create mode 100644 server/src/network/index.ts diff --git a/server/src/constants.ts b/server/src/constants.ts new file mode 100644 index 0000000..a2b3d12 --- /dev/null +++ b/server/src/constants.ts @@ -0,0 +1,6 @@ +export namespace Constants { + export const SERVER_PORT = 8080; + export const SERVER_TICK_RATE = (1 / 60) * 1000; + export const GAME_TOPIC = 'game'; + export const MAX_PLAYERS = 8; +} diff --git a/server/src/main.ts b/server/src/main.ts new file mode 100644 index 0000000..965e0d7 --- /dev/null +++ b/server/src/main.ts @@ -0,0 +1,32 @@ +import { Grid } from '@engine/structures'; +import { + ServerMessageProcessor, + ServerSocketMessagePublisher, + ServerSocketMessageReceiver +} from './network'; +import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems'; +import { Game } from '@engine/Game'; +import { Constants } from './constants'; +import { GameServer } from './server'; + +const messageReceiver = new ServerSocketMessageReceiver(); +const messagePublisher = new ServerSocketMessagePublisher(); +const messageProcessor = new ServerMessageProcessor(); + +const game = new Game(); + +const server = new GameServer(game, messageReceiver, messagePublisher); + +[ + new Physics(), + new Collision(new Grid()), + new WallBounds(), + new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor) +].forEach((system) => game.addSystem(system)); + +game.start(); +setInterval(() => { + game.doGameLoop(performance.now()); +}, Constants.SERVER_TICK_RATE); + +server.serve(); diff --git a/server/src/network/MessageProcessor.ts b/server/src/network/MessageProcessor.ts new file mode 100644 index 0000000..de42459 --- /dev/null +++ b/server/src/network/MessageProcessor.ts @@ -0,0 +1,8 @@ +import { MessageProcessor } from '@engine/network'; +import { ServerMessage } from '.'; + +export class ServerMessageProcessor implements MessageProcessor { + constructor() {} + + public process(_message: ServerMessage) {} +} diff --git a/server/src/network/MessagePublisher.ts b/server/src/network/MessagePublisher.ts new file mode 100644 index 0000000..9c6011f --- /dev/null +++ b/server/src/network/MessagePublisher.ts @@ -0,0 +1,31 @@ +import { Message, MessagePublisher } from '@engine/network'; +import { Server } from 'bun'; +import { Constants } from '../constants'; +import { stringify } from '@engine/utils'; + +export class ServerSocketMessagePublisher implements MessagePublisher { + private server?: Server; + private messages: Message[]; + + constructor(server?: Server) { + this.messages = []; + + if (server) this.setServer(server); + } + + public setServer(server: Server) { + this.server = server; + } + + public addMessage(message: Message) { + this.messages.push(message); + } + + public publish() { + if (this.messages.length) { + this.server?.publish(Constants.GAME_TOPIC, stringify(this.messages)); + + this.messages = []; + } + } +} diff --git a/server/src/network/MessageReceiver.ts b/server/src/network/MessageReceiver.ts new file mode 100644 index 0000000..fcac0a4 --- /dev/null +++ b/server/src/network/MessageReceiver.ts @@ -0,0 +1,22 @@ +import { MessageQueueProvider } from '@engine/network'; +import type { ServerMessage } from '.'; + +export class ServerSocketMessageReceiver implements MessageQueueProvider { + private messages: ServerMessage[]; + + constructor() { + this.messages = []; + } + + public addMessage(message: ServerMessage) { + this.messages.push(message); + } + + public getNewMessages() { + return this.messages; + } + + public clearMessages() { + this.messages = []; + } +} diff --git a/server/src/network/index.ts b/server/src/network/index.ts new file mode 100644 index 0000000..8ffa689 --- /dev/null +++ b/server/src/network/index.ts @@ -0,0 +1,16 @@ +import { Message } from '@engine/network'; + +export * from './MessageProcessor'; +export * from './MessagePublisher'; +export * from './MessageReceiver'; + +export type SessionData = { sessionId: string }; + +export type Session = { + sessionId: string; + controllableEntities: Set; +}; + +export interface ServerMessage extends Message { + sessionData: SessionData; +} diff --git a/server/src/server.ts b/server/src/server.ts index c77bfef..6acbe74 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,110 +1,124 @@ import { Game } from '@engine/Game'; import { EntityNames, Player } from '@engine/entities'; -import { WallBounds, Physics, Collision, NetworkUpdate } from '@engine/systems'; +import { MessageType } from '@engine/network'; +import { Constants } from './constants'; import { - type MessageQueueProvider, - type MessagePublisher, - MessageType, - type MessageProcessor, - type Message -} from '@engine/network'; -import { stringify, parse } from '@engine/utils'; -import { Grid } from '@engine/structures'; -import { Miscellaneous } from '@engine/config'; -import { Server } from 'bun'; + ServerSocketMessageReceiver, + ServerSocketMessagePublisher, + SessionData, + ServerMessage, + Session +} from './network'; +import { parse } from '@engine/utils'; +import { Server, ServerWebSocket } from 'bun'; -const SERVER_PORT = 8080; -const SERVER_TICK_RATE = (1 / 60) * 1000; -const GAME_TOPIC = 'game'; -const MAX_PLAYERS = 8; +export class GameServer { + private sessions: Map; -type SessionData = { sessionId: string }; - -interface ServerMessage extends Message { - sessionData: SessionData; -} - -class ServerSocketMessageReceiver implements MessageQueueProvider { - private messages: ServerMessage[]; - - constructor() { - this.messages = []; - } - - public addMessage(message: ServerMessage) { - this.messages.push(message); - } - - public getNewMessages() { - return this.messages; - } - - public clearMessages() { - this.messages = []; - } -} - -class ServerMessageProcessor implements MessageProcessor { - constructor() {} - - public process(_message: ServerMessage) {} -} - -class ServerSocketMessagePublisher implements MessagePublisher { private server?: Server; - private messages: Message[]; + private game: Game; + private messageReceiver: ServerSocketMessageReceiver; + private messagePublisher: ServerSocketMessagePublisher; - constructor(server?: Server) { - if (server) { - this.server = server; - } + constructor( + game: Game, + messageReceiver: ServerSocketMessageReceiver, + messagePublisher: ServerSocketMessagePublisher + ) { + this.sessions = new Map(); - this.messages = []; + this.game = game; + this.messageReceiver = messageReceiver; + this.messagePublisher = messagePublisher; } - public setServer(server: Server) { - this.server = server; + public serve() { + if (!this.server) + this.server = Bun.serve({ + port: Constants.SERVER_PORT, + fetch: (req, srv) => this.fetchHandler(req, srv), + websocket: { + open: (ws) => this.openWebsocket(ws), + message: (ws, msg) => this.websocketMessage(ws, msg), + close: (ws) => this.closeWebsocket(ws) + } + }); + + this.messagePublisher.setServer(this.server); + + console.log(`Listening on ${this.server.hostname}:${this.server.port}`); } - public addMessage(message: Message) { - this.messages.push(message); - } + private websocketMessage( + websocket: ServerWebSocket, + message: string | Uint8Array + ) { + if (typeof message == 'string') { + const receivedMessage = parse(message); + receivedMessage.sessionData = websocket.data; - public publish() { - if (this.messages.length) { - this.server?.publish(GAME_TOPIC, stringify(this.messages)); - - this.messages = []; + this.messageReceiver.addMessage(receivedMessage); } } -} -const game = new Game(); + private closeWebsocket(websocket: ServerWebSocket) { + const { sessionId } = websocket.data; -const messageReceiver = new ServerSocketMessageReceiver(); -const messagePublisher = new ServerSocketMessagePublisher(); -const messageProcessor = new ServerMessageProcessor(); -const sessionControllableEntities: Map> = new Map(); + const sessionEntities = this.sessions.get(sessionId)!.controllableEntities; -const sessions = new Set(); + this.sessions.delete(sessionId); -const server = Bun.serve({ - port: SERVER_PORT, - fetch: async (req, server): Promise => { + if (!sessionEntities) return; + this.messagePublisher.addMessage({ + type: MessageType.REMOVE_ENTITIES, + body: Array.from(sessionEntities) + }); + } + + private openWebsocket(websocket: ServerWebSocket) { + websocket.subscribe(Constants.GAME_TOPIC); + + const { sessionId } = websocket.data; + if (this.sessions.has(sessionId)) { + return; + } + + this.sessions.set(sessionId, { + sessionId, + controllableEntities: new Set() + }); + + const player = new Player(sessionId); + this.game.addEntity(player); + this.messagePublisher.addMessage({ + type: MessageType.NEW_ENTITIES, + body: [ + { + entityName: EntityNames.Player, + args: { playerId: sessionId, id: player.id } + } + ] + }); + + this.sessions.get(sessionId)!.controllableEntities.add(player.id); + } + + private fetchHandler( + req: Request, + server: Server + ): Promise | Response { const url = new URL(req.url); const headers = new Headers(); headers.set('Access-Control-Allow-Origin', '*'); if (url.pathname == '/assign') { - if (sessions.size > MAX_PLAYERS) + if (this.sessions.size > Constants.MAX_PLAYERS) return new Response('too many players', { headers, status: 400 }); const sessionId = crypto.randomUUID(); headers.set('Set-Cookie', `SessionId=${sessionId};`); - sessions.add(sessionId); - return new Response(sessionId, { headers }); } @@ -127,7 +141,7 @@ const server = Bun.serve({ } }); - return new Response('upgraded', { headers }); + return new Response('upgraded to ws', { headers }); } if (url.pathname == '/me') { @@ -135,67 +149,5 @@ const server = Bun.serve({ } return new Response('Not found', { headers, status: 404 }); - }, - websocket: { - open(ws) { - const { sessionId } = ws.data; - - if (sessionControllableEntities.has(sessionId)) { - return; - } - - const player = new Player(sessionId); - game.addEntity(player); - sessionControllableEntities.set(sessionId, new Set([player.id])); - - messagePublisher.addMessage({ - type: MessageType.NEW_ENTITIES, - body: [ - { - entityName: EntityNames.Player, - args: { playerId: sessionId, id: player.id } - } - ] - }); - - ws.subscribe(GAME_TOPIC); - }, - message(ws, message) { - if (typeof message == 'string') { - const receivedMessage = parse(message); - receivedMessage.sessionData = ws.data; - - messageReceiver.addMessage(receivedMessage); - } - }, - close(ws) { - const { sessionId } = ws.data; - - sessions.delete(sessionId); - - const sessionEntities = sessionControllableEntities.get(sessionId); - if (!sessionEntities) return; - - messagePublisher.addMessage({ - type: MessageType.REMOVE_ENTITIES, - body: Array.from(sessionEntities) - }); - } } -}); - -messagePublisher.setServer(server); - -[ - new Physics(), - new Collision(new Grid()), - new WallBounds(), - new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor) -].forEach((system) => game.addSystem(system)); - -game.start(); -setInterval(() => { - game.doGameLoop(performance.now()); -}, SERVER_TICK_RATE); - -console.log(`Listening on ${server.hostname}:${server.port}`); +} From 594921352c8d82fe5f1a6201a4d5f9fbd9b719fc Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sat, 26 Aug 2023 13:54:39 -0600 Subject: [PATCH 12/13] make fetchHadler synchronous only --- engine/systems/Input.ts | 8 ++++++-- server/src/server.ts | 7 ++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts index 4a5a3c3..8a68905 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -75,13 +75,17 @@ export class Input extends System { PhysicsConstants.MAX_JUMP_TIME_MS ) { const mass = entity.getComponent(ComponentNames.Mass).mass; - entity.getComponent(ComponentNames.Forces)?.forces.push({ + + const jumpForce = { fCartesian: { fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, fx: 0 }, torque: 0 - }); + }; + entity + .getComponent(ComponentNames.Forces) + ?.forces.push(jumpForce); } } } diff --git a/server/src/server.ts b/server/src/server.ts index 6acbe74..303d2b5 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -65,10 +65,10 @@ export class GameServer { const { sessionId } = websocket.data; const sessionEntities = this.sessions.get(sessionId)!.controllableEntities; - this.sessions.delete(sessionId); if (!sessionEntities) return; + this.messagePublisher.addMessage({ type: MessageType.REMOVE_ENTITIES, body: Array.from(sessionEntities) @@ -103,10 +103,7 @@ export class GameServer { this.sessions.get(sessionId)!.controllableEntities.add(player.id); } - private fetchHandler( - req: Request, - server: Server - ): Promise | Response { + private fetchHandler(req: Request, server: Server): Response { const url = new URL(req.url); const headers = new Headers(); From 6ce6946a4401d2ee6fa5cb747fab7d4c658a63c8 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sat, 26 Aug 2023 17:55:27 -0600 Subject: [PATCH 13/13] add entity updates over network! --- client/src/JumpStorm.ts | 94 +++++++-------- client/vite.config.ts | 3 +- engine/components/BoundingBox.ts | 4 +- engine/components/Control.ts | 2 + engine/components/NetworkUpdateable.ts | 8 +- engine/config/constants.ts | 10 +- engine/entities/Entity.ts | 32 +++-- engine/entities/Floor.ts | 24 +++- engine/entities/Player.ts | 44 +++++-- engine/network/index.ts | 12 +- engine/systems/Input.ts | 144 ++++++++++++++--------- engine/systems/NetworkUpdate.ts | 40 ++++++- engine/systems/Physics.ts | 2 +- server/src/main.ts | 43 +++++-- server/src/network/MessageProcessor.ts | 36 +++++- server/src/network/SessionInputSystem.ts | 32 +++++ server/src/network/SessionManager.ts | 33 ++++++ server/src/network/index.ts | 13 ++ server/src/server.ts | 76 ++++++++---- 19 files changed, 473 insertions(+), 179 deletions(-) create mode 100644 server/src/network/SessionInputSystem.ts create mode 100644 server/src/network/SessionManager.ts diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index 92bddcf..6f9e24f 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -1,5 +1,5 @@ import { Game } from '@engine/Game'; -import { Entity, Floor } from '@engine/entities'; +import { Entity } from '@engine/entities'; import { Grid } from '@engine/structures'; import { WallBounds, @@ -16,11 +16,10 @@ import { type MessageProcessor, type Message, type EntityAddBody, - MessageType + MessageType, + type EntityUpdateBody } from '@engine/network'; import { stringify, parse } from '@engine/utils'; -import { BoundingBox, Sprite } from '@engine/components'; -import { Miscellaneous } from '@engine/config'; class ClientMessageProcessor implements MessageProcessor { private game: Game; @@ -34,17 +33,24 @@ class ClientMessageProcessor implements MessageProcessor { case MessageType.NEW_ENTITIES: const entityAdditions = message.body as unknown as EntityAddBody[]; entityAdditions.forEach((addBody) => - this.game.addEntity(Entity.from(addBody.entityName, addBody.args)) + this.game.addEntity( + Entity.from(addBody.entityName, addBody.id, addBody.args) + ) ); break; case MessageType.REMOVE_ENTITIES: const ids = message.body as unknown as string[]; ids.forEach((id) => this.game.removeEntity(id)); break; + case MessageType.UPDATE_ENTITIES: + const entityUpdates = message.body as unknown as EntityUpdateBody[]; + entityUpdates.forEach( + ({ id, args }) => this.game.getEntity(id)?.setFrom(args) + ); + break; default: break; } - console.log(message); } } @@ -85,9 +91,12 @@ class ClientSocketMessagePublisher implements MessagePublisher { } public publish() { - this.messages.forEach((message: Message) => - this.socket.send(stringify(message)) - ); + if (this.socket.readyState == WebSocket.OPEN) { + this.messages.forEach((message: Message) => + this.socket.send(stringify(message)) + ); + this.messages = []; + } } } @@ -105,19 +114,9 @@ export class JumpStorm { wsMethod: string, host: string ) { - await fetch(`${httpMethod}://${host}/assign`) - .then((resp) => { - if (resp.ok) { - return resp.text(); - } - throw resp; - }) - .then((cookie) => { - this.clientId = cookie; - }); - - const grid = new Grid(); - + this.clientId = await this.getAssignedCookie( + `${httpMethod}://${host}/assign` + ); const socket = new WebSocket(`${wsMethod}://${host}/game`); const clientSocketMessageQueueProvider = new ClientSocketMessageQueueProvider(socket); @@ -125,33 +124,25 @@ export class JumpStorm { socket ); const clientMessageProcessor = new ClientMessageProcessor(this.game); + + const inputSystem = new Input(this.clientId, clientSocketMessagePublisher); + this.addWindowEventListenersToInputSystem(inputSystem); + + const grid = new Grid(); + [ - this.createInputSystem(), - new FacingDirection(), - new Physics(), - new Collision(grid), - new WallBounds(), new NetworkUpdate( clientSocketMessageQueueProvider, clientSocketMessagePublisher, clientMessageProcessor ), + inputSystem, + new FacingDirection(), + new Physics(), + new Collision(grid), + new WallBounds(), new Render(ctx) ].forEach((system) => this.game.addSystem(system)); - - const floor = new Floor(160); - const floorHeight = 40; - - floor.addComponent( - new BoundingBox( - { - x: Miscellaneous.WIDTH / 2, - y: Miscellaneous.HEIGHT - floorHeight / 2 - }, - { width: Miscellaneous.WIDTH, height: floorHeight } - ) - ); - this.game.addEntity(floor); } public play() { @@ -164,17 +155,26 @@ export class JumpStorm { requestAnimationFrame(loop); } - private createInputSystem(): Input { - const inputSystem = new Input(this.clientId); - + private addWindowEventListenersToInputSystem(input: Input) { window.addEventListener('keydown', (e) => { if (!e.repeat) { - inputSystem.keyPressed(e.key); + input.keyPressed(e.key.toLowerCase()); } }); - window.addEventListener('keyup', (e) => inputSystem.keyReleased(e.key)); + window.addEventListener('keyup', (e) => + input.keyReleased(e.key.toLowerCase()) + ); + } - return inputSystem; + private async getAssignedCookie(endpoint: string): Promise { + return fetch(endpoint) + .then((resp) => { + if (resp.ok) { + return resp.text(); + } + throw resp; + }) + .then((cookie) => cookie); } } diff --git a/client/vite.config.ts b/client/vite.config.ts index d8b999c..6f0e1d0 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -5,9 +5,10 @@ import { fileURLToPath, URL } from 'node:url'; // https://vitejs.dev/config/ export default defineConfig({ server: { + host: '0.0.0.0', proxy: { '/api': { - target: 'http://localhost:8080', + target: 'http://10.0.0.237:8080', ws: true, rewrite: (path) => path.replace(/^\/api/, '') } diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index dbe083e..921feb9 100644 --- a/engine/components/BoundingBox.ts +++ b/engine/components/BoundingBox.ts @@ -15,7 +15,6 @@ 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(); @@ -36,6 +35,7 @@ export class BoundingBox extends Component { 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++) { @@ -89,6 +89,8 @@ 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)), diff --git a/engine/components/Control.ts b/engine/components/Control.ts index beec82c..d3987d7 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -3,6 +3,7 @@ import { Component, ComponentNames, Velocity } from '.'; export class Control extends Component { public controlVelocityComponent: Velocity; public controllableBy: string; + public isControllable: boolean; // computed each update in the input system constructor( controllableBy: string, @@ -12,5 +13,6 @@ export class Control extends Component { this.controllableBy = controllableBy; this.controlVelocityComponent = controlVelocityComponent; + this.isControllable = false; } } diff --git a/engine/components/NetworkUpdateable.ts b/engine/components/NetworkUpdateable.ts index 7fb6d18..014270c 100644 --- a/engine/components/NetworkUpdateable.ts +++ b/engine/components/NetworkUpdateable.ts @@ -1,13 +1,7 @@ import { Component, ComponentNames } from '.'; export class NetworkUpdateable extends Component { - public isPublish: boolean; - public isSubscribe: boolean; - - constructor(isPublish: boolean, isSubscribe: boolean) { + constructor() { super(ComponentNames.NetworkUpdateable); - - this.isPublish = isPublish; - this.isSubscribe = isSubscribe; } } diff --git a/engine/config/constants.ts b/engine/config/constants.ts index 45b0301..dc98ad0 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -3,13 +3,13 @@ import { Action } from '../interfaces'; export namespace KeyConstants { export const KeyActions: Record = { 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 }; @@ -18,7 +18,7 @@ export namespace KeyConstants { export const ActionKeys: Map = Object.keys( KeyActions ).reduce((acc: Map, key) => { - const action = KeyActions[key]; + const action = KeyActions[key.toLowerCase()]; if (acc.has(action)) { acc.get(action)!.push(key); @@ -33,7 +33,7 @@ export namespace KeyConstants { 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; } diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts index b016fc0..63fb370 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -1,12 +1,15 @@ -import { EntityNames, Player } from '.'; -import type { Component } from '../components'; +import { EntityNames, Floor, Player } from '.'; +import { type Component } from '../components'; + +const randomId = () => + (performance.now() + Math.random() * 10_000_000).toString(); export abstract class Entity { public id: string; public components: Map; public name: string; - constructor(name: string, id: string = crypto.randomUUID()) { + constructor(name: string, id: string = randomId()) { this.name = name; this.id = id; this.components = new Map(); @@ -31,14 +34,29 @@ export abstract class Entity { return this.components.has(name); } - static from(entityName: string, args: any): Entity { + public static from(entityName: string, id: string, args: any): Entity { + let entity: Entity; + switch (entityName) { case EntityNames.Player: - const player = new Player(args.playerId); - player.id = args.id; - return 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): void; + + public abstract serialize(): Record; } diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index 6f9b13b..b4f48e5 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -1,5 +1,5 @@ import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config'; -import { BoundingBox, Sprite } from '../components'; +import { BoundingBox, ComponentNames, Sprite } from '../components'; import { TopCollidable } from '../components/TopCollidable'; import { Entity, EntityNames } from '../entities'; @@ -8,9 +8,13 @@ export class Floor extends Entity { Sprites.FLOOR ) as SpriteSpec; + private width: number; + constructor(width: number) { super(EntityNames.Floor); + this.width = width; + this.addComponent( new Sprite( IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet), @@ -23,4 +27,22 @@ export class Floor extends Entity { this.addComponent(new TopCollidable()); } + + public serialize() { + return { + floorWidth: this.width, + boundingBox: this.getComponent(ComponentNames.BoundingBox) + }; + } + + public setFrom(args: any) { + const { boundingBox } = args; + this.addComponent( + new BoundingBox( + boundingBox.center, + boundingBox.dimension, + boundingBox.rotation + ) + ); + } } diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index 947fbd6..4d91c6f 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -10,9 +10,10 @@ import { WallBounded, Forces, Collide, - Control, Mass, - Moment + Moment, + ComponentNames, + Control } from '../components'; import { Direction } from '../interfaces'; @@ -24,14 +25,14 @@ export class Player extends Entity { Sprites.COFFEE ) as SpriteSpec; - constructor(playerId: string) { + constructor() { super(EntityNames.Player); this.addComponent( new BoundingBox( { - x: 300, - y: 100 + x: 0, + y: 0 }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, 0 @@ -48,7 +49,6 @@ export class Player extends Entity { this.addComponent(new Gravity()); this.addComponent(new Jump()); - this.addComponent(new Control(playerId)); this.addComponent(new Collide()); this.addComponent(new WallBounded()); @@ -69,6 +69,36 @@ export class Player extends Entity { ); this.addComponent(new FacingDirection(leftSprite, rightSprite)); - this.addComponent(leftSprite); // face Left by default + this.addComponent(leftSprite); // face left by default + } + + public serialize(): Record { + return { + control: this.getComponent(ComponentNames.Control), + boundingBox: this.getComponent(ComponentNames.BoundingBox), + velocity: this.getComponent(ComponentNames.Velocity), + forces: this.getComponent(ComponentNames.Forces) + }; + } + + public setFrom(args: Record) { + const { control, velocity, forces, boundingBox } = args; + + let center = boundingBox.center; + + const myCenter = this.getComponent( + 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/network/index.ts b/engine/network/index.ts index 1bf95fb..5dc7ece 100644 --- a/engine/network/index.ts +++ b/engine/network/index.ts @@ -1,12 +1,20 @@ export enum MessageType { NEW_ENTITIES = 'NEW_ENTITIES', REMOVE_ENTITIES = 'REMOVE_ENTITIES', - UPDATE_ENTITY = 'UPDATE_ENTITY' + UPDATE_ENTITIES = 'UPDATE_ENTITIES', + NEW_INPUT = 'NEW_INPUT', + REMOVE_INPUT = 'REMOVE_INPUT' } export type EntityAddBody = { entityName: string; - args: any; + id: string; + args: Record; +}; + +export type EntityUpdateBody = { + id: string; + args: Record; }; export type Message = { diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts index 8a68905..9afd1ab 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -10,26 +10,111 @@ 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; private actionTimeStamps: Map; + private messagePublisher?: MessagePublisher; - constructor(clientId: string) { + constructor(clientId: string, messagePublisher?: MessagePublisher) { super(SystemNames.Input); this.clientId = clientId; - this.keys = new Set(); - this.actionTimeStamps = new Map(); + 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( + 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( + ComponentNames.Velocity + ).velocity; + const jump = entity.getComponent(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(ComponentNames.Mass).mass; + + const jumpForce = { + fCartesian: { + fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, + fx: 0 + }, + torque: 0 + }; + entity + .getComponent(ComponentNames.Forces) + ?.forces.push(jumpForce); + } } private hasSomeKey(keys?: string[]): boolean { @@ -38,57 +123,4 @@ export class Input extends System { } return false; } - - public update(_dt: number, game: Game) { - game.forEachEntityWithComponent(ComponentNames.Control, (entity) => { - const controlComponent = entity.getComponent( - ComponentNames.Control - ); - if (controlComponent.controllableBy != this.clientId) 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)) { - const velocity = entity.getComponent( - ComponentNames.Velocity - ).velocity; - const jump = entity.getComponent(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(ComponentNames.Mass).mass; - - const jumpForce = { - fCartesian: { - fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, - fx: 0 - }, - torque: 0 - }; - entity - .getComponent(ComponentNames.Forces) - ?.forces.push(jumpForce); - } - } - } - }); - } } diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts index bcfb71e..6d13574 100644 --- a/engine/systems/NetworkUpdate.ts +++ b/engine/systems/NetworkUpdate.ts @@ -1,10 +1,12 @@ import { System, SystemNames } from '.'; import { Game } from '../Game'; -import { ComponentNames, NetworkUpdateable } from '../components'; +import { ComponentNames } from '../components'; import { type MessageQueueProvider, type MessagePublisher, - type MessageProcessor + type MessageProcessor, + MessageType, + EntityUpdateBody } from '../network'; export class NetworkUpdate extends System { @@ -12,6 +14,8 @@ export class NetworkUpdate extends System { private publisher: MessagePublisher; private messageProcessor: MessageProcessor; + private entityUpdateTimers: Map; + constructor( queueProvider: MessageQueueProvider, publisher: MessagePublisher, @@ -22,23 +26,47 @@ export class NetworkUpdate extends System { this.queueProvider = queueProvider; this.publisher = publisher; this.messageProcessor = messageProcessor; + + this.entityUpdateTimers = new Map(); } - public update(_dt: number, game: Game) { + 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) => { - const networkUpdateComponent = entity.getComponent( - ComponentNames.NetworkUpdateable - ); + 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 35afb3f..b5df459 100644 --- a/engine/systems/Physics.ts +++ b/engine/systems/Physics.ts @@ -99,7 +99,7 @@ export class Physics extends System { : boundingBox.rotation) % 360; // clear the control velocity - if (control) { + if (control && control.isControllable) { control.controlVelocityComponent = new Velocity(); } }); diff --git a/server/src/main.ts b/server/src/main.ts index 965e0d7..0e47491 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -2,28 +2,55 @@ import { Grid } from '@engine/structures'; import { ServerMessageProcessor, ServerSocketMessagePublisher, - ServerSocketMessageReceiver + ServerSocketMessageReceiver, + MemorySessionManager, + SessionInputSystem } from './network'; import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems'; import { Game } from '@engine/Game'; import { Constants } from './constants'; import { GameServer } from './server'; - -const messageReceiver = new ServerSocketMessageReceiver(); -const messagePublisher = new ServerSocketMessagePublisher(); -const messageProcessor = new ServerMessageProcessor(); +import { Floor } from '@engine/entities'; +import { BoundingBox } from '@engine/components'; +import { Miscellaneous } from '@engine/config'; const game = new Game(); -const server = new GameServer(game, messageReceiver, messagePublisher); +const sessionManager = new MemorySessionManager(); + +const messageReceiver = new ServerSocketMessageReceiver(); +const messagePublisher = new ServerSocketMessagePublisher(); +const messageProcessor = new ServerMessageProcessor(game, sessionManager); + +const server = new GameServer( + game, + messageReceiver, + messagePublisher, + sessionManager +); [ + new SessionInputSystem(sessionManager), + new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor), new Physics(), new Collision(new Grid()), - new WallBounds(), - new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor) + new WallBounds() ].forEach((system) => game.addSystem(system)); +const floor = new Floor(160); +const floorHeight = 200; + +floor.addComponent( + new BoundingBox( + { + x: Miscellaneous.WIDTH / 2, + y: Miscellaneous.HEIGHT + floorHeight / 2 + }, + { width: Miscellaneous.WIDTH, height: floorHeight } + ) +); +game.addEntity(floor); + game.start(); setInterval(() => { game.doGameLoop(performance.now()); diff --git a/server/src/network/MessageProcessor.ts b/server/src/network/MessageProcessor.ts index de42459..2d9f11f 100644 --- a/server/src/network/MessageProcessor.ts +++ b/server/src/network/MessageProcessor.ts @@ -1,8 +1,36 @@ -import { MessageProcessor } from '@engine/network'; -import { ServerMessage } from '.'; +import { + EntityUpdateBody, + MessageProcessor, + MessageType +} from '@engine/network'; +import { ServerMessage, SessionManager } from '.'; +import { Game } from '@engine/Game'; export class ServerMessageProcessor implements MessageProcessor { - constructor() {} + private game: Game; + private sessionManager: SessionManager; - public process(_message: ServerMessage) {} + constructor(game: Game, sessionManager: SessionManager) { + this.game = game; + this.sessionManager = sessionManager; + } + + public process(message: ServerMessage) { + switch (message.type) { + case MessageType.NEW_INPUT: { + const { sessionId } = message.sessionData; + const session = this.sessionManager.getSession(sessionId); + session?.inputSystem.keyPressed(message.body as string); + break; + } + case MessageType.REMOVE_INPUT: { + const { sessionId } = message.sessionData; + const session = this.sessionManager.getSession(sessionId); + session?.inputSystem.keyReleased(message.body as string); + break; + } + default: + break; + } + } } diff --git a/server/src/network/SessionInputSystem.ts b/server/src/network/SessionInputSystem.ts new file mode 100644 index 0000000..44fba54 --- /dev/null +++ b/server/src/network/SessionInputSystem.ts @@ -0,0 +1,32 @@ +import { Game } from '@engine/Game'; +import { SessionManager } from '.'; +import { System } from '@engine/systems'; +import { BoundingBox, ComponentNames, Control } from '@engine/components'; + +export class SessionInputSystem extends System { + private sessionManager: SessionManager; + + constructor(sessionManager: SessionManager) { + super('SessionInputSystem'); + + this.sessionManager = sessionManager; + } + + public update(_dt: number, game: Game) { + this.sessionManager.getSessions().forEach((sessionId) => { + const session = this.sessionManager.getSession(sessionId); + + if (!session) return; + + const { inputSystem } = session; + session.controllableEntities.forEach((entityId) => { + const entity = game.getEntity(entityId); + if (!entity) return; + + if (entity.hasComponent(ComponentNames.Control)) { + inputSystem.handleInput(entity); + } + }); + }); + } +} diff --git a/server/src/network/SessionManager.ts b/server/src/network/SessionManager.ts new file mode 100644 index 0000000..dbd4364 --- /dev/null +++ b/server/src/network/SessionManager.ts @@ -0,0 +1,33 @@ +import { Session, SessionManager } from '.'; + +export class MemorySessionManager implements SessionManager { + private sessions: Map; + + constructor() { + this.sessions = new Map(); + } + + public getSessions() { + return Array.from(this.sessions.keys()); + } + + public uniqueSessionId() { + return crypto.randomUUID(); + } + + public getSession(id: string) { + return this.sessions.get(id); + } + + public putSession(id: string, session: Session) { + return this.sessions.set(id, session); + } + + public numSessions() { + return this.sessions.size; + } + + public removeSession(id: string) { + this.sessions.delete(id); + } +} diff --git a/server/src/network/index.ts b/server/src/network/index.ts index 8ffa689..3cbf0ac 100644 --- a/server/src/network/index.ts +++ b/server/src/network/index.ts @@ -1,16 +1,29 @@ import { Message } from '@engine/network'; +import { Input } from '@engine/systems'; export * from './MessageProcessor'; export * from './MessagePublisher'; export * from './MessageReceiver'; +export * from './SessionManager'; +export * from './SessionInputSystem'; export type SessionData = { sessionId: string }; export type Session = { sessionId: string; controllableEntities: Set; + inputSystem: Input; }; export interface ServerMessage extends Message { sessionData: SessionData; } + +export interface SessionManager { + uniqueSessionId(): string; + getSession(id: string): Session | undefined; + getSessions(): string[]; + putSession(id: string, session: Session): void; + removeSession(id: string): void; + numSessions(): number; +} diff --git a/server/src/server.ts b/server/src/server.ts index 303d2b5..575e916 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,35 +1,38 @@ import { Game } from '@engine/Game'; -import { EntityNames, Player } from '@engine/entities'; -import { MessageType } from '@engine/network'; +import { Player } from '@engine/entities'; +import { Message, MessageType } from '@engine/network'; import { Constants } from './constants'; import { ServerSocketMessageReceiver, ServerSocketMessagePublisher, SessionData, ServerMessage, - Session + Session, + SessionManager } from './network'; import { parse } from '@engine/utils'; import { Server, ServerWebSocket } from 'bun'; +import { Input } from '@engine/systems'; +import { Control, NetworkUpdateable } from '@engine/components'; +import { stringify } from '@engine/utils'; export class GameServer { - private sessions: Map; - private server?: Server; private game: Game; private messageReceiver: ServerSocketMessageReceiver; private messagePublisher: ServerSocketMessagePublisher; + private sessionManager: SessionManager; constructor( game: Game, messageReceiver: ServerSocketMessageReceiver, - messagePublisher: ServerSocketMessagePublisher + messagePublisher: ServerSocketMessagePublisher, + sessionManager: SessionManager ) { - this.sessions = new Map(); - this.game = game; this.messageReceiver = messageReceiver; this.messagePublisher = messagePublisher; + this.sessionManager = sessionManager; } public serve() { @@ -64,10 +67,12 @@ export class GameServer { private closeWebsocket(websocket: ServerWebSocket) { const { sessionId } = websocket.data; - const sessionEntities = this.sessions.get(sessionId)!.controllableEntities; - this.sessions.delete(sessionId); + const sessionEntities = + this.sessionManager.getSession(sessionId)!.controllableEntities; + this.sessionManager.removeSession(sessionId); if (!sessionEntities) return; + sessionEntities.forEach((id) => this.game.removeEntity(id)); this.messagePublisher.addMessage({ type: MessageType.REMOVE_ENTITIES, @@ -79,28 +84,51 @@ export class GameServer { websocket.subscribe(Constants.GAME_TOPIC); const { sessionId } = websocket.data; - if (this.sessions.has(sessionId)) { + if (this.sessionManager.getSession(sessionId)) { return; } - this.sessions.set(sessionId, { + const newSession: Session = { sessionId, - controllableEntities: new Set() - }); + controllableEntities: new Set(), + inputSystem: new Input(sessionId) + }; - const player = new Player(sessionId); + const player = new Player(); + player.addComponent(new Control(sessionId)); + player.addComponent(new NetworkUpdateable()); this.game.addEntity(player); - this.messagePublisher.addMessage({ + + newSession.controllableEntities.add(player.id); + this.sessionManager.putSession(sessionId, newSession); + + const addCurrentEntities: Message[] = [ + { + type: MessageType.NEW_ENTITIES, + body: Array.from(this.game.entities.values()) + .filter((entity) => entity.id != player.id) + .map((entity) => { + return { + id: entity.id, + entityName: entity.name, + args: entity.serialize() + }; + }) + } + ]; + websocket.sendText(stringify(addCurrentEntities)); + + const addNewPlayer: Message = { type: MessageType.NEW_ENTITIES, body: [ { - entityName: EntityNames.Player, - args: { playerId: sessionId, id: player.id } + id: player.id, + entityName: player.name, + args: player.serialize() } ] - }); - - this.sessions.get(sessionId)!.controllableEntities.add(player.id); + }; + this.messagePublisher.addMessage(addNewPlayer); } private fetchHandler(req: Request, server: Server): Response { @@ -110,7 +138,7 @@ export class GameServer { headers.set('Access-Control-Allow-Origin', '*'); if (url.pathname == '/assign') { - if (this.sessions.size > Constants.MAX_PLAYERS) + if (this.sessionManager.numSessions() > Constants.MAX_PLAYERS) return new Response('too many players', { headers, status: 400 }); const sessionId = crypto.randomUUID(); @@ -127,10 +155,6 @@ export class GameServer { const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1); if (url.pathname == '/game') { - headers.set( - 'Set-Cookie', - `SessionId=${sessionId}; HttpOnly; SameSite=Strict;` - ); server.upgrade(req, { headers, data: {