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) <noreply@anthropic.com>
This commit is contained in:
@@ -132,11 +132,18 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
|||||||
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
|
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
|
||||||
// o proxy fugir se a rede ficar muda.
|
// o proxy fugir se a rede ficar muda.
|
||||||
const FZeusProxySnapshot& Last = SnapshotBuffer.Last();
|
const FZeusProxySnapshot& Last = SnapshotBuffer.Last();
|
||||||
const float ExtrapSec = FMath::Clamp(
|
const float RawExtrapSec =
|
||||||
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f,
|
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f;
|
||||||
0.0f, MaxExtrapolationSeconds);
|
const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds);
|
||||||
NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
|
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;
|
bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -207,13 +214,25 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
|||||||
LastDiagLogSec = NowSec;
|
LastDiagLogSec = NowSec;
|
||||||
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
|
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
|
||||||
const int64 RenderLagMs = ServerNowMs - NewestMs;
|
const int64 RenderLagMs = ServerNowMs - NewestMs;
|
||||||
// DEBUG-PROXY (2026-06-12): Warning temporario p/ diagnosticar jitter-andando.
|
// === DIAG PERMANENTE -- jitter por FOME DE DELTAS ========================
|
||||||
// extrap=1 frequente => buffer faminto (subir InterpolationDelayMs/rate).
|
// Bug investigado/resolvido 2026-06-13 (ver memoria
|
||||||
// Rebaixar p/ Verbose depois de validar. speed em cm/s da velocidade visual.
|
// 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,
|
UE_LOG(LogZMMO, Warning,
|
||||||
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f speed=%.0f"),
|
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld newestMs=%lld secsSinceSnap=%.1f interpAlpha=%.2f extrap=%d speed=%.0f"),
|
||||||
EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha,
|
EntityId, SnapshotBuffer.Num(), RenderLagMs, NewestMs, SecsSinceSnap, InterpAlpha,
|
||||||
bExtrapolating ? 1 : 0, InterpolationDelayMs,
|
bExtrapolating ? 1 : 0,
|
||||||
FVector(VisualVelocity.X, VisualVelocity.Y, 0.0f).Size());
|
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)
|
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.
|
// 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp.
|
||||||
// Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte
|
// Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte
|
||||||
// do ServerTimeMs muda (publisher antigo -> novo) e o offset fica
|
// 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.
|
// pra evitar salto visual durante transicao.
|
||||||
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
|
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
|
||||||
const int64 OffsetCandidate = LocalNowMs - Snapshot.ServerTimeMs;
|
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)
|
if (SnapshotBuffer.Num() == 0)
|
||||||
{
|
{
|
||||||
// Primeira amostra: bootstrap direto (nao tem baseline pra suavizar).
|
// Primeira amostra: bootstrap direto (nao tem baseline pra suavizar).
|
||||||
ServerClockOffsetMs = OffsetCandidate;
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ protected:
|
|||||||
*/
|
*/
|
||||||
int64 ServerClockOffsetMs = 0;
|
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
|
/** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar
|
||||||
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
|
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
|
||||||
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;
|
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;
|
||||||
|
|||||||
Reference in New Issue
Block a user