From f759a3138b8c6d82fd8bbcc7519cd87dddf1720f Mon Sep 17 00:00:00 2001 From: Syncer Deploy Date: Mon, 31 Aug 2026 12:47:06 -0400 Subject: [PATCH] smooth Movers presentation and reconciliation --- apps/web/src/Movers3D.tsx | 120 ++++++++++++++++++++++++----- apps/web/src/MoversGame.tsx | 2 +- apps/web/src/useMoversClient.ts | 132 +++++++++++++++++++++++++++++--- 3 files changed, 226 insertions(+), 28 deletions(-) diff --git a/apps/web/src/Movers3D.tsx b/apps/web/src/Movers3D.tsx index 17caaf7..daf51d7 100644 --- a/apps/web/src/Movers3D.tsx +++ b/apps/web/src/Movers3D.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, type MutableRefObject } from "react"; import * as THREE from "three"; import { + MOVERS_TICK_RATE, MOVERS_WALLS, type FurnitureKind, type MoversClientState, @@ -8,9 +9,15 @@ import { type MoversPlayerView, type MoversTeam, } from "@syncer/shared"; +import { + MOVERS_CORRECTION_DECAY_MILLISECONDS, + type MoversPositionCorrection, + type MoversRenderFrame, + type MoversRenderSource, +} from "./useMoversClient.js"; interface Movers3DProps { - world: MoversClientState; + source: MoversRenderSource; playerId: number | null; } @@ -43,13 +50,14 @@ interface MoversRuntime { const yellow = 0xffc629; 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(null); - const worldRef = useRef(world); const playerIdRef = useRef(playerId); const lastEventIdRef = useRef(0); - worldRef.current = world; playerIdRef.current = playerId; useEffect(() => { @@ -110,7 +118,7 @@ export function Movers3D({ world, playerId }: Movers3DProps) { const animate = (time: number) => { const deltaSeconds = Math.min(0.05, Math.max(0.001, (time - runtime.lastTime) / 1_000)); 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); runtime.animationFrame = window.requestAnimationFrame(animate); }; @@ -130,20 +138,48 @@ export function Movers3D({ world, playerId }: Movers3DProps) { function updateRuntime( runtime: MoversRuntime, - world: MoversClientState, + frame: MoversRenderFrame, playerId: number | null, lastEventIdRef: MutableRefObject, time: number, deltaSeconds: number, ): void { - updatePlayers(runtime, world.players, playerId, deltaSeconds, time); - updateFurniture(runtime, world.furniture, deltaSeconds, time); + const world = frame.state; + 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); updateEffects(runtime, world, lastEventIdRef, time); const local = world.players.find((player) => player.id === playerId); - const targetX = local?.x ?? 0; - const targetZ = local?.z ?? 0; + const localCorrection = local ? frame.playerCorrections.get(local.id) : undefined; + 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); 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); @@ -161,6 +197,8 @@ function updatePlayers( runtime: MoversRuntime, players: MoversPlayerView[], playerId: number | null, + corrections: Map, + leadSeconds: number, deltaSeconds: number, time: number, ): void { @@ -173,10 +211,15 @@ function updatePlayers( runtime.players.set(player.id, 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); - group.position.x = THREE.MathUtils.lerp(group.position.x, player.x, alpha); - group.position.z = THREE.MathUtils.lerp(group.position.z, player.z, alpha); - group.rotation.y = lerpAngle(group.rotation.y, player.yaw, alpha); + group.position.x = THREE.MathUtils.lerp(group.position.x, targetX, alpha); + group.position.z = THREE.MathUtils.lerp(group.position.z, targetZ, alpha); + group.rotation.y = lerpAngle(group.rotation.y, targetYaw, alpha); const speed = Math.hypot(player.velocityX, player.velocityZ); 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); @@ -199,6 +242,8 @@ function updatePlayers( function updateFurniture( runtime: MoversRuntime, items: MoversFurnitureView[], + corrections: Map, + leadSeconds: number, deltaSeconds: number, time: number, ): void { @@ -212,17 +257,39 @@ function updateFurniture( runtime.furniture.set(item.id, 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); - group.position.x = THREE.MathUtils.lerp(group.position.x, item.x, alpha); - group.position.y = THREE.MathUtils.lerp(group.position.y, item.y, alpha); - group.position.z = THREE.MathUtils.lerp(group.position.z, item.z, alpha); - const targetRotation = new THREE.Quaternion( + group.position.x = THREE.MathUtils.lerp(group.position.x, targetX, alpha); + group.position.y = THREE.MathUtils.lerp(group.position.y, targetY, alpha); + group.position.z = THREE.MathUtils.lerp(group.position.z, targetZ, alpha); + furnitureTargetRotation.set( item.rotationX, item.rotationY, item.rotationZ, item.rotationW, ).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); const securedLight = group.getObjectByName("secured") as THREE.PointLight | undefined; if (securedLight) { @@ -238,6 +305,23 @@ function updateFurniture( } } +function correctionFactorAt( + corrections: Map, + 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( runtime: MoversRuntime, world: MoversClientState, diff --git a/apps/web/src/MoversGame.tsx b/apps/web/src/MoversGame.tsx index 05da215..b6772d5 100644 --- a/apps/web/src/MoversGame.tsx +++ b/apps/web/src/MoversGame.tsx @@ -36,7 +36,7 @@ export function MoversGame() { return (
- +