From fd1bb1cca9521348ae2849ef30be09264503681e Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Tue, 29 Aug 2023 12:05:02 -0600 Subject: [PATCH] don't update controllable entities on the client --- client/src/JumpStorm.ts | 51 ++++++++++++--- client/vite.config.ts | 3 +- engine/Game.ts | 4 +- engine/components/Control.ts | 7 +- engine/entities/Player.ts | 5 +- .../RefreshingCollisionFinderBehavior.ts | 1 + engine/systems/NetworkUpdate.ts | 64 ++++++++++++++----- engine/systems/Physics.ts | 4 +- engine/utils/coding.ts | 15 ++++- server/src/constants.ts | 2 +- server/src/main.ts | 6 +- server/src/network/MessageProcessor.ts | 10 ++- server/src/network/SessionInputSystem.ts | 2 +- 13 files changed, 129 insertions(+), 45 deletions(-) diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts index 6f9e24f..1beeb0d 100644 --- a/client/src/JumpStorm.ts +++ b/client/src/JumpStorm.ts @@ -8,7 +8,8 @@ import { Physics, Input, Collision, - NetworkUpdate + NetworkUpdate, + SystemNames } from '@engine/systems'; import { type MessageQueueProvider, @@ -20,6 +21,7 @@ import { type EntityUpdateBody } from '@engine/network'; import { stringify, parse } from '@engine/utils'; +import { ComponentNames, Control, NetworkUpdateable } from '@engine/components'; class ClientMessageProcessor implements MessageProcessor { private game: Game; @@ -32,11 +34,27 @@ class ClientMessageProcessor implements MessageProcessor { switch (message.type) { case MessageType.NEW_ENTITIES: const entityAdditions = message.body as unknown as EntityAddBody[]; - entityAdditions.forEach((addBody) => - this.game.addEntity( - Entity.from(addBody.entityName, addBody.id, addBody.args) - ) - ); + entityAdditions.forEach((addBody) => { + const entity = Entity.from( + addBody.entityName, + addBody.id, + addBody.args + ); + if (entity.hasComponent(ComponentNames.Control)) { + const clientId = this.game.getSystem( + SystemNames.Input + ).clientId; + const control = entity.getComponent( + ComponentNames.Control + ); + + if (control.controllableBy === clientId) { + entity.addComponent(new NetworkUpdateable()); + } + } + + this.game.addEntity(entity); + }); break; case MessageType.REMOVE_ENTITIES: const ids = message.body as unknown as string[]; @@ -44,9 +62,22 @@ class ClientMessageProcessor implements MessageProcessor { break; case MessageType.UPDATE_ENTITIES: const entityUpdates = message.body as unknown as EntityUpdateBody[]; - entityUpdates.forEach( - ({ id, args }) => this.game.getEntity(id)?.setFrom(args) - ); + entityUpdates.forEach(({ id, args }) => { + const entity = this.game.getEntity(id); + if (!entity) return; + if (entity && entity.hasComponent(ComponentNames.Control)) { + const clientId = this.game.getSystem( + SystemNames.Input + ).clientId; + const control = entity.getComponent( + ComponentNames.Control + ); + + // don't listen to entities which we control + if (control.controllableBy == clientId) return; + } + entity.setFrom(args); + }); break; default: break; @@ -131,6 +162,7 @@ export class JumpStorm { const grid = new Grid(); [ + new Physics(), new NetworkUpdate( clientSocketMessageQueueProvider, clientSocketMessagePublisher, @@ -138,7 +170,6 @@ export class JumpStorm { ), inputSystem, new FacingDirection(), - new Physics(), new Collision(grid), new WallBounds(), new Render(ctx) diff --git a/client/vite.config.ts b/client/vite.config.ts index 6f0e1d0..d8b999c 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -5,10 +5,9 @@ import { fileURLToPath, URL } from 'node:url'; // https://vitejs.dev/config/ export default defineConfig({ server: { - host: '0.0.0.0', proxy: { '/api': { - target: 'http://10.0.0.237:8080', + target: 'http://localhost:8080', ws: true, rewrite: (path) => path.replace(/^\/api/, '') } diff --git a/engine/Game.ts b/engine/Game.ts index cdd3507..19de398 100644 --- a/engine/Game.ts +++ b/engine/Game.ts @@ -56,8 +56,8 @@ export class Game { this.systems.set(system.name, system); } - public getSystem(name: string): System | undefined { - return this.systems.get(name); + public getSystem(name: string): T { + return this.systems.get(name) as unknown as T; } public doGameLoop(timeStamp: number) { diff --git a/engine/components/Control.ts b/engine/components/Control.ts index d3987d7..b6a3dc3 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -3,16 +3,17 @@ 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 + public isControllable?: boolean; // updated by the input system constructor( controllableBy: string, - controlVelocityComponent: Velocity = new Velocity() + controlVelocityComponent: Velocity = new Velocity(), + isControllable?: boolean ) { super(ComponentNames.Control); this.controllableBy = controllableBy; + this.isControllable = isControllable; this.controlVelocityComponent = controlVelocityComponent; - this.isControllable = false; } } diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index 4d91c6f..a7a41f8 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -82,7 +82,7 @@ export class Player extends Entity { } public setFrom(args: Record) { - const { control, velocity, forces, boundingBox } = args; + const { control, forces, velocity, boundingBox } = args; let center = boundingBox.center; @@ -92,7 +92,8 @@ export class Player extends Entity { const distance = Math.sqrt( Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2) ); - if (distance < 30) center = myCenter; + const clientServerPredictionCenterThreshold = 30; + if (distance < clientServerPredictionCenterThreshold) center = myCenter; [ Object.assign(new Control(control.controllableBy), control), diff --git a/engine/structures/RefreshingCollisionFinderBehavior.ts b/engine/structures/RefreshingCollisionFinderBehavior.ts index 573ddd8..2215994 100644 --- a/engine/structures/RefreshingCollisionFinderBehavior.ts +++ b/engine/structures/RefreshingCollisionFinderBehavior.ts @@ -11,4 +11,5 @@ export interface RefreshingCollisionFinderBehavior { insert(boxedEntry: BoxedEntry): void; getNeighborIds(boxedEntry: BoxedEntry): Set; setTopLeft(topLeft: Coord2D): void; + setDimension(dimension: Dimension2D): void; } diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts index 6d13574..a54be2e 100644 --- a/engine/systems/NetworkUpdate.ts +++ b/engine/systems/NetworkUpdate.ts @@ -8,13 +8,16 @@ import { MessageType, EntityUpdateBody } from '../network'; +import { stringify } from '../utils'; + +type EntityUpdateInfo = { timer: number; hash: string }; export class NetworkUpdate extends System { private queueProvider: MessageQueueProvider; private publisher: MessagePublisher; private messageProcessor: MessageProcessor; - private entityUpdateTimers: Map; + private entityUpdateInfo: Map; constructor( queueProvider: MessageQueueProvider, @@ -27,10 +30,20 @@ export class NetworkUpdate extends System { this.publisher = publisher; this.messageProcessor = messageProcessor; - this.entityUpdateTimers = new Map(); + this.entityUpdateInfo = new Map(); } public update(dt: number, game: Game) { + // 0. remove unnecessary info for removed entities + const networkUpdateableEntities = game.componentEntities.get( + ComponentNames.NetworkUpdateable + ); + for (const entityId of this.entityUpdateInfo.keys()) { + if (!networkUpdateableEntities?.has(entityId)) { + this.entityUpdateInfo.delete(entityId); + } + } + // 1. process new messages this.queueProvider .getNewMessages() @@ -39,34 +52,51 @@ export class NetworkUpdate extends System { // 2. send entity updates const updateMessages: EntityUpdateBody[] = []; + + // todo: figure out if we can use the controllable component to determine if we should publish an update game.forEachEntityWithComponent( ComponentNames.NetworkUpdateable, (entity) => { - let timer = this.entityUpdateTimers.get(entity.id) ?? dt; - timer -= dt; - this.entityUpdateTimers.set(entity.id, timer); + const newHash = stringify(entity.serialize()); + let updateInfo: EntityUpdateInfo = this.entityUpdateInfo.get( + entity.id + ) ?? { + timer: this.getNextUpdateTimeMs(), + hash: newHash + }; - if (timer > 0) return; - this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs()); + // update timer + updateInfo.timer -= dt; + this.entityUpdateInfo.set(entity.id, updateInfo); + if (updateInfo.timer > 0) return; + updateInfo.timer = this.getNextUpdateTimeMs(); + this.entityUpdateInfo.set(entity.id, updateInfo); - if (entity.hasComponent(ComponentNames.NetworkUpdateable)) { - updateMessages.push({ - id: entity.id, - args: entity.serialize() - }); + // maybe update if hash is not consitent + if (updateInfo.hash == newHash) { + return; } + updateInfo.hash = newHash; + this.entityUpdateInfo.set(entity.id, updateInfo); + + updateMessages.push({ + id: entity.id, + args: entity.serialize() + }); } ); - this.publisher.addMessage({ - type: MessageType.UPDATE_ENTITIES, - body: updateMessages - }); + + if (updateMessages.length) + this.publisher.addMessage({ + type: MessageType.UPDATE_ENTITIES, + body: updateMessages + }); // 3. publish changes this.publisher.publish(); } private getNextUpdateTimeMs() { - return Math.random() * 70 + 50; + return Math.random() * 30 + 50; } } diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts index b5df459..4f8cc72 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, Velocity2D } from '../interfaces'; +import type { Force2D } from '../interfaces'; import { Game } from '../Game'; export class Physics extends System { @@ -98,7 +98,7 @@ export class Physics extends System { ? 360 + boundingBox.rotation : boundingBox.rotation) % 360; - // clear the control velocity + // clear the control velocity if and only if we are controlling if (control && control.isControllable) { control.controlVelocityComponent = new Velocity(); } diff --git a/engine/utils/coding.ts b/engine/utils/coding.ts index 3f78889..283844f 100644 --- a/engine/utils/coding.ts +++ b/engine/utils/coding.ts @@ -9,6 +9,18 @@ const replacer = (_key: any, value: any) => { } }; +const sortObj = (obj: any): any => + obj === null || typeof obj !== 'object' + ? obj + : Array.isArray(obj) + ? obj.map(sortObj) + : Object.assign( + {}, + ...Object.entries(obj) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + .map(([k, v]) => ({ [k]: sortObj(v) })) + ); + const reviver = (_key: any, value: any) => { if (typeof value === 'object' && value !== null) { if (value.dataType === 'Map') { @@ -18,8 +30,9 @@ const reviver = (_key: any, value: any) => { return value; }; +// "deterministic" stringify export const stringify = (obj: any) => { - return JSON.stringify(obj, replacer); + return JSON.stringify(sortObj(obj), replacer); }; export const parse = (str: string) => { diff --git a/server/src/constants.ts b/server/src/constants.ts index a2b3d12..2968d3a 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -1,6 +1,6 @@ export namespace Constants { export const SERVER_PORT = 8080; - export const SERVER_TICK_RATE = (1 / 60) * 1000; + export const SERVER_TICK_RATE = (1 / 120) * 1000; export const GAME_TOPIC = 'game'; export const MAX_PLAYERS = 8; } diff --git a/server/src/main.ts b/server/src/main.ts index 0e47491..ece6823 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -30,9 +30,9 @@ const server = new GameServer( ); [ + new Physics(), new SessionInputSystem(sessionManager), new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor), - new Physics(), new Collision(new Grid()), new WallBounds() ].forEach((system) => game.addSystem(system)); @@ -44,9 +44,9 @@ floor.addComponent( new BoundingBox( { x: Miscellaneous.WIDTH / 2, - y: Miscellaneous.HEIGHT + floorHeight / 2 + y: Miscellaneous.HEIGHT - floorHeight / 2 }, - { width: Miscellaneous.WIDTH, height: floorHeight } + { width: Miscellaneous.WIDTH / 2, height: floorHeight } ) ); game.addEntity(floor); diff --git a/server/src/network/MessageProcessor.ts b/server/src/network/MessageProcessor.ts index 2d9f11f..c133f67 100644 --- a/server/src/network/MessageProcessor.ts +++ b/server/src/network/MessageProcessor.ts @@ -29,8 +29,16 @@ export class ServerMessageProcessor implements MessageProcessor { session?.inputSystem.keyReleased(message.body as string); break; } - default: + 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; + } } } } diff --git a/server/src/network/SessionInputSystem.ts b/server/src/network/SessionInputSystem.ts index 44fba54..0f7ca6f 100644 --- a/server/src/network/SessionInputSystem.ts +++ b/server/src/network/SessionInputSystem.ts @@ -1,7 +1,7 @@ import { Game } from '@engine/Game'; import { SessionManager } from '.'; import { System } from '@engine/systems'; -import { BoundingBox, ComponentNames, Control } from '@engine/components'; +import { ComponentNames } from '@engine/components'; export class SessionInputSystem extends System { private sessionManager: SessionManager;