diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp index 5644cfa..a76eaba 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp @@ -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(Vel.Z)); SendAccumulatorSec = 0.0f; TimeSinceLastSendSec = 0.0f; bPendingJumpPressed = false; bPendingJumpReleased = false; + bPreviousFalling = bIsFalling; } diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h index a617f3c..4523c42 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h @@ -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; diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.cpp b/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.cpp index 0cf3d59..bbeecf5 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.cpp +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.cpp @@ -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(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 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( + Snapshot.ServerTimeMs - PreviousSnapshotServerTimeMs) / 1000.0; + if (DeltaSec > KINDA_SMALL_NUMBER) + { + DerivedAccel = (NewVel - PreviousVisualVelocity) / static_cast(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(CMC)) + { + ProxyCMC->SetZMMOExternalAcceleration(LastDerivedAccelerationCmS2); + } } void AZMMOPlayerProxy::ApplyCollisionPolicy() diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.h b/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.h index 29506b5..0273d28 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.h +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerProxy.h @@ -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. */ diff --git a/Source/ZMMO/Game/Entity/ZMMOProxyMovementComponent.h b/Source/ZMMO/Game/Entity/ZMMOProxyMovementComponent.h new file mode 100644 index 0000000..e889c1e --- /dev/null +++ b/Source/ZMMO/Game/Entity/ZMMOProxyMovementComponent.h @@ -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; + } +};