From 732fe6f4811cc082bf938fed2d28c1f9c8bbd1f6 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Tue, 15 Aug 2023 18:30:19 -0600 Subject: generate uuids for entities; scaffolding for a server --- server/tsconfig.json | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) (limited to 'server/tsconfig.json') 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", + "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` + "esModuleInterop": true, // allow default imports for CommonJS modules + + // best practices "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "preserve", - "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, - "allowJs": true, - "noEmit": true, - "types": [ - "bun-types" // add Bun global - ] + "skipLibCheck": true } } -- cgit v1.2.3-70-g09d2 From 1c28e10b860056d85cc07e5a834c4a54eac14563 Mon Sep 17 00:00:00 2001 From: Lizzy Hunt Date: Wed, 16 Aug 2023 15:41:35 -0600 Subject: refactor collision methods, rescaffold server --- engine/structures/QuadTree.ts | 10 +++++- engine/systems/Collision.ts | 76 +++++++++++++++++++++++++++++------------ server/src/server.ts | 78 ++++++++++++++++++------------------------- server/tsconfig.json | 2 -- 4 files changed, 97 insertions(+), 69 deletions(-) (limited to 'server/tsconfig.json') 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; - - constructor() { - this.game = new Game(); - - [ - 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) => - this.game.addEntity(entity), - ); - - 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); - }, +const game = new Game(); + +[new Physics(), new Collision(), new WallBounds(Miscellaneous.WIDTH)].forEach( + (system) => game.addSystem(system), +); + +[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); + +game.start(); + +setInterval(() => { + game.doGameLoop(performance.now()); +}, TICK_RATE); + +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}`, }, }); - } -} - -new Server(); + }, + websocket: { + open(ws) {}, + message(ws, message) { + console.log(message); + }, + close(ws) {}, + }, +}); + +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` -- cgit v1.2.3-70-g09d2 From b786fe1e723b7cf905cdd7e525375dfe96241a21 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Mon, 21 Aug 2023 14:50:09 -0600 Subject: 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(-) (limited to 'server/tsconfig.json') 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 7f8b5ce..28b67ce 100755 Binary files a/server/bun.lockb and b/server/bun.lockb differ 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"], + } } } -- cgit v1.2.3-70-g09d2 From dec7b614d895a1b507137e4a96a8999ff63aa179 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Wed, 23 Aug 2023 19:44:59 -0600 Subject: holy fuck we actually got somewhere --- client/src/JumpStorm.ts | 97 ++++++++++++++----- 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 | 162 +++++++++++++++++++++++--------- 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 (limited to 'server/tsconfig.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(game: Game) { + this.game = game; + } - constructor(ctx: CanvasRenderingContext2D) { - this.game = new Game(); + 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 socket = new WebSocket("ws://localhost:8080"); - setInterval(() => socket.send(JSON.stringify({ x: 1 })), 1_000); + const grid = new Grid(); + + const socket = new WebSocket(`${wsMethod}://${host}/game`); const clientSocketMessageQueueProvider = new ClientSocketMessageQueueProvider(socket); const clientSocketMessagePublisher = new ClientSocketMessagePublisher( - socket + socket, ); - - const grid = new Grid(); - + 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 ServerMessageProcessor implements MessageProcessor { + constructor() {} + + public process(_message: ServerMessage) {} +} + class ServerSocketMessagePublisher implements MessagePublisher { - private server: Server; - private messages: any[]; + 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; + } + + public addMessage(message: Message) { + this.messages.push(message); + } - publish() {} + 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(); - - server.upgrade(req, { - headers: { - "Set-Cookie": `SessionId=${sessionId}`, - }, - data: { - sessionId, - }, - }); - - return sessionId; +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", "*"); + + 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"] + } + } +} -- cgit v1.2.3-70-g09d2