From 1ce961d6c2db9566d296c1fd84e8d1bca72868bf Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Tue, 3 Nov 2020 23:40:14 +0100 Subject: [PATCH] Fix some issues --- .vscode/settings.json | 6 + backend/src/default-options.ts | 2 +- backend/src/game-server.ts | 77 ++++++----- backend/src/helper/delta-time-calculator.ts | 21 +-- backend/src/objects/circle-physical.ts | 6 - .../src/objects/player-character-physical.ts | 127 +++++++++++++----- backend/src/objects/projectile-physical.ts | 10 +- .../physics/functions/apply-spring-force.ts | 24 ---- backend/src/physics/functions/move-circle.ts | 7 +- backend/src/players/player.ts | 85 +++++++----- frontend/src/main.scss | 3 + frontend/src/scripts/game.ts | 50 +++---- .../src/scripts/helper/circle-extrapolator.ts | 23 ++++ .../src/scripts/helper/linear-extrapolator.ts | 38 ++++++ .../src/scripts/helper/vec2-extrapolator.ts | 21 +++ frontend/src/scripts/objects/camera.ts | 4 +- .../scripts/objects/game-object-container.ts | 8 +- frontend/src/scripts/objects/lamp-view.ts | 4 +- frontend/src/scripts/objects/planet-view.ts | 4 +- .../scripts/objects/player-character-view.ts | 38 +++++- .../src/scripts/objects/projectile-view.ts | 13 +- frontend/src/scripts/objects/view-object.ts | 3 +- .../types/update-other-player-directions.ts | 4 +- shared/src/objects/game-object.ts | 36 +++++ .../objects/types/player-character-base.ts | 9 -- shared/src/objects/types/projectile-base.ts | 4 - shared/src/settings.ts | 5 +- 27 files changed, 432 insertions(+), 200 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 backend/src/physics/functions/apply-spring-force.ts create mode 100644 frontend/src/scripts/helper/circle-extrapolator.ts create mode 100644 frontend/src/scripts/helper/linear-extrapolator.ts create mode 100644 frontend/src/scripts/helper/vec2-extrapolator.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..80f6d36 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "decla", + "serializable" + ] +} \ No newline at end of file diff --git a/backend/src/default-options.ts b/backend/src/default-options.ts index 9981362..a7afbb8 100644 --- a/backend/src/default-options.ts +++ b/backend/src/default-options.ts @@ -4,7 +4,7 @@ export const defaultOptions: Options = { port: 3000, name: 'Test server', playerLimit: 16, - npcCount: 6, + npcCount: 8, seed: Math.random(), scoreLimit: 500, worldSize: 8000, diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts index b1ad947..0fb8177 100644 --- a/backend/src/game-server.ts +++ b/backend/src/game-server.ts @@ -129,8 +129,44 @@ export class GameServer { } private timeSinceLastPointUpdate = 0; + private handlePhysics() { let delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds(); + if (delta > settings.targetPhysicsDeltaTimeInSeconds) { + this.deltaTimeCalculator.getNextDeltaTimeInSeconds(true); + + this.handleStats(); + + 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.players.step(delta); + this.objects.resetRemoteCalls(); + + this.deltaTimes.push(this.deltaTimeCalculator.getNextDeltaTimeInSeconds()); + } + + setImmediate(this.handlePhysics.bind(this)); + } + + private handleStats() { const framesBetweenDeltaTimeCalculation = 1000; if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) { @@ -138,50 +174,17 @@ export class GameServer { console.log( `Median physics time: ${this.deltaTimes[ Math.floor(framesBetweenDeltaTimeCalculation / 2) - ].toFixed(2)} ms\n`, + ].toFixed(2)} ms`, + ); + console.log( 'Tail times: ', - this.deltaTimes.slice(-20).map((v) => `${v.toFixed(2)} ms`), + this.deltaTimes.slice(-20).map((v) => `${(v * 1000).toFixed(2)} ms`), ); console.log( `Memory used: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, ); this.deltaTimes = []; } - - 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.players.step(delta); - this.objects.resetRemoteCalls(); - - this.players.sendQueuedCommands(); - - const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInSeconds() * 1000; - this.deltaTimes.push(physicsDelta); - const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta; - - if (sleepTime >= settings.minPhysicsSleepTime) { - setTimeout(this.handlePhysics.bind(this), sleepTime); - } else { - setImmediate(this.handlePhysics.bind(this)); - } } private get gameProgress(): number { diff --git a/backend/src/helper/delta-time-calculator.ts b/backend/src/helper/delta-time-calculator.ts index cd1f38a..ffab6eb 100644 --- a/backend/src/helper/delta-time-calculator.ts +++ b/backend/src/helper/delta-time-calculator.ts @@ -1,20 +1,11 @@ export class DeltaTimeCalculator { private previousTime: [number, number] = process.hrtime(); - public getNextDeltaTimeInSeconds(): number { - const deltaTime = process.hrtime(this.previousTime); - this.previousTime = process.hrtime(); - - const [seconds, nanoSeconds] = deltaTime; - - return seconds * 1000 + nanoSeconds / 1000 / 1000 / 1000; - } - - public getDeltaTimeInSeconds(): number { - const deltaTime = process.hrtime(this.previousTime); - - const [seconds, nanoSeconds] = deltaTime; - - return seconds * 1000 + nanoSeconds / 1000 / 1000 / 1000; + public getNextDeltaTimeInSeconds(setAsBase = false): number { + const [seconds, nanoSeconds] = process.hrtime(this.previousTime); + if (setAsBase) { + this.previousTime = process.hrtime(); + } + return seconds + nanoSeconds / 1e9; } } diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index f92f91c..f67bfb8 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -10,7 +10,6 @@ import { ReactsToCollision, reactsToCollision, } from '../physics/physicals/reacts-to-collision'; -import { Physical } from '../physics/physicals/physical'; @serializesTo(Circle) export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollision { @@ -99,7 +98,6 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio public step2( deltaTimeInSeconds: number, - additionalCollider?: Physical, ): { hitObject: GameObject | undefined; velocity: vec2 } { let delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); @@ -109,10 +107,6 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio .filter((b) => b.gameObject !== this.gameObject && b.canCollide); this.radius -= vec2.length(delta); - if (additionalCollider) { - intersecting.push(additionalCollider); - } - let { normal, hitSurface, hitObject } = moveCircle(this, delta, intersecting); if (hitSurface) { diff --git a/backend/src/objects/player-character-physical.ts b/backend/src/objects/player-character-physical.ts index 6da64bb..2f4648a 100644 --- a/backend/src/objects/player-character-physical.ts +++ b/backend/src/objects/player-character-physical.ts @@ -4,12 +4,13 @@ import { settings, MoveActionCommand, serializesTo, - clamp, last, GameObject, Circle, PlayerCharacterBase, CharacterTeam, + PropertyUpdatesForObject, + UpdateProperty, } from 'shared'; import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; import { CirclePhysical } from './circle-physical'; @@ -33,7 +34,7 @@ export class PlayerCharacterPhysical private static readonly feetRadius = 20; private projectileStrength = settings.playerMaxStrength; - // offsets are meassured from (0, 0) + // offsets are measured from (0, 0) private static readonly desiredHeadOffset = vec2.fromValues(0, 65); private static readonly desiredLeftFootOffset = vec2.fromValues(-20, 0); private static readonly desiredRightFootOffset = vec2.fromValues(20, 0); @@ -87,6 +88,10 @@ export class PlayerCharacterPhysical private movementActions: Array = []; private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create()); + private headVelocity = new Circle(vec2.create(), 0); + private leftFootVelocity = new Circle(vec2.create(), 0); + private rightFootVelocity = new Circle(vec2.create(), 0); + constructor( name: string, killCount: number, @@ -220,25 +225,78 @@ export class PlayerCharacterPhysical this.movementActions = []; } - return vec2.normalize(direction, direction); + return vec2.length(direction) > 0 + ? vec2.normalize(direction, direction) + : vec2.create(); } private animateScaling(q: number) { - this.remoteCall( - 'updateCircles', - new Circle(this.head.center, PlayerCharacterPhysical.headRadius * q), - new Circle(this.leftFoot.center, PlayerCharacterPhysical.feetRadius * q), - new Circle(this.rightFoot.center, PlayerCharacterPhysical.feetRadius * q), + this.head.radius = PlayerCharacterPhysical.headRadius * q; + this.leftFoot.radius = this.rightFoot.radius = PlayerCharacterPhysical.feetRadius * q; + } + + public getPropertyUpdates(): PropertyUpdatesForObject { + return new PropertyUpdatesForObject(this.id, [ + new UpdateProperty('head', this.head, this.headVelocity), + new UpdateProperty('leftFoot', this.leftFoot, this.leftFootVelocity), + new UpdateProperty('rightFoot', this.rightFoot, this.rightFootVelocity), + ]); + } + + private setPropertyUpdates( + oldHead: Circle, + oldLeftFoot: Circle, + oldRightFoot: Circle, + deltaTime: number, + ) { + this.headVelocity = new Circle( + vec2.scale( + oldHead.center, + vec2.subtract(oldHead.center, this.head.center, oldHead.center), + 1 / deltaTime, + ), + (this.head.radius - oldHead.radius) / deltaTime, ); + + this.leftFootVelocity = new Circle( + vec2.scale( + oldLeftFoot.center, + vec2.subtract(oldLeftFoot.center, this.leftFoot.center, oldLeftFoot.center), + 1 / deltaTime, + ), + (this.leftFoot.radius - oldLeftFoot.radius) / deltaTime, + ); + + this.rightFootVelocity = new Circle( + vec2.scale( + oldRightFoot.center, + vec2.subtract(oldRightFoot.center, this.rightFoot.center, oldRightFoot.center), + 1 / deltaTime, + ), + (this.rightFoot.radius - oldRightFoot.radius) / deltaTime, + ); + + this.animateScaling(1); } public step(deltaTime: number) { + const oldHead = new Circle(vec2.clone(this.head.center), this.head.radius); + const oldLeftFoot = new Circle( + vec2.clone(this.leftFoot.center), + this.leftFoot.radius, + ); + const oldRightFoot = new Circle( + vec2.clone(this.rightFoot.center), + this.rightFoot.radius, + ); + if (this.isDestroyed) { if ((this.timeSinceDying += deltaTime) > settings.spawnDespawnTime) { this.destroy(); } else { this.animateScaling(1 - this.timeSinceDying / settings.spawnDespawnTime); } + this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTime); return; } @@ -248,6 +306,7 @@ export class PlayerCharacterPhysical } else { this.animateScaling(this.timeSinceBorn / settings.spawnDespawnTime); } + this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTime); return; } @@ -262,7 +321,7 @@ export class PlayerCharacterPhysical this.currentPlanet?.takeControl(this.team, deltaTime); - const intersectingWithForcefield = this.container.findIntersecting( + const intersectingWithForceField = this.container.findIntersecting( getBoundingBoxOfCircle( new Circle( this.center, @@ -273,15 +332,17 @@ export class PlayerCharacterPhysical const direction = this.averageAndResetMovementActions(); const movementForce = vec2.scale(direction, direction, settings.maxAcceleration); + this.applyForce(this.leftFoot, movementForce, deltaTime); + this.applyForce(this.rightFoot, movementForce, deltaTime); if (!this.currentPlanet) { const leftFootGravity = forceAtPosition( this.leftFoot.center, - intersectingWithForcefield, + intersectingWithForceField, ); const rightFootGravity = forceAtPosition( this.rightFoot.center, - intersectingWithForcefield, + intersectingWithForceField, ); this.applyForce(this.leftFoot, leftFootGravity, deltaTime); @@ -325,22 +386,19 @@ export class PlayerCharacterPhysical this.setDirection(gravity, deltaTime); } - this.applyForce(this.leftFoot, movementForce, deltaTime); - this.applyForce(this.rightFoot, movementForce, deltaTime); - this.keepPosture(deltaTime); this.stepBodyPart(this.leftFoot, deltaTime); this.stepBodyPart(this.rightFoot, deltaTime); this.stepBodyPart(this.head, deltaTime); - this.remoteCall('updateCircles', this.head, this.leftFoot, this.rightFoot); + this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTime); } private setDirection(direction: vec2, deltaTime: number) { this.direction = interpolateAngles( this.direction, Math.atan2(direction.y, direction.x) + Math.PI / 2, - Math.pow(2, deltaTime), + 0.2, ); } @@ -351,14 +409,14 @@ export class PlayerCharacterPhysical center, PlayerCharacterPhysical.leftFootOffset, deltaTime, - 150, + 3000, ); this.springMove( this.rightFoot, center, PlayerCharacterPhysical.rightFootOffset, deltaTime, - 150, + 3000, ); this.springMove( @@ -366,7 +424,7 @@ export class PlayerCharacterPhysical center, PlayerCharacterPhysical.headOffset, deltaTime, - 350, + 7000, ); } @@ -381,15 +439,26 @@ export class PlayerCharacterPhysical vec2.rotate(desiredPosition, desiredPosition, center, this.direction); const positionDelta = vec2.subtract(vec2.create(), desiredPosition, object.center); - const positionDeltaDirection = vec2.normalize(vec2.create(), positionDelta); const positionDeltaLength = vec2.length(positionDelta); - vec2.scale( - positionDelta, - positionDeltaDirection, - positionDeltaLength ** 2 * deltaTime * strength, - ); - object.applyForce(positionDelta, deltaTime); + if (positionDeltaLength > 0) { + const positionDeltaDirection = vec2.normalize(vec2.create(), positionDelta); + vec2.scale( + positionDelta, + positionDeltaDirection, + positionDeltaLength ** 2 * deltaTime * strength, + ); + + if (vec2.length(positionDelta) * deltaTime * deltaTime > positionDeltaLength) { + vec2.scale( + positionDelta, + positionDelta, + positionDeltaLength / (vec2.length(positionDelta) * deltaTime * deltaTime), + ); + } + + object.applyForce(positionDelta, deltaTime); + } } private stepBodyPart(part: CirclePhysical, deltaTime: number) { @@ -406,12 +475,6 @@ export class PlayerCharacterPhysical circle.velocity, vec2.scale(vec2.create(), force, timeInSeconds), ); - - vec2.set( - circle.velocity, - clamp(circle.velocity.x, -settings.maxVelocityX, settings.maxVelocityX), - clamp(circle.velocity.y, -settings.maxVelocityY, settings.maxVelocityY), - ); } public kill() { diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts index 6787fee..f0d77e4 100644 --- a/backend/src/objects/projectile-physical.ts +++ b/backend/src/objects/projectile-physical.ts @@ -6,6 +6,8 @@ import { ProjectileBase, GameObject, CharacterTeam, + PropertyUpdatesForObject, + UpdateProperty, } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { CirclePhysical } from './circle-physical'; @@ -98,6 +100,12 @@ export class ProjectilePhysical } } + public getPropertyUpdates(): PropertyUpdatesForObject { + return new PropertyUpdatesForObject(this.id, [ + new UpdateProperty('center', this.center, this.velocity), + ]); + } + public step(deltaTime: number) { super.step(deltaTime); @@ -127,7 +135,5 @@ export class ProjectilePhysical vec2.copy(this.object.velocity, this.velocity); const { velocity } = this.object.step2(deltaTime); vec2.copy(this.velocity, velocity); - - this.remoteCall('setCenter', this.center); } } diff --git a/backend/src/physics/functions/apply-spring-force.ts b/backend/src/physics/functions/apply-spring-force.ts deleted file mode 100644 index e2d3db3..0000000 --- a/backend/src/physics/functions/apply-spring-force.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Circle } from 'shared'; -import { CirclePhysical } from '../../objects/circle-physical'; - -export const applySpringForce = ( - a: CirclePhysical | Circle, - b: CirclePhysical | Circle, - distance: number, - strength: number, - deltaTimeInSeconds: number, -) => { - const length = vec2.dist(a.center, b.center) - distance; - - const abDirection = vec2.subtract(vec2.create(), b.center, a.center); - vec2.normalize(abDirection, abDirection); - const force = vec2.scale(abDirection, abDirection, strength * length); - if (a instanceof CirclePhysical) { - a.applyForce(force, deltaTimeInSeconds); - } - if (b instanceof CirclePhysical) { - vec2.scale(force, force, -1); - b.applyForce(force, deltaTimeInSeconds); - } -}; diff --git a/backend/src/physics/functions/move-circle.ts b/backend/src/physics/functions/move-circle.ts index 8b436f7..9d9b16d 100644 --- a/backend/src/physics/functions/move-circle.ts +++ b/backend/src/physics/functions/move-circle.ts @@ -17,7 +17,12 @@ export const moveCircle = ( tangent?: vec2; hitObject?: GameObject; } => { - const direction = vec2.normalize(vec2.create(), delta); + const direction = vec2.clone(delta); + + if (vec2.length(delta) > 0) { + vec2.normalize(direction, direction); + } + const deltaLength = vec2.length(delta); let travelled = 0; let rayEnd = vec2.create(); diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index 4d98eac..87d74b7 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -1,7 +1,6 @@ import { vec2 } from 'gl-matrix'; import { CommandExecutors, - CommandReceiver, CreateObjectsCommand, CreatePlayerCommand, DeleteObjectsCommand, @@ -13,7 +12,6 @@ import { SecondaryActionCommand, PlayerDiedCommand, settings, - Circle, PlayerInformation, CharacterTeam, UpdateOtherPlayerDirections, @@ -23,6 +21,8 @@ import { RemoteCallsForObject, RemoteCallsForObjects, ServerAnnouncement, + PropertyUpdatesForObjects, + PropertyUpdatesForObject, } from 'shared'; import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; @@ -93,8 +93,12 @@ export class Player extends PlayerBase { this.queueCommandSend(new CreatePlayerCommand(this.character!)); } + private timeSinceLastMessage = 0; + private messageInterval = 1 / 30; private timeUntilRespawn = 0; public step(deltaTimeInSeconds: number) { + this.timeSinceLastMessage += deltaTimeInSeconds; + if (this.character) { this.center = this.character?.center; @@ -107,9 +111,12 @@ export class Player extends PlayerBase { this.timeUntilRespawn = settings.playerDiedTimeout; } } else { - this.queueCommandSend( - new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}…`), - ); + if (this.timeSinceLastMessage > this.messageInterval) { + this.queueCommandSend( + new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}…`), + ); + } + if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) { this.createCharacter(); this.center = this.character!.center; @@ -117,33 +124,45 @@ export class Player extends PlayerBase { } } - const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.5); - const bb = new BoundingBox(); - bb.topLeft = viewArea.topLeft; - bb.size = viewArea.size; + if (this.timeSinceLastMessage > this.messageInterval) { + const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.5); + const bb = new BoundingBox(); + bb.topLeft = viewArea.topLeft; + bb.size = viewArea.size; - const objectsInViewArea = Array.from( - new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)), - ); - - const newlyIntersecting = objectsInViewArea.filter( - (o) => !this.objectsPreviouslyInViewArea.includes(o), - ); - - const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( - (o) => !objectsInViewArea.includes(o), - ); - - this.objectsPreviouslyInViewArea = objectsInViewArea; - - if (noLongerIntersecting.length > 0) { - this.queueCommandSend( - new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id)), + const objectsInViewArea = Array.from( + new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)), ); - } - if (newlyIntersecting.length > 0) { - this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting)); + const newlyIntersecting = objectsInViewArea.filter( + (o) => !this.objectsPreviouslyInViewArea.includes(o), + ); + + const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( + (o) => !objectsInViewArea.includes(o), + ); + + this.objectsPreviouslyInViewArea = objectsInViewArea; + + if (noLongerIntersecting.length > 0) { + this.queueCommandSend( + new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id)), + ); + } + + if (newlyIntersecting.length > 0) { + this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting)); + } + + this.queueCommandSend(new UpdateOtherPlayerDirections(this.getOtherPlayers())); + + this.queueCommandSend( + new PropertyUpdatesForObjects( + this.objectsPreviouslyInViewArea + .map((o) => o.getPropertyUpdates()) + .filter((u) => u) as Array, + ), + ); } this.queueCommandSend( @@ -154,7 +173,10 @@ export class Player extends PlayerBase { ), ); - this.queueCommandSend(new UpdateOtherPlayerDirections(this.getOtherPlayers())); + if (this.timeSinceLastMessage > this.messageInterval) { + this.sendQueuedCommandsToClient(); + this.timeSinceLastMessage = 0; + } } private getOtherPlayers(): Array { @@ -179,9 +201,10 @@ export class Player extends PlayerBase { return otherPlayers.map( (p) => new OtherPlayerDirection( + p.character!.id, vec2.normalize( vec2.create(), - vec2.subtract(vec2.create(), p.center, this.character!.center), + vec2.subtract(vec2.create(), p.character!.center, this.character!.center), ), p.team, ), diff --git a/frontend/src/main.scss b/frontend/src/main.scss index 9cc1921..bb5fc6d 100644 --- a/frontend/src/main.scss +++ b/frontend/src/main.scss @@ -180,6 +180,8 @@ body { } .other-player-arrow { + transition: transform 150ms; + @include square($large-icon); @media (max-width: $breakpoint) { @include square($small-icon); @@ -191,6 +193,7 @@ body { &.decla { background-color: $bright-decla; } + &.red { background-color: $bright-red; } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 13eaab4..34f99df 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -3,7 +3,6 @@ import { Renderer, renderNoise } from 'sdf-2d'; import { broadcastCommands, deserialize, - serialize, TransportEvents, SetAspectRatioActionCommand, PlayerInformation, @@ -27,7 +26,6 @@ import { CommandReceiverSocket } from './commands/receivers/command-receiver-soc 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'; import { VibrationHandler } from './vibration-handler'; @@ -44,7 +42,7 @@ export class Game extends CommandReceiver { private redPlanetCountElement = document.createElement('div'); private announcementText = document.createElement('h2'); private progressBar = document.createElement('div'); - private arrowElements: Array = []; + private arrows: { [id: number]: HTMLElement } = {}; private socketReceiver!: CommandReceiverSocket; constructor( @@ -66,6 +64,7 @@ export class Game extends CommandReceiver { this.socket?.close(); this.gameObjects = new GameObjectContainer(this); this.overlay.innerHTML = ''; + this.isEnding = false; this.lastAnnouncementText = ''; this.overlay.appendChild(this.progressBar); this.announcementText.innerText = ''; @@ -119,7 +118,7 @@ export class Game extends CommandReceiver { } private lastGameState?: UpdateGameState; - + private isEnding = false; private lastAnnouncementText = ''; protected commandExecutors: CommandExecutors = { [ServerAnnouncement.type]: (c: ServerAnnouncement) => @@ -127,11 +126,9 @@ export class Game extends CommandReceiver { [PlayerDiedCommand.type]: (c: PlayerDiedCommand) => VibrationHandler.vibrate(150), [UpdateGameState.type]: (c: UpdateGameState) => (this.lastGameState = c), [GameEnd.type]: (c: GameEnd) => { - const team = - c.winningTeam === CharacterTeam.decla - ? 'decla' - : 'red'; + const team = `${c.winningTeam}`; this.lastAnnouncementText = `Team ${team} won 🎉`; + this.isEnding = true; }, [UpdateOtherPlayerDirections.type]: (c: UpdateOtherPlayerDirections) => (this.lastOtherPlayerDirections = c), @@ -140,23 +137,16 @@ export class Game extends CommandReceiver { private lastOtherPlayerDirections?: UpdateOtherPlayerDirections; private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) { - this.arrowElements - .splice(command.otherPlayerDirections.length, this.arrowElements.length) - .forEach((e) => e.parentElement?.removeChild(e)); + command.otherPlayerDirections.forEach((d) => { + if (!(d.id! in this.arrows)) { + const element = document.createElement('div'); + this.arrows[d.id!] = element; + this.overlay.appendChild(element); + } - for ( - let i = this.arrowElements.length; - i < command.otherPlayerDirections.length; - i++ - ) { - const element = document.createElement('div'); - this.arrowElements.push(element); - this.overlay.appendChild(element); - } - - this.arrowElements.forEach((e, i) => { - const direction = command.otherPlayerDirections[i].direction; - const team = command.otherPlayerDirections[i].team; + const e = this.arrows[d.id!]; + const direction = d.direction; + const team = d.team; const angle = Math.atan2(direction.y, direction.x); e.className = 'other-player-arrow ' + team; @@ -191,6 +181,16 @@ export class Game extends CommandReceiver { p.y }px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `; }); + + for (let id in this.arrows) { + if ( + Object.prototype.hasOwnProperty.call(this.arrows, id) && + command.otherPlayerDirections.find((v) => v.id?.toString() === id) === undefined + ) { + this.arrows[id].parentElement?.removeChild(this.arrows[id]); + delete this.arrows[id]; + } + } } public async start(): Promise { @@ -237,7 +237,7 @@ export class Game extends CommandReceiver { this.renderer = renderer; this.socketReceiver.sendQueuedCommands(); - this.gameObjects.stepObjects(deltaTime); + this.gameObjects.stepObjects(this.isEnding ? 0 : deltaTime); this.gameObjects.drawObjects(this.renderer, this.overlay, shouldChangeLayout); return this.isActive; diff --git a/frontend/src/scripts/helper/circle-extrapolator.ts b/frontend/src/scripts/helper/circle-extrapolator.ts new file mode 100644 index 0000000..aef9d0c --- /dev/null +++ b/frontend/src/scripts/helper/circle-extrapolator.ts @@ -0,0 +1,23 @@ +import { vec2 } from 'gl-matrix'; +import { Circle } from 'shared'; +import { LinearExtrapolator } from './linear-extrapolator'; +import { Vec2Extrapolator } from './vec2-extrapolator'; + +export class CircleExtrapolator { + private center: Vec2Extrapolator; + private radius: LinearExtrapolator; + + constructor(currentValue: Circle) { + this.center = new Vec2Extrapolator(currentValue.center); + this.radius = new LinearExtrapolator(currentValue.radius); + } + + public addFrame(value: Circle, rateOfChange: Circle) { + this.center.addFrame(value.center, rateOfChange.center); + this.radius.addFrame(value.radius, rateOfChange.radius); + } + + public getValue(deltaTime: number): Circle { + return new Circle(this.center.getValue(deltaTime), this.radius.getValue(deltaTime)); + } +} diff --git a/frontend/src/scripts/helper/linear-extrapolator.ts b/frontend/src/scripts/helper/linear-extrapolator.ts new file mode 100644 index 0000000..dcf3a8c --- /dev/null +++ b/frontend/src/scripts/helper/linear-extrapolator.ts @@ -0,0 +1,38 @@ +import { clamp } from 'shared'; + +export class LinearExtrapolator { + private velocity = 0; + private compensationVelocity = 0; + private timeSinceSet = 0; + + constructor(private currentValue: number) {} + + public addFrame(value: number, rateOfChange: number) { + this.timeSinceSet = 0; + const differenceFromCurrent = value - this.currentValue; + + if (Math.abs(differenceFromCurrent) > 200) { + this.currentValue = value; + this.compensationVelocity = 0; + } else { + this.compensationVelocity = differenceFromCurrent / (1 / 30); + } + + this.velocity = rateOfChange; + } + + public getValue(deltaTime: number): number { + this.currentValue += deltaTime * this.velocity; + + const compensationTimeLeft = 1 / 30 - this.timeSinceSet; + + if (compensationTimeLeft > 0) { + this.currentValue += + Math.min(compensationTimeLeft, deltaTime) * this.compensationVelocity; + } + + this.timeSinceSet += deltaTime; + + return this.currentValue; + } +} diff --git a/frontend/src/scripts/helper/vec2-extrapolator.ts b/frontend/src/scripts/helper/vec2-extrapolator.ts new file mode 100644 index 0000000..72ae904 --- /dev/null +++ b/frontend/src/scripts/helper/vec2-extrapolator.ts @@ -0,0 +1,21 @@ +import { vec2 } from 'gl-matrix'; +import { LinearExtrapolator } from './linear-extrapolator'; + +export class Vec2Extrapolator { + private x: LinearExtrapolator; + private y: LinearExtrapolator; + + constructor(currentValue: vec2) { + this.x = new LinearExtrapolator(currentValue.x); + this.y = new LinearExtrapolator(currentValue.y); + } + + public addFrame(value: vec2, rateOfChange: vec2) { + this.x.addFrame(value.x, rateOfChange.x); + this.y.addFrame(value.y, rateOfChange.y); + } + + public getValue(deltaTime: number): vec2 { + return vec2.fromValues(this.x.getValue(deltaTime), this.y.getValue(deltaTime)); + } +} diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts index 2a0cb85..cc44a0a 100644 --- a/frontend/src/scripts/objects/camera.ts +++ b/frontend/src/scripts/objects/camera.ts @@ -1,6 +1,6 @@ import { vec2 } from 'gl-matrix'; import { Renderer } from 'sdf-2d'; -import { calculateViewArea, GameObject, mixRgb, settings } from 'shared'; +import { calculateViewArea, GameObject, mixRgb, settings, UpdateProperty } from 'shared'; import { Game } from '../game'; import { ViewObject } from './view-object'; @@ -14,6 +14,8 @@ export class Camera extends GameObject implements ViewObject { super(null); } + public updateProperties(update: UpdateProperty[]): void {} + public beforeDestroy(): void {} public step(deltaTimeInSeconds: number): void {} diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts index eb1f61f..b4875c6 100644 --- a/frontend/src/scripts/objects/game-object-container.ts +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -6,6 +6,7 @@ import { CreatePlayerCommand, DeleteObjectsCommand, Id, + PropertyUpdatesForObjects, RemoteCallsForObjects, } from 'shared'; import { Game } from '../game'; @@ -41,6 +42,9 @@ export class GameObjectContainer extends CommandReceiver { this.objects.get(c.id)?.processRemoteCalls(c.calls), ), + [PropertyUpdatesForObjects.type]: (c: PropertyUpdatesForObjects) => + c.updates.forEach((c) => this.objects.get(c.id)?.updateProperties(c.updates)), + [DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) => c.ids.forEach((id: Id) => this.deleteObject(id)), }; @@ -50,11 +54,11 @@ export class GameObjectContainer extends CommandReceiver { } public stepObjects(deltaTimeInSeconds: number) { + this.objects.forEach((o) => o.step(deltaTimeInSeconds)); + if (this.player) { this.camera.center = this.player.position; } - - this.objects.forEach((o) => o.step(deltaTimeInSeconds)); } public drawObjects( diff --git a/frontend/src/scripts/objects/lamp-view.ts b/frontend/src/scripts/objects/lamp-view.ts index c3deacf..df87fdf 100644 --- a/frontend/src/scripts/objects/lamp-view.ts +++ b/frontend/src/scripts/objects/lamp-view.ts @@ -1,6 +1,6 @@ import { vec2, vec3 } from 'gl-matrix'; import { CircleLight, Renderer } from 'sdf-2d'; -import { CommandExecutors, Id, LampBase } from 'shared'; +import { CommandExecutors, Id, LampBase, UpdateProperty } from 'shared'; import { RenderCommand } from '../commands/types/render'; import { ViewObject } from './view-object'; @@ -16,6 +16,8 @@ export class LampView extends LampBase implements ViewObject { this.light = new CircleLight(center, color, lightness); } + public updateProperties(update: UpdateProperty[]): void {} + public step(deltaTimeInSeconds: number): void {} public beforeDestroy(): void {} diff --git a/frontend/src/scripts/objects/planet-view.ts b/frontend/src/scripts/objects/planet-view.ts index 6cbc5b3..8bed4bb 100644 --- a/frontend/src/scripts/objects/planet-view.ts +++ b/frontend/src/scripts/objects/planet-view.ts @@ -1,6 +1,6 @@ import { vec2 } from 'gl-matrix'; import { Renderer } from 'sdf-2d'; -import { CommandExecutors, Id, Random, PlanetBase } from 'shared'; +import { CommandExecutors, Id, Random, PlanetBase, UpdateProperty } from 'shared'; import { RenderCommand } from '../commands/types/render'; import { PlanetShape } from '../shapes/planet-shape'; import { ViewObject } from './view-object'; @@ -30,6 +30,8 @@ export class PlanetView extends PlanetBase implements ViewObject { this.ownershipProgess.className = 'ownership'; } + public updateProperties(update: UpdateProperty[]): void {} + public step(deltaTimeInSeconds: number): void { this.shape.randomOffset += deltaTimeInSeconds / 4; this.shape.colorMixQ = this.ownership; diff --git a/frontend/src/scripts/objects/player-character-view.ts b/frontend/src/scripts/objects/player-character-view.ts index e453cc9..d2b804a 100644 --- a/frontend/src/scripts/objects/player-character-view.ts +++ b/frontend/src/scripts/objects/player-character-view.ts @@ -1,6 +1,14 @@ import { vec2 } from 'gl-matrix'; import { Renderer } from 'sdf-2d'; -import { Circle, Id, PlayerCharacterBase, CharacterTeam, settings } from 'shared'; +import { + Circle, + Id, + PlayerCharacterBase, + CharacterTeam, + settings, + UpdateProperty, +} from 'shared'; +import { CircleExtrapolator } from '../helper/circle-extrapolator'; import { BlobShape } from '../shapes/blob-shape'; import { SoundHandler, Sounds } from '../sound-handler'; import { VibrationHandler } from '../vibration-handler'; @@ -15,6 +23,10 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje public isMainCharacter = false; + private leftFootExtrapolator: CircleExtrapolator; + private rightFootExtrapolator: CircleExtrapolator; + private headExtrapolator: CircleExtrapolator; + constructor( id: Id, name: string, @@ -29,6 +41,10 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje super(id, name, killCount, deathCount, team, health, head, leftFoot, rightFoot); this.shape = new BlobShape(settings.colorIndices[team]); + this.leftFootExtrapolator = new CircleExtrapolator(this.leftFoot!); + this.rightFootExtrapolator = new CircleExtrapolator(this.rightFoot!); + this.headExtrapolator = new CircleExtrapolator(this.head!); + this.previousHealth = this.health; this.nameElement.className = 'player-tag ' + this.team; this.nameElement.innerText = this.name; @@ -40,6 +56,20 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje return this.head!.center; } + public updateProperties(update: Array) { + update.forEach((u) => { + if (u.propertyKey === 'head') { + this.headExtrapolator.addFrame(u.propertyValue, u.rateOfChange); + } + if (u.propertyKey === 'leftFoot') { + this.leftFootExtrapolator.addFrame(u.propertyValue, u.rateOfChange); + } + if (u.propertyKey === 'rightFoot') { + this.rightFootExtrapolator.addFrame(u.propertyValue, u.rateOfChange); + } + }); + } + public setHealth(health: number) { const previousHealth = this.health; super.setHealth(health); @@ -57,10 +87,14 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje this.previousHealth = this.health; } + + this.head! = this.headExtrapolator.getValue(deltaTimeInSeconds); + this.leftFoot! = this.leftFootExtrapolator.getValue(deltaTimeInSeconds); + this.rightFoot! = this.rightFootExtrapolator.getValue(deltaTimeInSeconds); } public onShoot(strength: number) { - SoundHandler.play(Sounds.shoot, (0.3 * 2 * strength) / settings.playerMaxStrength); + SoundHandler.play(Sounds.shoot, (0.6 * strength) / settings.playerMaxStrength); } public beforeDestroy(): void { diff --git a/frontend/src/scripts/objects/projectile-view.ts b/frontend/src/scripts/objects/projectile-view.ts index ede927b..ef97cb9 100644 --- a/frontend/src/scripts/objects/projectile-view.ts +++ b/frontend/src/scripts/objects/projectile-view.ts @@ -1,12 +1,15 @@ import { vec2 } from 'gl-matrix'; import { CircleLight, ColorfulCircle, Renderer } from 'sdf-2d'; -import { CharacterTeam, Id, ProjectileBase, settings } from 'shared'; +import { CharacterTeam, Id, ProjectileBase, settings, UpdateProperty } from 'shared'; +import { Vec2Extrapolator } from '../helper/vec2-extrapolator'; import { ViewObject } from './view-object'; export class ProjectileView extends ProjectileBase implements ViewObject { private circle: ColorfulCircle; private light: CircleLight; + private centerExtrapolator: Vec2Extrapolator; + constructor( id: Id, center: vec2, @@ -21,11 +24,19 @@ export class ProjectileView extends ProjectileBase implements ViewObject { settings.paletteDim[settings.colorIndices[team]], 0, ); + this.centerExtrapolator = new Vec2Extrapolator(center); + } + + public updateProperties(update: UpdateProperty[]): void { + update.forEach((u) => { + this.centerExtrapolator.addFrame(u.propertyValue, u.rateOfChange); + }); } public step(deltaTimeInSeconds: number): void { super.step(deltaTimeInSeconds); + this.center = this.centerExtrapolator.getValue(deltaTimeInSeconds); this.circle.center = this.center; this.light.center = this.center; this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength; diff --git a/frontend/src/scripts/objects/view-object.ts b/frontend/src/scripts/objects/view-object.ts index fbb5ba6..49013ac 100644 --- a/frontend/src/scripts/objects/view-object.ts +++ b/frontend/src/scripts/objects/view-object.ts @@ -1,8 +1,9 @@ import { Renderer } from 'sdf-2d'; -import { GameObject } from 'shared'; +import { GameObject, UpdateProperty } from 'shared'; export interface ViewObject extends GameObject { step(deltaTimeInMilliseconds: number): void; draw(renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean): void; + updateProperties(update: Array): void; beforeDestroy(): void; } diff --git a/shared/src/commands/types/update-other-player-directions.ts b/shared/src/commands/types/update-other-player-directions.ts index b5bc248..ab0775e 100644 --- a/shared/src/commands/types/update-other-player-directions.ts +++ b/shared/src/commands/types/update-other-player-directions.ts @@ -1,17 +1,19 @@ import { vec2 } from 'gl-matrix'; import { CharacterTeam } from '../../objects/types/character-team'; +import { Id } from '../../transport/identity'; import { serializable } from '../../transport/serialization/serializable'; import { Command } from '../command'; @serializable export class OtherPlayerDirection { public constructor( + public readonly id: Id, public readonly direction: vec2, public readonly team: CharacterTeam, ) {} public toArray(): Array { - return [this.direction, this.team]; + return [this.id, this.direction, this.team]; } } diff --git a/shared/src/objects/game-object.ts b/shared/src/objects/game-object.ts index fd8cedf..23e1ef8 100644 --- a/shared/src/objects/game-object.ts +++ b/shared/src/objects/game-object.ts @@ -1,3 +1,4 @@ +import { Command } from '../commands/command'; import { Id } from '../transport/identity'; import { serializable } from '../transport/serialization/serializable'; @@ -10,6 +11,39 @@ export class RemoteCall { } } +@serializable +export class UpdateProperty { + constructor( + public readonly propertyKey: string, + public readonly propertyValue: any, + public readonly rateOfChange: any, + ) {} + + public toArray(): Array { + return [this.propertyKey, this.propertyValue, this.rateOfChange]; + } +} + +@serializable +export class PropertyUpdatesForObject { + constructor(public readonly id: Id, public readonly updates: Array) {} + + public toArray(): Array { + return [this.id, this.updates]; + } +} + +@serializable +export class PropertyUpdatesForObjects extends Command { + constructor(public readonly updates: Array) { + super(); + } + + public toArray(): Array { + return [this.updates]; + } +} + export abstract class GameObject { private remoteCalls: Array = []; @@ -23,6 +57,8 @@ export abstract class GameObject { ); } + public getPropertyUpdates(): PropertyUpdatesForObject | void {} + public getRemoteCalls(): Array { return this.remoteCalls; } diff --git a/shared/src/objects/types/player-character-base.ts b/shared/src/objects/types/player-character-base.ts index 35581cd..1c32675 100644 --- a/shared/src/objects/types/player-character-base.ts +++ b/shared/src/objects/types/player-character-base.ts @@ -22,15 +22,6 @@ export class PlayerCharacterBase extends GameObject { public onShoot(strength: number) {} - public updateCircles(head: Circle, leftFoot: Circle, rightFoot: Circle) { - this.head!.center = head.center; - this.head!.radius = head.radius; - this.leftFoot!.center = leftFoot.center; - this.leftFoot!.radius = leftFoot.radius; - this.rightFoot!.center = rightFoot.center; - this.rightFoot!.radius = rightFoot.radius; - } - public setHealth(health: number) { this.health = health; } diff --git a/shared/src/objects/types/projectile-base.ts b/shared/src/objects/types/projectile-base.ts index 96aa789..b03ca0f 100644 --- a/shared/src/objects/types/projectile-base.ts +++ b/shared/src/objects/types/projectile-base.ts @@ -17,10 +17,6 @@ export class ProjectileBase extends GameObject { super(id); } - public setCenter(center: vec2) { - this.center = center; - } - public step(deltaTimeInSeconds: number) { this.strength -= settings.projectileFadeSpeed * deltaTimeInSeconds; this.strength = Math.max(0, this.strength); diff --git a/shared/src/settings.ts b/shared/src/settings.ts index d70b23f..12809c4 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -35,7 +35,7 @@ export const settings = { playerDiedTimeout: 5, playerStrengthRegenerationPerSeconds: 80, projectileMaxStrength: 40, - projectileSpeed: 4000, + projectileSpeed: 2500, projectileMaxBounceCount: 2, projectileTimeout: 3, projectileFadeSpeed: 20, @@ -111,7 +111,6 @@ export const settings = { }, palette: [declaColor, neutralColor, redColor], paletteDim: [declaColorDim, neutralColor, redColorDim], - targetPhysicsDeltaTimeInMilliseconds: 15, - minPhysicsSleepTime: 4, + targetPhysicsDeltaTimeInSeconds: 1 / 100, inViewAreaSize: 1920 * 1080 * 3, };