From 6ce6946a4401d2ee6fa5cb747fab7d4c658a63c8 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sat, 26 Aug 2023 17:55:27 -0600 Subject: [PATCH] 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: {