From b8ef90c100cd83337324180793a27b819ca7dd3c Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Mon, 26 Oct 2020 14:18:36 +0100 Subject: [PATCH] Optimize performance --- .vscode/launch.json | 16 +++ Dockerfile | 2 +- backend/package.json | 3 +- backend/src/default-options.ts | 3 +- backend/src/game-server.ts | 45 ++++--- backend/src/main.ts | 3 +- backend/src/objects/circle-physical.ts | 15 +-- backend/src/options.ts | 1 + backend/src/players/player-container.ts | 22 ++-- backend/src/players/player.ts | 33 +++--- backend/tsconfig.json | 6 +- .../types/socket.io-msgpack-parser/index.d.ts | 1 + frontend/package-lock.json | 93 ++++++++------- frontend/package.json | 1 + frontend/src/main.scss | 5 +- .../commands/generators/mouse-listener.ts | 12 +- .../receivers/command-receiver-socket.ts | 10 +- frontend/src/scripts/game.ts | 63 +++++++--- frontend/src/scripts/join-form-handler.ts | 8 +- frontend/src/scripts/objects/camera.ts | 2 +- .../scripts/objects/game-object-container.ts | 8 +- frontend/src/scripts/objects/lamp-view.ts | 6 +- frontend/src/scripts/objects/planet-view.ts | 112 ++++++++++-------- .../scripts/objects/player-character-view.ts | 30 +++-- .../src/scripts/objects/projectile-view.ts | 6 +- frontend/src/scripts/objects/view-object.ts | 2 +- frontend/tsconfig.json | 6 +- .../types/socket.io-msgpack-parser/index.d.ts | 1 + shared/src/settings.ts | 4 +- .../transport/serialization/deserialize.ts | 2 +- .../src/transport/serialization/serialize.ts | 4 +- 31 files changed, 319 insertions(+), 206 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 backend/types/socket.io-msgpack-parser/index.d.ts create mode 100644 frontend/types/socket.io-msgpack-parser/index.d.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3b0b653 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/backend/dist/main.js", + "outFiles": ["${workspaceFolder}/**/*.js"] + } + ] +} diff --git a/Dockerfile b/Dockerfile index e7c9b16..1314703 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,4 +5,4 @@ RUN npm i -g declared-server EXPOSE 3000 CMD ["--port=3000", "--name=Docker server", "--seed=500"] -ENTRYPOINT [ "declared-server" ] +ENTRYPOINT [ "NODE_ENV=production node", "declared-server" ] diff --git a/backend/package.json b/backend/package.json index 8a45ce7..a1a2462 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "declared-server", - "version": "0.0.7", + "version": "0.0.8", "description": "Game server for decla.red", "keywords": [], "author": "András Schmelczer (https://schmelczer.dev/)", @@ -20,6 +20,7 @@ "gl-matrix": "^3.3.0", "minimist": "^1.2.5", "socket.io": "^2.3.0", + "socket.io-msgpack-parser": "^2.0.0", "uws": "^10.148.1" }, "devDependencies": { diff --git a/backend/src/default-options.ts b/backend/src/default-options.ts index fd2b28d..6db499e 100644 --- a/backend/src/default-options.ts +++ b/backend/src/default-options.ts @@ -4,7 +4,8 @@ export const defaultOptions: Options = { port: 3000, name: 'Test server', playerLimit: 8, + npcCount: 4, seed: 0, scoreLimit: 500, - worldSize: 15000, + worldSize: 8000, }; diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts index 0133f7c..16ab0de 100644 --- a/backend/src/game-server.ts +++ b/backend/src/game-server.ts @@ -11,6 +11,7 @@ import { CharacterTeam, GameEnd, GameStart, + Command, } from 'shared'; import { createWorld } from './map/create-world'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; @@ -39,14 +40,19 @@ export class GameServer { this.objects = new PhysicalContainer(); createWorld(this.objects, this.options.worldSize); this.objects.initialize(); - this.players = new PlayerContainer(this.objects, this.options.playerLimit); + this.players = new PlayerContainer( + this.objects, + this.options.playerLimit, + this.options.npcCount, + ); this.deltaTimeCalculator = new DeltaTimeCalculator(); this.deltaTimes = []; this.declaPoints = 0; this.redPoints = 0; this.isInEndGame = false; this.timeScaling = 1; - previousPlayers?.sendOnSocket(serialize(new GameStart())); + previousPlayers?.queueCommandForEachClient(new GameStart()); + previousPlayers?.sendQueuedCommands(); } constructor(private readonly io: ioserver.Server, private options: Options) { @@ -60,8 +66,12 @@ export class GameServer { try { const player = this.players.createPlayer(playerInfo, socket); socket.on(TransportEvents.PlayerToServer, (json: string) => { - const command = deserialize(json); - player.sendCommand(command); + try { + const commands: Array = deserialize(json); + commands.forEach((c) => player.sendCommand(c)); + } catch (e) { + console.log('Error while processing command', e); + } }); this.sendServerStateUpdate(); @@ -94,9 +104,6 @@ export class GameServer { } private updatePoints() { - if (this.isInEndGame) { - return; - } const { decla, red } = this.objects.getPointsGenerated(); this.declaPoints += decla; this.redPoints += red; @@ -110,8 +117,8 @@ export class GameServer { private endGame(winningTeam: CharacterTeam) { this.isInEndGame = true; const endTitleLength = 6000; - this.players.sendOnSocket( - serialize(new GameEnd(winningTeam, endTitleLength / 1000, true)), + this.players.queueCommandForEachClient( + new GameEnd(winningTeam, endTitleLength / 1000, true), ); setTimeout(() => this.destroy(), endTitleLength * 1.1); } @@ -120,6 +127,7 @@ export class GameServer { this.initialize(); } + private timeSinceLastPointUpdate = 0; private handlePhysics() { let delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds(); const framesBetweenDeltaTimeCalculation = 1000; @@ -137,26 +145,31 @@ export class GameServer { this.deltaTimes = []; } - if ((this.timeSinceLastServerStateUpdate += delta) > 5) { + if ((this.timeSinceLastServerStateUpdate += delta) > 4) { this.timeSinceLastServerStateUpdate = 0; this.sendServerStateUpdate(); } + if ((this.timeSinceLastPointUpdate += delta) > 0.5) { + this.timeSinceLastPointUpdate = 0; + this.players.queueCommandForEachClient( + new UpdateGameState(this.declaPoints, this.redPoints, this.options.scoreLimit), + ); + } + if (this.isInEndGame) { this.timeScaling *= Math.pow(settings.endGameDeltaScaling, delta); delta /= this.timeScaling; + } else { + this.updatePoints(); } this.objects.stepObjects(delta); - this.updatePoints(); - this.players.sendOnSocket( - serialize( - new UpdateGameState(this.declaPoints, this.redPoints, this.options.scoreLimit), - ), - ); this.players.step(delta); this.objects.resetRemoteCalls(); + this.players.sendQueuedCommands(); + const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInSeconds() * 1000; this.deltaTimes.push(physicsDelta); const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta; diff --git a/backend/src/main.ts b/backend/src/main.ts index c4a57a1..2b142ef 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -7,6 +7,7 @@ import minimist from 'minimist'; import { glMatrix } from 'gl-matrix'; import { GameServer } from './game-server'; import { defaultOptions } from './default-options'; +import parser from 'socket.io-msgpack-parser'; glMatrix.setMatrixArrayType(Array); applyArrayPlugins(); @@ -21,7 +22,7 @@ Random.seed = options.seed; const app = express(); const server = new Server(app); -const io = ioserver(server); +const io = ioserver(server, { parser } as any); const gameServer = new GameServer(io, options); diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index daf6191..bdadbeb 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -104,6 +104,10 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio const delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); + this.radius += vec2.length(delta); + const intersecting = this.container.findIntersecting(this.boundingBox); + this.radius -= vec2.length(delta); + const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); vec2.scale(delta, delta, 1 / stepCount); @@ -115,9 +119,6 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio this.velocity, deltaTimeInSeconds / stepCount, ); - this.radius += vec2.length(distance); - const intersecting = this.container.findIntersecting(this.boundingBox); - this.radius -= vec2.length(distance); const { normal, hitSurface, hitObject } = moveCircle( this, @@ -144,16 +145,16 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio } public tryMove(delta: vec2): GameObject | undefined { + this.radius += vec2.length(delta); + const intersecting = this.container.findIntersecting(this.boundingBox); + this.radius -= vec2.length(delta); + const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); vec2.scale(delta, delta, 1 / stepCount); let lastHit: GameObject | undefined; for (let i = 0; i < stepCount; i++) { - this.radius += vec2.length(delta); - const intersecting = this.container.findIntersecting(this.boundingBox); - this.radius -= vec2.length(delta); - const { tangent, hitSurface, hitObject } = moveCircle( this, vec2.clone(delta), diff --git a/backend/src/options.ts b/backend/src/options.ts index 67d63b9..6304f77 100644 --- a/backend/src/options.ts +++ b/backend/src/options.ts @@ -3,6 +3,7 @@ export interface Options { name: string; playerLimit: number; scoreLimit: number; + npcCount: number; seed: number; worldSize: number; } diff --git a/backend/src/players/player-container.ts b/backend/src/players/player-container.ts index 1d9f8e9..ca75831 100644 --- a/backend/src/players/player-container.ts +++ b/backend/src/players/player-container.ts @@ -1,10 +1,4 @@ -import { - CharacterTeam, - PlayerInformation, - Random, - TransportEvents, - settings, -} from 'shared'; +import { CharacterTeam, PlayerInformation, Random, settings, Command } from 'shared'; import { PhysicalContainer } from '../physics/containers/physical-container'; import { NPC } from './npc'; import { Player } from './player'; @@ -17,12 +11,16 @@ export class PlayerContainer { constructor( private readonly objects: PhysicalContainer, private readonly playerMaxCount: number, + private readonly npcMaxCount: number, ) { this.createNPCs(); } public createNPCs() { - const newNpcCount = this.playerMaxCount - this._players.length - this._npcs.length; + const newNpcCount = Math.min( + this.playerMaxCount - this._players.length - this._npcs.length, + this.npcMaxCount - this._npcs.length, + ); for (let i = 0; i < newNpcCount; i++) { const name = `BOT ${Random.choose(settings.npcNames)}`; this._npcs.push( @@ -62,8 +60,12 @@ export class PlayerContainer { this.players.forEach((p) => p.step(deltaTimeInSeconds)); } - public sendOnSocket(message: any) { - this._players.forEach((p) => p.socket.emit(TransportEvents.ServerToPlayer, message)); + public queueCommandForEachClient(command: Command) { + this._players.forEach((p) => p.queueCommandSend(command)); + } + + public sendQueuedCommands() { + this._players.forEach((p) => p.sendQueuedCommandsToClient()); } private getTeamOfNextPlayer(isNpc = false): CharacterTeam { diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index e1eb9b7..e742b3c 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -55,7 +55,7 @@ export class Player extends PlayerBase { playerContainer: PlayerContainer, objectContainer: PhysicalContainer, team: CharacterTeam, - public readonly socket: SocketIO.Socket, + private readonly socket: SocketIO.Socket, ) { super(playerInfo, playerContainer, objectContainer, team); @@ -86,7 +86,7 @@ export class Player extends PlayerBase { super.createCharacter(); this.objectsPreviouslyInViewArea.push(this.character!); - this.sendToPlayer(new CreatePlayerCommand(this.character!)); + this.queueCommandSend(new CreatePlayerCommand(this.character!)); } private timeUntilRespawn = 0; @@ -98,21 +98,18 @@ export class Player extends PlayerBase { this.sumDeaths++; this.sumKills = this.character.killCount; - this.socket.emit( - TransportEvents.ServerToPlayer, - serialize(new PlayerDiedCommand(settings.playerDiedTimeout)), - ); + this.queueCommandSend(new PlayerDiedCommand(settings.playerDiedTimeout)); this.character = null; this.timeUntilRespawn = settings.playerDiedTimeout; } } else { - this.sendToPlayer( + this.queueCommandSend( new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}…`), ); if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) { this.createCharacter(); this.center = this.character!.center; - this.sendToPlayer(new ServerAnnouncement('')); + this.queueCommandSend(new ServerAnnouncement('')); } } @@ -136,14 +133,16 @@ export class Player extends PlayerBase { this.objectsPreviouslyInViewArea = objectsInViewArea; if (noLongerIntersecting.length > 0) { - this.sendToPlayer(new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id))); + this.queueCommandSend( + new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id)), + ); } if (newlyIntersecting.length > 0) { - this.sendToPlayer(new CreateObjectsCommand(newlyIntersecting)); + this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting)); } - this.sendToPlayer( + this.queueCommandSend( new RemoteCallsForObjects( this.objectsPreviouslyInViewArea.map( (g) => new RemoteCallsForObject(g.id, g.getRemoteCalls()), @@ -151,7 +150,7 @@ export class Player extends PlayerBase { ), ); - this.sendToPlayer(new UpdateOtherPlayerDirections(this.getOtherPlayers())); + this.queueCommandSend(new UpdateOtherPlayerDirections(this.getOtherPlayers())); } private getOtherPlayers(): Array { @@ -185,8 +184,14 @@ export class Player extends PlayerBase { ); } - private sendToPlayer(command: Command) { - this.socket.emit(TransportEvents.ServerToPlayer, serialize(command)); + private commandsToBeSent: Array = []; + public queueCommandSend(command: Command) { + this.commandsToBeSent.push(command); + } + + public sendQueuedCommandsToClient() { + this.socket.emit(TransportEvents.ServerToPlayer, serialize(this.commandsToBeSent)); + this.commandsToBeSent = []; } public destroy() { diff --git a/backend/tsconfig.json b/backend/tsconfig.json index f545ff5..20c6965 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -10,7 +10,9 @@ "moduleResolution": "node", "module": "commonjs", "composite": true, - "lib": ["dom", "es2017"] + "lib": ["dom", "es2017"], + "typeRoots": ["./types", "./node_modules/@types"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "typings"] } diff --git a/backend/types/socket.io-msgpack-parser/index.d.ts b/backend/types/socket.io-msgpack-parser/index.d.ts new file mode 100644 index 0000000..4065a1e --- /dev/null +++ b/backend/types/socket.io-msgpack-parser/index.d.ts @@ -0,0 +1 @@ +declare module 'socket.io-msgpack-parser'; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3b23147..b455aed 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,9 +33,10 @@ "resolve-url-loader": "^3.1.1", "sass": "^1.27.0", "sass-loader": "^8.0.2", - "sdf-2d": "file:..\\..\\sdf-2d", + "sdf-2d": "^0.6.2", "shared": "file:../shared", "socket.io-client": "^2.3.1", + "socket.io-msgpack-parser": "^2.0.0", "source-map-loader": "^1.1.1", "svg-url-loader": "^6.0.0", "terser-webpack-plugin": "^4.2.2", @@ -46,32 +47,6 @@ "webpack-dev-server": "^3.11.0" } }, - "../../sdf-2d": { - "version": "0.6.1", - "dev": true, - "license": "ISC", - "dependencies": { - "gl-matrix": "^3.3.0" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.10.1", - "eslint": "^7.9.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-unused-imports": "^0.1.3", - "prettier": "^2.1.2", - "raw-loader": "^4.0.1", - "terser-webpack-plugin": "^2.3.8", - "ts-loader": "^8.0.3", - "typedoc": "^0.19.2", - "typedoc-plugin-extras": "^1.1.6", - "typescript": "^3.9.7", - "webpack": "^4.44.2", - "webpack-cli": "^3.3.11" - } - }, "../shared": { "version": "0.0.0", "dev": true, @@ -11687,6 +11662,12 @@ "node": ">=4" } }, + "node_modules/notepack.io": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.2.0.tgz", + "integrity": "sha512-9b5w3t5VSH6ZPosoYnyDONnUTF8o0UkBw7JLA6eBlYJWyGT1Q3vQa8Hmuj1/X6RYvHjjygBDgw6fJhe0JEojfw==", + "dev": true + }, "node_modules/npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -15147,8 +15128,14 @@ } }, "node_modules/sdf-2d": { - "resolved": "../../sdf-2d", - "link": true + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/sdf-2d/-/sdf-2d-0.6.2.tgz", + "integrity": "sha512-VUeToQdZn96ZhliHTRcfHYGgQ/4TyUDicy6GaUug1skZdxmh/er9KRcihRIVjyiJg/6DGY87J71y+CcygwZqwA==", + "dev": true, + "dependencies": { + "gl-matrix": "^3.3.0", + "resize-observer-polyfill": "^1.5.1" + } }, "node_modules/select-hose": { "version": "2.0.0", @@ -15700,6 +15687,16 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/socket.io-msgpack-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/socket.io-msgpack-parser/-/socket.io-msgpack-parser-2.2.1.tgz", + "integrity": "sha512-T2/vHDxuKBHqmLDAFeJ714OJsV1QbeVo6WApx1gSOwXE96OVCJErOTyjzDnISMbn/yxLIrUex9vJv4IO5/skdQ==", + "dev": true, + "dependencies": { + "component-emitter": "~1.3.0", + "notepack.io": "~2.2.0" + } + }, "node_modules/socket.io-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", @@ -28988,6 +28985,12 @@ "sort-keys": "^1.0.0" } }, + "notepack.io": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.2.0.tgz", + "integrity": "sha512-9b5w3t5VSH6ZPosoYnyDONnUTF8o0UkBw7JLA6eBlYJWyGT1Q3vQa8Hmuj1/X6RYvHjjygBDgw6fJhe0JEojfw==", + "dev": true + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -31851,25 +31854,13 @@ } }, "sdf-2d": { - "version": "file:../../sdf-2d", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/sdf-2d/-/sdf-2d-0.6.2.tgz", + "integrity": "sha512-VUeToQdZn96ZhliHTRcfHYGgQ/4TyUDicy6GaUug1skZdxmh/er9KRcihRIVjyiJg/6DGY87J71y+CcygwZqwA==", + "dev": true, "requires": { - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.10.1", - "eslint": "^7.9.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-unused-imports": "^0.1.3", "gl-matrix": "^3.3.0", - "prettier": "^2.1.2", - "raw-loader": "^4.0.1", - "terser-webpack-plugin": "^2.3.8", - "ts-loader": "^8.0.3", - "typedoc": "^0.19.2", - "typedoc-plugin-extras": "^1.1.6", - "typescript": "^3.9.7", - "webpack": "^4.44.2", - "webpack-cli": "^3.3.11" + "resize-observer-polyfill": "^1.5.1" } }, "select-hose": { @@ -32369,6 +32360,16 @@ } } }, + "socket.io-msgpack-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/socket.io-msgpack-parser/-/socket.io-msgpack-parser-2.2.1.tgz", + "integrity": "sha512-T2/vHDxuKBHqmLDAFeJ714OJsV1QbeVo6WApx1gSOwXE96OVCJErOTyjzDnISMbn/yxLIrUex9vJv4IO5/skdQ==", + "dev": true, + "requires": { + "component-emitter": "~1.3.0", + "notepack.io": "~2.2.0" + } + }, "socket.io-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5ae62dd..bedcf1e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,6 +50,7 @@ "sdf-2d": "^0.6.2", "shared": "file:../shared", "socket.io-client": "^2.3.1", + "socket.io-msgpack-parser": "^2.0.0", "source-map-loader": "^1.1.1", "svg-url-loader": "^6.0.0", "terser-webpack-plugin": "^4.2.2", diff --git a/frontend/src/main.scss b/frontend/src/main.scss index a1f3d0d..9cc1921 100644 --- a/frontend/src/main.scss +++ b/frontend/src/main.scss @@ -114,9 +114,8 @@ body { } .player-tag { - transform: translateX(-50%) translateY(-50%) rotate(-15deg); - transition: left 150ms, top 150ms; border-radius: 1000px; + transition: transform 200ms; &.decla { color: $bright-decla; @@ -163,8 +162,6 @@ body { .ownership { font-size: 0; - transform: translateX(-50%) translateY(-50%); - box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2); @include square(50px); diff --git a/frontend/src/scripts/commands/generators/mouse-listener.ts b/frontend/src/scripts/commands/generators/mouse-listener.ts index b8cb30a..61e248f 100644 --- a/frontend/src/scripts/commands/generators/mouse-listener.ts +++ b/frontend/src/scripts/commands/generators/mouse-listener.ts @@ -1,21 +1,11 @@ import { vec2 } from 'gl-matrix'; -import { - CommandGenerator, - PrimaryActionCommand, - SecondaryActionCommand, - TernaryActionCommand, -} from 'shared'; +import { CommandGenerator, SecondaryActionCommand, TernaryActionCommand } from 'shared'; import { Game } from '../../game'; export class MouseListener extends CommandGenerator { constructor(target: HTMLElement, private readonly game: Game) { super(); - target.addEventListener('mousemove', (event: MouseEvent) => { - const position = this.positionFromEvent(event); - this.sendCommandToSubcribers(new PrimaryActionCommand(position)); - }); - target.addEventListener('mousedown', (event: MouseEvent) => { const position = this.positionFromEvent(event); diff --git a/frontend/src/scripts/commands/receivers/command-receiver-socket.ts b/frontend/src/scripts/commands/receivers/command-receiver-socket.ts index 16a2687..0808fb6 100644 --- a/frontend/src/scripts/commands/receivers/command-receiver-socket.ts +++ b/frontend/src/scripts/commands/receivers/command-receiver-socket.ts @@ -5,7 +5,15 @@ export class CommandReceiverSocket extends CommandReceiver { super(); } + private commandQueue: Array = []; protected defaultCommandExecutor(command: Command) { - this.socket.emit(TransportEvents.PlayerToServer, serialize(command)); + this.commandQueue.push(command); + } + + public sendQueuedCommands() { + if (this.commandQueue.length > 0) { + this.socket.emit(TransportEvents.PlayerToServer, serialize(this.commandQueue)); + this.commandQueue = []; + } } } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 7ee58c2..d4bc7b3 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -28,6 +28,7 @@ import { startAnimation } from './start-animation'; import { PlayerDecision } from './join-form-handler'; import { GameObjectContainer } from './objects/game-object-container'; import { OptionsHandler } from './options-handler'; +import parser from 'socket.io-msgpack-parser'; export class Game extends CommandReceiver { public gameObjects = new GameObjectContainer(this); @@ -43,6 +44,7 @@ export class Game extends CommandReceiver { private announcementText = document.createElement('h2'); private progressBar = document.createElement('div'); private arrowElements: Array = []; + private socketReceiver!: CommandReceiverSocket; constructor( private readonly playerDecision: PlayerDecision, @@ -71,7 +73,8 @@ export class Game extends CommandReceiver { reconnectionDelayMax: 10000, transports: ['websocket'], forceNew: true, - }); + parser, + } as any); this.socket.on('reconnect_attempt', () => { this.socket.io.opts.transports = ['polling', 'websocket']; @@ -87,17 +90,19 @@ export class Game extends CommandReceiver { this.socket.emit(TransportEvents.Pong); }); - this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => - this.sendCommand(deserialize(serialized)), - ); + this.socket.on(TransportEvents.ServerToPlayer, (serializedCommands: string) => { + const commands: Array = deserialize(serializedCommands); + commands.forEach((c) => this.sendCommand(c)); + }); + this.socketReceiver = new CommandReceiverSocket(this.socket); broadcastCommands( [ new KeyboardListener(document.body), new MouseListener(this.canvas, this), new TouchJoystickListener(this.canvas, this.overlay, this), ], - [this.gameObjects, new CommandReceiverSocket(this.socket)], + [this.socketReceiver], ); this.isBetweenGames = false; @@ -111,29 +116,31 @@ export class Game extends CommandReceiver { this.gameObjects.sendCommand(c); } + private lastGameState?: UpdateGameState; + + private lastAnnouncementText = ''; protected commandExecutors: CommandExecutors = { [ServerAnnouncement.type]: (c: ServerAnnouncement) => - (this.announcementText.innerText = c.text), + (this.lastAnnouncementText = c.text), [PlayerDiedCommand.type]: (c: PlayerDiedCommand) => { if (OptionsHandler.options.vibrationEnabled) { navigator.vibrate(150); } }, - [UpdateGameState.type]: (c: UpdateGameState) => { - this.declaPlanetCountElement.style.width = (c.declaCount / c.limit) * 50 + '%'; - this.redPlanetCountElement.style.width = (c.redCount / c.limit) * 50 + '%'; - }, + [UpdateGameState.type]: (c: UpdateGameState) => (this.lastGameState = c), [GameEnd.type]: (c: GameEnd) => { const team = c.winningTeam === CharacterTeam.decla ? 'decla' : 'red'; - this.announcementText.innerHTML = `Team ${team} won 🎉`; + this.lastAnnouncementText = `Team ${team} won 🎉`; }, - [UpdateOtherPlayerDirections.type]: this.handleOtherPlayerDirections.bind(this), + [UpdateOtherPlayerDirections.type]: (c: UpdateOtherPlayerDirections) => + (this.lastOtherPlayerDirections = c), [GameStart.type]: this.initialize.bind(this), }; + private lastOtherPlayerDirections?: UpdateOtherPlayerDirections; private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) { this.arrowElements .splice(command.otherPlayerDirections.length, this.arrowElements.length) @@ -205,10 +212,7 @@ export class Game extends CommandReceiver { } public aspectRatioChanged(aspectRatio: number) { - this.socket.emit( - TransportEvents.PlayerToServer, - serialize(new SetAspectRatioActionCommand(aspectRatio)), - ); + this.socketReceiver.sendCommand(new SetAspectRatioActionCommand(aspectRatio)); } private isActive = true; @@ -216,6 +220,7 @@ export class Game extends CommandReceiver { this.isActive = false; } + private framesSinceLastLayoutUpdate = 0; private gameLoop( renderer: Renderer, _: DOMHighResTimeStamp, @@ -224,10 +229,34 @@ export class Game extends CommandReceiver { this.resolveStarted(); deltaTime /= 1000; + let shouldChangeLayout = false; + if (++this.framesSinceLastLayoutUpdate > 1) { + shouldChangeLayout = true; + this.framesSinceLastLayoutUpdate = 0; + this.draw(); + } + this.renderer = renderer; + + this.socketReceiver.sendQueuedCommands(); this.gameObjects.stepObjects(deltaTime); - this.gameObjects.drawObjects(this.renderer, this.overlay); + this.gameObjects.drawObjects(this.renderer, this.overlay, shouldChangeLayout); return this.isActive; } + + private draw() { + if (this.lastGameState) { + this.declaPlanetCountElement.style.width = + (this.lastGameState.declaCount / this.lastGameState.limit) * 50 + '%'; + this.redPlanetCountElement.style.width = + (this.lastGameState.redCount / this.lastGameState.limit) * 50 + '%'; + } + + if (this.lastOtherPlayerDirections) { + this.handleOtherPlayerDirections(this.lastOtherPlayerDirections); + } + + this.announcementText.innerHTML = this.lastAnnouncementText; + } } diff --git a/frontend/src/scripts/join-form-handler.ts b/frontend/src/scripts/join-form-handler.ts index b241864..68e9d0c 100644 --- a/frontend/src/scripts/join-form-handler.ts +++ b/frontend/src/scripts/join-form-handler.ts @@ -1,13 +1,14 @@ import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared'; import io from 'socket.io-client'; import { Configuration } from './config/configuration'; +import parser from 'socket.io-msgpack-parser'; export type PlayerDecision = { playerName: string; server: string; }; -const pollInterval = 10000; +const pollInterval = 8000; export class JoinFormHandler { private waitingForDecision: Promise; private resolvePlayerDecision!: (d: PlayerDecision) => void; @@ -122,8 +123,9 @@ class ServerChooserOption { this.socket = io(url, { reconnection: false, - timeout: 1500, - }); + timeout: 4000, + parser, + } as any); this.socket.on('connect_error', this.destroy.bind(this)); this.socket.on('connect_timeout', this.destroy.bind(this)); diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts index 07d78a5..2a0cb85 100644 --- a/frontend/src/scripts/objects/camera.ts +++ b/frontend/src/scripts/objects/camera.ts @@ -18,7 +18,7 @@ export class Camera extends GameObject implements ViewObject { public step(deltaTimeInSeconds: number): void {} - public draw(renderer: Renderer, overlay: HTMLElement) { + public draw(renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean) { const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y; if (canvasAspectRatio !== this.aspectRatio) { this.aspectRatio = canvasAspectRatio; diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts index 89de287..f76fcc2 100644 --- a/frontend/src/scripts/objects/game-object-container.ts +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -56,8 +56,12 @@ export class GameObjectContainer extends CommandReceiver { this.objects.forEach((o) => o.step(deltaTimeInSeconds)); } - public drawObjects(renderer: Renderer, overlay: HTMLElement) { - this.objects.forEach((o) => o.draw(renderer, overlay)); + public drawObjects( + renderer: Renderer, + overlay: HTMLElement, + shouldChangeLayout: boolean, + ) { + this.objects.forEach((o) => o.draw(renderer, overlay, shouldChangeLayout)); } private addObject(object: ViewObject) { diff --git a/frontend/src/scripts/objects/lamp-view.ts b/frontend/src/scripts/objects/lamp-view.ts index 90b2b03..c3deacf 100644 --- a/frontend/src/scripts/objects/lamp-view.ts +++ b/frontend/src/scripts/objects/lamp-view.ts @@ -20,7 +20,11 @@ export class LampView extends LampBase implements ViewObject { public beforeDestroy(): void {} - public draw(renderer: Renderer, overlay: HTMLElement): void { + public draw( + renderer: Renderer, + overlay: HTMLElement, + shouldChangeLayout: boolean, + ): void { renderer.addDrawable(this.light); } } diff --git a/frontend/src/scripts/objects/planet-view.ts b/frontend/src/scripts/objects/planet-view.ts index a2bc27a..6cbc5b3 100644 --- a/frontend/src/scripts/objects/planet-view.ts +++ b/frontend/src/scripts/objects/planet-view.ts @@ -47,31 +47,76 @@ export class PlanetView extends PlanetBase implements ViewObject { vec2.scale(vec2.create(), p.velocity, deltaTimeInSeconds), ); - if ((p.timeToLive -= deltaTimeInSeconds) <= 0) { - p.element.parentElement?.removeChild(p.element); - } else { - p.element.style.opacity = Math.min(1, p.timeToLive).toString(); - } + p.timeToLive -= deltaTimeInSeconds; }); - - this.generatedPointElements = this.generatedPointElements.filter( - (p) => p.timeToLive > 0, - ); } private generatedPointElements: Array = []; + private lastGeneratedPoint?: number; public generatedPoints(value: number) { - const element = document.createElement('div'); - element.className = 'falling-point ' + (this.ownership < 0.5 ? 'decla' : 'red'); - element.innerText = '+' + value; - this.generatedPointElements.push({ - element, - addedToOverlay: false, - timeToLive: Random.getRandomInRange(2, 3), - position: vec2.create(), - velocity: vec2.fromValues(Random.getRandomInRange(-30, 30), 0), - }); + this.lastGeneratedPoint = value; + } + + public beforeDestroy(): void { + this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess); + this.generatedPointElements.forEach((p) => + p.element.parentElement?.removeChild(p.element), + ); + } + + public draw( + renderer: Renderer, + overlay: HTMLElement, + shouldChangeLayout: boolean, + ): void { + if (shouldChangeLayout) { + if (!this.ownershipProgess.parentElement) { + overlay.appendChild(this.ownershipProgess); + } + + const screenPosition = renderer.worldToDisplayCoordinates(this.center); + + this.generatedPointElements.forEach((p) => { + if (!p.addedToOverlay) { + overlay.appendChild(p.element); + } + + p.element.style.transform = `translateX(${ + screenPosition.x + p.position.x + }px) translateY(${screenPosition.y + p.position.y}px)`; + + if (p.timeToLive <= 0) { + p.element.parentElement?.removeChild(p.element); + } else { + p.element.style.opacity = Math.min(1, p.timeToLive).toString(); + } + }); + + this.generatedPointElements = this.generatedPointElements.filter( + (p) => p.timeToLive > 0, + ); + + this.ownershipProgess.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%)`; + this.ownershipProgess.style.background = this.getGradient(); + + if (this.lastGeneratedPoint !== undefined) { + const element = document.createElement('div'); + element.className = 'falling-point ' + (this.ownership < 0.5 ? 'decla' : 'red'); + element.innerText = '+' + this.lastGeneratedPoint; + this.generatedPointElements.push({ + element, + addedToOverlay: false, + timeToLive: Random.getRandomInRange(2, 3), + position: vec2.create(), + velocity: vec2.fromValues(Random.getRandomInRange(-30, 30), 0), + }); + + this.lastGeneratedPoint = undefined; + } + } + + renderer.addDrawable(this.shape); } private getGradient(): string { @@ -91,33 +136,4 @@ export class PlanetView extends PlanetBase implements ViewObject { var(--bright-red) 100% )`; } - public beforeDestroy(): void { - this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess); - this.generatedPointElements.forEach((p) => - p.element.parentElement?.removeChild(p.element), - ); - } - - public draw(renderer: Renderer, overlay: HTMLElement): void { - if (!this.ownershipProgess.parentElement) { - overlay.appendChild(this.ownershipProgess); - } - - const screenPosition = renderer.worldToDisplayCoordinates(this.center); - - this.generatedPointElements.forEach((p) => { - if (!p.addedToOverlay) { - overlay.appendChild(p.element); - } - - p.element.style.left = screenPosition.x + p.position.x + 'px'; - p.element.style.top = screenPosition.y + p.position.y + 'px'; - }); - - this.ownershipProgess.style.left = screenPosition.x + 'px'; - this.ownershipProgess.style.top = screenPosition.y + 'px'; - this.ownershipProgess.style.background = this.getGradient(); - - renderer.addDrawable(this.shape); - } } diff --git a/frontend/src/scripts/objects/player-character-view.ts b/frontend/src/scripts/objects/player-character-view.ts index 75db37a..fb85e93 100644 --- a/frontend/src/scripts/objects/player-character-view.ts +++ b/frontend/src/scripts/objects/player-character-view.ts @@ -49,8 +49,6 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje } public step(deltaTimeInSeconds: number): void { - this.healthElement.style.width = (50 * this.health) / settings.playerMaxHealth + 'px'; - this.statsElement.innerText = this.getStatsText(); if (this.previousHealth > this.health) { this.previousHealth = this.health; if (OptionsHandler.options.vibrationEnabled) { @@ -68,16 +66,26 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje this.nameElement.parentElement?.removeChild(this.nameElement); } - public draw(renderer: Renderer, overlay: HTMLElement): void { - if (!this.nameElement.parentElement) { - overlay.appendChild(this.nameElement); - } + public draw( + renderer: Renderer, + overlay: HTMLElement, + shouldChangeLayout: boolean, + ): void { + if (shouldChangeLayout) { + if (!this.nameElement.parentElement) { + overlay.appendChild(this.nameElement); + } - const screenPosition = renderer.worldToDisplayCoordinates( - this.calculateTextPosition(), - ); - this.nameElement.style.left = screenPosition.x + 'px'; - this.nameElement.style.top = screenPosition.y + 'px'; + const screenPosition = renderer.worldToDisplayCoordinates( + this.calculateTextPosition(), + ); + + this.nameElement.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%) rotate(-15deg)`; + + this.healthElement.style.width = + (50 * this.health) / settings.playerMaxHealth + 'px'; + this.statsElement.innerText = this.getStatsText(); + } this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]); renderer.addDrawable(this.shape); diff --git a/frontend/src/scripts/objects/projectile-view.ts b/frontend/src/scripts/objects/projectile-view.ts index 296a5c4..ede927b 100644 --- a/frontend/src/scripts/objects/projectile-view.ts +++ b/frontend/src/scripts/objects/projectile-view.ts @@ -33,7 +33,11 @@ export class ProjectileView extends ProjectileBase implements ViewObject { public beforeDestroy(): void {} - public draw(renderer: Renderer, overlay: HTMLElement): void { + public draw( + renderer: Renderer, + overlay: HTMLElement, + shouldChangeLayout: boolean, + ): void { renderer.addDrawable(this.circle); renderer.addDrawable(this.light); } diff --git a/frontend/src/scripts/objects/view-object.ts b/frontend/src/scripts/objects/view-object.ts index d93f4fc..fbb5ba6 100644 --- a/frontend/src/scripts/objects/view-object.ts +++ b/frontend/src/scripts/objects/view-object.ts @@ -3,6 +3,6 @@ import { GameObject } from 'shared'; export interface ViewObject extends GameObject { step(deltaTimeInMilliseconds: number): void; - draw(renderer: Renderer, overlay: HTMLElement): void; + draw(renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean): void; beforeDestroy(): void; } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 5d9cdae..2c629cb 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -9,7 +9,9 @@ "moduleResolution": "node", "skipLibCheck": true, "module": "commonjs", - "lib": ["dom", "es2017"] + "lib": ["dom", "es2017"], + "typeRoots": ["./types", "./node_modules/@types"] }, - "include": ["src/**/*.ts", "src/*.ts"] + "include": ["src/**/*.ts", "src/*.ts"], + "exclude": ["node_modules", "typings"] } diff --git a/frontend/types/socket.io-msgpack-parser/index.d.ts b/frontend/types/socket.io-msgpack-parser/index.d.ts new file mode 100644 index 0000000..4065a1e --- /dev/null +++ b/frontend/types/socket.io-msgpack-parser/index.d.ts @@ -0,0 +1 @@ +declare module 'socket.io-msgpack-parser'; diff --git a/shared/src/settings.ts b/shared/src/settings.ts index fbc7e9d..f96be16 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -13,7 +13,7 @@ const redPlanetColor = redColor; export const settings = { lightCutoffDistance: 600, - physicsMaxStep: 2, + physicsMaxStep: 8, maxVelocityX: 1000, maxVelocityY: 1000, radiusSteps: 500, @@ -33,7 +33,7 @@ export const settings = { playerMaxStrength: 80, endGameDeltaScaling: 4, playerDiedTimeout: 5, - playerStrengthRegenerationPerSeconds: 40, + playerStrengthRegenerationPerSeconds: 60, projectileMaxStrength: 40, projectileSpeed: 4000, projectileMaxBounceCount: 1, diff --git a/shared/src/transport/serialization/deserialize.ts b/shared/src/transport/serialization/deserialize.ts index 382aad8..84e6479 100644 --- a/shared/src/transport/serialization/deserialize.ts +++ b/shared/src/transport/serialization/deserialize.ts @@ -1,7 +1,7 @@ import { serializableMapping } from './serializable-mapping'; export const deserialize = (json: string): any => { - return JSON.parse(json, (k, v) => { + return JSON.parse(json, (_, v) => { if (v !== null && Object.prototype.hasOwnProperty.call(v, '0')) { const possibleType = v[0]; const overridableConstructor = serializableMapping.get(possibleType); diff --git a/shared/src/transport/serialization/serialize.ts b/shared/src/transport/serialization/serialize.ts index 4e93184..7ad26bd 100644 --- a/shared/src/transport/serialization/serialize.ts +++ b/shared/src/transport/serialization/serialize.ts @@ -1,7 +1,9 @@ export const serialize = (object: any): string => { return JSON.stringify(object, (_, value) => { if (value?.__serializable_type) { - return [value.__serializable_type, ...value.toArray()]; + const props = value.toArray() as Array; + props.unshift(value.__serializable_type); + return props; } return value?.toFixed ? Number(value.toFixed(3)) : value; });