[YAW-BODY-ORIENT] commit conjunto cliente + servidor. Sintoma: ao apertar D o proxy virava ~45 graus de uma vez, em vez de virar igual ao player local (suave, so um pouco), e parado seguia o mouse. Raiz: a fonte de verdade do yaw estava errada. O servidor derivava simpleYawDeg de atan2(velocidade) INSTANTANEO ao mover (snap pra direcao do movimento) e da mira (ControlRotation) ao parar -- nenhum dos dois e o yaw do corpo do dono. Correcao ponta-a-ponta: - Cliente: envia GetActorRotation().Yaw (yaw do CORPO; o CMC ja orienta pra direcao do movimento a 500 deg/s e bUseControllerRotationYaw=false faz o MOUSE nao girar o corpo) no lugar de GetControlRotation().Yaw. - Servidor: usa o yaw recebido DIRETO em simpleYawDeg (remove atan2 instantaneo + mira-no-idle). - Proxy: replica Snapshot.YawDeg interpolado shortest-path (remove a derivacao por Atan2 da velocidade). Resultado: o proxy vira identico ao corpo do dono e o mouse nao gira o corpo. Futuro (aim/combate/Motion Matching): adicionar um campo aimYawDeg dedicado em vez de reusar este. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
424 lines
16 KiB
C++
424 lines
16 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 RawExtrapSec =
|
|
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f;
|
|
const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds);
|
|
NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
|
|
// 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
|
|
{
|
|
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);
|
|
}
|
|
|
|
// ORIENTACAO DO CORPO = REPLICA do yaw autoritativo do servidor (Snapshot.YawDeg),
|
|
// interpolado shortest-path entre os MESMOS A/B da posicao. Esse yaw agora carrega
|
|
// o yaw do CORPO do dono (orient-to-movement do CMC: vira pra direcao do MOVIMENTO
|
|
// /WASD, suave a 500 deg/s; parado mantem; o MOUSE nao gira o corpo) -- o cliente
|
|
// dono manda GetActorRotation().Yaw e o servidor repassa direto. Por isso o proxy
|
|
// so' REPLICA: vira identico ao corpo do dono, sem re-derivar nada.
|
|
//
|
|
// NAO derivamos de Atan2(velocidade): isso era a fonte do bug 2026-06-13 ("virava
|
|
// demais 45 graus") -- a direcao da velocidade salta, o yaw do corpo nao. A
|
|
// suavizacao correta ja' aconteceu no CMC do dono; aqui so' interpolamos amostras.
|
|
//
|
|
// FUTURO -- Motion Matching/aim: quando "a mira dita a locomocao" (strafe/aim
|
|
// offset), o servidor passara a mandar um aimYawDeg separado e o AnimBP usara a
|
|
// velocidade relativa a este yaw pra escolher fwd/back/left/right. Por ORA, 1 yaw.
|
|
FRotator NewRot = GetActorRotation();
|
|
if (IdxB == INDEX_NONE)
|
|
{
|
|
NewRot.Yaw = SnapshotBuffer.Last().YawDeg; // extrapolando: segura o ultimo yaw
|
|
}
|
|
else
|
|
{
|
|
const float YawA = SnapshotBuffer[IdxB - 1].YawDeg;
|
|
const float YawB = SnapshotBuffer[IdxB].YawDeg;
|
|
// FindDeltaAngleDegrees -> menor arco (-180..180); evita girar "o lado longo".
|
|
const float DeltaYaw = FMath::FindDeltaAngleDegrees(YawA, YawB);
|
|
NewRot.Yaw = YawA + DeltaYaw * InterpAlpha;
|
|
}
|
|
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;
|
|
// === 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 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());
|
|
}
|
|
}
|
|
|
|
void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntityType InEntityType)
|
|
{
|
|
EntityId = InEntityId;
|
|
EntityType = InEntityType;
|
|
}
|
|
|
|
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
|
|
// permanentemente skewed, alimentando tremor pos-handoff e drift cumulativo
|
|
// de clock cliente vs server. Agora cada amostra recalcula candidato e
|
|
// suaviza (alpha=0.02, ~50 amostras pra convergir) com clamp max 2ms/snapshot
|
|
// pra evitar salto visual durante transicao.
|
|
const int64 LocalNowMs = static_cast<int64>(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
|
|
{
|
|
const double Alpha = 0.02;
|
|
const int64 RawTarget = static_cast<int64>(
|
|
static_cast<double>(ServerClockOffsetMs) * (1.0 - Alpha)
|
|
+ static_cast<double>(OffsetCandidate) * Alpha);
|
|
// Clamp max 2ms por amostra — em 30Hz isso converge a 60ms/s, mais que
|
|
// o suficiente pra absorver drift normal sem causar tremor visual.
|
|
const int64 Delta = RawTarget - ServerClockOffsetMs;
|
|
const int64 ClampedDelta = (Delta > 2) ? 2
|
|
: (Delta < -2) ? -2
|
|
: Delta;
|
|
ServerClockOffsetMs += ClampedDelta;
|
|
}
|
|
|
|
FZeusProxySnapshot S;
|
|
S.PosCm = Snapshot.PositionCm;
|
|
S.VelCmS = Snapshot.VelocityCmS;
|
|
S.YawDeg = Snapshot.YawDeg; // yaw do CORPO do dono (orient-to-movement; mouse nao gira)
|
|
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);
|
|
}
|