feat(client): proxy animation via custom CMC + injected acceleration

Porta a estratégia do ZClientMMO para o ZMMO: novo
UZMMOProxyMovementComponent expõe escrita externa de Acceleration,
e AZMMOPlayerProxy deriva accel a partir da variação de velocidade
entre snapshots (com smoothing e fallback sintético em locomotion
constante). Tick do proxy roda antes do mesh para alimentar
Velocity+Acceleration antes do NativeUpdateAnimation. Cliente local
agora também envia Vel.Z e bIsFalling para que proxies vejam pulo
completo (subida + queda).
This commit is contained in:
2026-05-08 22:54:54 -03:00
parent 1744383f6f
commit 385ebf6a7b
5 changed files with 165 additions and 20 deletions

View File

@@ -336,7 +336,15 @@ void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds)
const bool bRateReached = SendAccumulatorSec >= SendInterval; const bool bRateReached = SendAccumulatorSec >= SendInterval;
const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec; const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;
const bool bShouldSend = bJumpEvent || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached); // IsFalling é estado contínuo do CMC (espelhado em PlayerStatePayload::grounded
// para alimentar o MovementMode dos proxies). Forçar envio em transições
// walking↔falling garante que a animação de queda/pulo dispare nos outros
// clientes mesmo quando estamos em rate limit / sem input de movimento.
const UCharacterMovementComponent* CMCForFalling = GetCharacterMovement();
const bool bIsFalling = CMCForFalling != nullptr && CMCForFalling->IsFalling();
const bool bFallingChanged = (bIsFalling != bPreviousFalling);
const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached);
if (!bShouldSend) if (!bShouldSend)
{ {
return; return;
@@ -365,10 +373,13 @@ void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds)
ClientTimeMs, ClientTimeMs,
ViewYawDeg, ViewYawDeg,
PosCm, PosCm,
FVector2D(Vel.X, Vel.Y)); FVector2D(Vel.X, Vel.Y),
bIsFalling,
static_cast<float>(Vel.Z));
SendAccumulatorSec = 0.0f; SendAccumulatorSec = 0.0f;
TimeSinceLastSendSec = 0.0f; TimeSinceLastSendSec = 0.0f;
bPendingJumpPressed = false; bPendingJumpPressed = false;
bPendingJumpReleased = false; bPendingJumpReleased = false;
bPreviousFalling = bIsFalling;
} }

View File

@@ -151,6 +151,10 @@ private:
bool bPendingJumpPressed = false; bool bPendingJumpPressed = false;
bool bPendingJumpReleased = false; bool bPendingJumpReleased = false;
bool bPreviousJumpHeld = false; bool bPreviousJumpHeld = false;
/** Último estado IsFalling enviado ao servidor. Usado para forçar um envio
* imediato em transições walking↔falling, evitando que o rate limit segure
* a transição até o próximo heartbeat. */
bool bPreviousFalling = false;
float SendAccumulatorSec = 0.0f; float SendAccumulatorSec = 0.0f;
float TimeSinceLastSendSec = 0.0f; float TimeSinceLastSendSec = 0.0f;

View File

@@ -8,9 +8,10 @@
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
#include "UObject/ConstructorHelpers.h" #include "UObject/ConstructorHelpers.h"
#include "ZMMO.h" #include "ZMMO.h"
#include "ZMMOProxyMovementComponent.h"
AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer) AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer) : Super(ObjectInitializer.SetDefaultSubobjectClass<UZMMOProxyMovementComponent>(ACharacter::CharacterMovementComponentName))
{ {
PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true; PrimaryActorTick.bStartWithTickEnabled = true;
@@ -27,23 +28,22 @@ AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
{ {
// MOVE_Walking (em vez de MOVE_None) e necessario para o AnimBP do Quinn // MOVE_Walking (em vez de MOVE_None) e necessario para o AnimBP do Quinn
// transitar entre Idle/Run — a state machine consulta `IsFalling()` / // transitar entre Idle/Run — a state machine consulta `IsFalling()` /
// `MovementMode` no CMC mesmo quando este nao integra fisica. Mantemos // `MovementMode` no CMC mesmo quando este nao integra fisica.
// `bOrientRotationToMovement=false` e `RotationRate=0` porque a
// orientacao e ditada pelo snapshot do servidor; `bRunPhysicsWithNoController`
// evita que o CMC tente integrar movimento quando nao ha PlayerController.
// Espelha [F:/ZeusV1/ZeusClient ZeusRemoteCharacter:34-38].
CMC->bOrientRotationToMovement = false; CMC->bOrientRotationToMovement = false;
CMC->RotationRate = FRotator::ZeroRotator; CMC->RotationRate = FRotator::ZeroRotator;
CMC->bRunPhysicsWithNoController = false; CMC->bRunPhysicsWithNoController = false;
CMC->MaxWalkSpeed = 600.0f; CMC->MaxWalkSpeed = 600.0f;
CMC->SetMovementMode(MOVE_Walking); 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`. // Defaults visuais (mesh + AnimBP) em paridade com `AZMMOPlayerCharacter`.
// O `UZMMOWorldSubsystem` instancia `AZMMOPlayerProxy::StaticClass()`
// directamente — sem este bloco o `MeshComponent` ficaria sem
// `SkeletalMesh`/`AnimInstance` e o proxy seria invisivel em PIE. Uma BP
// filha continua opcional (basta atribuir em `RemoteEntityClasses[Player]`).
if (USkeletalMeshComponent* MeshComponent = GetMesh()) if (USkeletalMeshComponent* MeshComponent = GetMesh())
{ {
static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh( static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh(
@@ -80,6 +80,20 @@ AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
bUseControllerRotationRoll = 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) void AZMMOPlayerProxy::Tick(const float DeltaSeconds)
{ {
Super::Tick(DeltaSeconds); Super::Tick(DeltaSeconds);
@@ -118,10 +132,69 @@ void AZMMOPlayerProxy::ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot)
} }
} }
const FVector NewVel = Snapshot.VelocityCmS;
// Aceleração derivada entre snapshots (espelha ZeusInputBridgeComponent.cpp:443-486).
// O AnimBP do Quinn usa Acceleration!=0 nas formulas de ShouldMove e
// Jump_Start. Como proxies não têm input, derivamos a accel a partir da
// variação de velocidade entre snapshots consecutivos.
FVector DerivedAccel = FVector::ZeroVector;
if (bHasPreviousSnapshot && PreviousSnapshotServerTimeMs > 0)
{
const double DeltaSec = static_cast<double>(
Snapshot.ServerTimeMs - PreviousSnapshotServerTimeMs) / 1000.0;
if (DeltaSec > KINDA_SMALL_NUMBER)
{
DerivedAccel = (NewVel - PreviousVisualVelocity) / static_cast<float>(DeltaSec);
DerivedAccel = DerivedAccel.GetClampedToMaxSize(8000.0f);
}
}
constexpr float AccelSmoothingAlpha = 0.35f;
LastDerivedAccelerationCmS2 = bHasPreviousSnapshot
? FMath::Lerp(LastDerivedAccelerationCmS2, DerivedAccel, AccelSmoothingAlpha)
: DerivedAccel;
// Fallback visual: se o proxy está em locomotion (grounded + Speed alto)
// mas a accel derivada caiu para ~0 (velocidade constante entre snapshots),
// injetar accel sintética na direção da velocidade. Sem isto, andar em
// linha reta a velocidade constante deixaria Acceleration=0 e 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;
if (Snapshot.bGrounded && 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;
PreviousSnapshotServerTimeMs = Snapshot.ServerTimeMs;
bHasPreviousSnapshot = true;
SetActorLocationAndRotation(AdjustedPos, NewRotation); SetActorLocationAndRotation(AdjustedPos, NewRotation);
LastSnapshotPosition = AdjustedPos; LastSnapshotPosition = AdjustedPos;
VisualVelocity = Snapshot.VelocityCmS; VisualVelocity = NewVel;
bHasFirstSnapshot = true; bHasFirstSnapshot = true;
// Estado de queda/pulo replicado via `bGrounded` (eco do bit IsFalling do
// cliente local — ver `EInputAxisFlags::IsFalling` e CoreServerApp). Sem
// este passo o `MovementMode` do proxy ficaria sempre em MOVE_Walking
// (definido no construtor) e o AnimBP nunca tocaria a animação de queda.
if (UCharacterMovementComponent* CMC = GetCharacterMovement())
{
const EMovementMode DesiredMode = Snapshot.bGrounded ? MOVE_Walking : MOVE_Falling;
if (CMC->MovementMode != DesiredMode)
{
CMC->SetMovementMode(DesiredMode);
}
}
RefreshAnimationDrivers(); RefreshAnimationDrivers();
ApplyCollisionPolicy(); ApplyCollisionPolicy();
} }
@@ -147,10 +220,21 @@ void AZMMOPlayerProxy::RefreshAnimationDrivers()
return; return;
} }
// `Acceleration` em UCharacterMovementComponent e protected — o AnimBP usa // Defesa: snapshots em walking não devem injetar Z residual (jitter de
// `Velocity` como driver primario; quando precisarmos de Acceleration para // terminal velocity / drift) no CMC, que o AnimBP veria como queda.
// blends mais finos, expomos via AddInputVector ou um CMC custom. FVector VelForCMC = VisualVelocity;
CMC->Velocity = 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() void AZMMOPlayerProxy::ApplyCollisionPolicy()

View File

@@ -21,8 +21,13 @@
* continua a existir para preservar capsula + skeletal mesh + AnimBP, * continua a existir para preservar capsula + skeletal mesh + AnimBP,
* mas o CMC fica em modo "kinematic-ish" (nao integra fisica). * mas o CMC fica em modo "kinematic-ish" (nao integra fisica).
* *
* TODO V1: interpolacao temporal entre snapshots (ring-buffer + lerp), * Animacao no AnimBP do Quinn (`ABP_Unarmed`): a formula `ShouldMove` exige
* network smoothing visual no mesh, AnimBP-friendly velocity drivers. * `Acceleration != 0`. Como proxies nao tem input, adoptamos a mesma
* estrategia do ZClientMMO: CMC custom (`UZMMOProxyMovementComponent`) com
* escrita externa de `Acceleration`, alimentada pela derivada da velocidade
* entre snapshots. Tick do proxy roda **antes** do mesh tick para que
* Velocity+Acceleration estejam populados quando o `NativeUpdateAnimation`
* do AnimBP os ler.
*/ */
UCLASS(Blueprintable, BlueprintType) UCLASS(Blueprintable, BlueprintType)
class ZMMO_API AZMMOPlayerProxy : public ACharacter, public IZMMOEntityInterface class ZMMO_API AZMMOPlayerProxy : public ACharacter, public IZMMOEntityInterface
@@ -32,6 +37,7 @@ class ZMMO_API AZMMOPlayerProxy : public ACharacter, public IZMMOEntityInterface
public: public:
AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer); AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer);
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override; virtual void Tick(float DeltaSeconds) override;
// --- IZMMOEntityInterface --- // --- IZMMOEntityInterface ---
@@ -69,8 +75,23 @@ protected:
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity") UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
FVector VisualVelocity = FVector::ZeroVector; FVector VisualVelocity = FVector::ZeroVector;
/** Velocidade do snapshot anterior — base para derivar `Acceleration`. */
FVector PreviousVisualVelocity = FVector::ZeroVector;
/** ServerTimeMs do snapshot anterior — `dt` da derivada de Acceleration. */
int64 PreviousSnapshotServerTimeMs = 0;
/** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;
bool bHasPreviousSnapshot = false;
private: private:
/** Reaplica `CMC->Velocity` a partir de `VisualVelocity`. */ /** Reaplica `CMC->Velocity` (com clamp de Z em walking) e injeta a
* aceleracao derivada no CMC custom. Chamado em `Tick` (pre-mesh) e em
* `ApplyEntitySnapshot` (para refletir a chegada de novo snapshot
* imediatamente). */
void RefreshAnimationDrivers(); void RefreshAnimationDrivers();
/** Liga/desliga colisao da capsula consoante a relevancia + visual ready. */ /** Liga/desliga colisao da capsula consoante a relevancia + visual ready. */

View File

@@ -0,0 +1,25 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "ZMMOProxyMovementComponent.generated.h"
/**
* CMC customizado para AZMMOPlayerProxy. Expõe escrita direta de Acceleration
* (protected na base) para que o AnimBP veja Acceleration!=0 quando o proxy
* recebe snapshots, alimentando a fórmula ShouldMove e a transição Jump_Start
* do ABP_Unarmed (ambas dependem de aceleração não-nula). Espelha
* UZeusProxyMovementComponent do ZClientMMO.
*/
UCLASS(ClassGroup = (ZMMO), meta = (BlueprintSpawnableComponent))
class ZMMO_API UZMMOProxyMovementComponent : public UCharacterMovementComponent
{
GENERATED_BODY()
public:
void SetZMMOExternalAcceleration(const FVector& InAccelerationCmS2)
{
Acceleration = InAccelerationCmS2;
}
};