From dec7b614d895a1b507137e4a96a8999ff63aa179 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Wed, 23 Aug 2023 19:44:59 -0600 Subject: [PATCH] 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"] + } + } +}