Fix net code
This commit is contained in:
parent
1f10b9c750
commit
a1fb6755c7
23 changed files with 408 additions and 236 deletions
9
backend/src/commands/announce.ts
Normal file
9
backend/src/commands/announce.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Command } from 'shared';
|
||||
|
||||
// Internal request from a game object (e.g. a keystone planet flipping) asking
|
||||
// the GameServer to broadcast a one-off announcement to every connected client.
|
||||
export class AnnounceCommand extends Command {
|
||||
public constructor(public readonly text: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Random, PlanetBase, hsl, settings } from 'shared';
|
||||
import { Random, PlanetBase, hsl, settings, evaluateSdf } from 'shared';
|
||||
import { LampPhysical } from './objects/lamp-physical';
|
||||
import { PlanetPhysical } from './objects/planet-physical';
|
||||
import { PhysicalContainer } from './physics/containers/physical-container';
|
||||
import { evaluateSdf } from './physics/functions/evaluate-sdf';
|
||||
import { Physical } from './physics/physicals/physical';
|
||||
|
||||
export const createWorld = (objectContainer: PhysicalContainer) => {
|
||||
|
|
@ -45,13 +44,16 @@ export const createWorld = (objectContainer: PhysicalContainer) => {
|
|||
) {
|
||||
const planet =
|
||||
objects.length === 0
|
||||
? new PlanetPhysical(
|
||||
? // The first, central giant is the keystone "Heart": a named,
|
||||
// always-contested focal objective the whole match orbits.
|
||||
new PlanetPhysical(
|
||||
PlanetBase.createPlanetVertices(
|
||||
position,
|
||||
Random.getRandomInRange(1600, 2400),
|
||||
Random.getRandomInRange(1600, 2400),
|
||||
Random.getRandomInRange(80, 300),
|
||||
),
|
||||
true,
|
||||
)
|
||||
: new PlanetPhysical(
|
||||
PlanetBase.createPlanetVertices(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Options } from './options';
|
|||
import { PlayerContainer } from './players/player-container';
|
||||
import { StepCommand } from './commands/step';
|
||||
import { GeneratePointsCommand } from './commands/generate-points';
|
||||
import { AnnounceCommand } from './commands/announce';
|
||||
|
||||
const gameStateSubscribedRoom = 'gameStateSubscribedRoom';
|
||||
|
||||
|
|
@ -63,6 +64,8 @@ export class GameServer extends CommandReceiver {
|
|||
|
||||
protected commandExecutors: CommandExecutors = {
|
||||
[GeneratePointsCommand.type]: this.addPoints.bind(this),
|
||||
[AnnounceCommand.type]: ({ text }: AnnounceCommand) =>
|
||||
this.players.queueCommandForEachClient(new ServerAnnouncement(text)),
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
|
@ -164,6 +167,7 @@ export class GameServer extends CommandReceiver {
|
|||
}
|
||||
|
||||
private timeSinceLastPointUpdate = 0;
|
||||
private physicsAccumulator = 0;
|
||||
|
||||
private handlePhysics() {
|
||||
const delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds({ setAsBase: true });
|
||||
|
|
@ -183,14 +187,28 @@ export class GameServer extends CommandReceiver {
|
|||
);
|
||||
}
|
||||
|
||||
let scaledDelta = delta;
|
||||
if (this.isInEndGame) {
|
||||
this.timeScaling *= Math.pow(settings.endGameDeltaScaling, delta);
|
||||
scaledDelta /= this.timeScaling;
|
||||
const fixedDelta = settings.targetPhysicsDeltaTimeInSeconds;
|
||||
const maxSubstepsPerFrame = 5;
|
||||
|
||||
this.physicsAccumulator += delta;
|
||||
let substeps = Math.floor(this.physicsAccumulator / fixedDelta);
|
||||
if (substeps > maxSubstepsPerFrame) {
|
||||
substeps = maxSubstepsPerFrame;
|
||||
this.physicsAccumulator = 0;
|
||||
} else {
|
||||
this.physicsAccumulator -= substeps * fixedDelta;
|
||||
}
|
||||
|
||||
for (let i = 0; i < substeps; i++) {
|
||||
let scaledDelta = fixedDelta;
|
||||
if (this.isInEndGame) {
|
||||
this.timeScaling *= Math.pow(settings.endGameDeltaScaling, fixedDelta);
|
||||
scaledDelta /= this.timeScaling;
|
||||
}
|
||||
this.objects.handleCommand(new StepCommand(scaledDelta, this));
|
||||
this.players.step(scaledDelta);
|
||||
}
|
||||
|
||||
this.objects.handleCommand(new StepCommand(scaledDelta, this));
|
||||
this.players.step(scaledDelta);
|
||||
this.players.stepCommunication(delta);
|
||||
this.objects.resetRemoteCalls();
|
||||
|
||||
|
|
@ -198,7 +216,7 @@ export class GameServer extends CommandReceiver {
|
|||
|
||||
setTimeout(
|
||||
this.handlePhysics.bind(this),
|
||||
Math.max(0, settings.targetPhysicsDeltaTimeInSeconds - physicsDelta) * 1000,
|
||||
Math.max(0, fixedDelta - physicsDelta) * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
export const interpolateAngles = (from: number, to: number, q: number) => {
|
||||
const max = Math.PI * 2;
|
||||
const possibleDistance = (to - from) % max;
|
||||
const shorterDistance = ((2 * possibleDistance) % max) - possibleDistance;
|
||||
return from + shorterDistance * q;
|
||||
};
|
||||
|
|
@ -499,8 +499,8 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
// around the planet centre by the same per-tick angle the renderer and the
|
||||
// collision SDF use, so the player is carried around and turned with the
|
||||
// surface instead of it sliding frictionlessly underneath. The positional
|
||||
// change becomes velocity in setPropertyUpdates, so the motion syncs and the
|
||||
// client extrapolates it smoothly.
|
||||
// change becomes velocity in setPropertyUpdates, keeping the streamed
|
||||
// rate-of-change consistent with the streamed positions.
|
||||
private carryWithRotatingPlanet(deltaTimeInSeconds: number) {
|
||||
const planet = this.currentPlanet;
|
||||
if (!planet) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
clamp01,
|
||||
id,
|
||||
mix,
|
||||
Random,
|
||||
serializesTo,
|
||||
settings,
|
||||
PlanetBase,
|
||||
|
|
@ -28,6 +29,16 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
|
||||
public readonly sizePointMultiplier: number;
|
||||
|
||||
// Planets slowly spin. The angle is authoritative here and streamed to the
|
||||
// client (see getPropertyUpdates), so the rendered outline and this collision
|
||||
// polygon turn as one rigid body. cos/sin are memoised per angle because
|
||||
// distance() is called many times per tick by the raymarcher and SDF sampling.
|
||||
private rotation = 0;
|
||||
private readonly rotationSpeed: number;
|
||||
private cachedRotation = Number.NaN;
|
||||
private cosRotation = 1;
|
||||
private sinRotation = 0;
|
||||
|
||||
private _boundingBox?: ImmutableBoundingBox;
|
||||
|
||||
private readonly lamps: Array<LampPhysical> = [];
|
||||
|
|
@ -47,23 +58,30 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
|
||||
const sizeClass = clamp01(
|
||||
(this.radius - settings.planetMinReferenceRadius) /
|
||||
(settings.planetMaxReferenceRadius - settings.planetMinReferenceRadius),
|
||||
(settings.planetMaxReferenceRadius - settings.planetMinReferenceRadius),
|
||||
);
|
||||
|
||||
this.sizePointMultiplier = mix(1, settings.planetSizePointMultiplierMax, sizeClass);
|
||||
|
||||
this.rotationSpeed =
|
||||
(0.05 + Random.getRandom() * 0.07) * (Random.getRandom() < 0.5 ? -1 : 1);
|
||||
}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
// Evaluate the SDF in the planet's own rotating frame so this collision
|
||||
// outline turns in lockstep with the rendered planet (see planet-shape.ts).
|
||||
const local = this.toLocalFrame(target);
|
||||
|
||||
const startEnd = this.vertices[0];
|
||||
let vb = startEnd;
|
||||
|
||||
let d = vec2.squaredDistance(target, vb);
|
||||
let d = vec2.dist(local, vb);
|
||||
let sign = 1;
|
||||
|
||||
for (let i = 1; i <= this.vertices.length; i++) {
|
||||
const va = vb;
|
||||
vb = i === this.vertices.length ? startEnd : this.vertices[i];
|
||||
const targetFromDelta = vec2.subtract(vec2.create(), target, va);
|
||||
const targetFromDelta = vec2.subtract(vec2.create(), local, va);
|
||||
const toFromDelta = vec2.subtract(vec2.create(), vb, va);
|
||||
const h = clamp01(
|
||||
vec2.dot(targetFromDelta, toFromDelta) / vec2.squaredLength(toFromDelta),
|
||||
|
|
@ -75,8 +93,8 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
);
|
||||
|
||||
if (
|
||||
(target.y >= va.y && target.y < vb.y && ds.y > 0) ||
|
||||
(target.y < va.y && target.y >= vb.y && ds.y <= 0)
|
||||
(local.y >= va.y && local.y < vb.y && ds.y > 0) ||
|
||||
(local.y < va.y && local.y >= vb.y && ds.y <= 0)
|
||||
) {
|
||||
sign *= -1;
|
||||
}
|
||||
|
|
@ -87,11 +105,35 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
return sign * d;
|
||||
}
|
||||
|
||||
// Rotate a world point by -rotation about the centre, matching the shader's
|
||||
// `localTarget = center + R(rotation) * (target - center)` transform exactly.
|
||||
private toLocalFrame(target: vec2): vec2 {
|
||||
if (this.rotation !== this.cachedRotation) {
|
||||
this.cachedRotation = this.rotation;
|
||||
this.cosRotation = Math.cos(this.rotation);
|
||||
this.sinRotation = Math.sin(this.rotation);
|
||||
}
|
||||
|
||||
const dx = target.x - this.center.x;
|
||||
const dy = target.y - this.center.y;
|
||||
|
||||
return vec2.fromValues(
|
||||
this.center.x + this.cosRotation * dx - this.sinRotation * dy,
|
||||
this.center.y + this.sinRotation * dx + this.cosRotation * dy,
|
||||
);
|
||||
}
|
||||
|
||||
// Signed angular velocity in rad/s, exposed so a character standing on the
|
||||
// planet can ride its spin (see CharacterPhysical.carryWithRotatingPlanet).
|
||||
public get angularVelocity(): number {
|
||||
return this.rotationSpeed;
|
||||
}
|
||||
|
||||
public get team(): CharacterTeam {
|
||||
return Math.abs(this.ownership - 0.5) < 0.1
|
||||
? CharacterTeam.neutral
|
||||
: this.ownership < 0.5
|
||||
? CharacterTeam.decla
|
||||
? CharacterTeam.blue
|
||||
: CharacterTeam.red;
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +147,7 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
);
|
||||
game.handleCommand(
|
||||
new GeneratePointsCommand(
|
||||
this.team === CharacterTeam.decla ? value : 0,
|
||||
this.team === CharacterTeam.blue ? value : 0,
|
||||
this.team === CharacterTeam.red ? value : 0,
|
||||
),
|
||||
);
|
||||
|
|
@ -113,6 +155,7 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
}
|
||||
|
||||
private step({ deltaTimeInSeconds, game }: StepCommand) {
|
||||
this.rotation += deltaTimeInSeconds * this.rotationSpeed;
|
||||
this.timeSinceLastPointGeneration += deltaTimeInSeconds;
|
||||
|
||||
// In reverse order, so that teams can achieve a 100% control.
|
||||
|
|
@ -135,7 +178,7 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
this.remoteCall('generatedPoints', reward);
|
||||
game.handleCommand(
|
||||
new GeneratePointsCommand(
|
||||
currentTeam === CharacterTeam.decla ? reward : 0,
|
||||
currentTeam === CharacterTeam.blue ? reward : 0,
|
||||
currentTeam === CharacterTeam.red ? reward : 0,
|
||||
),
|
||||
);
|
||||
|
|
@ -153,11 +196,14 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
public getPropertyUpdates(): PropertyUpdatesForObject {
|
||||
return new PropertyUpdatesForObject(this.id, [
|
||||
new UpdatePropertyCommand('ownership', this.ownership, 0),
|
||||
// Stream rotation with rotationSpeed as the rate-of-change so the client
|
||||
// can keep the angle moving when snapshots run late (see planet-view.ts).
|
||||
new UpdatePropertyCommand('rotation', this.rotation, this.rotationSpeed),
|
||||
]);
|
||||
}
|
||||
|
||||
public takeControl(team: CharacterTeam, deltaTime: number) {
|
||||
if (team === CharacterTeam.decla) {
|
||||
if (team === CharacterTeam.blue) {
|
||||
this.ownership -= (0.5 / settings.takeControlTimeInSeconds) * deltaTime;
|
||||
} else if (team === CharacterTeam.red) {
|
||||
this.ownership += (0.5 / settings.takeControlTimeInSeconds) * deltaTime;
|
||||
|
|
@ -180,22 +226,20 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
|
||||
public get boundingBox(): ImmutableBoundingBox {
|
||||
if (!this._boundingBox) {
|
||||
const { xMin, xMax, yMin, yMax } = this.vertices.reduce(
|
||||
(extremities, vertex) => ({
|
||||
xMin: Math.min(extremities.xMin, vertex.x),
|
||||
xMax: Math.max(extremities.xMax, vertex.x),
|
||||
yMin: Math.min(extremities.yMin, vertex.y),
|
||||
yMax: Math.max(extremities.yMax, vertex.y),
|
||||
}),
|
||||
{
|
||||
xMin: Infinity,
|
||||
xMax: -Infinity,
|
||||
yMin: Infinity,
|
||||
yMax: -Infinity,
|
||||
},
|
||||
// The polygon spins about its centre (see distance), so this static box
|
||||
// has to cover every orientation, not just the spawn-time one: take the
|
||||
// circumscribed circle around the rotation centre.
|
||||
const maxVertexDistance = this.vertices.reduce(
|
||||
(max, vertex) => Math.max(max, vec2.distance(this.center, vertex)),
|
||||
0,
|
||||
);
|
||||
|
||||
this._boundingBox = new ImmutableBoundingBox(xMin, xMax, yMin, yMax);
|
||||
this._boundingBox = new ImmutableBoundingBox(
|
||||
this.center.x - maxVertexDistance,
|
||||
this.center.x + maxVertexDistance,
|
||||
this.center.y - maxVertexDistance,
|
||||
this.center.y + maxVertexDistance,
|
||||
);
|
||||
}
|
||||
|
||||
return this._boundingBox;
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { PhysicalBase } from '../physicals/physical-base';
|
||||
|
||||
export const evaluateSdf = (target: vec2, objects: Array<PhysicalBase>) =>
|
||||
objects
|
||||
.filter((i) => i.canCollide)
|
||||
.reduce((min, i) => (min = Math.min(min, i.distance(target))), 1000000);
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { GameObject } from 'shared';
|
||||
import { CirclePhysical } from '../../objects/circle-physical';
|
||||
import { evaluateSdf } from './evaluate-sdf';
|
||||
import { Physical } from '../physicals/physical';
|
||||
import { ReactToCollisionCommand } from '../../commands/react-to-collision';
|
||||
|
||||
export const moveCircle = (
|
||||
circle: CirclePhysical,
|
||||
delta: vec2,
|
||||
possibleIntersectors: Array<Physical>,
|
||||
ignoreCollision = false,
|
||||
): {
|
||||
hitSurface: boolean;
|
||||
normal?: vec2;
|
||||
hitObject?: GameObject;
|
||||
} => {
|
||||
const direction = vec2.clone(delta);
|
||||
|
||||
if (vec2.length(delta) > 0) {
|
||||
vec2.normalize(direction, direction);
|
||||
}
|
||||
|
||||
const deltaLength = vec2.length(delta);
|
||||
let travelled = 0;
|
||||
const rayEnd = vec2.create();
|
||||
let prevMinDistance = 0;
|
||||
while (travelled < deltaLength) {
|
||||
travelled += prevMinDistance;
|
||||
vec2.add(
|
||||
rayEnd,
|
||||
circle.center,
|
||||
vec2.scale(vec2.create(), direction, Math.min(travelled, deltaLength)),
|
||||
);
|
||||
|
||||
const minDistance = evaluateSdf(rayEnd, possibleIntersectors);
|
||||
|
||||
if (minDistance < circle.radius) {
|
||||
const intersecting = possibleIntersectors.find(
|
||||
(i) => i.distance(rayEnd) <= circle.radius,
|
||||
)!;
|
||||
|
||||
if (ignoreCollision) {
|
||||
circle.center = vec2.add(circle.center, circle.center, delta);
|
||||
} else {
|
||||
intersecting.handleCommand(new ReactToCollisionCommand(circle.gameObject));
|
||||
circle.handleCommand(new ReactToCollisionCommand(intersecting.gameObject));
|
||||
}
|
||||
|
||||
vec2.add(
|
||||
rayEnd,
|
||||
circle.center,
|
||||
vec2.scale(vec2.create(), direction, travelled - prevMinDistance),
|
||||
);
|
||||
|
||||
vec2.copy(circle.center, rayEnd);
|
||||
|
||||
const dx =
|
||||
evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(0.01, 0)), [
|
||||
intersecting,
|
||||
]) -
|
||||
evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(-0.01, 0)), [
|
||||
intersecting,
|
||||
]);
|
||||
const dy =
|
||||
evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(0, 0.01)), [
|
||||
intersecting,
|
||||
]) -
|
||||
evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(0, -0.01)), [
|
||||
intersecting,
|
||||
]);
|
||||
const normal = vec2.fromValues(dx, dy);
|
||||
vec2.normalize(normal, normal);
|
||||
return {
|
||||
hitSurface: true,
|
||||
normal,
|
||||
hitObject: intersecting?.gameObject,
|
||||
};
|
||||
}
|
||||
|
||||
prevMinDistance = minDistance;
|
||||
}
|
||||
|
||||
vec2.add(circle.center, circle.center, delta);
|
||||
|
||||
return {
|
||||
hitSurface: false,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { performance } from 'perf_hooks';
|
||||
import { vec2 } from 'gl-matrix';
|
||||
import {
|
||||
CommandExecutors,
|
||||
|
|
@ -126,6 +127,7 @@ export class Player extends PlayerBase {
|
|||
this.objectsPreviouslyInViewArea
|
||||
.map((o) => o.getPropertyUpdates())
|
||||
.filter((u) => u) as Array<PropertyUpdatesForObject>,
|
||||
performance.now() / 1000,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue