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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
25
Source/ZMMO/Game/Entity/ZMMOProxyMovementComponent.h
Normal file
25
Source/ZMMO/Game/Entity/ZMMOProxyMovementComponent.h
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user