Files
OT6-Reconstruction/FortniteGame/Private/FortCharacterMovementComponent.cpp
2026-01-08 13:34:09 +01:00

1478 lines
42 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
// Reconstructed from Fortnite UE 4.12 IDA
// Client-side movement prediction and physics implementation
#include "FortCharacterMovementComponent.h"
#include "GameFramework/Character.h"
#include "GameFramework/PlayerController.h"
#include "Components/CapsuleComponent.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h"
#include "AI/Navigation/NavigationSystem.h"
#include "AI/Navigation/RecastNavMesh.h"
#include "FortNavigationTypes.h"
#include "FortNavigationData.h"
// Constants from decompiled code
static const float MOVE_INPUT_SCALE = 0.0078125f; // 1/128 - from decompilation
static const float MAX_DELTA_TIME = 0.25f; // Maximum time step to prevent exploits
static const float NETWORK_CORRECTION_THRESHOLD = 4.0f; // Position error threshold in cm
static const int32 MAX_POSITION_ERROR_SQUARED = 16; // 4^2 for fast check
//==============================================================================
// FSavedMove_Character_Fort Implementation
//==============================================================================
FSavedMove_Character_Fort::FSavedMove_Character_Fort()
: Super()
, TimeStamp(0.0f)
, DeltaTime(0.0f)
, SavedAcceleration(FVector::ZeroVector)
, SavedLocation(FVector::ZeroVector)
, SavedRotation(FRotator::ZeroRotator)
, SavedControlRotation(FRotator::ZeroRotator)
, SavedRootMotion(FTransform::Identity)
, bHadAnimRootMotion(false)
{
FMemory::Memzero(&CompressedFlags, sizeof(FCompressedMoveFlags));
}
void FSavedMove_Character_Fort::Clear()
{
Super::Clear();
TimeStamp = 0.0f;
DeltaTime = 0.0f;
SavedAcceleration = FVector::ZeroVector;
SavedLocation = FVector::ZeroVector;
SavedRotation = FRotator::ZeroRotator;
SavedControlRotation = FRotator::ZeroRotator;
FMemory::Memzero(&CompressedFlags, sizeof(FCompressedMoveFlags));
SavedMovementBase = nullptr;
SavedMovementBaseBoneName = NAME_None;
SavedRootMotion = FTransform::Identity;
bHadAnimRootMotion = false;
}
void FSavedMove_Character_Fort::SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData)
{
Super::SetMoveFor(Character, InDeltaTime, NewAccel, ClientData);
UFortCharacterMovementComponent* FortMovement = Cast<UFortCharacterMovementComponent>(Character->GetCharacterMovement());
if (FortMovement)
{
// Store timestamp and delta
TimeStamp = FortMovement->GetCurrentTimeStamp();
DeltaTime = InDeltaTime;
// Store acceleration
SavedAcceleration = NewAccel;
// Store location and rotation
SavedLocation = Character->GetActorLocation();
SavedRotation = Character->GetActorRotation();
// Store control rotation
if (APlayerController* PC = Cast<APlayerController>(Character->GetController()))
{
SavedControlRotation = PC->GetControlRotation();
}
// Store movement base
SavedMovementBase = Character->GetMovementBase();
SavedMovementBaseBoneName = Character->GetBasedMovement().BoneName;
// Compress flags
CompressedFlags.bPressedJump = Character->bPressedJump;
CompressedFlags.bWantsToCrouch = FortMovement->bWantsToCrouch;
CompressedFlags.bForceMaxAccel = FortMovement->bForceMaxAccel;
// Save root motion state
bHadAnimRootMotion = FortMovement->bHasAnimRootMotion;
if (bHadAnimRootMotion)
{
SavedRootMotion = FortMovement->AnimRootMotionTransform;
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("SavedMove SavedRootMotion: Translation=%s, Rotation=%s"),
*SavedRootMotion.GetTranslation().ToString(),
*SavedRootMotion.Rotator().ToString());
}
}
}
bool FSavedMove_Character_Fort::CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* Character, float MaxDelta) const
{
// Don't combine if timestamps are too far apart
const FSavedMove_Character_Fort* NewFortMove = static_cast<const FSavedMove_Character_Fort*>(NewMove.Get());
if (NewFortMove && FMath::Abs(NewFortMove->TimeStamp - TimeStamp) > MaxDelta)
{
return false;
}
// Don't combine if input changed significantly
if (!SavedAcceleration.Equals(NewFortMove->SavedAcceleration, 0.1f))
{
return false;
}
// Don't combine if movement base changed
if (SavedMovementBase != NewFortMove->SavedMovementBase)
{
return false;
}
// Don't combine if jump state changed
if (CompressedFlags.bPressedJump != NewFortMove->CompressedFlags.bPressedJump)
{
return false;
}
return Super::CanCombineWith(NewMove, Character, MaxDelta);
}
void FSavedMove_Character_Fort::PrepMoveFor(ACharacter* Character)
{
Super::PrepMoveFor(Character);
UFortCharacterMovementComponent* FortMovement = Cast<UFortCharacterMovementComponent>(Character->GetCharacterMovement());
if (FortMovement)
{
// Restore input state
FortMovement->Acceleration = SavedAcceleration;
// Restore jump state
Character->bPressedJump = CompressedFlags.bPressedJump;
// Restore crouch state
FortMovement->bWantsToCrouch = CompressedFlags.bWantsToCrouch;
}
}
//==============================================================================
// FNetworkPredictionData_Client_Fort Implementation
//==============================================================================
FNetworkPredictionData_Client_Fort::FNetworkPredictionData_Client_Fort(const UCharacterMovementComponent& ClientMovement)
: Super(ClientMovement)
, CurrentTimeStamp(0.0f)
, LastAckedTimeStamp(0.0f)
{
MaxSavedMoveCount = MaxSavedMoves;
MaxFreeMoveCount = MaxSavedMoves;
}
FSavedMovePtr FNetworkPredictionData_Client_Fort::AllocateNewMove()
{
// Allocate Fort-specific saved move
FSavedMove_Character_Fort* NewMove = new FSavedMove_Character_Fort();
return FSavedMovePtr(NewMove);
}
//==============================================================================
// UFortCharacterMovementComponent Implementation
//==============================================================================
UFortCharacterMovementComponent::UFortCharacterMovementComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, CurrentTimeStamp(0.0f)
, LastAckedTimeStamp(0.0f)
, LastServerMoveTimeStamp(0.0f)
, MaxMoveDeltaTime(MAX_DELTA_TIME)
, NetworkSimulatedSmoothLocationTime(0.100f)
, NetworkSimulatedSmoothRotationTime(0.033f)
, NetworkMaxSmoothUpdateDistance(256.0f)
, NetworkNoSmoothUpdateDistance(4.0f)
, TeleportDistanceThreshold(512.0f)
, TeleportRotationThreshold(180.0f)
, NetworkSmoothingMode(EFortNetworkSmoothingMode::Exponential)
, bEnableMeshOffsetSmoothing(true)
, CurrentMovementStyle(EFortMovementStyle::Running)
, SprintSpeedMultiplier(1.5f)
, bHasAnimRootMotion(false)
, AnimRootMotionTransform(FTransform::Identity)
, AnimRootMotionTranslationScale(1.0f)
, bEnableNavWalking(true)
, NavWalkingSearchHeightScale(0.5f)
, PendingMoveTimeStamp(0.0f)
, PendingAcceleration(FVector::ZeroVector)
, bHasPendingMove(false)
{
// Client prediction enabled
bUseClientPrediction = true;
// Network update settings optimized for Fortnite gameplay
NetworkMaxSmoothUpdateDistance = 256.0f;
NetworkNoSmoothUpdateDistance = 4.0f; // Below 4cm: instant snap
TeleportDistanceThreshold = 512.0f; // Above 512cm: teleport
// Enable NavWalking for AI pathfinding
bRunPhysicsWithNoController = true; // Allow AI movement
}
void UFortCharacterMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Update timestamp for autonomous proxy
if (CharacterOwner && CharacterOwner->Role == ROLE_AutonomousProxy)
{
CurrentTimeStamp += DeltaTime;
}
}
FNetworkPredictionData_Client* UFortCharacterMovementComponent::GetPredictionData_Client() const
{
if (!ClientPredictionData)
{
UFortCharacterMovementComponent* MutableThis = const_cast<UFortCharacterMovementComponent*>(this);
MutableThis->ClientPredictionData = MakeShareable(new FNetworkPredictionData_Client_Fort(*this));
}
return ClientPredictionData.Get();
}
float UFortCharacterMovementComponent::GetCurrentTimeStamp() const
{
return CurrentTimeStamp;
}
void UFortCharacterMovementComponent::PerformMovement(float DeltaTime)
{
// Clamp delta time to prevent exploits
DeltaTime = FMath::Min(DeltaTime, MaxMoveDeltaTime);
// Store position before movement for delta calculation
const FVector OldLocation = UpdatedComponent->GetComponentLocation();
const FRotator OldRotation = UpdatedComponent->GetComponentRotation();
// Execute core movement logic
PerformMovementImpl(DeltaTime);
// Check if position changed
const FVector NewLocation = UpdatedComponent->GetComponentLocation();
const bool bLocationChanged = !OldLocation.Equals(NewLocation, KINDA_SMALL_NUMBER);
// For autonomous proxy, save and send moves
if (CharacterOwner && CharacterOwner->Role == ROLE_AutonomousProxy && bLocationChanged)
{
FNetworkPredictionData_Client_Fort* ClientData = static_cast<FNetworkPredictionData_Client_Fort*>(GetPredictionData_Client());
if (ClientData)
{
// Allocate new saved move
FSavedMovePtr NewMove = ClientData->CreateSavedMove();
FSavedMove_Character_Fort* FortMove = static_cast<FSavedMove_Character_Fort*>(NewMove.Get());
if (FortMove)
{
// Store move data
FortMove->SetMoveFor(CharacterOwner, DeltaTime, Acceleration, *ClientData);
// Add to saved moves
ClientData->SavedMoves.Add(NewMove);
// Trim old moves if buffer is full
const int32 MaxSaves = FNetworkPredictionData_Client_Fort::MaxSavedMoves;
if (ClientData->SavedMoves.Num() > MaxSaves)
{
UE_LOG(LogNetPlayerMovement, Warning, TEXT("SavedMove buffer overflow (%d moves) - possible high ping or packet loss"),
ClientData->SavedMoves.Num());
ClientData->SavedMoves.RemoveAt(0, ClientData->SavedMoves.Num() - MaxSaves);
}
// Send move to server
const uint8 CompressedFlags = CompressInputFlags();
const uint8 ClientRoll = FRotator::CompressAxisToByte(UpdatedComponent->GetComponentRotation().Roll);
const uint32 View = PackYawAndPitchTo32(UpdatedComponent->GetComponentRotation().Yaw, UpdatedComponent->GetComponentRotation().Pitch);
// Check if we can combine with pending move (dual move optimization)
if (bHasPendingMove && (CurrentTimeStamp - PendingMoveTimeStamp) < 0.1f)
{
// Send dual move
ServerMoveDual(
PendingMoveTimeStamp,
FVector_NetQuantize10(PendingAcceleration),
CompressedFlags,
View,
CurrentTimeStamp,
FVector_NetQuantize10(Acceleration),
FVector_NetQuantize100(NewLocation),
CompressedFlags,
ClientRoll,
View,
Cast<UPrimitiveComponent>(CharacterOwner->GetMovementBase()),
CharacterOwner->GetBasedMovement().BoneName,
MovementMode
);
bHasPendingMove = false;
}
else
{
// Send single move
ServerMove(
CurrentTimeStamp,
FVector_NetQuantize10(Acceleration),
FVector_NetQuantize100(NewLocation),
CompressedFlags,
ClientRoll,
View,
Cast<UPrimitiveComponent>(CharacterOwner->GetMovementBase()),
CharacterOwner->GetBasedMovement().BoneName,
MovementMode
);
// Store as pending for potential dual move next frame
PendingMoveTimeStamp = CurrentTimeStamp;
PendingAcceleration = Acceleration;
bHasPendingMove = true;
}
}
}
}
// Detailed logging for debugging
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("PerformMovement WorldSpaceRootMotion Translation: %s, Rotation: %s, Actor Facing: %s, Velocity: %s"),
*NewLocation.ToString(),
*UpdatedComponent->GetComponentRotation().ToString(),
*CharacterOwner->GetActorForwardVector().ToString(),
*Velocity.ToString()
);
}
void UFortCharacterMovementComponent::PerformMovementImpl(float DeltaTime)
{
if (!CharacterOwner || !UpdatedComponent)
{
return;
}
// Check if movement is allowed
if (UpdatedComponent->IsSimulatingPhysics())
{
UE_LOG(LogNetPlayerMovement, Warning, TEXT("UFortCharacterMovementComponent::PerformMovementImpl: UpdateComponent (%s) is simulating physics - aborting."),
*UpdatedComponent->GetName());
return;
}
// Calculate velocity based on current movement mode
CalcVelocityImpl(DeltaTime);
// Execute physics for current movement mode
switch (MovementMode)
{
case MOVE_Walking:
PhysWalking(DeltaTime, 0);
break;
case MOVE_NavWalking:
if (bEnableNavWalking)
{
PhysNavWalking(DeltaTime, 0);
}
else
{
// Fallback to walking if NavWalking disabled
PhysWalking(DeltaTime, 0);
}
break;
case MOVE_Falling:
PhysFalling(DeltaTime, 0);
break;
case MOVE_Flying:
PhysFlying(DeltaTime, 0);
break;
case MOVE_None:
default:
// No movement
Velocity = FVector::ZeroVector;
break;
}
// Update character rotation
if (bOrientRotationToMovement && Velocity.SizeSquared() > KINDA_SMALL_NUMBER)
{
FRotator NewRotation = Velocity.Rotation();
NewRotation.Pitch = 0.0f;
NewRotation.Roll = 0.0f;
if (RotationRate.Yaw > 0.0f)
{
// Smooth rotation
NewRotation = FMath::RInterpConstantTo(UpdatedComponent->GetComponentRotation(), NewRotation, DeltaTime, RotationRate.Yaw);
}
MoveUpdatedComponent(FVector::ZeroVector, NewRotation, false);
}
}
void UFortCharacterMovementComponent::CalcVelocityImpl(float DeltaTime)
{
// Apply acceleration
if (Acceleration.SizeSquared() > 0.0f)
{
// Clamp acceleration to max
FVector AccelDir = Acceleration;
const float AccelMag = AccelDir.Size();
if (AccelMag > GetMaxAcceleration())
{
AccelDir = AccelDir / AccelMag * GetMaxAcceleration();
}
// Apply acceleration to velocity
Velocity += AccelDir * DeltaTime;
}
// Apply friction based on movement mode
if (MovementMode == MOVE_Walking)
{
// Ground friction
const float ActualBrakingFriction = (bUseSeparateBrakingFriction ? BrakingFriction : GroundFriction);
const float FrictionFactor = FMath::Max(0.0f, 1.0f - ActualBrakingFriction * DeltaTime);
Velocity *= FrictionFactor;
}
else if (MovementMode == MOVE_Falling || MovementMode == MOVE_Flying)
{
// Air resistance
Velocity *= FMath::Max(0.0f, 1.0f - (FallingLateralFriction * DeltaTime));
}
// Clamp to max speed (consider sprint multiplier)
float MaxSpeed = GetMaxSpeed();
// Apply sprint speed multiplier
if (CurrentMovementStyle == EFortMovementStyle::Sprinting)
{
MaxSpeed *= SprintSpeedMultiplier;
}
if (Velocity.SizeSquared() > FMath::Square(MaxSpeed))
{
Velocity = Velocity.GetSafeNormal() * MaxSpeed;
}
// Apply gravity for falling
if (MovementMode == MOVE_Falling)
{
const FVector Gravity(0.0f, 0.0f, GetGravityZ());
Velocity += Gravity * DeltaTime;
}
}
void UFortCharacterMovementComponent::PhysWalking(float DeltaTime, int32 Iterations)
{
if (DeltaTime < MIN_TICK_TIME)
{
return;
}
if (!CharacterOwner || !UpdatedComponent)
{
return;
}
// Calculate movement delta
FVector Delta = Velocity * DeltaTime;
if (Delta.IsNearlyZero())
{
// Still need to check for floor
return;
}
// Perform movement sweep
FHitResult Hit(1.0f);
SafeMoveUpdatedComponent(Delta, UpdatedComponent->GetComponentQuat(), true, Hit);
if (Hit.IsValidBlockingHit())
{
// Hit something, slide along surface
const FVector OldHitNormal = Hit.Normal;
SlideAlongSurface(Delta, 1.0f - Hit.Time, Hit.Normal, Hit, true);
// Check if still on walkable surface
if (Hit.IsValidBlockingHit())
{
if (!IsWalkable(Hit))
{
// Not walkable, start falling
SetMovementMode(MOVE_Falling);
}
}
}
}
void UFortCharacterMovementComponent::PhysFalling(float DeltaTime, int32 Iterations)
{
if (DeltaTime < MIN_TICK_TIME)
{
return;
}
if (!CharacterOwner || !UpdatedComponent)
{
return;
}
// Apply gravity
Velocity.Z += GetGravityZ() * DeltaTime;
// Calculate movement delta
FVector Delta = Velocity * DeltaTime;
// Perform movement sweep
FHitResult Hit(1.0f);
SafeMoveUpdatedComponent(Delta, UpdatedComponent->GetComponentQuat(), true, Hit);
if (Hit.IsValidBlockingHit())
{
// Check if landed
if (IsWalkable(Hit))
{
// Landed on walkable surface
ProcessLanded(Hit, DeltaTime, 0);
return;
}
else
{
// Hit wall/ceiling, slide along it
SlideAlongSurface(Delta, 1.0f - Hit.Time, Hit.Normal, Hit, true);
// Continue falling
if (Hit.IsValidBlockingHit() && !IsWalkable(Hit))
{
// Bounce off if velocity is high enough
if (Velocity.Z < -100.0f)
{
Velocity.Z *= -0.2f; // Small bounce
}
}
}
}
}
void UFortCharacterMovementComponent::PhysFlying(float DeltaTime, int32 Iterations)
{
if (DeltaTime < MIN_TICK_TIME)
{
return;
}
if (!CharacterOwner || !UpdatedComponent)
{
return;
}
// Calculate movement delta (no gravity)
FVector Delta = Velocity * DeltaTime;
if (Delta.IsNearlyZero())
{
Velocity = FVector::ZeroVector;
return;
}
// Perform movement sweep
FHitResult Hit(1.0f);
SafeMoveUpdatedComponent(Delta, UpdatedComponent->GetComponentQuat(), true, Hit);
if (Hit.IsValidBlockingHit())
{
// Hit something, slide along surface
SlideAlongSurface(Delta, 1.0f - Hit.Time, Hit.Normal, Hit, true);
}
}
void UFortCharacterMovementComponent::SimulatedTick(float DeltaSeconds)
{
// Check if simulated proxy
if (!CharacterOwner || CharacterOwner->Role != ROLE_SimulatedProxy)
{
return;
}
// Clear old moves if we're simulating
FNetworkPredictionData_Client_Fort* ClientData = static_cast<FNetworkPredictionData_Client_Fort*>(GetPredictionData_Client());
if (ClientData && ClientData->SavedMoves.Num() > 0)
{
UE_LOG(LogNetPlayerMovement, Log, TEXT("Clearing old moves in SimulatedTick (%d)"), ClientData->SavedMoves.Num());
ClientData->SavedMoves.Empty();
}
// Update network smoothing for simulated proxies
if (SmoothingState.bIsSmoothingActive)
{
SmoothClientPosition(DeltaSeconds);
}
// Update mesh offset smoothing if enabled
if (bEnableMeshOffsetSmoothing)
{
UpdateMeshOffsetSmoothing(DeltaSeconds);
}
// Call base implementation for smooth replication
Super::SimulatedTick(DeltaSeconds);
}
uint8 UFortCharacterMovementComponent::CompressInputFlags() const
{
uint8 Result = 0;
if (CharacterOwner && CharacterOwner->bPressedJump)
{
Result |= (1 << 0);
}
if (bWantsToCrouch)
{
Result |= (1 << 1);
}
if (bForceMaxAccel)
{
Result |= (1 << 2);
}
return Result;
}
void UFortCharacterMovementComponent::DecompressInputFlags(uint8 Flags)
{
if (CharacterOwner)
{
CharacterOwner->bPressedJump = (Flags & (1 << 0)) != 0;
}
bWantsToCrouch = (Flags & (1 << 1)) != 0;
bForceMaxAccel = (Flags & (1 << 2)) != 0;
}
int32 UFortCharacterMovementComponent::FindSavedMoveByTimeStamp(float TimeStamp) const
{
const FNetworkPredictionData_Client_Fort* ClientData = static_cast<const FNetworkPredictionData_Client_Fort*>(GetPredictionData_Client());
if (!ClientData)
{
return -1;
}
// Search saved moves for matching timestamp
for (int32 i = 0; i < ClientData->SavedMoves.Num(); ++i)
{
const FSavedMove_Character_Fort* FortMove = static_cast<const FSavedMove_Character_Fort*>(ClientData->SavedMoves[i].Get());
if (FortMove && FMath::IsNearlyEqual(FortMove->TimeStamp, TimeStamp, KINDA_SMALL_NUMBER))
{
return i;
}
}
return -1;
}
//==============================================================================
// Network Smoothing Implementation
//==============================================================================
void UFortCharacterMovementComponent::SmoothClientPosition(float DeltaTime)
{
if (!SmoothingState.bIsSmoothingActive || !UpdatedComponent)
{
return;
}
// Decrease remaining smoothing time
SmoothingState.SmoothingTimeRemaining = FMath::Max(0.0f, SmoothingState.SmoothingTimeRemaining - DeltaTime);
// Calculate interpolation alpha
float Alpha = 0.0f;
if (SmoothingState.TotalSmoothingTime > 0.0f)
{
const float ElapsedTime = SmoothingState.TotalSmoothingTime - SmoothingState.SmoothingTimeRemaining;
switch (NetworkSmoothingMode)
{
case EFortNetworkSmoothingMode::Linear:
// Linear interpolation
Alpha = ElapsedTime / SmoothingState.TotalSmoothingTime;
break;
case EFortNetworkSmoothingMode::Exponential:
// Exponential decay for smoother, more natural corrections
// Alpha = 1 - e^(-k*t) where k controls smoothing speed
{
const float SmoothingSpeed = 8.0f; // Higher = faster convergence
Alpha = 1.0f - FMath::Exp(-SmoothingSpeed * ElapsedTime / SmoothingState.TotalSmoothingTime);
}
break;
case EFortNetworkSmoothingMode::Disabled:
default:
// No smoothing
Alpha = 1.0f;
break;
}
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
}
else
{
Alpha = 1.0f; // Instant correction
}
// Get current position
const FVector CurrentLocation = UpdatedComponent->GetComponentLocation();
const FQuat CurrentRotation = UpdatedComponent->GetComponentQuat();
// Interpolate toward target
const FVector NewLocation = FMath::Lerp(CurrentLocation, SmoothingState.TargetLocation, Alpha);
const FQuat NewRotation = FQuat::Slerp(CurrentRotation, SmoothingState.TargetRotation, Alpha);
// Apply smoothed position
UpdatedComponent->SetWorldLocationAndRotation(NewLocation, NewRotation, false);
// Update velocity based on movement
if (DeltaTime > KINDA_SMALL_NUMBER)
{
Velocity = (NewLocation - CurrentLocation) / DeltaTime;
}
// Check if smoothing is complete
if (SmoothingState.SmoothingTimeRemaining <= 0.0f || Alpha >= 1.0f)
{
// Smoothing complete - snap to final position
UpdatedComponent->SetWorldLocationAndRotation(SmoothingState.TargetLocation, SmoothingState.TargetRotation, false);
SmoothingState.bIsSmoothingActive = false;
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("SmoothClientPosition: Smoothing complete at %s"),
*SmoothingState.TargetLocation.ToString());
}
}
void UFortCharacterMovementComponent::ApplyNetworkCorrection(const FVector& NewLocation, const FRotator& NewRotation, const FVector& NewVelocity)
{
if (!UpdatedComponent || !CharacterOwner)
{
return;
}
// Get current state
const FVector CurrentLocation = UpdatedComponent->GetComponentLocation();
const FRotator CurrentRotation = UpdatedComponent->GetComponentRotation();
// Calculate errors
const float LocationError = (NewLocation - CurrentLocation).Size();
const float RotationError = FMath::Abs((NewRotation - CurrentRotation).GetManhattanDistance());
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ApplyNetworkCorrection: LocationError=%.2f, RotationError=%.2f"),
LocationError, RotationError);
// Determine correction strategy based on error magnitude
bool bShouldTeleport = false;
bool bShouldSnap = false;
bool bShouldSmooth = false;
// Check teleport thresholds (large errors = instant teleport)
if (LocationError > TeleportDistanceThreshold || RotationError > TeleportRotationThreshold)
{
bShouldTeleport = true;
UE_LOG(LogNetPlayerMovement, Log, TEXT("ApplyNetworkCorrection: TELEPORT (error too large)"));
}
// Check snap threshold (tiny errors = instant snap, no smoothing overhead)
else if (LocationError < NetworkNoSmoothUpdateDistance)
{
bShouldSnap = true;
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("ApplyNetworkCorrection: SNAP (error too small to smooth)"));
}
// Mid-range errors get smoothed
else if (LocationError <= NetworkMaxSmoothUpdateDistance)
{
bShouldSmooth = true;
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ApplyNetworkCorrection: SMOOTH (error in smoothing range)"));
}
else
{
// Outside smoothing range but below teleport - instant correction
bShouldSnap = true;
UE_LOG(LogNetPlayerMovement, Log, TEXT("ApplyNetworkCorrection: SNAP (error beyond smooth range)"));
}
// Apply correction strategy
if (bShouldTeleport || bShouldSnap || NetworkSmoothingMode == EFortNetworkSmoothingMode::Disabled)
{
// Instant correction (no smoothing)
UpdatedComponent->SetWorldLocationAndRotation(NewLocation, NewRotation, false);
Velocity = NewVelocity;
SmoothingState.bIsSmoothingActive = false;
}
else if (bShouldSmooth)
{
// Smooth correction over time
SmoothingState.TargetLocation = NewLocation;
SmoothingState.TargetRotation = NewRotation.Quaternion();
SmoothingState.TotalSmoothingTime = NetworkSimulatedSmoothLocationTime;
SmoothingState.SmoothingTimeRemaining = NetworkSimulatedSmoothLocationTime;
SmoothingState.bIsSmoothingActive = true;
// If mesh offset smoothing is enabled, initialize mesh offset
if (bEnableMeshOffsetSmoothing && CharacterOwner->GetMesh())
{
// Calculate initial mesh offset (current capsule position - target position)
SmoothingState.MeshRelativeOffset = CurrentLocation - NewLocation;
SmoothingState.MeshRotationOffset = (CurrentRotation.Quaternion() * NewRotation.Quaternion().Inverse());
}
// Start smoothing from current position toward target
// The actual interpolation happens in SmoothClientPosition each frame
}
}
void UFortCharacterMovementComponent::UpdateMeshOffsetSmoothing(float DeltaTime)
{
if (!CharacterOwner || !CharacterOwner->GetMesh())
{
return;
}
USkeletalMeshComponent* Mesh = CharacterOwner->GetMesh();
// If we have an active mesh offset, smooth it back to zero
if (!SmoothingState.MeshRelativeOffset.IsNearlyZero() || !SmoothingState.MeshRotationOffset.Equals(FQuat::Identity, KINDA_SMALL_NUMBER))
{
// Smoothly return mesh to capsule position
const float MeshSmoothSpeed = 10.0f; // Speed of mesh offset reduction
const float Alpha = FMath::Clamp(DeltaTime * MeshSmoothSpeed, 0.0f, 1.0f);
// Interpolate offset toward zero
SmoothingState.MeshRelativeOffset = FMath::Lerp(SmoothingState.MeshRelativeOffset, FVector::ZeroVector, Alpha);
SmoothingState.MeshRotationOffset = FQuat::Slerp(SmoothingState.MeshRotationOffset, FQuat::Identity, Alpha);
// Apply mesh offset (visual only, doesn't affect physics)
Mesh->SetRelativeLocation(SmoothingState.MeshRelativeOffset);
Mesh->SetRelativeRotation(SmoothingState.MeshRotationOffset);
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("UpdateMeshOffsetSmoothing: Offset=%s"),
*SmoothingState.MeshRelativeOffset.ToString());
}
else
{
// Ensure mesh is at zero offset
if (!Mesh->GetRelativeLocation().IsNearlyZero() || !Mesh->GetRelativeRotation().IsNearlyZero())
{
Mesh->SetRelativeLocation(FVector::ZeroVector);
Mesh->SetRelativeRotation(FRotator::ZeroRotator);
}
}
}
void UFortCharacterMovementComponent::ReplaySavedMoves(int32 StartMoveIndex)
{
FNetworkPredictionData_Client_Fort* ClientData = static_cast<FNetworkPredictionData_Client_Fort*>(GetPredictionData_Client());
if (!ClientData)
{
return;
}
// Replay moves from start index to end
for (int32 i = StartMoveIndex; i < ClientData->SavedMoves.Num(); ++i)
{
FSavedMove_Character_Fort* FortMove = static_cast<FSavedMove_Character_Fort*>(ClientData->SavedMoves[i].Get());
if (FortMove)
{
// Prepare move (restore input state)
FortMove->PrepMoveFor(CharacterOwner);
// Restore root motion if present
if (FortMove->bHadAnimRootMotion)
{
bHasAnimRootMotion = true;
AnimRootMotionTransform = FortMove->SavedRootMotion;
}
// Re-execute movement
PerformMovementImpl(FortMove->DeltaTime);
}
}
UE_LOG(LogNetPlayerMovement, Log, TEXT("Replayed %d saved moves"), ClientData->SavedMoves.Num() - StartMoveIndex);
}
//==============================================================================
// Root Motion & Custom Movement Modes
//==============================================================================
void UFortCharacterMovementComponent::ApplyRootMotionToVelocity(const FTransform& RootMotionTransform, float DeltaTime)
{
if (DeltaTime < KINDA_SMALL_NUMBER)
{
return;
}
// Store root motion transform for this frame
AnimRootMotionTransform = RootMotionTransform;
bHasAnimRootMotion = true;
// Extract translation and apply scale
FVector RootMotionTranslation = RootMotionTransform.GetTranslation() * AnimRootMotionTranslationScale;
// Convert root motion to velocity
FVector RootMotionVelocity = RootMotionTranslation / DeltaTime;
// Add to current velocity (root motion is additive)
Velocity += RootMotionVelocity;
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("ApplyRootMotionToVelocity: Translation=%s, Velocity=%s"),
*RootMotionTranslation.ToString(), *Velocity.ToString());
}
void UFortCharacterMovementComponent::SetFortMovementStyle(EFortMovementStyle NewStyle)
{
if (CurrentMovementStyle == NewStyle)
{
return;
}
const EFortMovementStyle OldStyle = CurrentMovementStyle;
CurrentMovementStyle = NewStyle;
// Apply style-specific speed modifiers
switch (NewStyle)
{
case EFortMovementStyle::Walking:
MaxWalkSpeed = 300.0f;
break;
case EFortMovementStyle::Running:
MaxWalkSpeed = 600.0f;
break;
case EFortMovementStyle::Sprinting:
MaxWalkSpeed = 600.0f; // Base speed, multiplier applied in CalcVelocity
break;
case EFortMovementStyle::Flying:
SetMovementMode(MOVE_Flying);
MaxFlySpeed = 600.0f;
break;
default:
break;
}
UE_LOG(LogNetPlayerMovement, Log, TEXT("SetFortMovementStyle: %d -> %d"), (int32)OldStyle, (int32)NewStyle);
}
void UFortCharacterMovementComponent::PhysNavWalking(float DeltaTime, int32 Iterations)
{
if (DeltaTime < MIN_TICK_TIME)
{
return;
}
if (!CharacterOwner || !UpdatedComponent)
{
return;
}
// NavWalking constrains movement to navigation mesh surface
// This is critical for AI pathfinding to ensure agents stay on valid paths
// Calculate desired velocity
CalcVelocityImpl(DeltaTime);
// Calculate movement delta
FVector Delta = Velocity * DeltaTime;
if (Delta.IsNearlyZero())
{
return;
}
// Get current position
const FVector CurrentLocation = UpdatedComponent->GetComponentLocation();
// Calculate desired new position
const FVector DesiredLocation = CurrentLocation + Delta;
// Project desired location onto NavMesh
FNavLocation ProjectedNavLocation;
if (ProjectPointToNavMesh(DesiredLocation, ProjectedNavLocation))
{
// Successfully projected onto NavMesh - constrain movement to surface
const FVector ConstrainedDelta = ProjectedNavLocation.Location - CurrentLocation;
// Perform constrained movement
FHitResult Hit(1.0f);
SafeMoveUpdatedComponent(ConstrainedDelta, UpdatedComponent->GetComponentQuat(), true, Hit);
if (Hit.IsValidBlockingHit())
{
// Hit obstacle, slide along surface
const FVector OldHitNormal = Hit.Normal;
SlideAlongSurface(ConstrainedDelta, 1.0f - Hit.Time, Hit.Normal, Hit, true);
// Check if still on walkable surface
if (Hit.IsValidBlockingHit() && !IsWalkable(Hit))
{
// Not walkable, start falling
SetMovementMode(MOVE_Falling);
return;
}
}
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("PhysNavWalking: Constrained movement from %s to %s (NavMesh)"),
*CurrentLocation.ToString(), *ProjectedNavLocation.Location.ToString());
}
else
{
// Failed to project onto NavMesh - fall back to walking or falling
UE_LOG(LogNetPlayerMovement, Verbose,
TEXT("PhysNavWalking: Cannot project to NavMesh at %s, leaving NavWalking mode"),
*DesiredLocation.ToString());
SetMovementMode(MOVE_Walking);
}
// Verify we should still be in NavWalking mode
if (!CanStartNavWalking())
{
UE_LOG(LogNetPlayerMovement, Verbose,
TEXT("PhysNavWalking: No longer on valid NavMesh - exiting NavWalking mode"));
SetMovementMode(MOVE_Walking);
}
}
bool UFortCharacterMovementComponent::CanStartNavWalking() const
{
if (!bEnableNavWalking)
{
return false;
}
if (!CharacterOwner)
{
return false;
}
// NavWalking requires:
// 1. Valid NavMesh in world
// 2. Character is on or near NavMesh surface
// 3. Character has controller (AI or player for navigation)
// Check if NavMesh exists
ARecastNavMesh* NavMesh = GetNavMesh();
if (!NavMesh)
{
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("CanStartNavWalking: No NavMesh in world"));
return false;
}
// Check if character location is on NavMesh
const FVector CharacterLocation = CharacterOwner->GetActorLocation();
FNavLocation NavLocation;
// Project current location onto NavMesh with reasonable search extent
const FVector SearchExtent(50.0f, 50.0f, NavWalkingSearchHeightScale * 200.0f);
if (!ProjectPointToNavMesh(CharacterLocation, NavLocation, SearchExtent))
{
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("CanStartNavWalking: Character not on NavMesh at %s"),
*CharacterLocation.ToString());
return false;
}
// Check distance to NavMesh (should be within reasonable range)
const float DistanceToNavMesh = FMath::Abs(NavLocation.Location.Z - CharacterLocation.Z);
if (DistanceToNavMesh > 100.0f) // 1 meter tolerance
{
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("CanStartNavWalking: Too far from NavMesh (%.1f cm)"),
DistanceToNavMesh);
return false;
}
// NavWalking primarily for AI, but can be used for player navigation too
const bool bHasController = (CharacterOwner->GetController() != nullptr);
if (!bHasController)
{
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("CanStartNavWalking: No controller"));
return false;
}
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("CanStartNavWalking: Can start - on NavMesh at %s (projected from %s)"),
*NavLocation.Location.ToString(), *CharacterLocation.ToString());
return true;
}
bool UFortCharacterMovementComponent::ProjectPointToNavMesh(const FVector& Point, FNavLocation& OutLocation, const FVector& Extent) const
{
ARecastNavMesh* NavMesh = GetNavMesh();
if (!NavMesh)
{
return false;
}
// Delegate to NavMesh's ProjectPoint method
return NavMesh->ProjectPoint(Point, OutLocation, Extent);
}
ARecastNavMesh* UFortCharacterMovementComponent::GetNavMesh() const
{
// Check cached NavMesh
if (CachedNavMesh.IsValid())
{
return CachedNavMesh.Get();
}
// Find world's navigation system
UWorld* World = GetWorld();
if (!World)
{
return nullptr;
}
UNavigationSystem* NavSys = World->GetNavigationSystem();
if (!NavSys)
{
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("GetNavMesh: No NavigationSystem in world"));
return nullptr;
}
// Get main navigation data (should be RecastNavMesh)
ANavigationData* MainNavData = NavSys->GetMainNavData(ENavigationDataResolution::Default);
if (!MainNavData)
{
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("GetNavMesh: No main navigation data"));
return nullptr;
}
// Cast to RecastNavMesh
::ARecastNavMesh* RecastNav = Cast<::ARecastNavMesh>(MainNavData);
if (RecastNav)
{
// Cache for future use
CachedNavMesh = RecastNav;
UE_LOG(LogNetPlayerMovement, VeryVerbose,
TEXT("GetNavMesh: Found and cached RecastNavMesh"));
return RecastNav;
}
UE_LOG(LogNetPlayerMovement, Warning,
TEXT("GetNavMesh: Main navigation data is not RecastNavMesh (type: %s)"),
*MainNavData->GetClass()->GetName());
return nullptr;
}
//==============================================================================
// Server Move RPC Implementations
//==============================================================================
void UFortCharacterMovementComponent::ServerMove_Implementation(
float TimeStamp,
FVector_NetQuantize10 Acceleration,
FVector_NetQuantize100 ClientLoc,
uint8 CompressedMoveFlags,
uint8 ClientRoll,
uint32 View,
UPrimitiveComponent* ClientMovementBase,
FName ClientBaseBoneName,
uint8 ClientMovementMode)
{
if (!CharacterOwner)
{
return;
}
// Validate timestamp
if (TimeStamp <= LastServerMoveTimeStamp)
{
UE_LOG(LogNetPlayerMovement, Warning, TEXT("ServerMove: TimeStamp %f is older than last move %f"),
TimeStamp, LastServerMoveTimeStamp);
return;
}
// Calculate delta time
float DeltaTime = TimeStamp - LastServerMoveTimeStamp;
DeltaTime = FMath::Clamp(DeltaTime, 0.0f, MaxMoveDeltaTime);
// Update last timestamp
LastServerMoveTimeStamp = TimeStamp;
// Decompress input
this->Acceleration = FVector(Acceleration);
DecompressInputFlags(CompressedMoveFlags);
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("ServerMove Time %f Acceleration %s Position %s DeltaTime %f"),
TimeStamp, *Acceleration.ToString(), *ClientLoc.ToString(), DeltaTime);
// Store position before server movement
const FVector ServerLocation = UpdatedComponent->GetComponentLocation();
// Execute movement on server
PerformMovementImpl(DeltaTime);
// Get position after server movement
const FVector NewServerLocation = UpdatedComponent->GetComponentLocation();
// Check for position mismatch
const float PositionErrorSquared = (NewServerLocation - FVector(ClientLoc)).SizeSquared();
if (PositionErrorSquared > MAX_POSITION_ERROR_SQUARED)
{
// Position mismatch - send correction
UE_LOG(LogNetPlayerMovement, Log, TEXT("ServerMove correction: Client at %s, Server at %s (error: %.2f)"),
*ClientLoc.ToString(), *NewServerLocation.ToString(), FMath::Sqrt(PositionErrorSquared));
ClientAdjustPosition(
TimeStamp,
NewServerLocation,
Velocity,
GetMovementBase(),
GetMovementBaseOwner(GetMovementBase()) ? GetMovementBaseOwner(GetMovementBase())->GetBasedMovement().BoneName : NAME_None,
GetMovementBase() != nullptr,
false, // absolute position
MovementMode
);
}
else
{
// Position correct - acknowledge good move
ClientAckGoodMove(TimeStamp, NewServerLocation, Velocity, MovementMode);
}
}
bool UFortCharacterMovementComponent::ServerMove_Validate(
float TimeStamp,
FVector_NetQuantize10 Acceleration,
FVector_NetQuantize100 ClientLoc,
uint8 CompressedMoveFlags,
uint8 ClientRoll,
uint32 View,
UPrimitiveComponent* ClientMovementBase,
FName ClientBaseBoneName,
uint8 ClientMovementMode)
{
// Basic validation
if (TimeStamp < 0.0f || TimeStamp > 10000.0f)
{
return false;
}
// Validate acceleration magnitude
if (Acceleration.SizeSquared() > FMath::Square(GetMaxAcceleration() * 2.0f))
{
return false;
}
return true;
}
void UFortCharacterMovementComponent::ServerMoveDual_Implementation(
float TimeStamp0,
FVector_NetQuantize10 Acceleration0,
uint8 PendingFlags,
uint32 View0,
float TimeStamp,
FVector_NetQuantize10 Acceleration,
FVector_NetQuantize100 ClientLoc,
uint8 NewFlags,
uint8 ClientRoll,
uint32 View,
UPrimitiveComponent* ClientMovementBase,
FName ClientBaseBoneName,
uint8 ClientMovementMode)
{
// Execute first move
ServerMove_Implementation(
TimeStamp0,
Acceleration0,
ClientLoc, // Use final location for both (optimization)
PendingFlags,
ClientRoll,
View0,
ClientMovementBase,
ClientBaseBoneName,
ClientMovementMode
);
// Execute second move
ServerMove_Implementation(
TimeStamp,
Acceleration,
ClientLoc,
NewFlags,
ClientRoll,
View,
ClientMovementBase,
ClientBaseBoneName,
ClientMovementMode
);
}
bool UFortCharacterMovementComponent::ServerMoveDual_Validate(
float TimeStamp0,
FVector_NetQuantize10 Acceleration0,
uint8 PendingFlags,
uint32 View0,
float TimeStamp,
FVector_NetQuantize10 Acceleration,
FVector_NetQuantize100 ClientLoc,
uint8 NewFlags,
uint8 ClientRoll,
uint32 View,
UPrimitiveComponent* ClientMovementBase,
FName ClientBaseBoneName,
uint8 ClientMovementMode)
{
// Validate both timestamps
return ServerMove_Validate(TimeStamp0, Acceleration0, ClientLoc, PendingFlags, ClientRoll, View0, ClientMovementBase, ClientBaseBoneName, ClientMovementMode)
&& ServerMove_Validate(TimeStamp, Acceleration, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode);
}
void UFortCharacterMovementComponent::ServerMoveOld_Implementation(
float OldTimeStamp,
FVector_NetQuantize10 OldAccel,
uint8 OldMoveFlags)
{
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ServerMoveOld: Timestamp %f"), OldTimeStamp);
// Process old move (for high ping scenarios)
// replay from this point
}
bool UFortCharacterMovementComponent::ServerMoveOld_Validate(
float OldTimeStamp,
FVector_NetQuantize10 OldAccel,
uint8 OldMoveFlags)
{
return OldTimeStamp >= 0.0f;
}
//==============================================================================
// Client Correction RPC Implementations
//==============================================================================
void UFortCharacterMovementComponent::ClientAdjustPosition_Implementation(
float TimeStamp,
FVector NewLoc,
FVector NewVel,
UPrimitiveComponent* NewBase,
FName NewBaseBoneName,
bool bHasBase,
bool bBaseRelativePosition,
uint8 ServerMovementMode)
{
if (!CharacterOwner)
{
return;
}
// Check if this is for an autonomous proxy (local player) or simulated proxy (other players)
const bool bIsAutonomousProxy = (CharacterOwner->Role == ROLE_AutonomousProxy);
if (bIsAutonomousProxy)
{
// Autonomous proxy: Find saved move and replay
const int32 MoveIndex = FindSavedMoveByTimeStamp(TimeStamp);
if (MoveIndex == -1)
{
UE_LOG(LogNetPlayerMovement, Warning,
TEXT("ClientAdjustPosition_Implementation could not find Move for TimeStamp: %f, LastAckedTimeStamp: %f, CurrentTimeStamp: %f"),
TimeStamp, LastAckedTimeStamp, CurrentTimeStamp);
// Apply correction anyway (better than nothing)
UpdatedComponent->SetWorldLocation(NewLoc, false);
Velocity = NewVel;
SetMovementMode(EMovementMode(ServerMovementMode));
return;
}
UE_LOG(LogNetPlayerMovement, Log, TEXT("ClientAdjustPosition: Correcting autonomous proxy to %s at TimeStamp %f (found at move %d)"),
*NewLoc.ToString(), TimeStamp, MoveIndex);
// Apply corrected state (instant for autonomous proxy)
UpdatedComponent->SetWorldLocation(NewLoc, false);
Velocity = NewVel;
SetMovementMode(EMovementMode(ServerMovementMode));
// Set movement base
if (bHasBase && NewBase)
{
CharacterOwner->SetBase(NewBase, NewBaseBoneName);
}
// Clear moves before the corrected one
FNetworkPredictionData_Client_Fort* ClientData = static_cast<FNetworkPredictionData_Client_Fort*>(GetPredictionData_Client());
if (ClientData)
{
ClientData->SavedMoves.RemoveAt(0, MoveIndex);
LastAckedTimeStamp = TimeStamp;
}
// Replay moves after correction
ReplaySavedMoves(0); // Start from 0 since we removed old moves
}
else
{
// Simulated proxy: Apply smooth correction
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ClientAdjustPosition: Smoothing simulated proxy to %s"),
*NewLoc.ToString());
// Set movement mode
SetMovementMode(EMovementMode(ServerMovementMode));
// Set movement base
if (bHasBase && NewBase)
{
CharacterOwner->SetBase(NewBase, NewBaseBoneName);
}
// Apply correction with smoothing (for visual quality)
const FRotator NewRot = UpdatedComponent->GetComponentRotation(); // Keep current rotation
ApplyNetworkCorrection(NewLoc, NewRot, NewVel);
}
}
void UFortCharacterMovementComponent::ClientAckGoodMove_Implementation(
float TimeStamp,
FVector NewLoc,
FVector NewVel,
uint8 ServerMovementMode)
{
// Find acknowledged move
const int32 MoveIndex = FindSavedMoveByTimeStamp(TimeStamp);
if (MoveIndex != -1)
{
// Free acknowledged moves
FNetworkPredictionData_Client_Fort* ClientData = static_cast<FNetworkPredictionData_Client_Fort*>(GetPredictionData_Client());
if (ClientData)
{
ClientData->SavedMoves.RemoveAt(0, MoveIndex + 1);
LastAckedTimeStamp = TimeStamp;
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("ClientAckGoodMove: Timestamp %f acknowledged, freed %d moves"),
TimeStamp, MoveIndex + 1);
}
}
else
{
UE_LOG(LogNetPlayerMovement, Warning,
TEXT("ClientAckGoodMove_Implementation could not find Move for TimeStamp: %f, LastAckedTimeStamp: %f, CurrentTimeStamp: %f"),
TimeStamp, LastAckedTimeStamp, CurrentTimeStamp);
}
}