Implementa o padrao Source/Quake/Unreal: o proxy nao aplica mais a posicao recebida diretamente — empurra para um buffer ordenado de snapshots e o Tick interpola entre os dois snapshots que cercam (server_now - InterpolationDelayMs). - Novo USTRUCT FZMMOProxySnapshot guardado em TArray ordenado por ServerTimeMs (insercao defensiva + dedupe; UDP pode trocar ordem). - ApplyEntitySnapshot vira "push to buffer"; estabelece o offset de relogio na primeira amostra (local_now - server_time). Mudanca de bGrounded continua aplicada imediatamente (MovementMode) para o pulo nao herdar o delay de interpolacao. - Tick acha o par (A,B), faz Lerp em pos/vel/yaw; extrapolacao curta clampada por MaxExtrapolationSeconds quando faltam snapshots futuros. - Aceleracao derivada agora vem da curva interpolada (mais estavel que a derivada snapshot-a-snapshot anterior). Fallback sintetico de accel para o AnimBP do Quinn preservado. - Removido o line-trace de Z (causava oscilacao apos a interpolacao; a Z autoritativa do dono ja vem no snapshot, ADR 0041). - Separacao explicita AuthoritativePosCm vs InterpolatedPosCm, com LastSnapshotPosition mantido como alias para compat de Blueprint. - Defaults: InterpolationDelayMs=100, SnapshotBufferCapacity=8, MaxExtrapolationSeconds=0.1 (UPROPERTY EditAnywhere para tunar em PIE). - Log Verbose 1x/s com renderLagMs, interpAlpha, flag de extrapolacao. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
334 lines
11 KiB
C++
334 lines
11 KiB
C++
#include "ZMMOPlayerProxy.h"
|
|
|
|
#include "Animation/AnimInstance.h"
|
|
#include "Components/CapsuleComponent.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/CharacterMovementComponent.h"
|
|
#include "UObject/ConstructorHelpers.h"
|
|
#include "ZMMO.h"
|
|
#include "ZMMOProxyMovementComponent.h"
|
|
|
|
AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer.SetDefaultSubobjectClass<UZMMOProxyMovementComponent>(ACharacter::CharacterMovementComponentName))
|
|
{
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
PrimaryActorTick.bStartWithTickEnabled = true;
|
|
|
|
UCapsuleComponent* Capsule = GetCapsuleComponent();
|
|
if (Capsule)
|
|
{
|
|
Capsule->InitCapsuleSize(42.0f, 96.0f);
|
|
Capsule->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
|
}
|
|
|
|
UCharacterMovementComponent* CMC = GetCharacterMovement();
|
|
if (CMC)
|
|
{
|
|
// MOVE_Walking (em vez de MOVE_None) e necessario para o AnimBP do Quinn
|
|
// transitar entre Idle/Run — a state machine consulta `IsFalling()` /
|
|
// `MovementMode` no CMC mesmo quando este nao integra fisica.
|
|
CMC->bOrientRotationToMovement = false;
|
|
CMC->RotationRate = FRotator::ZeroRotator;
|
|
CMC->bRunPhysicsWithNoController = false;
|
|
CMC->MaxWalkSpeed = 600.0f;
|
|
CMC->SetMovementMode(MOVE_Walking);
|
|
|
|
// Sem input local, o CMC zerava `Velocity` por braking/friction antes do
|
|
// `NativeUpdateAnimation` lê-la, deixando o AnimBP em Idle. Zerar friction
|
|
// preserva a Velocity escrita por `RefreshAnimationDrivers` entre snapshots.
|
|
CMC->BrakingDecelerationWalking = 0.0f;
|
|
CMC->GroundFriction = 0.0f;
|
|
CMC->BrakingFrictionFactor = 0.0f;
|
|
}
|
|
|
|
// Defaults visuais (mesh + AnimBP) em paridade com `AZMMOPlayerCharacter`.
|
|
if (USkeletalMeshComponent* MeshComponent = GetMesh())
|
|
{
|
|
static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh(
|
|
TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple"));
|
|
static ConstructorHelpers::FClassFinder<UAnimInstance> QuinnAnimBp(
|
|
TEXT("/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed"));
|
|
|
|
if (QuinnMesh.Succeeded())
|
|
{
|
|
MeshComponent->SetSkeletalMesh(QuinnMesh.Object);
|
|
MeshComponent->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
|
|
MeshComponent->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("AZMMOPlayerProxy: SKM_Quinn_Simple nao encontrado em /Game/Characters/Mannequins/Meshes."));
|
|
}
|
|
|
|
if (QuinnAnimBp.Succeeded())
|
|
{
|
|
MeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
|
|
MeshComponent->SetAnimInstanceClass(QuinnAnimBp.Class);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("AZMMOPlayerProxy: ABP_Unarmed nao encontrado em /Game/Characters/Mannequins/Anims/Unarmed."));
|
|
}
|
|
}
|
|
|
|
bUseControllerRotationPitch = false;
|
|
bUseControllerRotationYaw = false;
|
|
bUseControllerRotationRoll = false;
|
|
}
|
|
|
|
void AZMMOPlayerProxy::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
// Tick do proxy precisa rodar **antes** do tick do mesh para que
|
|
// `RefreshAnimationDrivers` populate `Velocity`+`Acceleration` no CMC custom
|
|
// antes que `NativeUpdateAnimation` do AnimBP os leia. Sem isto, AnimBP
|
|
// recalcula `ShouldMove` com Acceleration=0 → state machine presa em Idle.
|
|
if (USkeletalMeshComponent* MeshComponent = GetMesh())
|
|
{
|
|
MeshComponent->AddTickPrerequisiteActor(this);
|
|
}
|
|
}
|
|
|
|
void AZMMOPlayerProxy::Tick(const float DeltaSeconds)
|
|
{
|
|
Super::Tick(DeltaSeconds);
|
|
|
|
if (SnapshotBuffer.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Tempo do servidor estimado: relogio local em ms - offset estabelecido na
|
|
// primeira amostra. RenderMs = tempo "atrasado" usado para procurar o par
|
|
// de snapshots a interpolar (snapshot interpolation a la Source/Quake).
|
|
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
|
|
const int64 ServerNowMs = LocalNowMs - ServerClockOffsetMs;
|
|
const int64 RenderMs = ServerNowMs - static_cast<int64>(InterpolationDelayMs);
|
|
|
|
// Procura o snapshot B tal que A.t <= RenderMs <= B.t (A = B-1).
|
|
int32 IdxB = INDEX_NONE;
|
|
for (int32 i = 1; i < SnapshotBuffer.Num(); ++i)
|
|
{
|
|
if (SnapshotBuffer[i].ServerTimeMs >= RenderMs)
|
|
{
|
|
IdxB = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
FVector NewPos;
|
|
FVector NewVel;
|
|
float InterpAlpha = 0.0f;
|
|
bool bExtrapolating = false;
|
|
|
|
if (IdxB == INDEX_NONE)
|
|
{
|
|
// Sem snapshot futuro: extrapolacao curta com a ultima velocidade
|
|
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
|
|
// o proxy fugir se a rede ficar muda.
|
|
const FZMMOProxySnapshot& Last = SnapshotBuffer.Last();
|
|
const float ExtrapSec = FMath::Clamp(
|
|
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f,
|
|
0.0f, MaxExtrapolationSeconds);
|
|
NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
|
|
NewVel = Last.VelCmS;
|
|
bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER;
|
|
}
|
|
else
|
|
{
|
|
const FZMMOProxySnapshot& A = SnapshotBuffer[IdxB - 1];
|
|
const FZMMOProxySnapshot& B = SnapshotBuffer[IdxB];
|
|
const double Span = FMath::Max<double>(1.0, static_cast<double>(B.ServerTimeMs - A.ServerTimeMs));
|
|
InterpAlpha = static_cast<float>(FMath::Clamp(
|
|
static_cast<double>(RenderMs - A.ServerTimeMs) / Span, 0.0, 1.0));
|
|
NewPos = FMath::Lerp(A.PosCm, B.PosCm, InterpAlpha);
|
|
NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha);
|
|
}
|
|
|
|
// Yaw deriva da velocidade interpolada: locomotion mantem face na direcao
|
|
// do movimento; em idle preserva o yaw atual para nao "snappar".
|
|
FRotator NewRot = GetActorRotation();
|
|
if (FVector(NewVel.X, NewVel.Y, 0.0f).SizeSquared() > 1.0f)
|
|
{
|
|
NewRot.Yaw = FMath::RadiansToDegrees(FMath::Atan2(NewVel.Y, NewVel.X));
|
|
}
|
|
NewRot.Pitch = 0.0f;
|
|
NewRot.Roll = 0.0f;
|
|
|
|
// Aceleracao derivada da curva interpolada (mais estavel que a derivada
|
|
// snapshot-a-snapshot que tinhamos antes; AnimBP do Quinn precisa de
|
|
// `Acceleration != 0` para sair de Idle).
|
|
FVector DerivedAccel = FVector::ZeroVector;
|
|
if (bHasPreviousSnapshot && DeltaSeconds > KINDA_SMALL_NUMBER)
|
|
{
|
|
DerivedAccel = (NewVel - PreviousVisualVelocity) / DeltaSeconds;
|
|
DerivedAccel = DerivedAccel.GetClampedToMaxSize(8000.0f);
|
|
}
|
|
constexpr float AccelSmoothingAlpha = 0.35f;
|
|
LastDerivedAccelerationCmS2 = bHasPreviousSnapshot
|
|
? FMath::Lerp(LastDerivedAccelerationCmS2, DerivedAccel, AccelSmoothingAlpha)
|
|
: DerivedAccel;
|
|
|
|
// Fallback sintetico: locomotion a velocidade constante => derivada ~ 0.
|
|
// Sem este nudge o AnimBP voltaria para Idle.
|
|
const FVector2D Vel2D(NewVel.X, NewVel.Y);
|
|
const float Speed2D = Vel2D.Size();
|
|
const FVector2D Accel2D(LastDerivedAccelerationCmS2.X, LastDerivedAccelerationCmS2.Y);
|
|
constexpr float MinSpeedForFallbackCmS = 50.0f;
|
|
constexpr float MinAccelFallbackCmS2 = 200.0f;
|
|
const bool bGroundedNow = SnapshotBuffer.Last().bGrounded;
|
|
if (bGroundedNow && Speed2D >= MinSpeedForFallbackCmS && Accel2D.Size() < MinAccelFallbackCmS2)
|
|
{
|
|
const FVector2D VelDir = Vel2D.GetSafeNormal();
|
|
const FVector2D FallbackAccel2D = VelDir * MinAccelFallbackCmS2;
|
|
LastDerivedAccelerationCmS2.X = FallbackAccel2D.X;
|
|
LastDerivedAccelerationCmS2.Y = FallbackAccel2D.Y;
|
|
LastDerivedAccelerationCmS2.Z = 0.0f;
|
|
}
|
|
|
|
PreviousVisualVelocity = NewVel;
|
|
bHasPreviousSnapshot = true;
|
|
|
|
InterpolatedPosCm = NewPos;
|
|
VisualVelocity = NewVel;
|
|
SetActorLocationAndRotation(NewPos, NewRot);
|
|
|
|
RefreshAnimationDrivers();
|
|
|
|
// Diagnostico 1x/s: render lag, buffer, alpha, extrapolating?
|
|
const double NowSec = FPlatformTime::Seconds();
|
|
if (NowSec - LastDiagLogSec >= 1.0)
|
|
{
|
|
LastDiagLogSec = NowSec;
|
|
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
|
|
const int64 RenderLagMs = ServerNowMs - NewestMs;
|
|
UE_LOG(LogZMMO, Verbose,
|
|
TEXT("ZMMOPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f"),
|
|
EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha,
|
|
bExtrapolating ? 1 : 0, InterpolationDelayMs);
|
|
}
|
|
}
|
|
|
|
void AZMMOPlayerProxy::SetZMMOIdentity(const int64 InEntityId, const EZMMOEntityType InEntityType)
|
|
{
|
|
EntityId = InEntityId;
|
|
EntityType = InEntityType;
|
|
}
|
|
|
|
void AZMMOPlayerProxy::ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot)
|
|
{
|
|
// Estabiliza o offset de relogio na primeira amostra. Subsequente nao
|
|
// re-sincroniza para evitar saltos visuais; um esquema mais sofisticado
|
|
// (ema do offset) cabe quando tivermos ping/RTT estabilizado por sessao.
|
|
if (SnapshotBuffer.Num() == 0)
|
|
{
|
|
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
|
|
ServerClockOffsetMs = LocalNowMs - Snapshot.ServerTimeMs;
|
|
}
|
|
|
|
FZMMOProxySnapshot S;
|
|
S.PosCm = Snapshot.PositionCm;
|
|
S.VelCmS = Snapshot.VelocityCmS;
|
|
S.bGrounded = Snapshot.bGrounded;
|
|
S.ServerTimeMs = Snapshot.ServerTimeMs;
|
|
|
|
// Insercao ordenada defensiva (UDP pode trocar a ordem de pacotes).
|
|
int32 InsertAt = SnapshotBuffer.Num();
|
|
for (int32 i = SnapshotBuffer.Num() - 1; i >= 0; --i)
|
|
{
|
|
if (SnapshotBuffer[i].ServerTimeMs < S.ServerTimeMs)
|
|
{
|
|
InsertAt = i + 1;
|
|
break;
|
|
}
|
|
if (SnapshotBuffer[i].ServerTimeMs == S.ServerTimeMs)
|
|
{
|
|
// Dedupe: substitui pelo mais recente (este).
|
|
SnapshotBuffer[i] = S;
|
|
InsertAt = INDEX_NONE;
|
|
break;
|
|
}
|
|
InsertAt = i;
|
|
}
|
|
if (InsertAt != INDEX_NONE)
|
|
{
|
|
SnapshotBuffer.Insert(S, InsertAt);
|
|
}
|
|
while (SnapshotBuffer.Num() > SnapshotBufferCapacity)
|
|
{
|
|
SnapshotBuffer.RemoveAt(0);
|
|
}
|
|
|
|
AuthoritativePosCm = Snapshot.PositionCm;
|
|
LastSnapshotPosition = Snapshot.PositionCm;
|
|
bHasFirstSnapshot = true;
|
|
|
|
// `bGrounded` e evento — aplicar `MovementMode` imediatamente para que o
|
|
// pulo nao espere `InterpolationDelayMs` para mudar a state machine do AnimBP.
|
|
if (UCharacterMovementComponent* CMC = GetCharacterMovement())
|
|
{
|
|
const EMovementMode DesiredMode = Snapshot.bGrounded ? MOVE_Walking : MOVE_Falling;
|
|
if (CMC->MovementMode != DesiredMode)
|
|
{
|
|
CMC->SetMovementMode(DesiredMode);
|
|
}
|
|
}
|
|
|
|
ApplyCollisionPolicy();
|
|
}
|
|
|
|
void AZMMOPlayerProxy::SetEntityRelevant(const bool bRelevant)
|
|
{
|
|
if (bIsZMMORelevant == bRelevant)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bIsZMMORelevant = bRelevant;
|
|
SetActorHiddenInGame(!bRelevant);
|
|
SetActorTickEnabled(bRelevant);
|
|
ApplyCollisionPolicy();
|
|
}
|
|
|
|
void AZMMOPlayerProxy::RefreshAnimationDrivers()
|
|
{
|
|
UCharacterMovementComponent* CMC = GetCharacterMovement();
|
|
if (!CMC)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Defesa: snapshots em walking não devem injetar Z residual (jitter de
|
|
// terminal velocity / drift) no CMC, que o AnimBP veria como queda.
|
|
FVector VelForCMC = VisualVelocity;
|
|
if (CMC->MovementMode == MOVE_Walking)
|
|
{
|
|
VelForCMC.Z = 0.0f;
|
|
}
|
|
CMC->Velocity = VelForCMC;
|
|
|
|
// Acceleration externa via CMC custom — alimenta `Get Current Acceleration`
|
|
// que o AnimBP do Quinn usa em `ShouldMove` e nas transições de Jump_Start.
|
|
if (UZMMOProxyMovementComponent* ProxyCMC = Cast<UZMMOProxyMovementComponent>(CMC))
|
|
{
|
|
ProxyCMC->SetZMMOExternalAcceleration(LastDerivedAccelerationCmS2);
|
|
}
|
|
}
|
|
|
|
void AZMMOPlayerProxy::ApplyCollisionPolicy()
|
|
{
|
|
UCapsuleComponent* Capsule = GetCapsuleComponent();
|
|
if (!Capsule)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const bool bShouldCollide = bIsZMMORelevant && bHasFirstSnapshot;
|
|
Capsule->SetCollisionEnabled(bShouldCollide ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
|
|
}
|