This commit is contained in:
Andras Schmelczer 2026-08-26 07:47:30 +01:00
commit 86e97df0dc
3 changed files with 414 additions and 35 deletions

View file

@ -301,6 +301,7 @@ export class GameServer implements GameEvents {
private handlePhysics() {
const frameStart = process.hrtime.bigint();
const frameStartMs = performance.now();
const delta = Number(frameStart - this.lastPhysicsBase) / 1e9;
this.lastPhysicsBase = frameStart;
@ -348,8 +349,13 @@ export class GameServer implements GameEvents {
this.players.step(scaledDelta);
}
// Physics has only advanced to the start of this frame less whatever is still in the
// accumulator. Timing anything off the send time instead would put up to one physics step
// of noise on every interpolated position and on the client's replay anchor.
const simulatedThroughMs = frameStartMs - this.physicsAccumulator * 1000;
const propertyUpdates = new Map<GameObject, PropertyUpdatesForObject | undefined>();
this.players.stepCommunication(delta, (object) => {
this.players.stepCommunication(delta, simulatedThroughMs, (object) => {
if (!propertyUpdates.has(object)) {
propertyUpdates.set(object, object.getPropertyUpdates());
}

View file

@ -82,10 +82,11 @@ export class PlayerContainer {
public stepCommunication(
deltaTimeInSeconds: number,
simulatedThroughMs: number,
propertyUpdatesOf: (object: GameObject) => PropertyUpdatesForObject | undefined,
) {
this._players.forEach((p) =>
p.stepCommunications(deltaTimeInSeconds, propertyUpdatesOf),
p.stepCommunications(deltaTimeInSeconds, simulatedThroughMs, propertyUpdatesOf),
);
}

View file

@ -160,6 +160,7 @@ export class Player extends PlayerBase {
public stepCommunications(
deltaTime: number,
simulatedThroughMs: number,
propertyUpdatesOf: (object: GameObject) => PropertyUpdatesForObject | undefined,
) {
const remoteCalls: Array<RemoteCallsForObject> = [];
@ -180,15 +181,20 @@ export class Player extends PlayerBase {
this.socket.emit(TransportEvents.Ping, this.pendingPingNonce);
}
// Subtracted, not zeroed: zeroing would stretch every interval to the next physics frame.
if ((this.timeSinceLastMessage += deltaTime) > settings.updateMessageInterval) {
this.timeSinceLastMessage = 0;
this.timeSinceLastMessage = Math.min(
this.timeSinceLastMessage - settings.updateMessageInterval,
settings.updateMessageInterval,
);
this.queueAnnouncement();
this.queueSnapshot(propertyUpdatesOf);
this.queueSnapshot(simulatedThroughMs, propertyUpdatesOf);
this.sendQueuedCommandsToClient();
}
}
private queueSnapshot(
simulatedThroughMs: number,
propertyUpdatesOf: (object: GameObject) => PropertyUpdatesForObject | undefined,
) {
const { topLeft, size } = calculateViewArea(this.center, this.aspectRatio, 1.2);
@ -227,7 +233,7 @@ export class Player extends PlayerBase {
}
}
this.queueCommandSend(
new PropertyUpdatesForObjects(propertyUpdates, performance.now() / 1000),
new PropertyUpdatesForObjects(propertyUpdates, simulatedThroughMs / 1000),
);
if (this.character) {
@ -236,8 +242,10 @@ export class Player extends PlayerBase {
this.lastInputClientTimeMs,
this.character.movementSnapshot,
this.lastLeapClientTimeMs,
// Aged against the instant the pose is from, not the send time: the difference is
// the un-simulated remainder, and it would land straight in the client's replay.
this.lastInputReceiptMs > 0
? Math.max(0, performance.now() - this.lastInputReceiptMs)
? Math.max(0, simulatedThroughMs - this.lastInputReceiptMs)
: 0,
),
);