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 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)
|
||||
{
|
||||
return;
|
||||
@@ -365,10 +373,13 @@ void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
||||
ClientTimeMs,
|
||||
ViewYawDeg,
|
||||
PosCm,
|
||||
FVector2D(Vel.X, Vel.Y));
|
||||
FVector2D(Vel.X, Vel.Y),
|
||||
bIsFalling,
|
||||
static_cast<float>(Vel.Z));
|
||||
|
||||
SendAccumulatorSec = 0.0f;
|
||||
TimeSinceLastSendSec = 0.0f;
|
||||
bPendingJumpPressed = false;
|
||||
bPendingJumpReleased = false;
|
||||
bPreviousFalling = bIsFalling;
|
||||
}
|
||||
|
||||
@@ -151,6 +151,10 @@ private:
|
||||
bool bPendingJumpPressed = false;
|
||||
bool bPendingJumpReleased = 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 TimeSinceLastSendSec = 0.0f;
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "UObject/ConstructorHelpers.h"
|
||||
#include "ZMMO.h"
|
||||
#include "ZMMOProxyMovementComponent.h"
|
||||
|
||||
AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
: Super(ObjectInitializer.SetDefaultSubobjectClass<UZMMOProxyMovementComponent>(ACharacter::CharacterMovementComponentName))
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = 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
|
||||
// transitar entre Idle/Run — a state machine consulta `IsFalling()` /
|
||||
// `MovementMode` no CMC mesmo quando este nao integra fisica. Mantemos
|
||||
// `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].
|
||||
// `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`.
|
||||
// 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())
|
||||
{
|
||||
static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh(
|
||||
@@ -80,6 +80,20 @@ AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
|
||||
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);
|
||||
@@ -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);
|
||||
LastSnapshotPosition = AdjustedPos;
|
||||
VisualVelocity = Snapshot.VelocityCmS;
|
||||
VisualVelocity = NewVel;
|
||||
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();
|
||||
ApplyCollisionPolicy();
|
||||
}
|
||||
@@ -147,10 +220,21 @@ void AZMMOPlayerProxy::RefreshAnimationDrivers()
|
||||
return;
|
||||
}
|
||||
|
||||
// `Acceleration` em UCharacterMovementComponent e protected — o AnimBP usa
|
||||
// `Velocity` como driver primario; quando precisarmos de Acceleration para
|
||||
// blends mais finos, expomos via AddInputVector ou um CMC custom.
|
||||
CMC->Velocity = VisualVelocity;
|
||||
// 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()
|
||||
|
||||
@@ -21,8 +21,13 @@
|
||||
* continua a existir para preservar capsula + skeletal mesh + AnimBP,
|
||||
* mas o CMC fica em modo "kinematic-ish" (nao integra fisica).
|
||||
*
|
||||
* TODO V1: interpolacao temporal entre snapshots (ring-buffer + lerp),
|
||||
* network smoothing visual no mesh, AnimBP-friendly velocity drivers.
|
||||
* Animacao no AnimBP do Quinn (`ABP_Unarmed`): a formula `ShouldMove` exige
|
||||
* `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)
|
||||
class ZMMO_API AZMMOPlayerProxy : public ACharacter, public IZMMOEntityInterface
|
||||
@@ -32,6 +37,7 @@ class ZMMO_API AZMMOPlayerProxy : public ACharacter, public IZMMOEntityInterface
|
||||
public:
|
||||
AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
// --- IZMMOEntityInterface ---
|
||||
@@ -69,8 +75,23 @@ protected:
|
||||
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
|
||||
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:
|
||||
/** 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();
|
||||
|
||||
/** 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