Files
ZMMO/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp
Mateus Rodrigues 6a6a28a086 refactor: prefixo ZMMO -> Zeus em classes/módulos C++ do cliente
- Renomeia ~50 classes UCLASS/USTRUCT/UENUM/UINTERFACE pra Zeus*
  (AZeusGameMode, AZeusCharacter, UZeusGameInstance, IZeusEntityInterface,
  UZeusWorldSubsystem, FZeusEntitySnapshot, etc.)
- Encurta AZMMOPlayerCharacter -> AZeusCharacter
- Renomeia módulos satélite ZMMOAttributes -> ZeusAttributes e
  ZMMOJobs -> ZeusJobs (pasta + .Build.cs + .uproject)
- Mantém módulo principal ZMMO (renomeia depois quando jogo ganhar nome)
- DefaultEngine.ini: ActiveClassRedirects ZMMOX -> ZeusX (preserva BPs/assets)
- DefaultGame.ini: sections atualizadas pra ZeusThemeSubsystem/ZeusPlayerState
- Category="ZMMO|..." -> "Zeus|..." em UPROPERTYs
- Preserva LogZMMO, ZMMO_API, /Script/ZMMO., /Game/ZMMO/, classes Target

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:24:38 -03:00

334 lines
11 KiB
C++

#include "ZeusPlayerProxy.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 "ZeusProxyMovementComponent.h"
AZeusPlayerProxy::AZeusPlayerProxy(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UZeusProxyMovementComponent>(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 `AZeusCharacter`.
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("AZeusPlayerProxy: 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("AZeusPlayerProxy: ABP_Unarmed nao encontrado em /Game/Characters/Mannequins/Anims/Unarmed."));
}
}
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
}
void AZeusPlayerProxy::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 AZeusPlayerProxy::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 FZeusProxySnapshot& 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 FZeusProxySnapshot& A = SnapshotBuffer[IdxB - 1];
const FZeusProxySnapshot& 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("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f"),
EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha,
bExtrapolating ? 1 : 0, InterpolationDelayMs);
}
}
void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntityType InEntityType)
{
EntityId = InEntityId;
EntityType = InEntityType;
}
void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& 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;
}
FZeusProxySnapshot 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 AZeusPlayerProxy::SetEntityRelevant(const bool bRelevant)
{
if (bIsZeusRelevant == bRelevant)
{
return;
}
bIsZeusRelevant = bRelevant;
SetActorHiddenInGame(!bRelevant);
SetActorTickEnabled(bRelevant);
ApplyCollisionPolicy();
}
void AZeusPlayerProxy::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 (UZeusProxyMovementComponent* ProxyCMC = Cast<UZeusProxyMovementComponent>(CMC))
{
ProxyCMC->SetZeusExternalAcceleration(LastDerivedAccelerationCmS2);
}
}
void AZeusPlayerProxy::ApplyCollisionPolicy()
{
UCapsuleComponent* Capsule = GetCapsuleComponent();
if (!Capsule)
{
return;
}
const bool bShouldCollide = bIsZeusRelevant && bHasFirstSnapshot;
Capsule->SetCollisionEnabled(bShouldCollide ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
}