diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts index 71838de..1aae894 100644 --- a/backend/src/game-server.ts +++ b/backend/src/game-server.ts @@ -1,5 +1,4 @@ import { PhysicalContainer } from './physics/containers/physical-container'; -import { Player } from './players/player'; import ioserver from 'socket.io'; import { TransportEvents, @@ -11,18 +10,22 @@ import { import { createWorld } from './map/create-world'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { Options } from './options'; +import { PlayerContainer } from './players/player-container'; const playerCountSubscribedRoom = 'playerCountUpdates'; export class GameServer { private objects = new PhysicalContainer(); - private players: Array = []; + private players: PlayerContainer; private deltaTimes: Array = []; private deltaTimeCalculator = new DeltaTimeCalculator(); private serverName: string; private playerLimit: number; + constructor(private readonly io: ioserver.Server, options: Options) { + this.players = new PlayerContainer(this.objects); + this.serverName = options.name; this.playerLimit = options.playerLimit; @@ -31,8 +34,7 @@ export class GameServer { io.on('connection', (socket: SocketIO.Socket) => { socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => { - const player = new Player(playerInfo, this.players, this.objects, socket); - this.players.push(player); + const player = this.players.createPlayer(playerInfo, socket); socket.on(TransportEvents.PlayerToServer, (json: string) => { const command = deserialize(json); player.sendCommand(command); @@ -42,7 +44,7 @@ export class GameServer { socket.on('disconnect', () => { player.destroy(); - this.players = this.players.filter((p) => p !== player); + this.players.deletePlayer(player); this.sendPlayerCountUpdate(); }); }); @@ -56,7 +58,7 @@ export class GameServer { public sendPlayerCountUpdate() { this.io .to(playerCountSubscribedRoom) - .emit(TransportEvents.PlayerCountUpdate, this.players.length); + .emit(TransportEvents.PlayerCountUpdate, this.players.count); } public start() { @@ -64,8 +66,7 @@ export class GameServer { } private handlePhysics() { - const delta = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(); - this.deltaTimes.push(delta); + const delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds(); const framesBetweenDeltaTimeCalculation = 1000; if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) { @@ -79,15 +80,15 @@ export class GameServer { `Memory used: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, ); this.deltaTimes = []; - console.log(this.players.map((p) => p.latency)); } - this.objects.stepObjects(delta / 1000); - this.players.forEach((p) => p.step(delta / 1000)); + this.objects.stepObjects(delta); + this.players.step(delta); - const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInMilliseconds(); + 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 { @@ -98,7 +99,7 @@ export class GameServer { public get serverInfo(): ServerInformation { return { serverName: this.serverName, - playerCount: this.players.length, + playerCount: this.players.count, playerLimit: this.playerLimit, }; } diff --git a/backend/src/helper/delta-time-calculator.ts b/backend/src/helper/delta-time-calculator.ts index d52526c..cd1f38a 100644 --- a/backend/src/helper/delta-time-calculator.ts +++ b/backend/src/helper/delta-time-calculator.ts @@ -1,20 +1,20 @@ export class DeltaTimeCalculator { private previousTime: [number, number] = process.hrtime(); - public getNextDeltaTimeInMilliseconds(): number { + public getNextDeltaTimeInSeconds(): number { const deltaTime = process.hrtime(this.previousTime); this.previousTime = process.hrtime(); const [seconds, nanoSeconds] = deltaTime; - return seconds * 1000 + nanoSeconds / 1000 / 1000; + return seconds * 1000 + nanoSeconds / 1000 / 1000 / 1000; } - public getDeltaTimeInMilliseconds(): number { + public getDeltaTimeInSeconds(): number { const deltaTime = process.hrtime(this.previousTime); const [seconds, nanoSeconds] = deltaTime; - return seconds * 1000 + nanoSeconds / 1000 / 1000; + return seconds * 1000 + nanoSeconds / 1000 / 1000 / 1000; } } diff --git a/backend/src/objects/player-character-physical.ts b/backend/src/objects/player-character-physical.ts index 87301fe..7703920 100644 --- a/backend/src/objects/player-character-physical.ts +++ b/backend/src/objects/player-character-physical.ts @@ -97,12 +97,11 @@ export class PlayerCharacterPhysical name: string, killCount: number, deathCount: number, - public readonly colorIndex: number, team: CharacterTeam, private readonly container: PhysicalContainer, startPosition: vec2, ) { - super(id(), name, killCount, deathCount, colorIndex, team, settings.playerMaxHealth); + super(id(), name, killCount, deathCount, team, settings.playerMaxHealth); this.head = new CirclePhysical( vec2.add(vec2.create(), startPosition, PlayerCharacterPhysical.headOffset), @@ -176,7 +175,6 @@ export class PlayerCharacterPhysical const projectile = new ProjectilePhysical( vec2.clone(this.center), 20, - this.colorIndex, strength, this.team, velocity, diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts index 6a9662c..95b160d 100644 --- a/backend/src/objects/projectile-physical.ts +++ b/backend/src/objects/projectile-physical.ts @@ -34,14 +34,13 @@ export class ProjectilePhysical constructor( center: vec2, radius: number, - colorIndex: number, public strength: number, - public team: CharacterTeam, + team: CharacterTeam, private velocity: vec2, public readonly originator: PlayerCharacterPhysical, readonly container: PhysicalContainer, ) { - super(id(), center, radius, colorIndex, strength); + super(id(), center, radius, team, strength); this.object = new CirclePhysical(center, radius, this, container, 0.9); this.moveOutsideOfObject(); diff --git a/backend/src/physics/physicals/physical-base.ts b/backend/src/physics/physicals/physical-base.ts index 3f923b6..66b0018 100644 --- a/backend/src/physics/physicals/physical-base.ts +++ b/backend/src/physics/physicals/physical-base.ts @@ -9,4 +9,5 @@ export interface PhysicalBase { readonly gameObject: GameObject; distance(target: vec2): number; + step(deltaTimeInMilliseconds: number): void; } diff --git a/backend/src/players/player-container.ts b/backend/src/players/player-container.ts new file mode 100644 index 0000000..2f8dbdd --- /dev/null +++ b/backend/src/players/player-container.ts @@ -0,0 +1,49 @@ +import { CharacterTeam, PlayerInformation, Random } from 'shared'; +import { PhysicalContainer } from '../physics/containers/physical-container'; +import { Player } from './player'; + +export class PlayerContainer { + private _players: Array = []; + + constructor(private readonly objects: PhysicalContainer) {} + + public createPlayer(playerInfo: PlayerInformation, socket: SocketIO.Socket): Player { + const player = new Player( + playerInfo, + this, + this.objects, + socket, + this.getTeamOfNextPlayer(), + ); + this._players.push(player); + + return player; + } + + public get players(): Array { + return this._players; + } + + public get count(): number { + return this.players.length; + } + + public step(deltaTimeInSeconds: number) { + this.players.forEach((p) => p.step(deltaTimeInSeconds)); + } + + private getTeamOfNextPlayer(): CharacterTeam { + const declaCount = this._players.filter((p) => p.team === CharacterTeam.decla).length; + const redCount = this._players.filter((p) => p.team === CharacterTeam.red).length; + + if ((declaCount === redCount && Random.getRandom() >= 0.5) || declaCount < redCount) { + return CharacterTeam.decla; + } else { + return CharacterTeam.red; + } + } + + public deletePlayer(player: Player) { + this._players = this._players.filter((p) => p !== player); + } +} diff --git a/backend/src/players/player-team-service.ts b/backend/src/players/player-team-service.ts deleted file mode 100644 index 39d1ce3..0000000 --- a/backend/src/players/player-team-service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CharacterTeam, Random } from 'shared'; - -let declaCount = 0; -let redCount = 0; - -export const requestTeam = (): { team: CharacterTeam; colorIndex: number } => { - if ((declaCount === redCount && Random.getRandom() > 0.5) || declaCount < redCount) { - declaCount++; - return { team: CharacterTeam.decla, colorIndex: 0 }; - } else { - redCount++; - return { team: CharacterTeam.red, colorIndex: 1 }; - } -}; - -export const freeTeam = (team: CharacterTeam) => { - if (team === CharacterTeam.decla) { - declaCount--; - } else { - redCount--; - } -}; diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index cdc53fa..86eaab7 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -29,8 +29,8 @@ import { PhysicalContainer } from '../physics/containers/physical-container'; import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle'; import { isCircleIntersecting } from '../physics/functions/is-circle-intersecting'; import { PlayerCharacterPhysical } from '../objects/player-character-physical'; -import { freeTeam, requestTeam } from './player-team-service'; import { PlanetPhysical } from '../objects/planet-physical'; +import { PlayerContainer } from './player-container'; export class Player extends CommandReceiver { private character?: PlayerCharacterPhysical | null; @@ -66,52 +66,15 @@ export class Player extends CommandReceiver { }, }; - private findEmptyPositionForPlayer(): vec2 { - let possibleCenter = this.players.find( - (p) => p.character?.isAlive && p.team === this.team, - )?.center; - - if (!possibleCenter) { - possibleCenter = vec2.create(); - } - - let rotation = 0; - let radius = 0; - for (;;) { - const playerPosition = vec2.fromValues( - radius * Math.cos(rotation) + possibleCenter.x, - radius * Math.sin(rotation) + possibleCenter.y, - ); - - const playerBoundingCircle = new Circle( - playerPosition, - PlayerCharacterPhysical.boundRadius, - ); - - const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle); - const possibleIntersectors = this.objects.findIntersecting(playerBoundingBox); - if (!isCircleIntersecting(playerBoundingCircle, possibleIntersectors)) { - return playerPosition; - } - - rotation += Math.PI / 8; - radius += 30; - } - } - - public readonly team: CharacterTeam; - private colorIndex: number; - constructor( private readonly playerInfo: PlayerInformation, - private readonly players: Array, - private readonly objects: PhysicalContainer, + private readonly playerContainer: PlayerContainer, + private readonly objectContainer: PhysicalContainer, private readonly socket: SocketIO.Socket, + public readonly team: CharacterTeam, ) { super(); - const { team, colorIndex } = requestTeam(); this.team = team; - this.colorIndex = colorIndex; this.createCharacter(); @@ -129,13 +92,12 @@ export class Player extends CommandReceiver { this.playerInfo.name.slice(0, 20), this.sumKills, this.sumDeaths, - this.colorIndex, this.team, - this.objects, + this.objectContainer, this.findEmptyPositionForPlayer(), ); - this.objects.addObject(this.character); + this.objectContainer.addObject(this.character); this.objectsPreviouslyInViewArea.push(this.character); this.socket.emit( @@ -146,7 +108,7 @@ export class Player extends CommandReceiver { private center: vec2 = vec2.create(); private timeUntilRespawn = 0; - public step(deltaTime: number) { + public step(deltaTimeInSeconds: number) { if (this.character) { this.center = this.character?.center; @@ -161,7 +123,7 @@ export class Player extends CommandReceiver { this.character = null; this.timeUntilRespawn = settings.playerDiedTimeout; } - } else if ((this.timeUntilRespawn -= deltaTime) < 0) { + } else if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) { this.createCharacter(); } @@ -171,7 +133,7 @@ export class Player extends CommandReceiver { bb.size = viewArea.size; const objectsInViewArea = Array.from( - new Set(this.objects.findIntersecting(bb).map((o) => o.gameObject)), + new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)), ); const newlyIntersecting = objectsInViewArea.filter( @@ -210,6 +172,41 @@ export class Player extends CommandReceiver { ); } + private findEmptyPositionForPlayer(): vec2 { + let possibleCenter = this.playerContainer.players.find( + (p) => p.character?.isAlive && p.team === this.team, + )?.center; + + if (!possibleCenter) { + possibleCenter = vec2.create(); + } + + let rotation = 0; + let radius = 0; + for (;;) { + const playerPosition = vec2.fromValues( + radius * Math.cos(rotation) + possibleCenter.x, + radius * Math.sin(rotation) + possibleCenter.y, + ); + + const playerBoundingCircle = new Circle( + playerPosition, + PlayerCharacterPhysical.boundRadius, + ); + + const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle); + const possibleIntersectors = this.objectContainer.findIntersecting( + playerBoundingBox, + ); + if (!isCircleIntersecting(playerBoundingCircle, possibleIntersectors)) { + return playerPosition; + } + + rotation += Math.PI / 8; + radius += 30; + } + } + private getOtherPlayers(): Array { if (!this.character) { return []; @@ -220,12 +217,12 @@ export class Player extends CommandReceiver { bb.topLeft = viewArea.topLeft; bb.size = viewArea.size; - const playersInViewArea = this.objects + const playersInViewArea = this.objectContainer .findIntersecting(bb) .map((o) => o.gameObject) .filter((g) => g instanceof PlayerCharacterPhysical); - const otherPlayers = this.players.filter( + const otherPlayers = this.playerContainer.players.filter( (p) => playersInViewArea.indexOf(p.character!) < 0, ); @@ -247,10 +244,7 @@ export class Player extends CommandReceiver { public destroy() { this.isActive = false; - freeTeam(this.team); - if (this.character) { - this.character.destroy(); - } + this.character?.destroy(); } } diff --git a/frontend/src/scripts/objects/character-view.ts b/frontend/src/scripts/objects/character-view.ts index 3e88c5c..d065f2d 100644 --- a/frontend/src/scripts/objects/character-view.ts +++ b/frontend/src/scripts/objects/character-view.ts @@ -1,6 +1,13 @@ import { vec2 } from 'gl-matrix'; import { Renderer } from 'sdf-2d'; -import { CharacterBase, CharacterTeam, Circle, Id, UpdateMessage } from 'shared'; +import { + CharacterBase, + CharacterTeam, + Circle, + Id, + settings, + UpdateMessage, +} from 'shared'; import { BlobShape } from '../shapes/blob-shape'; import { ViewObject } from './view-object'; @@ -10,15 +17,14 @@ export class CharacterView extends CharacterBase implements ViewObject { constructor( id: Id, - colorIndex: number, team: CharacterTeam, health: number, head?: Circle, leftFoot?: Circle, rightFoot?: Circle, ) { - super(id, colorIndex, team, health, head, leftFoot, rightFoot); - this.shape = new BlobShape(colorIndex); + super(id, team, health, head, leftFoot, rightFoot); + this.shape = new BlobShape(settings.colorIndices[team]); } public get position(): vec2 { diff --git a/frontend/src/scripts/objects/player-character-view.ts b/frontend/src/scripts/objects/player-character-view.ts index 977cd55..732fa6e 100644 --- a/frontend/src/scripts/objects/player-character-view.ts +++ b/frontend/src/scripts/objects/player-character-view.ts @@ -19,26 +19,14 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje name: string, killCount: number, deathCount: number, - colorIndex: number, team: CharacterTeam, health: number, head?: Circle, leftFoot?: Circle, rightFoot?: Circle, ) { - super( - id, - name, - killCount, - deathCount, - colorIndex, - team, - health, - head, - leftFoot, - rightFoot, - ); - this.shape = new BlobShape(colorIndex); + super(id, name, killCount, deathCount, team, health, head, leftFoot, rightFoot); + this.shape = new BlobShape(settings.colorIndices[team]); this.previousHealth = this.health; this.nameElement.className = 'player-tag ' + this.team; diff --git a/frontend/src/scripts/objects/projectile-view.ts b/frontend/src/scripts/objects/projectile-view.ts index 9717e36..04e481f 100644 --- a/frontend/src/scripts/objects/projectile-view.ts +++ b/frontend/src/scripts/objects/projectile-view.ts @@ -1,6 +1,6 @@ import { vec2 } from 'gl-matrix'; import { CircleLight, ColorfulCircle, Renderer } from 'sdf-2d'; -import { Id, ProjectileBase, settings, UpdateMessage } from 'shared'; +import { CharacterTeam, Id, ProjectileBase, settings, UpdateMessage } from 'shared'; import { ViewObject } from './view-object'; export class ProjectileView extends ProjectileBase implements ViewObject { @@ -11,12 +11,16 @@ export class ProjectileView extends ProjectileBase implements ViewObject { id: Id, center: vec2, radius: number, - colorIndex: number, + team: CharacterTeam, strength: number, ) { - super(id, center, radius, colorIndex, strength); - this.circle = new ColorfulCircle(center, radius / 2, colorIndex); - this.light = new CircleLight(center, settings.palette[colorIndex], 0); + super(id, center, radius, team, strength); + this.circle = new ColorfulCircle(center, radius / 2, settings.colorIndices[team]); + this.light = new CircleLight( + center, + settings.paletteDim[settings.colorIndices[team]], + 0, + ); } public step(deltaTimeInMilliseconds: number): void { diff --git a/shared/src/objects/types/character-base.ts b/shared/src/objects/types/character-base.ts index 7e8b981..d785d22 100644 --- a/shared/src/objects/types/character-base.ts +++ b/shared/src/objects/types/character-base.ts @@ -8,7 +8,6 @@ import { CharacterTeam } from './character-team'; export class CharacterBase extends GameObject { constructor( id: Id, - public colorIndex: number, public team: CharacterTeam, public health: number, public head?: Circle, @@ -19,7 +18,7 @@ export class CharacterBase extends GameObject { } public toArray(): Array { - const { id, colorIndex, team, health, head, leftFoot, rightFoot } = this; - return [id, colorIndex, team, health, head, leftFoot, rightFoot]; + const { id, team, health, head, leftFoot, rightFoot } = this; + return [id, team, health, head, leftFoot, rightFoot]; } } diff --git a/shared/src/objects/types/character-team.ts b/shared/src/objects/types/character-team.ts index 000a78b..8550036 100644 --- a/shared/src/objects/types/character-team.ts +++ b/shared/src/objects/types/character-team.ts @@ -1,4 +1,5 @@ export enum CharacterTeam { decla = 'decla', + neutral = 'neutral', red = 'red', } diff --git a/shared/src/objects/types/player-character-base.ts b/shared/src/objects/types/player-character-base.ts index eb713da..889c0dc 100644 --- a/shared/src/objects/types/player-character-base.ts +++ b/shared/src/objects/types/player-character-base.ts @@ -11,14 +11,13 @@ export class PlayerCharacterBase extends CharacterBase { public name: string, public killCount: number, public deathCount: number, - colorIndex: number, team: CharacterTeam, health: number, head?: Circle, leftFoot?: Circle, rightFoot?: Circle, ) { - super(id, colorIndex, team, health, head, leftFoot, rightFoot); + super(id, team, health, head, leftFoot, rightFoot); } public toArray(): Array { @@ -27,7 +26,6 @@ export class PlayerCharacterBase extends CharacterBase { this.name, this.killCount, this.deathCount, - this.colorIndex, this.team, this.health, this.head, diff --git a/shared/src/objects/types/projectile-base.ts b/shared/src/objects/types/projectile-base.ts index ab51e75..89d27db 100644 --- a/shared/src/objects/types/projectile-base.ts +++ b/shared/src/objects/types/projectile-base.ts @@ -2,6 +2,7 @@ import { vec2 } from 'gl-matrix'; import { Id } from '../../transport/identity'; import { serializable } from '../../transport/serialization/serializable'; import { GameObject } from '../game-object'; +import { CharacterTeam } from './character-team'; @serializable export class ProjectileBase extends GameObject { @@ -9,13 +10,13 @@ export class ProjectileBase extends GameObject { id: Id, public center: vec2, public radius: number, - public colorIndex: number, + public team: CharacterTeam, public strength: number, ) { super(id); } public toArray(): Array { - return [this.id, this.center, this.radius, this.colorIndex, this.strength]; + return [this.id, this.center, this.radius, this.team, this.strength]; } } diff --git a/shared/src/settings.ts b/shared/src/settings.ts index dcf37ce..a6bb3d8 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -1,8 +1,13 @@ import { rgb255 } from './helper/rgb255'; +import { CharacterTeam } from './objects/types/character-team'; const q = 2.5; +const qDim = 1.5; const declaColor = rgb255(64 * q, 105 * q, 165 * q); +const neutralColor = rgb255(82 * q, 165 * q, 64 * q); const redColor = rgb255(209 * q, 86 * q, 82 * q); +const declaColorDim = rgb255(64 * qDim, 105 * qDim, 165 * qDim); +const redColorDim = rgb255(209 * qDim, 8 * qDim, 82 * qDim); const declaPlanetColor = declaColor; const redPlanetColor = redColor; @@ -32,14 +37,18 @@ export const settings = { projectileFadeSpeed: 20, projectileCreationInterval: 0.1, playerColorIndexOffset: 3, - backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)], + backgroundGradient: [rgb255(90, 38, 43), rgb255(0, 0, 0), rgb255(43, 39, 73)], declaColor, declaPlanetColor, redColor, redPlanetColor, - declaIndex: 0, - redIndex: 1, - palette: [declaColor, redColor], + colorIndices: { + [CharacterTeam.decla]: 0, + [CharacterTeam.neutral]: 1, + [CharacterTeam.red]: 2, + }, + palette: [declaColor, neutralColor, redColor], + paletteDim: [declaColorDim, neutralColor, redColorDim], targetPhysicsDeltaTimeInMilliseconds: 20, minPhysicsSleepTime: 4, velocityAttenuation: 0.25,