smooth Movers presentation and reconciliation
All checks were successful
build / image (push) Successful in 34s
All checks were successful
build / image (push) Successful in 34s
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, type MutableRefObject } from "react";
|
import { useEffect, useRef, type MutableRefObject } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import {
|
import {
|
||||||
|
MOVERS_TICK_RATE,
|
||||||
MOVERS_WALLS,
|
MOVERS_WALLS,
|
||||||
type FurnitureKind,
|
type FurnitureKind,
|
||||||
type MoversClientState,
|
type MoversClientState,
|
||||||
@@ -8,9 +9,15 @@ import {
|
|||||||
type MoversPlayerView,
|
type MoversPlayerView,
|
||||||
type MoversTeam,
|
type MoversTeam,
|
||||||
} from "@syncer/shared";
|
} from "@syncer/shared";
|
||||||
|
import {
|
||||||
|
MOVERS_CORRECTION_DECAY_MILLISECONDS,
|
||||||
|
type MoversPositionCorrection,
|
||||||
|
type MoversRenderFrame,
|
||||||
|
type MoversRenderSource,
|
||||||
|
} from "./useMoversClient.js";
|
||||||
|
|
||||||
interface Movers3DProps {
|
interface Movers3DProps {
|
||||||
world: MoversClientState;
|
source: MoversRenderSource;
|
||||||
playerId: number | null;
|
playerId: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +50,14 @@ interface MoversRuntime {
|
|||||||
|
|
||||||
const yellow = 0xffc629;
|
const yellow = 0xffc629;
|
||||||
const blue = 0x2997ff;
|
const blue = 0x2997ff;
|
||||||
|
const furnitureTargetRotation = new THREE.Quaternion();
|
||||||
|
const furnitureExtrapolatedRotation = new THREE.Quaternion();
|
||||||
|
const furnitureAngularAxis = new THREE.Vector3();
|
||||||
|
|
||||||
export function Movers3D({ world, playerId }: Movers3DProps) {
|
export function Movers3D({ source, playerId }: Movers3DProps) {
|
||||||
const hostRef = useRef<HTMLDivElement>(null);
|
const hostRef = useRef<HTMLDivElement>(null);
|
||||||
const worldRef = useRef(world);
|
|
||||||
const playerIdRef = useRef(playerId);
|
const playerIdRef = useRef(playerId);
|
||||||
const lastEventIdRef = useRef(0);
|
const lastEventIdRef = useRef(0);
|
||||||
worldRef.current = world;
|
|
||||||
playerIdRef.current = playerId;
|
playerIdRef.current = playerId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -110,7 +118,7 @@ export function Movers3D({ world, playerId }: Movers3DProps) {
|
|||||||
const animate = (time: number) => {
|
const animate = (time: number) => {
|
||||||
const deltaSeconds = Math.min(0.05, Math.max(0.001, (time - runtime.lastTime) / 1_000));
|
const deltaSeconds = Math.min(0.05, Math.max(0.001, (time - runtime.lastTime) / 1_000));
|
||||||
runtime.lastTime = time;
|
runtime.lastTime = time;
|
||||||
updateRuntime(runtime, worldRef.current, playerIdRef.current, lastEventIdRef, time, deltaSeconds);
|
updateRuntime(runtime, source.current, playerIdRef.current, lastEventIdRef, time, deltaSeconds);
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
runtime.animationFrame = window.requestAnimationFrame(animate);
|
runtime.animationFrame = window.requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
@@ -130,20 +138,48 @@ export function Movers3D({ world, playerId }: Movers3DProps) {
|
|||||||
|
|
||||||
function updateRuntime(
|
function updateRuntime(
|
||||||
runtime: MoversRuntime,
|
runtime: MoversRuntime,
|
||||||
world: MoversClientState,
|
frame: MoversRenderFrame,
|
||||||
playerId: number | null,
|
playerId: number | null,
|
||||||
lastEventIdRef: MutableRefObject<number>,
|
lastEventIdRef: MutableRefObject<number>,
|
||||||
time: number,
|
time: number,
|
||||||
deltaSeconds: number,
|
deltaSeconds: number,
|
||||||
): void {
|
): void {
|
||||||
updatePlayers(runtime, world.players, playerId, deltaSeconds, time);
|
const world = frame.state;
|
||||||
updateFurniture(runtime, world.furniture, deltaSeconds, time);
|
const leadSeconds = Math.min(
|
||||||
|
1 / MOVERS_TICK_RATE,
|
||||||
|
Math.max(0, frame.interpolationAlpha / MOVERS_TICK_RATE),
|
||||||
|
);
|
||||||
|
updatePlayers(
|
||||||
|
runtime,
|
||||||
|
world.players,
|
||||||
|
playerId,
|
||||||
|
frame.playerCorrections,
|
||||||
|
leadSeconds,
|
||||||
|
deltaSeconds,
|
||||||
|
time,
|
||||||
|
);
|
||||||
|
updateFurniture(
|
||||||
|
runtime,
|
||||||
|
world.furniture,
|
||||||
|
frame.furnitureCorrections,
|
||||||
|
leadSeconds,
|
||||||
|
deltaSeconds,
|
||||||
|
time,
|
||||||
|
);
|
||||||
updateTrucks(runtime, world, deltaSeconds, time);
|
updateTrucks(runtime, world, deltaSeconds, time);
|
||||||
updateEffects(runtime, world, lastEventIdRef, time);
|
updateEffects(runtime, world, lastEventIdRef, time);
|
||||||
|
|
||||||
const local = world.players.find((player) => player.id === playerId);
|
const local = world.players.find((player) => player.id === playerId);
|
||||||
const targetX = local?.x ?? 0;
|
const localCorrection = local ? frame.playerCorrections.get(local.id) : undefined;
|
||||||
const targetZ = local?.z ?? 0;
|
const localCorrectionFactor = local
|
||||||
|
? correctionFactorAt(frame.playerCorrections, local.id, time)
|
||||||
|
: 0;
|
||||||
|
const targetX = local
|
||||||
|
? local.x + local.velocityX * leadSeconds + (localCorrection?.x ?? 0) * localCorrectionFactor
|
||||||
|
: 0;
|
||||||
|
const targetZ = local
|
||||||
|
? local.z + local.velocityZ * leadSeconds + (localCorrection?.z ?? 0) * localCorrectionFactor
|
||||||
|
: 0;
|
||||||
const cameraAlpha = 1 - Math.exp(-5.8 * deltaSeconds);
|
const cameraAlpha = 1 - Math.exp(-5.8 * deltaSeconds);
|
||||||
runtime.camera.position.x = THREE.MathUtils.lerp(runtime.camera.position.x, targetX, cameraAlpha);
|
runtime.camera.position.x = THREE.MathUtils.lerp(runtime.camera.position.x, targetX, cameraAlpha);
|
||||||
runtime.camera.position.y = THREE.MathUtils.lerp(runtime.camera.position.y, world.demolitionStarted ? 22 : 24, cameraAlpha);
|
runtime.camera.position.y = THREE.MathUtils.lerp(runtime.camera.position.y, world.demolitionStarted ? 22 : 24, cameraAlpha);
|
||||||
@@ -161,6 +197,8 @@ function updatePlayers(
|
|||||||
runtime: MoversRuntime,
|
runtime: MoversRuntime,
|
||||||
players: MoversPlayerView[],
|
players: MoversPlayerView[],
|
||||||
playerId: number | null,
|
playerId: number | null,
|
||||||
|
corrections: Map<number, MoversPositionCorrection>,
|
||||||
|
leadSeconds: number,
|
||||||
deltaSeconds: number,
|
deltaSeconds: number,
|
||||||
time: number,
|
time: number,
|
||||||
): void {
|
): void {
|
||||||
@@ -173,10 +211,15 @@ function updatePlayers(
|
|||||||
runtime.players.set(player.id, group);
|
runtime.players.set(player.id, group);
|
||||||
runtime.scene.add(group);
|
runtime.scene.add(group);
|
||||||
}
|
}
|
||||||
|
const correction = corrections.get(player.id);
|
||||||
|
const correctionFactor = correctionFactorAt(corrections, player.id, time);
|
||||||
|
const targetX = player.x + player.velocityX * leadSeconds + (correction?.x ?? 0) * correctionFactor;
|
||||||
|
const targetZ = player.z + player.velocityZ * leadSeconds + (correction?.z ?? 0) * correctionFactor;
|
||||||
|
const targetYaw = player.yaw + (correction?.yaw ?? 0) * correctionFactor;
|
||||||
const alpha = 1 - Math.exp(-(player.id === playerId ? 22 : 13) * deltaSeconds);
|
const alpha = 1 - Math.exp(-(player.id === playerId ? 22 : 13) * deltaSeconds);
|
||||||
group.position.x = THREE.MathUtils.lerp(group.position.x, player.x, alpha);
|
group.position.x = THREE.MathUtils.lerp(group.position.x, targetX, alpha);
|
||||||
group.position.z = THREE.MathUtils.lerp(group.position.z, player.z, alpha);
|
group.position.z = THREE.MathUtils.lerp(group.position.z, targetZ, alpha);
|
||||||
group.rotation.y = lerpAngle(group.rotation.y, player.yaw, alpha);
|
group.rotation.y = lerpAngle(group.rotation.y, targetYaw, alpha);
|
||||||
const speed = Math.hypot(player.velocityX, player.velocityZ);
|
const speed = Math.hypot(player.velocityX, player.velocityZ);
|
||||||
const body = group.getObjectByName("body");
|
const body = group.getObjectByName("body");
|
||||||
if (body) body.position.y = 1.12 + Math.sin(time * 0.014 + player.id) * Math.min(0.08, speed * 0.01);
|
if (body) body.position.y = 1.12 + Math.sin(time * 0.014 + player.id) * Math.min(0.08, speed * 0.01);
|
||||||
@@ -199,6 +242,8 @@ function updatePlayers(
|
|||||||
function updateFurniture(
|
function updateFurniture(
|
||||||
runtime: MoversRuntime,
|
runtime: MoversRuntime,
|
||||||
items: MoversFurnitureView[],
|
items: MoversFurnitureView[],
|
||||||
|
corrections: Map<number, MoversPositionCorrection>,
|
||||||
|
leadSeconds: number,
|
||||||
deltaSeconds: number,
|
deltaSeconds: number,
|
||||||
time: number,
|
time: number,
|
||||||
): void {
|
): void {
|
||||||
@@ -212,17 +257,39 @@ function updateFurniture(
|
|||||||
runtime.furniture.set(item.id, group);
|
runtime.furniture.set(item.id, group);
|
||||||
runtime.scene.add(group);
|
runtime.scene.add(group);
|
||||||
}
|
}
|
||||||
|
const correction = corrections.get(item.id);
|
||||||
|
const correctionFactor = correctionFactorAt(corrections, item.id, time);
|
||||||
|
const targetX = item.x + item.velocityX * leadSeconds + (correction?.x ?? 0) * correctionFactor;
|
||||||
|
const targetY = item.y + item.velocityY * leadSeconds + (correction?.y ?? 0) * correctionFactor;
|
||||||
|
const targetZ = item.z + item.velocityZ * leadSeconds + (correction?.z ?? 0) * correctionFactor;
|
||||||
const alpha = 1 - Math.exp(-(item.carriedBy === null ? 14 : 24) * deltaSeconds);
|
const alpha = 1 - Math.exp(-(item.carriedBy === null ? 14 : 24) * deltaSeconds);
|
||||||
group.position.x = THREE.MathUtils.lerp(group.position.x, item.x, alpha);
|
group.position.x = THREE.MathUtils.lerp(group.position.x, targetX, alpha);
|
||||||
group.position.y = THREE.MathUtils.lerp(group.position.y, item.y, alpha);
|
group.position.y = THREE.MathUtils.lerp(group.position.y, targetY, alpha);
|
||||||
group.position.z = THREE.MathUtils.lerp(group.position.z, item.z, alpha);
|
group.position.z = THREE.MathUtils.lerp(group.position.z, targetZ, alpha);
|
||||||
const targetRotation = new THREE.Quaternion(
|
furnitureTargetRotation.set(
|
||||||
item.rotationX,
|
item.rotationX,
|
||||||
item.rotationY,
|
item.rotationY,
|
||||||
item.rotationZ,
|
item.rotationZ,
|
||||||
item.rotationW,
|
item.rotationW,
|
||||||
).normalize();
|
).normalize();
|
||||||
group.quaternion.slerp(targetRotation, alpha);
|
const angularSpeed = Math.hypot(
|
||||||
|
item.angularVelocityX,
|
||||||
|
item.angularVelocityY,
|
||||||
|
item.angularVelocityZ,
|
||||||
|
);
|
||||||
|
if (angularSpeed > 0.0001 && leadSeconds > 0) {
|
||||||
|
furnitureAngularAxis.set(
|
||||||
|
item.angularVelocityX / angularSpeed,
|
||||||
|
item.angularVelocityY / angularSpeed,
|
||||||
|
item.angularVelocityZ / angularSpeed,
|
||||||
|
);
|
||||||
|
furnitureExtrapolatedRotation.setFromAxisAngle(
|
||||||
|
furnitureAngularAxis,
|
||||||
|
angularSpeed * leadSeconds,
|
||||||
|
);
|
||||||
|
furnitureTargetRotation.premultiply(furnitureExtrapolatedRotation);
|
||||||
|
}
|
||||||
|
group.quaternion.slerp(furnitureTargetRotation, alpha);
|
||||||
updateDamageAppearance(group, item.damage);
|
updateDamageAppearance(group, item.damage);
|
||||||
const securedLight = group.getObjectByName("secured") as THREE.PointLight | undefined;
|
const securedLight = group.getObjectByName("secured") as THREE.PointLight | undefined;
|
||||||
if (securedLight) {
|
if (securedLight) {
|
||||||
@@ -238,6 +305,23 @@ function updateFurniture(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function correctionFactorAt(
|
||||||
|
corrections: Map<number, MoversPositionCorrection>,
|
||||||
|
id: number,
|
||||||
|
now: number,
|
||||||
|
): number {
|
||||||
|
const correction = corrections.get(id);
|
||||||
|
if (!correction) return 0;
|
||||||
|
const decay = Math.exp(
|
||||||
|
-(now - correction.updatedAt) / MOVERS_CORRECTION_DECAY_MILLISECONDS,
|
||||||
|
);
|
||||||
|
if (decay < 0.002) {
|
||||||
|
corrections.delete(id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return decay;
|
||||||
|
}
|
||||||
|
|
||||||
function updateTrucks(
|
function updateTrucks(
|
||||||
runtime: MoversRuntime,
|
runtime: MoversRuntime,
|
||||||
world: MoversClientState,
|
world: MoversClientState,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function MoversGame() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={`movers-game movers-game--${team}${client.world.demolitionStarted ? " movers-game--demolition" : ""}`}>
|
<main className={`movers-game movers-game--${team}${client.world.demolitionStarted ? " movers-game--demolition" : ""}`}>
|
||||||
<Movers3D world={client.world} playerId={client.playerId} />
|
<Movers3D source={client.renderSource} playerId={client.playerId} />
|
||||||
<div className="movers-grade" aria-hidden="true" />
|
<div className="movers-grade" aria-hidden="true" />
|
||||||
|
|
||||||
{!started ? (
|
{!started ? (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import type { ConnectionStatus, ValidationStatus } from "./useGameClient.js";
|
import type { ConnectionStatus, ValidationStatus } from "./useGameClient.js";
|
||||||
|
|
||||||
export type MoversControl = "grab" | "throwItem" | "closeDoors";
|
export type MoversControl = "grab" | "throwItem" | "closeDoors";
|
||||||
|
export const MOVERS_CORRECTION_DECAY_MILLISECONDS = 115;
|
||||||
|
|
||||||
export interface MoversClientView {
|
export interface MoversClientView {
|
||||||
connection: ConnectionStatus;
|
connection: ConnectionStatus;
|
||||||
@@ -21,10 +22,30 @@ export interface MoversClientView {
|
|||||||
tick: number;
|
tick: number;
|
||||||
inputLeadTicks: number;
|
inputLeadTicks: number;
|
||||||
world: MoversClientState;
|
world: MoversClientState;
|
||||||
|
renderSource: MoversRenderSource;
|
||||||
network: NetworkStats;
|
network: NetworkStats;
|
||||||
setControl(control: MoversControl, active: boolean): void;
|
setControl(control: MoversControl, active: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MoversPositionCorrection {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
yaw: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoversRenderFrame {
|
||||||
|
state: Readonly<MoversClientState>;
|
||||||
|
interpolationAlpha: number;
|
||||||
|
playerCorrections: Map<number, MoversPositionCorrection>;
|
||||||
|
furnitureCorrections: Map<number, MoversPositionCorrection>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoversRenderSource {
|
||||||
|
readonly current: MoversRenderFrame;
|
||||||
|
}
|
||||||
|
|
||||||
const emptyNetworkStats: NetworkStats = {
|
const emptyNetworkStats: NetworkStats = {
|
||||||
roundTripTime: 0,
|
roundTripTime: 0,
|
||||||
jitter: 0,
|
jitter: 0,
|
||||||
@@ -58,11 +79,17 @@ export function useMoversClient(): MoversClientView {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const controlRef = useRef<(control: MoversControl, active: boolean) => void>(() => undefined);
|
const controlRef = useRef<(control: MoversControl, active: boolean) => void>(() => undefined);
|
||||||
|
const renderSource = useRef<MoversRenderFrame>({
|
||||||
|
state: moversGame.client.createInitialState(),
|
||||||
|
interpolationAlpha: 0,
|
||||||
|
playerCorrections: new Map(),
|
||||||
|
furnitureCorrections: new Map(),
|
||||||
|
});
|
||||||
const setControl = useCallback(
|
const setControl = useCallback(
|
||||||
(control: MoversControl, active: boolean) => controlRef.current(control, active),
|
(control: MoversControl, active: boolean) => controlRef.current(control, active),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const [view, setView] = useState<Omit<MoversClientView, "setControl">>({
|
const [view, setView] = useState<Omit<MoversClientView, "setControl" | "renderSource">>({
|
||||||
connection: "connecting",
|
connection: "connecting",
|
||||||
validation: "waiting",
|
validation: "waiting",
|
||||||
playerId: null,
|
playerId: null,
|
||||||
@@ -84,10 +111,18 @@ export function useMoversClient(): MoversClientView {
|
|||||||
const inputStream = createInputStateStream<MoversInput>(moversGame);
|
const inputStream = createInputStateStream<MoversInput>(moversGame);
|
||||||
let lastInputFrame: ArrayBuffer | null = null;
|
let lastInputFrame: ArrayBuffer | null = null;
|
||||||
let socketUrlIndex = 0;
|
let socketUrlIndex = 0;
|
||||||
|
let lastPublishedAt = Number.NEGATIVE_INFINITY;
|
||||||
const connectionUrls = socketUrls();
|
const connectionUrls = socketUrls();
|
||||||
|
|
||||||
const publish = () => {
|
const refreshRenderSource = () => {
|
||||||
|
renderSource.current.state = engine.currentState as MoversClientState;
|
||||||
|
renderSource.current.interpolationAlpha = clock.interpolationAlpha;
|
||||||
|
};
|
||||||
|
|
||||||
|
const publish = (force = false, now = performance.now()) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
|
if (!force && now - lastPublishedAt < 100) return;
|
||||||
|
lastPublishedAt = now;
|
||||||
setView({
|
setView({
|
||||||
connection,
|
connection,
|
||||||
validation,
|
validation,
|
||||||
@@ -137,14 +172,14 @@ export function useMoversClient(): MoversClientView {
|
|||||||
const connect = () => {
|
const connect = () => {
|
||||||
let opened = false;
|
let opened = false;
|
||||||
connection = engine.initialized ? "reconnecting" : "connecting";
|
connection = engine.initialized ? "reconnecting" : "connecting";
|
||||||
publish();
|
publish(true);
|
||||||
socket = new WebSocket(connectionUrls[socketUrlIndex]!);
|
socket = new WebSocket(connectionUrls[socketUrlIndex]!);
|
||||||
socket.binaryType = "arraybuffer";
|
socket.binaryType = "arraybuffer";
|
||||||
socket.addEventListener("open", () => {
|
socket.addEventListener("open", () => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
opened = true;
|
opened = true;
|
||||||
connection = "live";
|
connection = "live";
|
||||||
publish();
|
publish(true);
|
||||||
});
|
});
|
||||||
socket.addEventListener("message", (event: MessageEvent<ArrayBuffer>) => {
|
socket.addEventListener("message", (event: MessageEvent<ArrayBuffer>) => {
|
||||||
if (!active || !(event.data instanceof ArrayBuffer)) return;
|
if (!active || !(event.data instanceof ArrayBuffer)) return;
|
||||||
@@ -161,9 +196,19 @@ export function useMoversClient(): MoversClientView {
|
|||||||
validation = "waiting";
|
validation = "waiting";
|
||||||
sendInput(true);
|
sendInput(true);
|
||||||
break;
|
break;
|
||||||
case "snapshot":
|
case "snapshot": {
|
||||||
|
const before = engine.currentState as MoversClientState;
|
||||||
|
const receivedAt = performance.now();
|
||||||
engine.reconcile(message.snapshot);
|
engine.reconcile(message.snapshot);
|
||||||
|
accumulateCorrections(
|
||||||
|
renderSource.current,
|
||||||
|
before,
|
||||||
|
engine.currentState as MoversClientState,
|
||||||
|
receivedAt,
|
||||||
|
clock.interpolationAlpha / moversGame.tickRateHz,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "acknowledge":
|
case "acknowledge":
|
||||||
engine.acknowledge(message.sequence);
|
engine.acknowledge(message.sequence);
|
||||||
break;
|
break;
|
||||||
@@ -188,7 +233,8 @@ export function useMoversClient(): MoversClientView {
|
|||||||
case "replay-end":
|
case "replay-end":
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
publish();
|
refreshRenderSource();
|
||||||
|
publish(message.kind !== "snapshot");
|
||||||
} catch {
|
} catch {
|
||||||
socket?.close(1003, "Invalid moving manifest");
|
socket?.close(1003, "Invalid moving manifest");
|
||||||
}
|
}
|
||||||
@@ -199,7 +245,7 @@ export function useMoversClient(): MoversClientView {
|
|||||||
socketUrlIndex = (socketUrlIndex + 1) % connectionUrls.length;
|
socketUrlIndex = (socketUrlIndex + 1) % connectionUrls.length;
|
||||||
} else if (opened) socketUrlIndex = 0;
|
} else if (opened) socketUrlIndex = 0;
|
||||||
connection = "reconnecting";
|
connection = "reconnecting";
|
||||||
publish();
|
publish(true);
|
||||||
retryTimer = window.setTimeout(connect, 1_000);
|
retryTimer = window.setTimeout(connect, 1_000);
|
||||||
});
|
});
|
||||||
socket.addEventListener("error", () => socket?.close());
|
socket.addEventListener("error", () => socket?.close());
|
||||||
@@ -250,7 +296,8 @@ export function useMoversClient(): MoversClientView {
|
|||||||
const animate = (now: number) => {
|
const animate = (now: number) => {
|
||||||
sendInput();
|
sendInput();
|
||||||
clock.advance(now, () => engine.step());
|
clock.advance(now, () => engine.step());
|
||||||
publish();
|
refreshRenderSource();
|
||||||
|
publish(false, now);
|
||||||
animationFrame = window.requestAnimationFrame(animate);
|
animationFrame = window.requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,6 +317,73 @@ export function useMoversClient(): MoversClientView {
|
|||||||
};
|
};
|
||||||
}, [clock, engine, protocol]);
|
}, [clock, engine, protocol]);
|
||||||
|
|
||||||
return { ...view, setControl };
|
return { ...view, renderSource, setControl };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function accumulateCorrections(
|
||||||
|
frame: MoversRenderFrame,
|
||||||
|
before: MoversClientState,
|
||||||
|
after: Readonly<MoversClientState>,
|
||||||
|
now: number,
|
||||||
|
leadSeconds: number,
|
||||||
|
): void {
|
||||||
|
if (before.round !== after.round) {
|
||||||
|
frame.playerCorrections.clear();
|
||||||
|
frame.furnitureCorrections.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const player of before.players) {
|
||||||
|
const corrected = after.players.find((candidate) => candidate.id === player.id);
|
||||||
|
if (!corrected) continue;
|
||||||
|
accumulateCorrection(frame.playerCorrections, player.id, {
|
||||||
|
x: player.x + player.velocityX * leadSeconds - corrected.x - corrected.velocityX * leadSeconds,
|
||||||
|
y: 0,
|
||||||
|
z: player.z + player.velocityZ * leadSeconds - corrected.z - corrected.velocityZ * leadSeconds,
|
||||||
|
yaw: normalizeAngle(player.yaw - corrected.yaw),
|
||||||
|
}, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of before.furniture) {
|
||||||
|
const corrected = after.furniture.find((candidate) => candidate.id === item.id);
|
||||||
|
if (!corrected) continue;
|
||||||
|
if (
|
||||||
|
item.carriedBy !== corrected.carriedBy ||
|
||||||
|
item.securedBy !== corrected.securedBy
|
||||||
|
) {
|
||||||
|
frame.furnitureCorrections.delete(item.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
accumulateCorrection(frame.furnitureCorrections, item.id, {
|
||||||
|
x: item.x + item.velocityX * leadSeconds - corrected.x - corrected.velocityX * leadSeconds,
|
||||||
|
y: item.y + item.velocityY * leadSeconds - corrected.y - corrected.velocityY * leadSeconds,
|
||||||
|
z: item.z + item.velocityZ * leadSeconds - corrected.z - corrected.velocityZ * leadSeconds,
|
||||||
|
yaw: 0,
|
||||||
|
}, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function accumulateCorrection(
|
||||||
|
corrections: Map<number, MoversPositionCorrection>,
|
||||||
|
id: number,
|
||||||
|
delta: Pick<MoversPositionCorrection, "x" | "y" | "z" | "yaw">,
|
||||||
|
now: number,
|
||||||
|
): void {
|
||||||
|
const existing = corrections.get(id);
|
||||||
|
const decay = existing
|
||||||
|
? Math.exp(-(now - existing.updatedAt) / MOVERS_CORRECTION_DECAY_MILLISECONDS)
|
||||||
|
: 0;
|
||||||
|
const x = (existing?.x ?? 0) * decay + delta.x;
|
||||||
|
const y = (existing?.y ?? 0) * decay + delta.y;
|
||||||
|
const z = (existing?.z ?? 0) * decay + delta.z;
|
||||||
|
const yaw = normalizeAngle((existing?.yaw ?? 0) * decay + delta.yaw);
|
||||||
|
if (Math.hypot(x, y, z, yaw) < 0.0001) {
|
||||||
|
corrections.delete(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
corrections.set(id, { x, y, z, yaw, updatedAt: now });
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAngle(value: number): number {
|
||||||
|
return Math.atan2(Math.sin(value), Math.cos(value));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user