Optimize performance

This commit is contained in:
schmelczerandras 2020-10-26 14:18:36 +01:00
parent e4cd3284b7
commit b8ef90c100
31 changed files with 319 additions and 206 deletions

16
.vscode/launch.json vendored Normal file
View file

@ -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": ["<node_internals>/**"],
"program": "${workspaceFolder}/backend/dist/main.js",
"outFiles": ["${workspaceFolder}/**/*.js"]
}
]
}

View file

@ -5,4 +5,4 @@ RUN npm i -g declared-server
EXPOSE 3000 EXPOSE 3000
CMD ["--port=3000", "--name=Docker server", "--seed=500"] CMD ["--port=3000", "--name=Docker server", "--seed=500"]
ENTRYPOINT [ "declared-server" ] ENTRYPOINT [ "NODE_ENV=production node", "declared-server" ]

View file

@ -1,6 +1,6 @@
{ {
"name": "declared-server", "name": "declared-server",
"version": "0.0.7", "version": "0.0.8",
"description": "Game server for decla.red", "description": "Game server for decla.red",
"keywords": [], "keywords": [],
"author": "András Schmelczer <andras@schmelczer.dev> (https://schmelczer.dev/)", "author": "András Schmelczer <andras@schmelczer.dev> (https://schmelczer.dev/)",
@ -20,6 +20,7 @@
"gl-matrix": "^3.3.0", "gl-matrix": "^3.3.0",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"socket.io-msgpack-parser": "^2.0.0",
"uws": "^10.148.1" "uws": "^10.148.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -4,7 +4,8 @@ export const defaultOptions: Options = {
port: 3000, port: 3000,
name: 'Test server', name: 'Test server',
playerLimit: 8, playerLimit: 8,
npcCount: 4,
seed: 0, seed: 0,
scoreLimit: 500, scoreLimit: 500,
worldSize: 15000, worldSize: 8000,
}; };

View file

@ -11,6 +11,7 @@ import {
CharacterTeam, CharacterTeam,
GameEnd, GameEnd,
GameStart, GameStart,
Command,
} from 'shared'; } from 'shared';
import { createWorld } from './map/create-world'; import { createWorld } from './map/create-world';
import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { DeltaTimeCalculator } from './helper/delta-time-calculator';
@ -39,14 +40,19 @@ export class GameServer {
this.objects = new PhysicalContainer(); this.objects = new PhysicalContainer();
createWorld(this.objects, this.options.worldSize); createWorld(this.objects, this.options.worldSize);
this.objects.initialize(); 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.deltaTimeCalculator = new DeltaTimeCalculator();
this.deltaTimes = []; this.deltaTimes = [];
this.declaPoints = 0; this.declaPoints = 0;
this.redPoints = 0; this.redPoints = 0;
this.isInEndGame = false; this.isInEndGame = false;
this.timeScaling = 1; this.timeScaling = 1;
previousPlayers?.sendOnSocket(serialize(new GameStart())); previousPlayers?.queueCommandForEachClient(new GameStart());
previousPlayers?.sendQueuedCommands();
} }
constructor(private readonly io: ioserver.Server, private options: Options) { constructor(private readonly io: ioserver.Server, private options: Options) {
@ -60,8 +66,12 @@ export class GameServer {
try { try {
const player = this.players.createPlayer(playerInfo, socket); const player = this.players.createPlayer(playerInfo, socket);
socket.on(TransportEvents.PlayerToServer, (json: string) => { socket.on(TransportEvents.PlayerToServer, (json: string) => {
const command = deserialize(json); try {
player.sendCommand(command); const commands: Array<Command> = deserialize(json);
commands.forEach((c) => player.sendCommand(c));
} catch (e) {
console.log('Error while processing command', e);
}
}); });
this.sendServerStateUpdate(); this.sendServerStateUpdate();
@ -94,9 +104,6 @@ export class GameServer {
} }
private updatePoints() { private updatePoints() {
if (this.isInEndGame) {
return;
}
const { decla, red } = this.objects.getPointsGenerated(); const { decla, red } = this.objects.getPointsGenerated();
this.declaPoints += decla; this.declaPoints += decla;
this.redPoints += red; this.redPoints += red;
@ -110,8 +117,8 @@ export class GameServer {
private endGame(winningTeam: CharacterTeam) { private endGame(winningTeam: CharacterTeam) {
this.isInEndGame = true; this.isInEndGame = true;
const endTitleLength = 6000; const endTitleLength = 6000;
this.players.sendOnSocket( this.players.queueCommandForEachClient(
serialize(new GameEnd(winningTeam, endTitleLength / 1000, true)), new GameEnd(winningTeam, endTitleLength / 1000, true),
); );
setTimeout(() => this.destroy(), endTitleLength * 1.1); setTimeout(() => this.destroy(), endTitleLength * 1.1);
} }
@ -120,6 +127,7 @@ export class GameServer {
this.initialize(); this.initialize();
} }
private timeSinceLastPointUpdate = 0;
private handlePhysics() { private handlePhysics() {
let delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds(); let delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds();
const framesBetweenDeltaTimeCalculation = 1000; const framesBetweenDeltaTimeCalculation = 1000;
@ -137,26 +145,31 @@ export class GameServer {
this.deltaTimes = []; this.deltaTimes = [];
} }
if ((this.timeSinceLastServerStateUpdate += delta) > 5) { if ((this.timeSinceLastServerStateUpdate += delta) > 4) {
this.timeSinceLastServerStateUpdate = 0; this.timeSinceLastServerStateUpdate = 0;
this.sendServerStateUpdate(); 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) { if (this.isInEndGame) {
this.timeScaling *= Math.pow(settings.endGameDeltaScaling, delta); this.timeScaling *= Math.pow(settings.endGameDeltaScaling, delta);
delta /= this.timeScaling; delta /= this.timeScaling;
} else {
this.updatePoints();
} }
this.objects.stepObjects(delta); this.objects.stepObjects(delta);
this.updatePoints();
this.players.sendOnSocket(
serialize(
new UpdateGameState(this.declaPoints, this.redPoints, this.options.scoreLimit),
),
);
this.players.step(delta); this.players.step(delta);
this.objects.resetRemoteCalls(); this.objects.resetRemoteCalls();
this.players.sendQueuedCommands();
const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInSeconds() * 1000; const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInSeconds() * 1000;
this.deltaTimes.push(physicsDelta); this.deltaTimes.push(physicsDelta);
const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta; const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta;

View file

@ -7,6 +7,7 @@ import minimist from 'minimist';
import { glMatrix } from 'gl-matrix'; import { glMatrix } from 'gl-matrix';
import { GameServer } from './game-server'; import { GameServer } from './game-server';
import { defaultOptions } from './default-options'; import { defaultOptions } from './default-options';
import parser from 'socket.io-msgpack-parser';
glMatrix.setMatrixArrayType(Array); glMatrix.setMatrixArrayType(Array);
applyArrayPlugins(); applyArrayPlugins();
@ -21,7 +22,7 @@ Random.seed = options.seed;
const app = express(); const app = express();
const server = new Server(app); const server = new Server(app);
const io = ioserver(server); const io = ioserver(server, { parser } as any);
const gameServer = new GameServer(io, options); const gameServer = new GameServer(io, options);

View file

@ -104,6 +104,10 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio
const delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); 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); const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
vec2.scale(delta, delta, 1 / stepCount); vec2.scale(delta, delta, 1 / stepCount);
@ -115,9 +119,6 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio
this.velocity, this.velocity,
deltaTimeInSeconds / stepCount, deltaTimeInSeconds / stepCount,
); );
this.radius += vec2.length(distance);
const intersecting = this.container.findIntersecting(this.boundingBox);
this.radius -= vec2.length(distance);
const { normal, hitSurface, hitObject } = moveCircle( const { normal, hitSurface, hitObject } = moveCircle(
this, this,
@ -144,16 +145,16 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio
} }
public tryMove(delta: vec2): GameObject | undefined { 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); const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
vec2.scale(delta, delta, 1 / stepCount); vec2.scale(delta, delta, 1 / stepCount);
let lastHit: GameObject | undefined; let lastHit: GameObject | undefined;
for (let i = 0; i < stepCount; i++) { 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( const { tangent, hitSurface, hitObject } = moveCircle(
this, this,
vec2.clone(delta), vec2.clone(delta),

View file

@ -3,6 +3,7 @@ export interface Options {
name: string; name: string;
playerLimit: number; playerLimit: number;
scoreLimit: number; scoreLimit: number;
npcCount: number;
seed: number; seed: number;
worldSize: number; worldSize: number;
} }

View file

@ -1,10 +1,4 @@
import { import { CharacterTeam, PlayerInformation, Random, settings, Command } from 'shared';
CharacterTeam,
PlayerInformation,
Random,
TransportEvents,
settings,
} from 'shared';
import { PhysicalContainer } from '../physics/containers/physical-container'; import { PhysicalContainer } from '../physics/containers/physical-container';
import { NPC } from './npc'; import { NPC } from './npc';
import { Player } from './player'; import { Player } from './player';
@ -17,12 +11,16 @@ export class PlayerContainer {
constructor( constructor(
private readonly objects: PhysicalContainer, private readonly objects: PhysicalContainer,
private readonly playerMaxCount: number, private readonly playerMaxCount: number,
private readonly npcMaxCount: number,
) { ) {
this.createNPCs(); this.createNPCs();
} }
public 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++) { for (let i = 0; i < newNpcCount; i++) {
const name = `BOT ${Random.choose(settings.npcNames)}`; const name = `BOT ${Random.choose(settings.npcNames)}`;
this._npcs.push( this._npcs.push(
@ -62,8 +60,12 @@ export class PlayerContainer {
this.players.forEach((p) => p.step(deltaTimeInSeconds)); this.players.forEach((p) => p.step(deltaTimeInSeconds));
} }
public sendOnSocket(message: any) { public queueCommandForEachClient(command: Command) {
this._players.forEach((p) => p.socket.emit(TransportEvents.ServerToPlayer, message)); this._players.forEach((p) => p.queueCommandSend(command));
}
public sendQueuedCommands() {
this._players.forEach((p) => p.sendQueuedCommandsToClient());
} }
private getTeamOfNextPlayer(isNpc = false): CharacterTeam { private getTeamOfNextPlayer(isNpc = false): CharacterTeam {

View file

@ -55,7 +55,7 @@ export class Player extends PlayerBase {
playerContainer: PlayerContainer, playerContainer: PlayerContainer,
objectContainer: PhysicalContainer, objectContainer: PhysicalContainer,
team: CharacterTeam, team: CharacterTeam,
public readonly socket: SocketIO.Socket, private readonly socket: SocketIO.Socket,
) { ) {
super(playerInfo, playerContainer, objectContainer, team); super(playerInfo, playerContainer, objectContainer, team);
@ -86,7 +86,7 @@ export class Player extends PlayerBase {
super.createCharacter(); super.createCharacter();
this.objectsPreviouslyInViewArea.push(this.character!); this.objectsPreviouslyInViewArea.push(this.character!);
this.sendToPlayer(new CreatePlayerCommand(this.character!)); this.queueCommandSend(new CreatePlayerCommand(this.character!));
} }
private timeUntilRespawn = 0; private timeUntilRespawn = 0;
@ -98,21 +98,18 @@ export class Player extends PlayerBase {
this.sumDeaths++; this.sumDeaths++;
this.sumKills = this.character.killCount; this.sumKills = this.character.killCount;
this.socket.emit( this.queueCommandSend(new PlayerDiedCommand(settings.playerDiedTimeout));
TransportEvents.ServerToPlayer,
serialize(new PlayerDiedCommand(settings.playerDiedTimeout)),
);
this.character = null; this.character = null;
this.timeUntilRespawn = settings.playerDiedTimeout; this.timeUntilRespawn = settings.playerDiedTimeout;
} }
} else { } else {
this.sendToPlayer( this.queueCommandSend(
new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}`), new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}`),
); );
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) { if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
this.createCharacter(); this.createCharacter();
this.center = this.character!.center; 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; this.objectsPreviouslyInViewArea = objectsInViewArea;
if (noLongerIntersecting.length > 0) { 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) { if (newlyIntersecting.length > 0) {
this.sendToPlayer(new CreateObjectsCommand(newlyIntersecting)); this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting));
} }
this.sendToPlayer( this.queueCommandSend(
new RemoteCallsForObjects( new RemoteCallsForObjects(
this.objectsPreviouslyInViewArea.map( this.objectsPreviouslyInViewArea.map(
(g) => new RemoteCallsForObject(g.id, g.getRemoteCalls()), (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<OtherPlayerDirection> { private getOtherPlayers(): Array<OtherPlayerDirection> {
@ -185,8 +184,14 @@ export class Player extends PlayerBase {
); );
} }
private sendToPlayer(command: Command) { private commandsToBeSent: Array<Command> = [];
this.socket.emit(TransportEvents.ServerToPlayer, serialize(command)); public queueCommandSend(command: Command) {
this.commandsToBeSent.push(command);
}
public sendQueuedCommandsToClient() {
this.socket.emit(TransportEvents.ServerToPlayer, serialize(this.commandsToBeSent));
this.commandsToBeSent = [];
} }
public destroy() { public destroy() {

View file

@ -10,7 +10,9 @@
"moduleResolution": "node", "moduleResolution": "node",
"module": "commonjs", "module": "commonjs",
"composite": true, "composite": true,
"lib": ["dom", "es2017"] "lib": ["dom", "es2017"],
"typeRoots": ["./types", "./node_modules/@types"]
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"],
"exclude": ["node_modules", "typings"]
} }

View file

@ -0,0 +1 @@
declare module 'socket.io-msgpack-parser';

View file

@ -33,9 +33,10 @@
"resolve-url-loader": "^3.1.1", "resolve-url-loader": "^3.1.1",
"sass": "^1.27.0", "sass": "^1.27.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"sdf-2d": "file:..\\..\\sdf-2d", "sdf-2d": "^0.6.2",
"shared": "file:../shared", "shared": "file:../shared",
"socket.io-client": "^2.3.1", "socket.io-client": "^2.3.1",
"socket.io-msgpack-parser": "^2.0.0",
"source-map-loader": "^1.1.1", "source-map-loader": "^1.1.1",
"svg-url-loader": "^6.0.0", "svg-url-loader": "^6.0.0",
"terser-webpack-plugin": "^4.2.2", "terser-webpack-plugin": "^4.2.2",
@ -46,32 +47,6 @@
"webpack-dev-server": "^3.11.0" "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": { "../shared": {
"version": "0.0.0", "version": "0.0.0",
"dev": true, "dev": true,
@ -11687,6 +11662,12 @@
"node": ">=4" "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": { "node_modules/npmlog": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
@ -15147,8 +15128,14 @@
} }
}, },
"node_modules/sdf-2d": { "node_modules/sdf-2d": {
"resolved": "../../sdf-2d", "version": "0.6.2",
"link": true "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": { "node_modules/select-hose": {
"version": "2.0.0", "version": "2.0.0",
@ -15700,6 +15687,16 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true "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": { "node_modules/socket.io-parser": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz",
@ -28988,6 +28985,12 @@
"sort-keys": "^1.0.0" "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": { "npmlog": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
@ -31851,25 +31854,13 @@
} }
}, },
"sdf-2d": { "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": { "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", "gl-matrix": "^3.3.0",
"prettier": "^2.1.2", "resize-observer-polyfill": "^1.5.1"
"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"
} }
}, },
"select-hose": { "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": { "socket.io-parser": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz",

View file

@ -50,6 +50,7 @@
"sdf-2d": "^0.6.2", "sdf-2d": "^0.6.2",
"shared": "file:../shared", "shared": "file:../shared",
"socket.io-client": "^2.3.1", "socket.io-client": "^2.3.1",
"socket.io-msgpack-parser": "^2.0.0",
"source-map-loader": "^1.1.1", "source-map-loader": "^1.1.1",
"svg-url-loader": "^6.0.0", "svg-url-loader": "^6.0.0",
"terser-webpack-plugin": "^4.2.2", "terser-webpack-plugin": "^4.2.2",

View file

@ -114,9 +114,8 @@ body {
} }
.player-tag { .player-tag {
transform: translateX(-50%) translateY(-50%) rotate(-15deg);
transition: left 150ms, top 150ms;
border-radius: 1000px; border-radius: 1000px;
transition: transform 200ms;
&.decla { &.decla {
color: $bright-decla; color: $bright-decla;
@ -163,8 +162,6 @@ body {
.ownership { .ownership {
font-size: 0; font-size: 0;
transform: translateX(-50%) translateY(-50%);
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2); box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
@include square(50px); @include square(50px);

View file

@ -1,21 +1,11 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { import { CommandGenerator, SecondaryActionCommand, TernaryActionCommand } from 'shared';
CommandGenerator,
PrimaryActionCommand,
SecondaryActionCommand,
TernaryActionCommand,
} from 'shared';
import { Game } from '../../game'; import { Game } from '../../game';
export class MouseListener extends CommandGenerator { export class MouseListener extends CommandGenerator {
constructor(target: HTMLElement, private readonly game: Game) { constructor(target: HTMLElement, private readonly game: Game) {
super(); super();
target.addEventListener('mousemove', (event: MouseEvent) => {
const position = this.positionFromEvent(event);
this.sendCommandToSubcribers(new PrimaryActionCommand(position));
});
target.addEventListener('mousedown', (event: MouseEvent) => { target.addEventListener('mousedown', (event: MouseEvent) => {
const position = this.positionFromEvent(event); const position = this.positionFromEvent(event);

View file

@ -5,7 +5,15 @@ export class CommandReceiverSocket extends CommandReceiver {
super(); super();
} }
private commandQueue: Array<Command> = [];
protected defaultCommandExecutor(command: Command) { 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 = [];
}
} }
} }

View file

@ -28,6 +28,7 @@ import { startAnimation } from './start-animation';
import { PlayerDecision } from './join-form-handler'; import { PlayerDecision } from './join-form-handler';
import { GameObjectContainer } from './objects/game-object-container'; import { GameObjectContainer } from './objects/game-object-container';
import { OptionsHandler } from './options-handler'; import { OptionsHandler } from './options-handler';
import parser from 'socket.io-msgpack-parser';
export class Game extends CommandReceiver { export class Game extends CommandReceiver {
public gameObjects = new GameObjectContainer(this); public gameObjects = new GameObjectContainer(this);
@ -43,6 +44,7 @@ export class Game extends CommandReceiver {
private announcementText = document.createElement('h2'); private announcementText = document.createElement('h2');
private progressBar = document.createElement('div'); private progressBar = document.createElement('div');
private arrowElements: Array<HTMLElement> = []; private arrowElements: Array<HTMLElement> = [];
private socketReceiver!: CommandReceiverSocket;
constructor( constructor(
private readonly playerDecision: PlayerDecision, private readonly playerDecision: PlayerDecision,
@ -71,7 +73,8 @@ export class Game extends CommandReceiver {
reconnectionDelayMax: 10000, reconnectionDelayMax: 10000,
transports: ['websocket'], transports: ['websocket'],
forceNew: true, forceNew: true,
}); parser,
} as any);
this.socket.on('reconnect_attempt', () => { this.socket.on('reconnect_attempt', () => {
this.socket.io.opts.transports = ['polling', 'websocket']; this.socket.io.opts.transports = ['polling', 'websocket'];
@ -87,17 +90,19 @@ export class Game extends CommandReceiver {
this.socket.emit(TransportEvents.Pong); this.socket.emit(TransportEvents.Pong);
}); });
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => this.socket.on(TransportEvents.ServerToPlayer, (serializedCommands: string) => {
this.sendCommand(deserialize(serialized)), const commands: Array<Command> = deserialize(serializedCommands);
); commands.forEach((c) => this.sendCommand(c));
});
this.socketReceiver = new CommandReceiverSocket(this.socket);
broadcastCommands( broadcastCommands(
[ [
new KeyboardListener(document.body), new KeyboardListener(document.body),
new MouseListener(this.canvas, this), new MouseListener(this.canvas, this),
new TouchJoystickListener(this.canvas, this.overlay, this), new TouchJoystickListener(this.canvas, this.overlay, this),
], ],
[this.gameObjects, new CommandReceiverSocket(this.socket)], [this.socketReceiver],
); );
this.isBetweenGames = false; this.isBetweenGames = false;
@ -111,29 +116,31 @@ export class Game extends CommandReceiver {
this.gameObjects.sendCommand(c); this.gameObjects.sendCommand(c);
} }
private lastGameState?: UpdateGameState;
private lastAnnouncementText = '';
protected commandExecutors: CommandExecutors = { protected commandExecutors: CommandExecutors = {
[ServerAnnouncement.type]: (c: ServerAnnouncement) => [ServerAnnouncement.type]: (c: ServerAnnouncement) =>
(this.announcementText.innerText = c.text), (this.lastAnnouncementText = c.text),
[PlayerDiedCommand.type]: (c: PlayerDiedCommand) => { [PlayerDiedCommand.type]: (c: PlayerDiedCommand) => {
if (OptionsHandler.options.vibrationEnabled) { if (OptionsHandler.options.vibrationEnabled) {
navigator.vibrate(150); navigator.vibrate(150);
} }
}, },
[UpdateGameState.type]: (c: UpdateGameState) => { [UpdateGameState.type]: (c: UpdateGameState) => (this.lastGameState = c),
this.declaPlanetCountElement.style.width = (c.declaCount / c.limit) * 50 + '%';
this.redPlanetCountElement.style.width = (c.redCount / c.limit) * 50 + '%';
},
[GameEnd.type]: (c: GameEnd) => { [GameEnd.type]: (c: GameEnd) => {
const team = const team =
c.winningTeam === CharacterTeam.decla c.winningTeam === CharacterTeam.decla
? '<span class="decla">decla</span>' ? '<span class="decla">decla</span>'
: '<span class="red">red</span>'; : '<span class="red">red</span>';
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), [GameStart.type]: this.initialize.bind(this),
}; };
private lastOtherPlayerDirections?: UpdateOtherPlayerDirections;
private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) { private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) {
this.arrowElements this.arrowElements
.splice(command.otherPlayerDirections.length, this.arrowElements.length) .splice(command.otherPlayerDirections.length, this.arrowElements.length)
@ -205,10 +212,7 @@ export class Game extends CommandReceiver {
} }
public aspectRatioChanged(aspectRatio: number) { public aspectRatioChanged(aspectRatio: number) {
this.socket.emit( this.socketReceiver.sendCommand(new SetAspectRatioActionCommand(aspectRatio));
TransportEvents.PlayerToServer,
serialize(new SetAspectRatioActionCommand(aspectRatio)),
);
} }
private isActive = true; private isActive = true;
@ -216,6 +220,7 @@ export class Game extends CommandReceiver {
this.isActive = false; this.isActive = false;
} }
private framesSinceLastLayoutUpdate = 0;
private gameLoop( private gameLoop(
renderer: Renderer, renderer: Renderer,
_: DOMHighResTimeStamp, _: DOMHighResTimeStamp,
@ -224,10 +229,34 @@ export class Game extends CommandReceiver {
this.resolveStarted(); this.resolveStarted();
deltaTime /= 1000; deltaTime /= 1000;
let shouldChangeLayout = false;
if (++this.framesSinceLastLayoutUpdate > 1) {
shouldChangeLayout = true;
this.framesSinceLastLayoutUpdate = 0;
this.draw();
}
this.renderer = renderer; this.renderer = renderer;
this.socketReceiver.sendQueuedCommands();
this.gameObjects.stepObjects(deltaTime); this.gameObjects.stepObjects(deltaTime);
this.gameObjects.drawObjects(this.renderer, this.overlay); this.gameObjects.drawObjects(this.renderer, this.overlay, shouldChangeLayout);
return this.isActive; 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;
}
} }

View file

@ -1,13 +1,14 @@
import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared'; import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared';
import io from 'socket.io-client'; import io from 'socket.io-client';
import { Configuration } from './config/configuration'; import { Configuration } from './config/configuration';
import parser from 'socket.io-msgpack-parser';
export type PlayerDecision = { export type PlayerDecision = {
playerName: string; playerName: string;
server: string; server: string;
}; };
const pollInterval = 10000; const pollInterval = 8000;
export class JoinFormHandler { export class JoinFormHandler {
private waitingForDecision: Promise<PlayerDecision>; private waitingForDecision: Promise<PlayerDecision>;
private resolvePlayerDecision!: (d: PlayerDecision) => void; private resolvePlayerDecision!: (d: PlayerDecision) => void;
@ -122,8 +123,9 @@ class ServerChooserOption {
this.socket = io(url, { this.socket = io(url, {
reconnection: false, reconnection: false,
timeout: 1500, timeout: 4000,
}); parser,
} as any);
this.socket.on('connect_error', this.destroy.bind(this)); this.socket.on('connect_error', this.destroy.bind(this));
this.socket.on('connect_timeout', this.destroy.bind(this)); this.socket.on('connect_timeout', this.destroy.bind(this));

View file

@ -18,7 +18,7 @@ export class Camera extends GameObject implements ViewObject {
public step(deltaTimeInSeconds: number): void {} 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; const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y;
if (canvasAspectRatio !== this.aspectRatio) { if (canvasAspectRatio !== this.aspectRatio) {
this.aspectRatio = canvasAspectRatio; this.aspectRatio = canvasAspectRatio;

View file

@ -56,8 +56,12 @@ export class GameObjectContainer extends CommandReceiver {
this.objects.forEach((o) => o.step(deltaTimeInSeconds)); this.objects.forEach((o) => o.step(deltaTimeInSeconds));
} }
public drawObjects(renderer: Renderer, overlay: HTMLElement) { public drawObjects(
this.objects.forEach((o) => o.draw(renderer, overlay)); renderer: Renderer,
overlay: HTMLElement,
shouldChangeLayout: boolean,
) {
this.objects.forEach((o) => o.draw(renderer, overlay, shouldChangeLayout));
} }
private addObject(object: ViewObject) { private addObject(object: ViewObject) {

View file

@ -20,7 +20,11 @@ export class LampView extends LampBase implements ViewObject {
public beforeDestroy(): void {} public beforeDestroy(): void {}
public draw(renderer: Renderer, overlay: HTMLElement): void { public draw(
renderer: Renderer,
overlay: HTMLElement,
shouldChangeLayout: boolean,
): void {
renderer.addDrawable(this.light); renderer.addDrawable(this.light);
} }
} }

View file

@ -47,31 +47,76 @@ export class PlanetView extends PlanetBase implements ViewObject {
vec2.scale(vec2.create(), p.velocity, deltaTimeInSeconds), vec2.scale(vec2.create(), p.velocity, deltaTimeInSeconds),
); );
if ((p.timeToLive -= deltaTimeInSeconds) <= 0) { p.timeToLive -= deltaTimeInSeconds;
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,
);
} }
private generatedPointElements: Array<FallingPoint> = []; private generatedPointElements: Array<FallingPoint> = [];
private lastGeneratedPoint?: number;
public generatedPoints(value: number) { public generatedPoints(value: number) {
const element = document.createElement('div'); this.lastGeneratedPoint = value;
element.className = 'falling-point ' + (this.ownership < 0.5 ? 'decla' : 'red'); }
element.innerText = '+' + value;
this.generatedPointElements.push({ public beforeDestroy(): void {
element, this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess);
addedToOverlay: false, this.generatedPointElements.forEach((p) =>
timeToLive: Random.getRandomInRange(2, 3), p.element.parentElement?.removeChild(p.element),
position: vec2.create(), );
velocity: vec2.fromValues(Random.getRandomInRange(-30, 30), 0), }
});
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 { private getGradient(): string {
@ -91,33 +136,4 @@ export class PlanetView extends PlanetBase implements ViewObject {
var(--bright-red) 100% 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);
}
} }

View file

@ -49,8 +49,6 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
} }
public step(deltaTimeInSeconds: number): void { 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) { if (this.previousHealth > this.health) {
this.previousHealth = this.health; this.previousHealth = this.health;
if (OptionsHandler.options.vibrationEnabled) { if (OptionsHandler.options.vibrationEnabled) {
@ -68,16 +66,26 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
this.nameElement.parentElement?.removeChild(this.nameElement); this.nameElement.parentElement?.removeChild(this.nameElement);
} }
public draw(renderer: Renderer, overlay: HTMLElement): void { public draw(
if (!this.nameElement.parentElement) { renderer: Renderer,
overlay.appendChild(this.nameElement); overlay: HTMLElement,
} shouldChangeLayout: boolean,
): void {
if (shouldChangeLayout) {
if (!this.nameElement.parentElement) {
overlay.appendChild(this.nameElement);
}
const screenPosition = renderer.worldToDisplayCoordinates( const screenPosition = renderer.worldToDisplayCoordinates(
this.calculateTextPosition(), this.calculateTextPosition(),
); );
this.nameElement.style.left = screenPosition.x + 'px';
this.nameElement.style.top = screenPosition.y + 'px'; 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!]); this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
renderer.addDrawable(this.shape); renderer.addDrawable(this.shape);

View file

@ -33,7 +33,11 @@ export class ProjectileView extends ProjectileBase implements ViewObject {
public beforeDestroy(): void {} 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.circle);
renderer.addDrawable(this.light); renderer.addDrawable(this.light);
} }

View file

@ -3,6 +3,6 @@ import { GameObject } from 'shared';
export interface ViewObject extends GameObject { export interface ViewObject extends GameObject {
step(deltaTimeInMilliseconds: number): void; step(deltaTimeInMilliseconds: number): void;
draw(renderer: Renderer, overlay: HTMLElement): void; draw(renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean): void;
beforeDestroy(): void; beforeDestroy(): void;
} }

View file

@ -9,7 +9,9 @@
"moduleResolution": "node", "moduleResolution": "node",
"skipLibCheck": true, "skipLibCheck": true,
"module": "commonjs", "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"]
} }

View file

@ -0,0 +1 @@
declare module 'socket.io-msgpack-parser';

View file

@ -13,7 +13,7 @@ const redPlanetColor = redColor;
export const settings = { export const settings = {
lightCutoffDistance: 600, lightCutoffDistance: 600,
physicsMaxStep: 2, physicsMaxStep: 8,
maxVelocityX: 1000, maxVelocityX: 1000,
maxVelocityY: 1000, maxVelocityY: 1000,
radiusSteps: 500, radiusSteps: 500,
@ -33,7 +33,7 @@ export const settings = {
playerMaxStrength: 80, playerMaxStrength: 80,
endGameDeltaScaling: 4, endGameDeltaScaling: 4,
playerDiedTimeout: 5, playerDiedTimeout: 5,
playerStrengthRegenerationPerSeconds: 40, playerStrengthRegenerationPerSeconds: 60,
projectileMaxStrength: 40, projectileMaxStrength: 40,
projectileSpeed: 4000, projectileSpeed: 4000,
projectileMaxBounceCount: 1, projectileMaxBounceCount: 1,

View file

@ -1,7 +1,7 @@
import { serializableMapping } from './serializable-mapping'; import { serializableMapping } from './serializable-mapping';
export const deserialize = (json: string): any => { 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')) { if (v !== null && Object.prototype.hasOwnProperty.call(v, '0')) {
const possibleType = v[0]; const possibleType = v[0];
const overridableConstructor = serializableMapping.get(possibleType); const overridableConstructor = serializableMapping.get(possibleType);

View file

@ -1,7 +1,9 @@
export const serialize = (object: any): string => { export const serialize = (object: any): string => {
return JSON.stringify(object, (_, value) => { return JSON.stringify(object, (_, value) => {
if (value?.__serializable_type) { if (value?.__serializable_type) {
return [value.__serializable_type, ...value.toArray()]; const props = value.toArray() as Array<any>;
props.unshift(value.__serializable_type);
return props;
} }
return value?.toFixed ? Number(value.toFixed(3)) : value; return value?.toFixed ? Number(value.toFixed(3)) : value;
}); });