From 4d737829f5a2cdbbdfcb1e95e3d2ec525a04dcf1 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 13 Jun 2026 01:26:33 -0300 Subject: [PATCH] ZeusPlayerProxy: fix jitter por fome de deltas (mitigacao + anti-stale) [Change-Set: JITTER-STARVATION-2026-06-13] Player remoto parado nao gera DELTA_BATCH (ComputeDelta suprime no server) -> o proxy fica "mudo": NewestMs congela, renderLagMs cresce 1:1 com o wall-clock -> extrapola sempre -> "anda no lugar" (moonwalk) e salta quando o player volta. Provado em log (secsSinceSnap subindo enquanto o remoto esta moving=0). NAO e' clock errado nem self-proxy (red-herrings descartados); o seed do relogio esta' OK. - Tick: quando faminto (extrap alem de MaxExtrapolationSeconds+0.15s), zera a vel visual -> proxy vai pra Idle em vez de moonwalk. - ApplyEntitySnapshot: anti-stale -- snapshot > 1000ms a frente do topo do buffer descarta o buffer + re-bootstrapa o ServerClockOffsetMs -> snap limpo (sem span gigante na interpolacao). Loga "buffer STALE gap=Xms -> reset+reseed". - Logs DIAG PERMANENTES (Warning, mantidos de proposito -- ver memoria project_proxy_delta_starvation_jitter): SEED clock + renderLagMs/newestMs/ secsSinceSnap/speed no Tick. Sonda anti-regressao. Cobre tambem o interserver: shadow proxies parados sofrem a mesma fome. Validado em jogo (user: "sem jitter nenhum"). Par server: PubV2-DIAG (mesmo Change-Set). Co-Authored-By: Claude Opus 4.8 (1M context) --- Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp | 65 +++++++++++++++++---- Source/ZMMO/Game/Entity/ZeusPlayerProxy.h | 6 ++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp index 521bfcc..f955685 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp @@ -132,11 +132,18 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds) // conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar // o proxy fugir se a rede ficar muda. const FZeusProxySnapshot& Last = SnapshotBuffer.Last(); - const float ExtrapSec = FMath::Clamp( - static_cast(RenderMs - Last.ServerTimeMs) / 1000.0f, - 0.0f, MaxExtrapolationSeconds); + const float RawExtrapSec = + static_cast(RenderMs - Last.ServerTimeMs) / 1000.0f; + const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds); NewPos = Last.PosCm + Last.VelCmS * ExtrapSec; - NewVel = Last.VelCmS; + // STARVATION (buffer=1 por muito tempo -- ex: 1o login sem DELTA_BATCH por + // segundos): a extrapolacao satura no clamp e a POSE congela, mas manter a + // velocidade faz o AnimBP rodar locomocao PARADO ("andar no lugar"/moonwalk + // = o "jitter" reportado). Se ja passamos bem do clamp, zera a vel visual + // pra o proxy ir pra Idle ate' os deltas voltarem (em vez de animar + // andando travado). Quando os deltas chegam, o ramo de interpolacao retoma. + const bool bStarved = RawExtrapSec > (MaxExtrapolationSeconds + 0.15f); + NewVel = bStarved ? FVector::ZeroVector : Last.VelCmS; bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER; } else @@ -207,13 +214,25 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds) LastDiagLogSec = NowSec; const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs; const int64 RenderLagMs = ServerNowMs - NewestMs; - // DEBUG-PROXY (2026-06-12): Warning temporario p/ diagnosticar jitter-andando. - // extrap=1 frequente => buffer faminto (subir InterpolationDelayMs/rate). - // Rebaixar p/ Verbose depois de validar. speed em cm/s da velocidade visual. + // === DIAG PERMANENTE -- jitter por FOME DE DELTAS ======================== + // Bug investigado/resolvido 2026-06-13 (ver memoria + // project_proxy_delta_starvation_jitter). Este Warning FICA DE PROPOSITO + // (decisao do dono: NAO rebaixar p/ Verbose nem remover) -- e' a sonda pra + // cacar regressao do jitter de proxy. Como ler: + // secsSinceSnap = ha quanto tempo o proxy NAO recebe snapshot. Sobe = FOME + // (player remoto parado -> ComputeDelta suprime no server -> sem DELTA_BATCH). + // newestMs = serverTimeMs do ultimo snapshot; CONGELA na fome (deveria avancar). + // renderLagMs = ServerNowMs - newestMs; cresce 1:1 com o wall-clock na fome. + // speed=0 na fome = a mitigacao (zera a vel visual -> Idle, sem "moonwalk") agindo. + // Ao o player voltar a se mover sai "buffer STALE ... reset+reseed" (anti-stale) + // e renderLagMs/secsSinceSnap voltam a ~0 (snap limpo, sem salto/jitter). + // ========================================================================= + const double SecsSinceSnap = + (LastSnapshotRecvSec > 0.0) ? (NowSec - LastSnapshotRecvSec) : -1.0; UE_LOG(LogZMMO, Warning, - TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f speed=%.0f"), - EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha, - bExtrapolating ? 1 : 0, InterpolationDelayMs, + TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld newestMs=%lld secsSinceSnap=%.1f interpAlpha=%.2f extrap=%d speed=%.0f"), + EntityId, SnapshotBuffer.Num(), RenderLagMs, NewestMs, SecsSinceSnap, InterpAlpha, + bExtrapolating ? 1 : 0, FVector(VisualVelocity.X, VisualVelocity.Y, 0.0f).Size()); } } @@ -226,6 +245,8 @@ void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntity void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot) { + LastSnapshotRecvSec = FPlatformTime::Seconds(); // DIAG: marca chegada (mede a fome no Tick) + // 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp. // Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte // do ServerTimeMs muda (publisher antigo -> novo) e o offset fica @@ -235,10 +256,34 @@ void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot) // pra evitar salto visual durante transicao. const int64 LocalNowMs = static_cast(FPlatformTime::Seconds() * 1000.0); const int64 OffsetCandidate = LocalNowMs - Snapshot.ServerTimeMs; + + // Anti-stale (2026-06-13): se este snapshot esta MUITO a frente do mais novo + // do buffer, o proxy ficou "mudo" por segundos -- player remoto parado -> + // ComputeDelta suprime -> sem DELTA_BATCH novo -> renderLagMs disparou pra + // dezenas de segundos. Manter os snapshots velhos faz a interpolacao usar um + // span gigante (pose de ~100s atras -> agora) quando o player volta a se mover, + // causando rasteio/salto (o jitter reportado). Descarta o buffer obsoleto + + // re-bootstrapa o relogio pra a interpolacao recomecar limpa neste keyframe. + if (SnapshotBuffer.Num() > 0 + && (Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs) > 1000) + { + UE_LOG(LogZMMO, Warning, + TEXT("ZeusPlayerProxy[%lld] buffer STALE gap=%lldms -> reset+reseed"), + EntityId, Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs); + SnapshotBuffer.Reset(); + ServerClockOffsetMs = OffsetCandidate; + } + if (SnapshotBuffer.Num() == 0) { // Primeira amostra: bootstrap direto (nao tem baseline pra suavizar). ServerClockOffsetMs = OffsetCandidate; + // DIAG (jitter 1o login): confirma o seed do relogio. serverTimeMs deve ser + // != 0 e o offset razoavel; se vier 0 aqui, o keyframe/catch-up corrompeu o + // bootstrap (hipotese descartada, mas o log fecha a questao em campo). + UE_LOG(LogZMMO, Warning, + TEXT("ZeusPlayerProxy[%lld] SEED clock serverTimeMs=%lld localNowMs=%lld offset=%lld"), + EntityId, Snapshot.ServerTimeMs, LocalNowMs, ServerClockOffsetMs); } else { diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h index 5a21096..c8f91d2 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h @@ -140,6 +140,12 @@ protected: */ int64 ServerClockOffsetMs = 0; + /** DIAG (2026-06-13): instante (s, monotonic) do ultimo snapshot recebido em + * ApplyEntitySnapshot. Usado no log do Tick pra medir "secsSinceSnap" = quanto + * tempo o proxy ficou MUDO (sem delta). secsSinceSnap grande == fome (player + * remoto parado -> ComputeDelta suprime). 0 = nenhum snapshot ainda. */ + double LastSnapshotRecvSec = 0.0; + /** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar * o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */ FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;