Last active
September 17, 2024 17:01
-
-
Save JoeyDeVries/58373f456bd94ef38548f4d8257b31c4 to your computer and use it in GitHub Desktop.
Custom client interpolated network movement for MMORPG - See: https://youtu.be/CxiIaFFay_M
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fill out your copyright notice in the Description page of Project Settings. | |
#include "MMOAIMovement.h" | |
#include "DrawDebugHelpers.h" | |
#include "Kismet/GameplayStatics.h" | |
#include "Kismet/KismetMathLibrary.h" | |
#include "Net/UnrealNetwork.h" | |
// CONSTANTS | |
const int SIMULATION_BUFFER_SIZE = 2; | |
UMMOAIMovement::UMMOAIMovement(const FObjectInitializer& ObjectInitializer) | |
: Super(ObjectInitializer) | |
{ | |
MaxSpeed = 1200.f; | |
Acceleration = 4000.f; | |
Deceleration = 8000.f; | |
TurningBoost = 8.0f; | |
bPositionCorrected = false; | |
SetIsReplicatedByDefault(true); | |
ResetMoveState(); | |
} | |
void UMMOAIMovement::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const | |
{ | |
DOREPLIFETIME(UMMOAIMovement, ServerPositionTime); | |
DOREPLIFETIME(UMMOAIMovement, ServerVelocityYawTime); | |
Super::GetLifetimeReplicatedProps(OutLifetimeProps); | |
} | |
void UMMOAIMovement::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) | |
{ | |
if (ShouldSkipUpdate(DeltaTime)) | |
{ | |
return; | |
} | |
Super::TickComponent(DeltaTime, TickType, ThisTickFunction); | |
if (!PawnOwner || !UpdatedComponent) | |
{ | |
return; | |
} | |
const AController* Controller = PawnOwner->GetController(); | |
if (Controller && Controller->IsLocalController()) | |
{ | |
// apply input for local players but also for AI that's not following a navigation path at the moment | |
if (Controller->IsLocalPlayerController() == true || Controller->IsFollowingAPath() == false || bUseAccelerationForPaths) | |
{ | |
ApplyControlInputToVelocity(DeltaTime); | |
} | |
// if it's not player controller, but we do have a controller, then it's AI | |
// (that's not following a path) and we need to limit the speed | |
else if (IsExceedingMaxSpeed(MaxSpeed) == true) | |
{ | |
Velocity = Velocity.GetUnsafeNormal() * MaxSpeed; | |
} | |
LimitWorldBounds(); | |
bPositionCorrected = false; | |
// Move actor | |
FVector Delta = Velocity * DeltaTime; | |
if (!Delta.IsNearlyZero(1e-1f)) | |
{ | |
IsMoving = true; | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
FQuat Rotation = UpdatedComponent->GetComponentQuat(); | |
if (OrientRotationToMovement) | |
{ | |
FVector start = GetActorLocation(); | |
FVector target = start + Velocity; | |
FQuat newRot = UKismetMathLibrary::FindLookAtRotation(start, target).Quaternion(); | |
Rotation = FQuat::Slerp(Rotation, newRot, DeltaTime * TurnSpeed); | |
} | |
FHitResult Hit(1.f); | |
SafeMoveUpdatedComponent(Delta, Rotation, true, Hit); | |
if (Hit.IsValidBlockingHit()) | |
{ | |
HandleImpact(Hit, DeltaTime, Delta); | |
// Try to slide the remaining distance along the surface. | |
SlideAlongSurface(Delta, 1.f - Hit.Time, Hit.Normal, Hit, true); | |
} | |
// Update velocity | |
// We don't want position changes to vastly reverse our direction (which can happen due to penetration fixups etc) | |
if (!bPositionCorrected) | |
{ | |
const FVector NewLocation = UpdatedComponent->GetComponentLocation(); | |
Velocity = ((NewLocation - OldLocation) / DeltaTime); | |
} | |
} | |
else | |
{ | |
// inform clients through replicated state velocity is now zero (only once, until moving again) | |
if (IsMoving) | |
ServerVelocityYawTime = FVector(0.0f, UpdatedComponent->GetComponentRotation().Yaw, UGameplayStatics::GetRealTimeSeconds(GetWorld())); | |
IsMoving = false; | |
} | |
// Finalize | |
UpdateComponentVelocity(); | |
// Replicate state from server to client, only when state has not been 0 for both since 1 frame | |
if (PawnOwner->GetLocalRole() == ROLE_Authority) | |
{ | |
if (IsMoving) | |
{ | |
float time = UGameplayStatics::GetRealTimeSeconds(GetWorld()); | |
ServerPositionTime = FVector4(UpdatedComponent->GetComponentLocation(), time); | |
ServerVelocityYawTime = FVector(Velocity.Size(), UpdatedComponent->GetComponentRotation().Yaw, time); | |
} | |
} | |
} | |
else | |
{ | |
// AI Controllers don't exist on clients, in that case run simulation logic | |
if (PawnOwner->GetLocalRole() == ROLE_SimulatedProxy) | |
{ | |
// in case of simulated proxy, don't use velocity directly, but use interpolated (simulated) velocity | |
// as difference in current and last position of the server. This to show a smooth movement experience | |
// regardless of net bandwidth (unless it gets TOO laggy) | |
// only do movement logic when server velocity is actually > 0.0 | |
if (ServerVelocityYawTime.X > 0.1f) | |
{ | |
IsMoving = true; | |
Velocity = FVector::ZeroVector; | |
for (int i = 0; i < ServerLastPositionTime.Num() - 1; ++i) | |
{ | |
FVector oldPos = FVector(ServerLastPositionTime[i].X, ServerLastPositionTime[i].Y, ServerLastPositionTime[i].Z); | |
FVector currPos = FVector(ServerLastPositionTime[i + 1].X, ServerLastPositionTime[i + 1].Y, ServerLastPositionTime[i + 1].Z); | |
float oldTime = ServerLastPositionTime[i].W; | |
float currTime = ServerLastPositionTime[i + 1].W; | |
FVector simVelocity = (currPos - oldPos) / (currTime - oldTime); | |
Velocity += simVelocity / (ServerLastPositionTime.Num() - 1); | |
} | |
float RotVelocity = 0.0f; | |
for (int i = 0; i < ServerLastPositionTime.Num() - 1; ++i) | |
{ | |
float oldYaw = ServerLastVelocityYawTime[i].Y; | |
float currYaw = ServerLastVelocityYawTime[i + 1].Y; | |
float oldTime = ServerLastVelocityYawTime[i].Z; | |
float currTime = ServerLastVelocityYawTime[i + 1].Z; | |
float simRotVelocity = (currYaw - oldYaw) / (currTime - oldTime); | |
RotVelocity += simRotVelocity / (ServerLastVelocityYawTime.Num() - 1); | |
} | |
// UE_LOG(LogTemp, Warning, TEXT("SIMULATING MOVEMENT: %s"), *(Velocity.ToString())); | |
// UE_LOG(LogTemp, Warning, TEXT("SIMULATING ROTATION: %f"), RotVelocity); | |
LimitWorldBounds(); | |
bPositionCorrected = false; | |
// Move actor | |
FVector Delta = Velocity * DeltaTime; | |
if (!Delta.IsNearlyZero(1e-2f)) | |
{ | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
FVector4 currServerPos = ServerPositionTime; | |
UpdatedComponent->SetWorldLocation(OldLocation + Delta, false, nullptr, ETeleportType::None); | |
if (FMath::Abs(RotVelocity) > 0.001f) | |
{ | |
FRotator currRot = UpdatedComponent->GetComponentRotation(); | |
currRot.Add(0.0f, RotVelocity * DeltaTime, 0.0f); | |
UpdatedComponent->SetWorldRotation(currRot, false, nullptr, ETeleportType::None); | |
} | |
} | |
// Finalize | |
UpdateComponentVelocity(); | |
} | |
else | |
{ | |
// when velocity from server is zero, simply force position to last server position | |
if (IsMoving) // note, we only do this if last farme we were still moving | |
{ | |
Velocity = FVector::ZeroVector; | |
UpdatedComponent->SetWorldLocation(FVector(ServerPositionTime.X, ServerPositionTime.Y, ServerPositionTime.Z)); | |
} | |
else | |
IsMoving = false; | |
// Finalize | |
UpdateComponentVelocity(); | |
} | |
// UE_LOG(LogTemp, Error, TEXT("MMOAIMovement: velocity length: %f, server velocity length: %f"), Velocity.Size(), ServerVelocityYawTime.X); | |
} | |
} | |
}; | |
bool UMMOAIMovement::LimitWorldBounds() | |
{ | |
AWorldSettings* WorldSettings = PawnOwner ? PawnOwner->GetWorldSettings() : NULL; | |
if (!WorldSettings || !WorldSettings->bEnableWorldBoundsChecks || !UpdatedComponent) | |
{ | |
return false; | |
} | |
const FVector CurrentLocation = UpdatedComponent->GetComponentLocation(); | |
if (CurrentLocation.Z < WorldSettings->KillZ) | |
{ | |
Velocity.Z = FMath::Min(GetMaxSpeed(), WorldSettings->KillZ - CurrentLocation.Z + 2.0f); | |
return true; | |
} | |
return false; | |
} | |
void UMMOAIMovement::ApplyControlInputToVelocity(float DeltaTime) | |
{ | |
const FVector ControlAcceleration = GetPendingInputVector().GetClampedToMaxSize(1.f); | |
const float AnalogInputModifier = (ControlAcceleration.SizeSquared() > 0.f ? ControlAcceleration.Size() : 0.f); | |
const float MaxPawnSpeed = GetMaxSpeed() * AnalogInputModifier; | |
const bool bExceedingMaxSpeed = IsExceedingMaxSpeed(MaxPawnSpeed); | |
if (AnalogInputModifier > 0.f && !bExceedingMaxSpeed) | |
{ | |
// Apply change in velocity direction | |
if (Velocity.SizeSquared() > 0.f) | |
{ | |
// Change direction faster than only using acceleration, but never increase velocity magnitude. | |
const float TimeScale = FMath::Clamp(DeltaTime * TurningBoost, 0.f, 1.f); | |
Velocity = Velocity + (ControlAcceleration * Velocity.Size() - Velocity) * TimeScale; | |
} | |
} | |
else | |
{ | |
// Dampen velocity magnitude based on deceleration. | |
if (Velocity.SizeSquared() > 0.f) | |
{ | |
const FVector OldVelocity = Velocity; | |
const float VelSize = FMath::Max(Velocity.Size() - FMath::Abs(Deceleration) * DeltaTime, 0.f); | |
Velocity = Velocity.GetSafeNormal() * VelSize; | |
// Don't allow braking to lower us below max speed if we started above it. | |
if (bExceedingMaxSpeed && Velocity.SizeSquared() < FMath::Square(MaxPawnSpeed)) | |
{ | |
Velocity = OldVelocity.GetSafeNormal() * MaxPawnSpeed; | |
} | |
} | |
} | |
// Apply acceleration and clamp velocity magnitude. | |
const float NewMaxSpeed = (IsExceedingMaxSpeed(MaxPawnSpeed)) ? Velocity.Size() : MaxPawnSpeed; | |
Velocity += ControlAcceleration * FMath::Abs(Acceleration) * DeltaTime; | |
Velocity = Velocity.GetClampedToMaxSize(NewMaxSpeed); | |
ConsumeInputVector(); | |
} | |
bool UMMOAIMovement::ResolvePenetrationImpl(const FVector& Adjustment, const FHitResult& Hit, const FQuat& NewRotationQuat) | |
{ | |
bPositionCorrected |= Super::ResolvePenetrationImpl(Adjustment, Hit, NewRotationQuat); | |
return bPositionCorrected; | |
} | |
void UMMOAIMovement::OnRep_ServerPositionTime(FVector4 oldValue) | |
{ | |
UpdatedComponent->SetWorldLocation(FVector(ServerPositionTime.X, ServerPositionTime.Y, ServerPositionTime.Z)); | |
// Add to buffer, keep fixed-size buffer | |
ServerLastPositionTime.Add(ServerPositionTime); | |
if (ServerLastPositionTime.Num() > SIMULATION_BUFFER_SIZE) | |
ServerLastPositionTime.RemoveAt(0); | |
} | |
void UMMOAIMovement::OnRep_ServerVelocityYawTime(FVector oldValue) | |
{ | |
if (FMath::Abs(ServerVelocityYawTime.Y - oldValue.Y) > 0.001f) | |
{ | |
FRotator rotator = UpdatedComponent->GetComponentRotation(); | |
rotator.Yaw = ServerVelocityYawTime.Y; | |
UpdatedComponent->SetWorldRotation(rotator, false, nullptr, ETeleportType::None); | |
} | |
// Add to buffer, keep fixed-size buffer | |
ServerLastVelocityYawTime.Add(oldValue); | |
if (ServerLastVelocityYawTime.Num() > SIMULATION_BUFFER_SIZE) | |
ServerLastVelocityYawTime.RemoveAt(0); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fill out your copyright notice in the Description page of Project Settings. | |
#pragma once | |
#include "CoreMinimal.h" | |
#include "GameFramework/PawnMovementComponent.h" | |
#include "MMOAIMovement.generated.h" | |
/** | |
* | |
*/ | |
UCLASS(ClassGroup = Movement, meta = (BlueprintSpawnableComponent)) | |
class MMOEY_API UMMOAIMovement : public UPawnMovementComponent | |
{ | |
GENERATED_UCLASS_BODY() | |
public: | |
/** Maximum velocity magnitude allowed for the controlled Pawn. */ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMOMovementComponent") | |
float MaxSpeed; | |
/** Acceleration applied by input (rate of change of velocity) */ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMOMovementComponent") | |
float Acceleration; | |
/** Deceleration applied when there is no input (rate of change of velocity) */ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMOMovementComponent") | |
float Deceleration; | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMOMovementComponent") | |
bool OrientRotationToMovement = true; | |
/** Deceleration applied when there is no input (rate of change of velocity) */ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMOMovementComponent") | |
float TurnSpeed = 5.0f; | |
/** | |
* Setting affecting extra force applied when changing direction, making turns have less drift and become more responsive. | |
* Velocity magnitude is not allowed to increase, that only happens due to normal acceleration. It may decrease with large direction changes. | |
* Larger values apply extra force to reach the target direction more quickly, while a zero value disables any extra turn force. | |
*/ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMOMovementComponent", meta = (ClampMin = "0", UIMin = "0")) | |
float TurningBoost; | |
private: | |
/** | |
* Get replicated position from server each time position is updated, simulated proxies can then | |
* do proper network simulation for smooth (interpolated) movement. XYZ = Position, W = Time. | |
*/ | |
UPROPERTY(ReplicatedUsing = OnRep_ServerPositionTime) | |
FVector4 ServerPositionTime; | |
TArray<FVector4> ServerLastPositionTime; | |
/** | |
* Server's velocity. X as velocity magnitude, and Y as rotation around Z axis (yaw), Z = time. | |
* When velocity reaches 0 we can stop simulating. | |
* | |
*/ | |
UPROPERTY(ReplicatedUsing = OnRep_ServerVelocityYawTime) | |
FVector ServerVelocityYawTime; | |
TArray<FVector4> ServerLastVelocityYawTime; | |
float targetYaw = 0.0f; | |
bool IsMoving = false; | |
public: | |
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; | |
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; | |
virtual float GetMaxSpeed() const override { return MaxSpeed; } | |
protected: | |
virtual bool ResolvePenetrationImpl(const FVector& Adjustment, const FHitResult& Hit, const FQuat& NewRotation) override; | |
/** Update Velocity based on input. Also applies gravity. */ | |
virtual void ApplyControlInputToVelocity(float DeltaTime); | |
/** Prevent Pawn from leaving the world bounds (if that restriction is enabled in WorldSettings) */ | |
virtual bool LimitWorldBounds(); | |
/** Set to true when a position correction is applied. Used to avoid recalculating velocity when this occurs. */ | |
UPROPERTY(Transient) | |
uint32 bPositionCorrected : 1; | |
private: | |
UFUNCTION() | |
void OnRep_ServerPositionTime(FVector4 oldValue); | |
UFUNCTION() | |
void OnRep_ServerVelocityYawTime(FVector oldValue); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Epic Games, Inc. All Rights Reserved. | |
/*============================================================================= | |
Character.cpp: AMMOCharacter implementation | |
=============================================================================*/ | |
#include "MMOCharacter.h" | |
#include "GameFramework/DamageType.h" | |
#include "GameFramework/Controller.h" | |
#include "Components/SkinnedMeshComponent.h" | |
#include "Components/SkeletalMeshComponent.h" | |
#include "Components/ArrowComponent.h" | |
#include "Engine/CollisionProfile.h" | |
#include "Components/CapsuleComponent.h" | |
#include "GameFramework/CharacterMovementComponent.h" | |
#include "Net/UnrealNetwork.h" | |
#include "DisplayDebugHelpers.h" | |
#include "Engine/Canvas.h" | |
#include "Animation/AnimInstance.h" | |
#include "MMOPlayerMovement.h" | |
DEFINE_LOG_CATEGORY_STATIC(LogCharacter, Log, All); | |
DEFINE_LOG_CATEGORY_STATIC(LogAvatar, Log, All); | |
DECLARE_CYCLE_STAT(TEXT("Char OnNetUpdateSimulatedPosition"), STAT_MMO_CharacterOnNetUpdateSimulatedPosition, STATGROUP_Character); | |
FName AMMOCharacter::MeshComponentName(TEXT("CharacterMesh0")); | |
FName AMMOCharacter::CharacterMovementComponentName(TEXT("CharMoveComp")); | |
FName AMMOCharacter::CapsuleComponentName(TEXT("CollisionCylinder")); | |
AMMOCharacter::AMMOCharacter(const FObjectInitializer& ObjectInitializer) | |
: Super(ObjectInitializer.SetDefaultSubobjectClass<UMMOPlayerMovement>(ACharacter::CharacterMovementComponentName)) | |
{ | |
// Structure to hold one-time initialization | |
struct FConstructorStatics | |
{ | |
FName ID_Characters; | |
FText NAME_Characters; | |
FConstructorStatics() | |
: ID_Characters(TEXT("Characters")) | |
, NAME_Characters(NSLOCTEXT("SpriteCategory", "Characters", "Characters")) | |
{ | |
} | |
}; | |
static FConstructorStatics ConstructorStatics; | |
// Character rotation only changes in Yaw, to prevent the capsule from changing orientation. | |
// Ask the Controller for the full rotation if desired (ie for aiming). | |
bUseControllerRotationPitch = false; | |
bUseControllerRotationRoll = false; | |
bUseControllerRotationYaw = true; | |
CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(AMMOCharacter::CapsuleComponentName); | |
CapsuleComponent->InitCapsuleSize(34.0f, 88.0f); | |
CapsuleComponent->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName); | |
CapsuleComponent->CanCharacterStepUpOn = ECB_No; | |
CapsuleComponent->SetShouldUpdatePhysicsVolume(true); | |
CapsuleComponent->SetCanEverAffectNavigation(false); | |
CapsuleComponent->bDynamicObstacle = true; | |
RootComponent = CapsuleComponent; | |
bClientCheckEncroachmentOnNetUpdate = true; | |
JumpKeyHoldTime = 0.0f; | |
JumpMaxHoldTime = 0.0f; | |
JumpMaxCount = 1; | |
JumpCurrentCount = 0; | |
bWasJumping = false; | |
AnimRootMotionTranslationScale = 1.0f; | |
#if WITH_EDITORONLY_DATA | |
ArrowComponent = CreateEditorOnlyDefaultSubobject<UArrowComponent>(TEXT("Arrow")); | |
if (ArrowComponent) | |
{ | |
ArrowComponent->ArrowColor = FColor(150, 200, 255); | |
ArrowComponent->bTreatAsASprite = true; | |
ArrowComponent->SpriteInfo.Category = ConstructorStatics.ID_Characters; | |
ArrowComponent->SpriteInfo.DisplayName = ConstructorStatics.NAME_Characters; | |
ArrowComponent->SetupAttachment(CapsuleComponent); | |
ArrowComponent->bIsScreenSizeScaled = true; | |
} | |
#endif // WITH_EDITORONLY_DATA | |
CharacterMovement = CreateDefaultSubobject<UMMOPlayerMovement>(AMMOCharacter::CharacterMovementComponentName); | |
if (CharacterMovement) | |
{ | |
CharacterMovement->UpdatedComponent = CapsuleComponent; | |
CrouchedEyeHeight = CharacterMovement->CrouchedHalfHeight * 0.80f; | |
// UE_LOG(LogTemp, Error, TEXT("MMOCharacter: Succesfully created movement component %s") ,*(CharacterMovement->GetClass()->GetName())); | |
} | |
else | |
UE_LOG(LogTemp, Error, TEXT("MMOCharacter: Failed to create MMO Player Movement!")); | |
Mesh = CreateOptionalDefaultSubobject<USkeletalMeshComponent>(AMMOCharacter::MeshComponentName); | |
if (Mesh) | |
{ | |
Mesh->AlwaysLoadOnClient = true; | |
Mesh->AlwaysLoadOnServer = true; | |
Mesh->bOwnerNoSee = false; | |
Mesh->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::AlwaysTickPose; | |
Mesh->bCastDynamicShadow = true; | |
Mesh->bAffectDynamicIndirectLighting = true; | |
Mesh->PrimaryComponentTick.TickGroup = TG_PrePhysics; | |
Mesh->SetupAttachment(CapsuleComponent); | |
static FName MeshCollisionProfileName(TEXT("CharacterMesh")); | |
Mesh->SetCollisionProfileName(MeshCollisionProfileName); | |
Mesh->SetGenerateOverlapEvents(false); | |
Mesh->SetCanEverAffectNavigation(false); | |
} | |
BaseRotationOffset = FQuat::Identity; | |
} | |
void AMMOCharacter::PostInitializeComponents() | |
{ | |
QUICK_SCOPE_CYCLE_COUNTER(STAT_MMO_Character_PostInitComponents); | |
Super::PostInitializeComponents(); | |
if (!IsPendingKill()) | |
{ | |
if (Mesh) | |
{ | |
CacheInitialMeshOffset(Mesh->GetRelativeLocation(), Mesh->GetRelativeRotation()); | |
// force animation tick after movement component updates | |
if (Mesh->PrimaryComponentTick.bCanEverTick && CharacterMovement) | |
{ | |
Mesh->PrimaryComponentTick.AddPrerequisite(CharacterMovement, CharacterMovement->PrimaryComponentTick); | |
} | |
} | |
if (CharacterMovement && CapsuleComponent) | |
{ | |
CharacterMovement->UpdateNavAgent(*CapsuleComponent); | |
} | |
if (Controller == nullptr && GetNetMode() != NM_Client) | |
{ | |
if (CharacterMovement && CharacterMovement->bRunPhysicsWithNoController) | |
{ | |
CharacterMovement->SetDefaultMovementMode(); | |
} | |
} | |
} | |
} | |
void AMMOCharacter::BeginPlay() | |
{ | |
Super::BeginPlay(); | |
} | |
void AMMOCharacter::CacheInitialMeshOffset(FVector MeshRelativeLocation, FRotator MeshRelativeRotation) | |
{ | |
BaseTranslationOffset = MeshRelativeLocation; | |
BaseRotationOffset = MeshRelativeRotation.Quaternion(); | |
#if ENABLE_NAN_DIAGNOSTIC | |
if (BaseRotationOffset.ContainsNaN()) | |
{ | |
logOrEnsureNanError(TEXT("AMMOCharacter::PostInitializeComponents detected NaN in BaseRotationOffset! (%s)"), *BaseRotationOffset.ToString()); | |
} | |
const FRotator LocalRotation = Mesh->GetRelativeRotation(); | |
if (LocalRotation.ContainsNaN()) | |
{ | |
logOrEnsureNanError(TEXT("AMMOCharacter::PostInitializeComponents detected NaN in Mesh->RelativeRotation! (%s)"), *LocalRotation.ToString()); | |
} | |
#endif | |
} | |
UPawnMovementComponent* AMMOCharacter::GetMovementComponent() const | |
{ | |
if(!CharacterMovement) | |
UE_LOG(LogTemp, Error, TEXT("MMOCharacter: GetMovementComponent(): Character Movement is null!")) | |
return CharacterMovement; | |
} | |
void AMMOCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) | |
{ | |
check(PlayerInputComponent); | |
} | |
void AMMOCharacter::GetSimpleCollisionCylinder(float& CollisionRadius, float& CollisionHalfHeight) const | |
{ | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (IsTemplate()) | |
{ | |
UE_LOG(LogCharacter, Log, TEXT("WARNING AMMOCharacter::GetSimpleCollisionCylinder : Called on default object '%s'. Will likely return zero size. Consider using GetDefaultHalfHeight() instead."), *this->GetPathName()); | |
} | |
#endif | |
if (RootComponent == CapsuleComponent && IsRootComponentCollisionRegistered()) | |
{ | |
// Note: we purposefully ignore the component transform here aside from scale, always treating it as vertically aligned. | |
// This improves performance and is also how we stated the CapsuleComponent would be used. | |
CapsuleComponent->GetScaledCapsuleSize(CollisionRadius, CollisionHalfHeight); | |
} | |
else | |
{ | |
Super::GetSimpleCollisionCylinder(CollisionRadius, CollisionHalfHeight); | |
} | |
} | |
void AMMOCharacter::UpdateNavigationRelevance() | |
{ | |
if (CapsuleComponent) | |
{ | |
CapsuleComponent->SetCanEverAffectNavigation(bCanAffectNavigationGeneration); | |
} | |
} | |
float AMMOCharacter::GetDefaultHalfHeight() const | |
{ | |
UCapsuleComponent* DefaultCapsule = GetClass()->GetDefaultObject<AMMOCharacter>()->CapsuleComponent; | |
if (DefaultCapsule) | |
{ | |
return DefaultCapsule->GetScaledCapsuleHalfHeight(); | |
} | |
else | |
{ | |
return Super::GetDefaultHalfHeight(); | |
} | |
} | |
UActorComponent* AMMOCharacter::FindComponentByClass(const TSubclassOf<UActorComponent> ComponentClass) const | |
{ | |
// If the character has a Mesh, treat it as the first 'hit' when finding components | |
if (Mesh && ComponentClass && Mesh->IsA(ComponentClass)) | |
{ | |
return Mesh; | |
} | |
return Super::FindComponentByClass(ComponentClass); | |
} | |
void AMMOCharacter::OnWalkingOffLedge_Implementation(const FVector& PreviousFloorImpactNormal, const FVector& PreviousFloorContactNormal, const FVector& PreviousLocation, float TimeDelta) | |
{ | |
} | |
void AMMOCharacter::NotifyJumpApex() | |
{ | |
// Call delegate callback | |
if (OnReachedJumpApex.IsBound()) | |
{ | |
OnReachedJumpApex.Broadcast(); | |
} | |
} | |
void AMMOCharacter::Landed(const FHitResult& Hit) | |
{ | |
OnLanded(Hit); | |
LandedDelegate.Broadcast(Hit); | |
} | |
bool AMMOCharacter::CanJump() const | |
{ | |
return CanJumpInternal(); | |
} | |
bool AMMOCharacter::CanJumpInternal_Implementation() const | |
{ | |
// Ensure the character isn't currently crouched. | |
bool bCanJump = !bIsCrouched; | |
// Ensure that the CharacterMovement state is valid | |
bCanJump &= CharacterMovement->CanAttemptJump(); | |
if (bCanJump) | |
{ | |
// Ensure JumpHoldTime and JumpCount are valid. | |
if (!bWasJumping || GetJumpMaxHoldTime() <= 0.0f) | |
{ | |
if (JumpCurrentCount == 0 && CharacterMovement->IsFalling()) | |
{ | |
bCanJump = JumpCurrentCount + 1 < JumpMaxCount; | |
} | |
else | |
{ | |
bCanJump = JumpCurrentCount < JumpMaxCount; | |
} | |
} | |
else | |
{ | |
// Only consider JumpKeyHoldTime as long as: | |
// A) The jump limit hasn't been met OR | |
// B) The jump limit has been met AND we were already jumping | |
const bool bJumpKeyHeld = (bPressedJump && JumpKeyHoldTime < GetJumpMaxHoldTime()); | |
bCanJump = bJumpKeyHeld && | |
((JumpCurrentCount < JumpMaxCount) || (bWasJumping && JumpCurrentCount == JumpMaxCount)); | |
} | |
} | |
return bCanJump; | |
} | |
void AMMOCharacter::ResetJumpState() | |
{ | |
bPressedJump = false; | |
bWasJumping = false; | |
JumpKeyHoldTime = 0.0f; | |
JumpForceTimeRemaining = 0.0f; | |
if (CharacterMovement && !CharacterMovement->IsFalling()) | |
{ | |
JumpCurrentCount = 0; | |
} | |
} | |
void AMMOCharacter::OnJumped_Implementation() | |
{ | |
} | |
bool AMMOCharacter::IsJumpProvidingForce() const | |
{ | |
if (JumpForceTimeRemaining > 0.0f) | |
{ | |
return true; | |
} | |
else if (bProxyIsJumpForceApplied && (GetLocalRole() == ROLE_SimulatedProxy)) | |
{ | |
return GetWorld()->TimeSince(ProxyJumpForceStartedTime) <= GetJumpMaxHoldTime(); | |
} | |
return false; | |
} | |
void AMMOCharacter::RecalculateBaseEyeHeight() | |
{ | |
if (!bIsCrouched) | |
{ | |
Super::RecalculateBaseEyeHeight(); | |
} | |
else | |
{ | |
BaseEyeHeight = CrouchedEyeHeight; | |
} | |
} | |
void AMMOCharacter::OnRep_IsCrouched() | |
{ | |
if (CharacterMovement) | |
{ | |
if (bIsCrouched) | |
{ | |
CharacterMovement->bWantsToCrouch = true; | |
CharacterMovement->Crouch(true); | |
} | |
else | |
{ | |
CharacterMovement->bWantsToCrouch = false; | |
CharacterMovement->UnCrouch(true); | |
} | |
CharacterMovement->bNetworkUpdateReceived = true; | |
} | |
} | |
void AMMOCharacter::SetReplicateMovement(bool bInReplicateMovement) | |
{ | |
Super::SetReplicateMovement(bInReplicateMovement); | |
if (CharacterMovement != nullptr && GetLocalRole() == ROLE_Authority) | |
{ | |
// Set prediction data time stamp to current time to stop extrapolating | |
// from time bReplicateMovement was turned off to when it was turned on again | |
FNetworkPredictionData_Server* NetworkPrediction = CharacterMovement->HasPredictionData_Server() ? CharacterMovement->GetPredictionData_Server() : nullptr; | |
if (NetworkPrediction != nullptr) | |
{ | |
NetworkPrediction->ServerTimeStamp = GetWorld()->GetTimeSeconds(); | |
} | |
} | |
} | |
bool AMMOCharacter::CanCrouch() const | |
{ | |
return !bIsCrouched && CharacterMovement && CharacterMovement->CanEverCrouch() && GetRootComponent() && !GetRootComponent()->IsSimulatingPhysics(); | |
} | |
void AMMOCharacter::Crouch(bool bClientSimulation) | |
{ | |
if (CharacterMovement) | |
{ | |
if (CanCrouch()) | |
{ | |
CharacterMovement->bWantsToCrouch = true; | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
else if (!CharacterMovement->CanEverCrouch()) | |
{ | |
UE_LOG(LogCharacter, Log, TEXT("%s is trying to crouch, but crouching is disabled on this character! (check CharacterMovement NavAgentSettings)"), *GetName()); | |
} | |
#endif | |
} | |
} | |
void AMMOCharacter::UnCrouch(bool bClientSimulation) | |
{ | |
if (CharacterMovement) | |
{ | |
CharacterMovement->bWantsToCrouch = false; | |
} | |
} | |
void AMMOCharacter::OnEndCrouch(float HeightAdjust, float ScaledHeightAdjust) | |
{ | |
RecalculateBaseEyeHeight(); | |
const AMMOCharacter* DefaultChar = GetDefault<AMMOCharacter>(GetClass()); | |
if (Mesh && DefaultChar->Mesh) | |
{ | |
FVector& MeshRelativeLocation = Mesh->GetRelativeLocation_DirectMutable(); | |
MeshRelativeLocation.Z = DefaultChar->Mesh->GetRelativeLocation().Z; | |
BaseTranslationOffset.Z = MeshRelativeLocation.Z; | |
} | |
else | |
{ | |
BaseTranslationOffset.Z = DefaultChar->BaseTranslationOffset.Z; | |
} | |
K2_OnEndCrouch(HeightAdjust, ScaledHeightAdjust); | |
} | |
void AMMOCharacter::OnStartCrouch(float HeightAdjust, float ScaledHeightAdjust) | |
{ | |
RecalculateBaseEyeHeight(); | |
const AMMOCharacter* DefaultChar = GetDefault<AMMOCharacter>(GetClass()); | |
if (Mesh && DefaultChar->Mesh) | |
{ | |
FVector& MeshRelativeLocation = Mesh->GetRelativeLocation_DirectMutable(); | |
MeshRelativeLocation.Z = DefaultChar->Mesh->GetRelativeLocation().Z + HeightAdjust; | |
BaseTranslationOffset.Z = MeshRelativeLocation.Z; | |
} | |
else | |
{ | |
BaseTranslationOffset.Z = DefaultChar->BaseTranslationOffset.Z + HeightAdjust; | |
} | |
K2_OnStartCrouch(HeightAdjust, ScaledHeightAdjust); | |
} | |
void AMMOCharacter::ApplyDamageMomentum(float DamageTaken, FDamageEvent const& DamageEvent, APawn* PawnInstigator, AActor* DamageCauser) | |
{ | |
UDamageType const* const DmgTypeCDO = DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>(); | |
float const ImpulseScale = DmgTypeCDO->DamageImpulse; | |
if ((ImpulseScale > 3.f) && (CharacterMovement != nullptr)) | |
{ | |
FHitResult HitInfo; | |
FVector ImpulseDir; | |
DamageEvent.GetBestHitInfo(this, PawnInstigator, HitInfo, ImpulseDir); | |
FVector Impulse = ImpulseDir * ImpulseScale; | |
bool const bMassIndependentImpulse = !DmgTypeCDO->bScaleMomentumByMass; | |
// limit Z momentum added if already going up faster than jump (to avoid blowing character way up into the sky) | |
{ | |
FVector MassScaledImpulse = Impulse; | |
if (!bMassIndependentImpulse && CharacterMovement->Mass > SMALL_NUMBER) | |
{ | |
MassScaledImpulse = MassScaledImpulse / CharacterMovement->Mass; | |
} | |
if ((CharacterMovement->Velocity.Z > GetDefault<UCharacterMovementComponent>(CharacterMovement->GetClass())->JumpZVelocity) && (MassScaledImpulse.Z > 0.f)) | |
{ | |
Impulse.Z *= 0.5f; | |
} | |
} | |
CharacterMovement->AddImpulse(Impulse, bMassIndependentImpulse); | |
} | |
} | |
void AMMOCharacter::ClearCrossLevelReferences() | |
{ | |
if (BasedMovement.MovementBase != nullptr && GetOutermost() != BasedMovement.MovementBase->GetOutermost()) | |
{ | |
SetBase(nullptr); | |
} | |
Super::ClearCrossLevelReferences(); | |
} | |
namespace MMOMovementBaseUtility | |
{ | |
bool IsDynamicBase(const UPrimitiveComponent* MovementBase) | |
{ | |
return (MovementBase && MovementBase->Mobility == EComponentMobility::Movable); | |
} | |
bool IsSimulatedBase(const UPrimitiveComponent* MovementBase) | |
{ | |
bool bBaseIsSimulatingPhysics = false; | |
const USceneComponent* AttachParent = MovementBase; | |
while (!bBaseIsSimulatingPhysics && AttachParent) | |
{ | |
bBaseIsSimulatingPhysics = AttachParent->IsSimulatingPhysics(); | |
AttachParent = AttachParent->GetAttachParent(); | |
} | |
return bBaseIsSimulatingPhysics; | |
} | |
void AddTickDependency(FTickFunction& BasedObjectTick, UPrimitiveComponent* NewBase) | |
{ | |
if (NewBase && MMOMovementBaseUtility::UseRelativeLocation(NewBase)) | |
{ | |
if (NewBase->PrimaryComponentTick.bCanEverTick) | |
{ | |
BasedObjectTick.AddPrerequisite(NewBase, NewBase->PrimaryComponentTick); | |
} | |
AActor* NewBaseOwner = NewBase->GetOwner(); | |
if (NewBaseOwner) | |
{ | |
if (NewBaseOwner->PrimaryActorTick.bCanEverTick) | |
{ | |
BasedObjectTick.AddPrerequisite(NewBaseOwner, NewBaseOwner->PrimaryActorTick); | |
} | |
// @TODO: We need to find a more efficient way of finding all ticking components in an actor. | |
for (UActorComponent* Component : NewBaseOwner->GetComponents()) | |
{ | |
// Dont allow a based component (e.g. a particle system) to push us into a different tick group | |
if (Component && Component->PrimaryComponentTick.bCanEverTick && Component->PrimaryComponentTick.TickGroup <= BasedObjectTick.TickGroup) | |
{ | |
BasedObjectTick.AddPrerequisite(Component, Component->PrimaryComponentTick); | |
} | |
} | |
} | |
} | |
} | |
void RemoveTickDependency(FTickFunction& BasedObjectTick, UPrimitiveComponent* OldBase) | |
{ | |
if (OldBase && MMOMovementBaseUtility::UseRelativeLocation(OldBase)) | |
{ | |
BasedObjectTick.RemovePrerequisite(OldBase, OldBase->PrimaryComponentTick); | |
AActor* OldBaseOwner = OldBase->GetOwner(); | |
if (OldBaseOwner) | |
{ | |
BasedObjectTick.RemovePrerequisite(OldBaseOwner, OldBaseOwner->PrimaryActorTick); | |
// @TODO: We need to find a more efficient way of finding all ticking components in an actor. | |
for (UActorComponent* Component : OldBaseOwner->GetComponents()) | |
{ | |
if (Component && Component->PrimaryComponentTick.bCanEverTick) | |
{ | |
BasedObjectTick.RemovePrerequisite(Component, Component->PrimaryComponentTick); | |
} | |
} | |
} | |
} | |
} | |
FVector GetMovementBaseVelocity(const UPrimitiveComponent* MovementBase, const FName BoneName) | |
{ | |
FVector BaseVelocity = FVector::ZeroVector; | |
if (MMOMovementBaseUtility::IsDynamicBase(MovementBase)) | |
{ | |
if (BoneName != NAME_None) | |
{ | |
const FBodyInstance* BodyInstance = MovementBase->GetBodyInstance(BoneName); | |
if (BodyInstance) | |
{ | |
BaseVelocity = BodyInstance->GetUnrealWorldVelocity(); | |
return BaseVelocity; | |
} | |
} | |
BaseVelocity = MovementBase->GetComponentVelocity(); | |
if (BaseVelocity.IsZero()) | |
{ | |
// Fall back to actor's Root component | |
const AActor* Owner = MovementBase->GetOwner(); | |
if (Owner) | |
{ | |
// Component might be moved manually (not by simulated physics or a movement component), see if the root component of the actor has a velocity. | |
BaseVelocity = MovementBase->GetOwner()->GetVelocity(); | |
} | |
} | |
// Fall back to physics velocity. | |
if (BaseVelocity.IsZero()) | |
{ | |
if (FBodyInstance* BaseBodyInstance = MovementBase->GetBodyInstance()) | |
{ | |
BaseVelocity = BaseBodyInstance->GetUnrealWorldVelocity(); | |
} | |
} | |
} | |
return BaseVelocity; | |
} | |
FVector GetMovementBaseTangentialVelocity(const UPrimitiveComponent* MovementBase, const FName BoneName, const FVector& WorldLocation) | |
{ | |
if (MMOMovementBaseUtility::IsDynamicBase(MovementBase)) | |
{ | |
if (const FBodyInstance* BodyInstance = MovementBase->GetBodyInstance(BoneName)) | |
{ | |
const FVector BaseAngVelInRad = BodyInstance->GetUnrealWorldAngularVelocityInRadians(); | |
if (!BaseAngVelInRad.IsNearlyZero()) | |
{ | |
FVector BaseLocation; | |
FQuat BaseRotation; | |
if (MMOMovementBaseUtility::GetMovementBaseTransform(MovementBase, BoneName, BaseLocation, BaseRotation)) | |
{ | |
const FVector RadialDistanceToBase = WorldLocation - BaseLocation; | |
const FVector TangentialVel = BaseAngVelInRad ^ RadialDistanceToBase; | |
return TangentialVel; | |
} | |
} | |
} | |
} | |
return FVector::ZeroVector; | |
} | |
bool GetMovementBaseTransform(const UPrimitiveComponent* MovementBase, const FName BoneName, FVector& OutLocation, FQuat& OutQuat) | |
{ | |
if (MovementBase) | |
{ | |
if (BoneName != NAME_None) | |
{ | |
bool bFoundBone = false; | |
if (MovementBase) | |
{ | |
// Check if this socket or bone exists (DoesSocketExist checks for either, as does requesting the transform). | |
if (MovementBase->DoesSocketExist(BoneName)) | |
{ | |
MovementBase->GetSocketWorldLocationAndRotation(BoneName, OutLocation, OutQuat); | |
bFoundBone = true; | |
} | |
else | |
{ | |
UE_LOG(LogCharacter, Warning, TEXT("GetMovementBaseTransform(): Invalid bone or socket '%s' for PrimitiveComponent base %s"), *BoneName.ToString(), *GetPathNameSafe(MovementBase)); | |
} | |
} | |
if (!bFoundBone) | |
{ | |
OutLocation = MovementBase->GetComponentLocation(); | |
OutQuat = MovementBase->GetComponentQuat(); | |
} | |
return bFoundBone; | |
} | |
// No bone supplied | |
OutLocation = MovementBase->GetComponentLocation(); | |
OutQuat = MovementBase->GetComponentQuat(); | |
return true; | |
} | |
// nullptr MovementBase | |
OutLocation = FVector::ZeroVector; | |
OutQuat = FQuat::Identity; | |
return false; | |
} | |
} | |
/** Change the Pawn's base. */ | |
void AMMOCharacter::SetBase(UPrimitiveComponent* NewBaseComponent, const FName InBoneName, bool bNotifyPawn) | |
{ | |
// If NewBaseComponent is nullptr, ignore bone name. | |
const FName BoneName = (NewBaseComponent ? InBoneName : NAME_None); | |
// See what changed. | |
const bool bBaseChanged = (NewBaseComponent != BasedMovement.MovementBase); | |
const bool bBoneChanged = (BoneName != BasedMovement.BoneName); | |
if (bBaseChanged || bBoneChanged) | |
{ | |
// Verify no recursion. | |
APawn* Loop = (NewBaseComponent ? Cast<APawn>(NewBaseComponent->GetOwner()) : nullptr); | |
while (Loop) | |
{ | |
if (Loop == this) | |
{ | |
UE_LOG(LogCharacter, Warning, TEXT(" SetBase failed! Recursion detected. Pawn %s already based on %s."), *GetName(), *NewBaseComponent->GetName()); //-V595 | |
return; | |
} | |
if (UPrimitiveComponent* LoopBase = Loop->GetMovementBase()) | |
{ | |
Loop = Cast<APawn>(LoopBase->GetOwner()); | |
} | |
else | |
{ | |
break; | |
} | |
} | |
// Set base. | |
UPrimitiveComponent* OldBase = BasedMovement.MovementBase; | |
BasedMovement.MovementBase = NewBaseComponent; | |
BasedMovement.BoneName = BoneName; | |
if (CharacterMovement) | |
{ | |
const bool bBaseIsSimulating = MMOMovementBaseUtility::IsSimulatedBase(NewBaseComponent); | |
if (bBaseChanged) | |
{ | |
MMOMovementBaseUtility::RemoveTickDependency(CharacterMovement->PrimaryComponentTick, OldBase); | |
// We use a special post physics function if simulating, otherwise add normal tick prereqs. | |
if (!bBaseIsSimulating) | |
{ | |
MMOMovementBaseUtility::AddTickDependency(CharacterMovement->PrimaryComponentTick, NewBaseComponent); | |
} | |
} | |
if (NewBaseComponent) | |
{ | |
// Update OldBaseLocation/Rotation as those were referring to a different base | |
// ... but not when handling replication for proxies (since they are going to copy this data from the replicated values anyway) | |
if (!bInBaseReplication) | |
{ | |
// Force base location and relative position to be computed since we have a new base or bone so the old relative offset is meaningless. | |
CharacterMovement->SaveBaseLocation(); | |
} | |
// Enable PostPhysics tick if we are standing on a physics object, as we need to to use post-physics transforms | |
CharacterMovement->PostPhysicsTickFunction.SetTickFunctionEnable(bBaseIsSimulating); | |
} | |
else | |
{ | |
BasedMovement.BoneName = NAME_None; // None, regardless of whether user tried to set a bone name, since we have no base component. | |
BasedMovement.bRelativeRotation = false; | |
CharacterMovement->CurrentFloor.Clear(); | |
CharacterMovement->PostPhysicsTickFunction.SetTickFunctionEnable(false); | |
} | |
const ENetRole LocalRole = GetLocalRole(); | |
if (LocalRole == ROLE_Authority || LocalRole == ROLE_AutonomousProxy) | |
{ | |
BasedMovement.bServerHasBaseComponent = (BasedMovement.MovementBase != nullptr); // Also set on proxies for nicer debugging. | |
UE_LOG(LogCharacter, Verbose, TEXT("Setting base on %s for '%s' to '%s'"), LocalRole == ROLE_Authority ? TEXT("Server") : TEXT("AutoProxy"), *GetName(), *GetFullNameSafe(NewBaseComponent)); | |
} | |
else | |
{ | |
UE_LOG(LogCharacter, Verbose, TEXT("Setting base on Client for '%s' to '%s'"), *GetName(), *GetFullNameSafe(NewBaseComponent)); | |
} | |
} | |
// Notify this actor of his new floor. | |
if (bNotifyPawn) | |
{ | |
BaseChange(); | |
} | |
} | |
} | |
void AMMOCharacter::SaveRelativeBasedMovement(const FVector& NewRelativeLocation, const FRotator& NewRotation, bool bRelativeRotation) | |
{ | |
checkSlow(BasedMovement.HasRelativeLocation()); | |
BasedMovement.Location = NewRelativeLocation; | |
BasedMovement.Rotation = NewRotation; | |
BasedMovement.bRelativeRotation = bRelativeRotation; | |
} | |
FVector AMMOCharacter::GetNavAgentLocation() const | |
{ | |
FVector AgentLocation = FNavigationSystem::InvalidLocation; | |
if (GetCharacterMovement() != nullptr) | |
{ | |
AgentLocation = GetCharacterMovement()->GetActorFeetLocation(); | |
} | |
if (FNavigationSystem::IsValidLocation(AgentLocation) == false && CapsuleComponent != nullptr) | |
{ | |
AgentLocation = GetActorLocation() - FVector(0, 0, CapsuleComponent->GetScaledCapsuleHalfHeight()); | |
} | |
return AgentLocation; | |
} | |
void AMMOCharacter::TurnOff() | |
{ | |
if (CharacterMovement != nullptr) | |
{ | |
CharacterMovement->StopMovementImmediately(); | |
CharacterMovement->DisableMovement(); | |
} | |
if (GetNetMode() != NM_DedicatedServer && Mesh != nullptr) | |
{ | |
Mesh->bPauseAnims = true; | |
if (Mesh->IsSimulatingPhysics()) | |
{ | |
Mesh->bBlendPhysics = true; | |
Mesh->KinematicBonesUpdateType = EKinematicBonesUpdateToPhysics::SkipAllBones; | |
} | |
} | |
Super::TurnOff(); | |
} | |
void AMMOCharacter::Restart() | |
{ | |
Super::Restart(); | |
JumpCurrentCount = 0; | |
bPressedJump = false; | |
ResetJumpState(); | |
UnCrouch(true); | |
if (CharacterMovement) | |
{ | |
CharacterMovement->SetDefaultMovementMode(); | |
} | |
} | |
void AMMOCharacter::PawnClientRestart() | |
{ | |
if (CharacterMovement != nullptr) | |
{ | |
CharacterMovement->StopMovementImmediately(); | |
CharacterMovement->ResetPredictionData_Client(); | |
} | |
Super::PawnClientRestart(); | |
} | |
void AMMOCharacter::PossessedBy(AController* NewController) | |
{ | |
Super::PossessedBy(NewController); | |
// If we are controlled remotely, set animation timing to be driven by client's network updates. So timing and events remain in sync. | |
if (Mesh && IsReplicatingMovement() && (GetRemoteRole() == ROLE_AutonomousProxy && GetNetConnection() != nullptr)) | |
{ | |
Mesh->bOnlyAllowAutonomousTickPose = true; | |
} | |
} | |
void AMMOCharacter::UnPossessed() | |
{ | |
Super::UnPossessed(); | |
if (CharacterMovement) | |
{ | |
CharacterMovement->ResetPredictionData_Client(); | |
CharacterMovement->ResetPredictionData_Server(); | |
} | |
// We're no longer controlled remotely, resume regular ticking of animations. | |
if (Mesh) | |
{ | |
Mesh->bOnlyAllowAutonomousTickPose = false; | |
} | |
} | |
void AMMOCharacter::TornOff() | |
{ | |
Super::TornOff(); | |
if (CharacterMovement) | |
{ | |
CharacterMovement->ResetPredictionData_Client(); | |
CharacterMovement->ResetPredictionData_Server(); | |
} | |
// We're no longer controlled remotely, resume regular ticking of animations. | |
if (Mesh) | |
{ | |
Mesh->bOnlyAllowAutonomousTickPose = false; | |
} | |
} | |
void AMMOCharacter::NotifyActorBeginOverlap(AActor* OtherActor) | |
{ | |
NumActorOverlapEventsCounter++; | |
Super::NotifyActorBeginOverlap(OtherActor); | |
} | |
void AMMOCharacter::NotifyActorEndOverlap(AActor* OtherActor) | |
{ | |
NumActorOverlapEventsCounter++; | |
Super::NotifyActorEndOverlap(OtherActor); | |
} | |
void AMMOCharacter::BaseChange() | |
{ | |
if (CharacterMovement && CharacterMovement->MovementMode != MOVE_None) | |
{ | |
AActor* ActualMovementBase = GetMovementBaseActor(this); | |
if ((ActualMovementBase != nullptr) && !ActualMovementBase->CanBeBaseForCharacter(this)) | |
{ | |
CharacterMovement->JumpOff(ActualMovementBase); | |
} | |
} | |
} | |
void AMMOCharacter::DisplayDebug(UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos) | |
{ | |
Super::DisplayDebug(Canvas, DebugDisplay, YL, YPos); | |
float Indent = 0.f; | |
static FName NAME_Physics = FName(TEXT("Physics")); | |
if (DebugDisplay.IsDisplayOn(NAME_Physics)) | |
{ | |
FIndenter PhysicsIndent(Indent); | |
FString BaseString; | |
if (CharacterMovement == nullptr || BasedMovement.MovementBase == nullptr) | |
{ | |
BaseString = "Not Based"; | |
} | |
else | |
{ | |
BaseString = BasedMovement.MovementBase->IsWorldGeometry() ? "World Geometry" : BasedMovement.MovementBase->GetName(); | |
BaseString = FString::Printf(TEXT("Based On %s"), *BaseString); | |
} | |
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager; | |
DisplayDebugManager.DrawString(FString::Printf(TEXT("RelativeLoc: %s Rot: %s %s"), *BasedMovement.Location.ToCompactString(), *BasedMovement.Rotation.ToCompactString(), *BaseString), Indent); | |
if (CharacterMovement != nullptr) | |
{ | |
CharacterMovement->DisplayDebug(Canvas, DebugDisplay, YL, YPos); | |
} | |
const bool Crouched = CharacterMovement && CharacterMovement->IsCrouching(); | |
FString T = FString::Printf(TEXT("Crouched %i"), Crouched); | |
DisplayDebugManager.DrawString(T, Indent); | |
} | |
} | |
void AMMOCharacter::LaunchCharacter(FVector LaunchVelocity, bool bXYOverride, bool bZOverride) | |
{ | |
UE_LOG(LogCharacter, Verbose, TEXT("AMMOCharacter::LaunchCharacter '%s' (%f,%f,%f)"), *GetName(), LaunchVelocity.X, LaunchVelocity.Y, LaunchVelocity.Z); | |
if (CharacterMovement) | |
{ | |
FVector FinalVel = LaunchVelocity; | |
const FVector Velocity = GetVelocity(); | |
if (!bXYOverride) | |
{ | |
FinalVel.X += Velocity.X; | |
FinalVel.Y += Velocity.Y; | |
} | |
if (!bZOverride) | |
{ | |
FinalVel.Z += Velocity.Z; | |
} | |
CharacterMovement->Launch(FinalVel); | |
OnLaunched(LaunchVelocity, bXYOverride, bZOverride); | |
} | |
} | |
void AMMOCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PrevCustomMode) | |
{ | |
if (!bPressedJump || !CharacterMovement->IsFalling()) | |
{ | |
ResetJumpState(); | |
} | |
// Record jump force start time for proxies. Allows us to expire the jump even if not continually ticking down a timer. | |
if (bProxyIsJumpForceApplied && CharacterMovement->IsFalling()) | |
{ | |
ProxyJumpForceStartedTime = GetWorld()->GetTimeSeconds(); | |
} | |
K2_OnMovementModeChanged(PrevMovementMode, CharacterMovement->MovementMode, PrevCustomMode, CharacterMovement->CustomMovementMode); | |
MovementModeChangedDelegate.Broadcast(this, PrevMovementMode, PrevCustomMode); | |
} | |
/** Don't process landed notification if updating client position by replaying moves. | |
* Allow event to be called if Pawn was initially falling (before starting to replay moves), | |
* and this is going to cause him to land. . */ | |
bool AMMOCharacter::ShouldNotifyLanded(const FHitResult& Hit) | |
{ | |
if (bClientUpdating && !bClientWasFalling) | |
{ | |
return false; | |
} | |
// Just in case, only allow Landed() to be called once when replaying moves. | |
bClientWasFalling = false; | |
return true; | |
} | |
void AMMOCharacter::Jump() | |
{ | |
bPressedJump = true; | |
JumpKeyHoldTime = 0.0f; | |
} | |
void AMMOCharacter::StopJumping() | |
{ | |
bPressedJump = false; | |
ResetJumpState(); | |
} | |
void AMMOCharacter::CheckJumpInput(float DeltaTime) | |
{ | |
if (CharacterMovement) | |
{ | |
if (bPressedJump) | |
{ | |
// If this is the first jump and we're already falling, | |
// then increment the JumpCount to compensate. | |
const bool bFirstJump = JumpCurrentCount == 0; | |
if (bFirstJump && CharacterMovement->IsFalling()) | |
{ | |
JumpCurrentCount++; | |
} | |
const bool bDidJump = CanJump() && CharacterMovement->DoJump(bClientUpdating); | |
if (bDidJump) | |
{ | |
// Transition from not (actively) jumping to jumping. | |
if (!bWasJumping) | |
{ | |
JumpCurrentCount++; | |
JumpForceTimeRemaining = GetJumpMaxHoldTime(); | |
OnJumped(); | |
} | |
} | |
bWasJumping = bDidJump; | |
} | |
} | |
} | |
void AMMOCharacter::ClearJumpInput(float DeltaTime) | |
{ | |
if (bPressedJump) | |
{ | |
JumpKeyHoldTime += DeltaTime; | |
// Don't disable bPressedJump right away if it's still held. | |
// Don't modify JumpForceTimeRemaining because a frame of update may be remaining. | |
if (JumpKeyHoldTime >= GetJumpMaxHoldTime()) | |
{ | |
bPressedJump = false; | |
} | |
} | |
else | |
{ | |
JumpForceTimeRemaining = 0.0f; | |
bWasJumping = false; | |
} | |
} | |
float AMMOCharacter::GetJumpMaxHoldTime() const | |
{ | |
return JumpMaxHoldTime; | |
} | |
// | |
// Static variables for networking. | |
// | |
static uint8 SavedMovementMode; | |
void AMMOCharacter::PreNetReceive() | |
{ | |
SavedMovementMode = ReplicatedMovementMode; | |
Super::PreNetReceive(); | |
} | |
void AMMOCharacter::PostNetReceive() | |
{ | |
if (GetLocalRole() == ROLE_SimulatedProxy) | |
{ | |
CharacterMovement->bNetworkMovementModeChanged |= (SavedMovementMode != ReplicatedMovementMode); | |
CharacterMovement->bNetworkUpdateReceived |= CharacterMovement->bNetworkMovementModeChanged || CharacterMovement->bJustTeleported; | |
} | |
Super::PostNetReceive(); | |
} | |
void AMMOCharacter::OnRep_ReplicatedBasedMovement() | |
{ | |
if (GetLocalRole() != ROLE_SimulatedProxy) | |
{ | |
return; | |
} | |
// Skip base updates while playing root motion, it is handled inside of OnRep_RootMotion | |
if (IsPlayingNetworkedRootMotionMontage()) | |
{ | |
return; | |
} | |
CharacterMovement->bNetworkUpdateReceived = true; | |
TGuardValue<bool> bInBaseReplicationGuard(bInBaseReplication, true); | |
const bool bBaseChanged = (BasedMovement.MovementBase != ReplicatedBasedMovement.MovementBase || BasedMovement.BoneName != ReplicatedBasedMovement.BoneName); | |
if (bBaseChanged) | |
{ | |
// Even though we will copy the replicated based movement info, we need to use SetBase() to set up tick dependencies and trigger notifications. | |
SetBase(ReplicatedBasedMovement.MovementBase, ReplicatedBasedMovement.BoneName); | |
} | |
// Make sure to use the values of relative location/rotation etc from the server. | |
BasedMovement = ReplicatedBasedMovement; | |
if (ReplicatedBasedMovement.HasRelativeLocation()) | |
{ | |
// Update transform relative to movement base | |
const FVector OldLocation = GetActorLocation(); | |
const FQuat OldRotation = GetActorQuat(); | |
MMOMovementBaseUtility::GetMovementBaseTransform(ReplicatedBasedMovement.MovementBase, ReplicatedBasedMovement.BoneName, CharacterMovement->OldBaseLocation, CharacterMovement->OldBaseQuat); | |
const FVector NewLocation = CharacterMovement->OldBaseLocation + ReplicatedBasedMovement.Location; | |
FRotator NewRotation; | |
if (ReplicatedBasedMovement.HasRelativeRotation()) | |
{ | |
// Relative location, relative rotation | |
NewRotation = (FRotationMatrix(ReplicatedBasedMovement.Rotation) * FQuatRotationMatrix(CharacterMovement->OldBaseQuat)).Rotator(); | |
if (CharacterMovement->ShouldRemainVertical()) | |
{ | |
NewRotation.Pitch = 0.f; | |
NewRotation.Roll = 0.f; | |
} | |
} | |
else | |
{ | |
// Relative location, absolute rotation | |
NewRotation = ReplicatedBasedMovement.Rotation; | |
} | |
// When position or base changes, movement mode will need to be updated. This assumes rotation changes don't affect that. | |
CharacterMovement->bJustTeleported |= (bBaseChanged || NewLocation != OldLocation); | |
CharacterMovement->bNetworkSmoothingComplete = false; | |
CharacterMovement->SmoothCorrection(OldLocation, OldRotation, NewLocation, NewRotation.Quaternion()); | |
OnUpdateSimulatedPosition(OldLocation, OldRotation); | |
} | |
} | |
void AMMOCharacter::OnRep_ReplicatedMovement() | |
{ | |
if (CharacterMovement && (CharacterMovement->NetworkSmoothingMode == ENetworkSmoothingMode::Replay)) | |
{ | |
return; | |
} | |
// Skip standard position correction if we are playing root motion, OnRep_RootMotion will handle it. | |
if (!IsPlayingNetworkedRootMotionMontage()) // animation root motion | |
{ | |
if (!CharacterMovement || !CharacterMovement->CurrentRootMotion.HasActiveRootMotionSources()) // root motion sources | |
{ | |
Super::OnRep_ReplicatedMovement(); | |
} | |
} | |
} | |
void AMMOCharacter::OnRep_ReplayLastTransformUpdateTimeStamp() | |
{ | |
ReplicatedServerLastTransformUpdateTimeStamp = ReplayLastTransformUpdateTimeStamp; | |
} | |
/** Get FAnimMontageInstance playing RootMotion */ | |
FAnimMontageInstance* AMMOCharacter::GetRootMotionAnimMontageInstance() const | |
{ | |
return (Mesh && Mesh->GetAnimInstance()) ? Mesh->GetAnimInstance()->GetRootMotionMontageInstance() : nullptr; | |
} | |
void AMMOCharacter::OnRep_RootMotion() | |
{ | |
if (CharacterMovement && (CharacterMovement->NetworkSmoothingMode == ENetworkSmoothingMode::Replay)) | |
{ | |
return; | |
} | |
if (GetLocalRole() == ROLE_SimulatedProxy) | |
{ | |
UE_LOG(LogRootMotion, Log, TEXT("AMMOCharacter::OnRep_RootMotion")); | |
// Save received move in queue, we'll try to use it during Tick(). | |
if (RepRootMotion.bIsActive) | |
{ | |
// Add new move | |
RootMotionRepMoves.AddZeroed(1); | |
FMMOSimulatedRootMotionReplicatedMove& NewMove = RootMotionRepMoves.Last(); | |
NewMove.RootMotion = RepRootMotion; | |
NewMove.Time = GetWorld()->GetTimeSeconds(); | |
} | |
else | |
{ | |
// Clear saved moves. | |
RootMotionRepMoves.Empty(); | |
} | |
if (CharacterMovement) | |
{ | |
CharacterMovement->bNetworkUpdateReceived = true; | |
} | |
} | |
} | |
void AMMOCharacter::SimulatedRootMotionPositionFixup(float DeltaSeconds) | |
{ | |
const FAnimMontageInstance* ClientMontageInstance = GetRootMotionAnimMontageInstance(); | |
if (ClientMontageInstance && CharacterMovement && Mesh) | |
{ | |
// Find most recent buffered move that we can use. | |
const int32 MoveIndex = FindRootMotionRepMove(*ClientMontageInstance); | |
if (MoveIndex != INDEX_NONE) | |
{ | |
const FVector OldLocation = GetActorLocation(); | |
const FQuat OldRotation = GetActorQuat(); | |
// Move Actor back to position of that buffered move. (server replicated position). | |
const FMMOSimulatedRootMotionReplicatedMove& RootMotionRepMove = RootMotionRepMoves[MoveIndex]; | |
if (RestoreReplicatedMove(RootMotionRepMove)) | |
{ | |
const float ServerPosition = RootMotionRepMove.RootMotion.Position; | |
const float ClientPosition = ClientMontageInstance->GetPosition(); | |
const float DeltaPosition = (ClientPosition - ServerPosition); | |
if (FMath::Abs(DeltaPosition) > KINDA_SMALL_NUMBER) | |
{ | |
// Find Root Motion delta move to get back to where we were on the client. | |
const FTransform LocalRootMotionTransform = ClientMontageInstance->Montage->ExtractRootMotionFromTrackRange(ServerPosition, ClientPosition); | |
// Simulate Root Motion for delta move. | |
if (CharacterMovement) | |
{ | |
const float MontagePlayRate = ClientMontageInstance->GetPlayRate(); | |
// Guess time it takes for this delta track position, so we can get falling physics accurate. | |
if (!FMath::IsNearlyZero(MontagePlayRate)) | |
{ | |
const float DeltaTime = DeltaPosition / MontagePlayRate; | |
// Even with negative playrate deltatime should be positive. | |
check(DeltaTime > 0.f); | |
CharacterMovement->SimulateRootMotion(DeltaTime, LocalRootMotionTransform); | |
// After movement correction, smooth out error in position if any. | |
const FVector NewLocation = GetActorLocation(); | |
CharacterMovement->bNetworkSmoothingComplete = false; | |
CharacterMovement->bJustTeleported |= (OldLocation != NewLocation); | |
CharacterMovement->SmoothCorrection(OldLocation, OldRotation, NewLocation, GetActorQuat()); | |
} | |
} | |
} | |
} | |
// Delete this move and any prior one, we don't need them anymore. | |
UE_LOG(LogRootMotion, Log, TEXT("\tClearing old moves (%d)"), MoveIndex + 1); | |
RootMotionRepMoves.RemoveAt(0, MoveIndex + 1); | |
} | |
} | |
} | |
int32 AMMOCharacter::FindRootMotionRepMove(const FAnimMontageInstance& ClientMontageInstance) const | |
{ | |
int32 FoundIndex = INDEX_NONE; | |
// Start with most recent move and go back in time to find a usable move. | |
for (int32 MoveIndex = RootMotionRepMoves.Num() - 1; MoveIndex >= 0; MoveIndex--) | |
{ | |
if (CanUseRootMotionRepMove(RootMotionRepMoves[MoveIndex], ClientMontageInstance)) | |
{ | |
FoundIndex = MoveIndex; | |
break; | |
} | |
} | |
UE_LOG(LogRootMotion, Log, TEXT("\tAMMOCharacter::FindRootMotionRepMove FoundIndex: %d, NumSavedMoves: %d"), FoundIndex, RootMotionRepMoves.Num()); | |
return FoundIndex; | |
} | |
bool AMMOCharacter::CanUseRootMotionRepMove(const FMMOSimulatedRootMotionReplicatedMove& RootMotionRepMove, const FAnimMontageInstance& ClientMontageInstance) const | |
{ | |
// Ignore outdated moves. | |
if (GetWorld()->TimeSince(RootMotionRepMove.Time) <= 0.5f) | |
{ | |
// Make sure montage being played matched between client and server. | |
if (RootMotionRepMove.RootMotion.AnimMontage && (RootMotionRepMove.RootMotion.AnimMontage == ClientMontageInstance.Montage)) | |
{ | |
UAnimMontage* AnimMontage = ClientMontageInstance.Montage; | |
const float ServerPosition = RootMotionRepMove.RootMotion.Position; | |
const float ClientPosition = ClientMontageInstance.GetPosition(); | |
const float DeltaPosition = (ClientPosition - ServerPosition); | |
const int32 CurrentSectionIndex = AnimMontage->GetSectionIndexFromPosition(ClientPosition); | |
if (CurrentSectionIndex != INDEX_NONE) | |
{ | |
const int32 NextSectionIndex = ClientMontageInstance.GetNextSectionID(CurrentSectionIndex); | |
// We can only extract root motion if we are within the same section. | |
// It's not trivial to jump through sections in a deterministic manner, but that is luckily not frequent. | |
const bool bSameSections = (AnimMontage->GetSectionIndexFromPosition(ServerPosition) == CurrentSectionIndex); | |
// if we are looping and just wrapped over, skip. That's also not easy to handle and not frequent. | |
const bool bHasLooped = (NextSectionIndex == CurrentSectionIndex) && (FMath::Abs(DeltaPosition) > (AnimMontage->GetSectionLength(CurrentSectionIndex) / 2.f)); | |
// Can only simulate forward in time, so we need to make sure server move is not ahead of the client. | |
const bool bServerAheadOfClient = ((DeltaPosition * ClientMontageInstance.GetPlayRate()) < 0.f); | |
UE_LOG(LogRootMotion, Log, TEXT("\t\tAMMOCharacter::CanUseRootMotionRepMove ServerPosition: %.3f, ClientPosition: %.3f, DeltaPosition: %.3f, bSameSections: %d, bHasLooped: %d, bServerAheadOfClient: %d"), | |
ServerPosition, ClientPosition, DeltaPosition, bSameSections, bHasLooped, bServerAheadOfClient); | |
return bSameSections && !bHasLooped && !bServerAheadOfClient; | |
} | |
} | |
} | |
return false; | |
} | |
bool AMMOCharacter::RestoreReplicatedMove(const FMMOSimulatedRootMotionReplicatedMove& RootMotionRepMove) | |
{ | |
UPrimitiveComponent* ServerBase = RootMotionRepMove.RootMotion.MovementBase; | |
const FName ServerBaseBoneName = RootMotionRepMove.RootMotion.MovementBaseBoneName; | |
// Relative Position | |
if (RootMotionRepMove.RootMotion.bRelativePosition) | |
{ | |
bool bSuccess = false; | |
if (MMOMovementBaseUtility::UseRelativeLocation(ServerBase)) | |
{ | |
FVector BaseLocation; | |
FQuat BaseRotation; | |
MMOMovementBaseUtility::GetMovementBaseTransform(ServerBase, ServerBaseBoneName, BaseLocation, BaseRotation); | |
const FVector ServerLocation = BaseLocation + RootMotionRepMove.RootMotion.Location; | |
FRotator ServerRotation; | |
if (RootMotionRepMove.RootMotion.bRelativeRotation) | |
{ | |
// Relative rotation | |
ServerRotation = (FRotationMatrix(RootMotionRepMove.RootMotion.Rotation) * FQuatRotationTranslationMatrix(BaseRotation, FVector::ZeroVector)).Rotator(); | |
} | |
else | |
{ | |
// Absolute rotation | |
ServerRotation = RootMotionRepMove.RootMotion.Rotation; | |
} | |
SetActorLocationAndRotation(ServerLocation, ServerRotation); | |
bSuccess = true; | |
} | |
// If we received local space position, but can't resolve parent, then move can't be used. :( | |
if (!bSuccess) | |
{ | |
return false; | |
} | |
} | |
// Absolute position | |
else | |
{ | |
FVector LocalLocation = FRepMovement::RebaseOntoLocalOrigin(RootMotionRepMove.RootMotion.Location, this); | |
SetActorLocationAndRotation(LocalLocation, RootMotionRepMove.RootMotion.Rotation); | |
} | |
CharacterMovement->bJustTeleported = true; | |
SetBase(ServerBase, ServerBaseBoneName); | |
return true; | |
} | |
void AMMOCharacter::OnUpdateSimulatedPosition(const FVector& OldLocation, const FQuat& OldRotation) | |
{ | |
SCOPE_CYCLE_COUNTER(STAT_MMO_CharacterOnNetUpdateSimulatedPosition); | |
bSimGravityDisabled = false; | |
const bool bLocationChanged = (OldLocation != GetActorLocation()); | |
if (bClientCheckEncroachmentOnNetUpdate) | |
{ | |
// Only need to check for encroachment when teleported without any velocity. | |
// Normal movement pops the character out of geometry anyway, no use doing it before and after (with different rules). | |
// Always consider Location as changed if we were spawned this tick as in that case our replicated Location was set as part of spawning, before PreNetReceive() | |
if (CharacterMovement->Velocity.IsZero() && (bLocationChanged || CreationTime == GetWorld()->TimeSeconds)) | |
{ | |
if (GetWorld()->EncroachingBlockingGeometry(this, GetActorLocation(), GetActorRotation())) | |
{ | |
bSimGravityDisabled = true; | |
} | |
} | |
} | |
CharacterMovement->bJustTeleported |= bLocationChanged; | |
CharacterMovement->bNetworkUpdateReceived = true; | |
} | |
void AMMOCharacter::PostNetReceiveLocationAndRotation() | |
{ | |
if (GetLocalRole() == ROLE_SimulatedProxy) | |
{ | |
// Don't change transform if using relative position (it should be nearly the same anyway, or base may be slightly out of sync) | |
if (!ReplicatedBasedMovement.HasRelativeLocation()) | |
{ | |
const FRepMovement& ConstRepMovement = GetReplicatedMovement(); | |
const FVector OldLocation = GetActorLocation(); | |
const FVector NewLocation = FRepMovement::RebaseOntoLocalOrigin(ConstRepMovement.Location, this); | |
const FQuat OldRotation = GetActorQuat(); | |
CharacterMovement->bNetworkSmoothingComplete = false; | |
CharacterMovement->bJustTeleported |= (OldLocation != NewLocation); | |
CharacterMovement->SmoothCorrection(OldLocation, OldRotation, NewLocation, ConstRepMovement.Rotation.Quaternion()); | |
OnUpdateSimulatedPosition(OldLocation, OldRotation); | |
} | |
CharacterMovement->bNetworkUpdateReceived = true; | |
} | |
} | |
void AMMOCharacter::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) | |
{ | |
Super::PreReplication(ChangedPropertyTracker); | |
if (CharacterMovement->CurrentRootMotion.HasActiveRootMotionSources() || IsPlayingNetworkedRootMotionMontage()) | |
{ | |
const FAnimMontageInstance* RootMotionMontageInstance = GetRootMotionAnimMontageInstance(); | |
RepRootMotion.bIsActive = true; | |
// Is position stored in local space? | |
RepRootMotion.bRelativePosition = BasedMovement.HasRelativeLocation(); | |
RepRootMotion.bRelativeRotation = BasedMovement.HasRelativeRotation(); | |
RepRootMotion.Location = RepRootMotion.bRelativePosition ? BasedMovement.Location : FRepMovement::RebaseOntoZeroOrigin(GetActorLocation(), GetWorld()->OriginLocation); | |
RepRootMotion.Rotation = RepRootMotion.bRelativeRotation ? BasedMovement.Rotation : GetActorRotation(); | |
RepRootMotion.MovementBase = BasedMovement.MovementBase; | |
RepRootMotion.MovementBaseBoneName = BasedMovement.BoneName; | |
if (RootMotionMontageInstance) | |
{ | |
RepRootMotion.AnimMontage = RootMotionMontageInstance->Montage; | |
RepRootMotion.Position = RootMotionMontageInstance->GetPosition(); | |
} | |
else | |
{ | |
RepRootMotion.AnimMontage = nullptr; | |
} | |
RepRootMotion.AuthoritativeRootMotion = CharacterMovement->CurrentRootMotion; | |
RepRootMotion.Acceleration = CharacterMovement->GetCurrentAcceleration(); | |
RepRootMotion.LinearVelocity = CharacterMovement->Velocity; | |
DOREPLIFETIME_ACTIVE_OVERRIDE(AMMOCharacter, RepRootMotion, true); | |
} | |
else | |
{ | |
RepRootMotion.Clear(); | |
DOREPLIFETIME_ACTIVE_OVERRIDE(AMMOCharacter, RepRootMotion, false); | |
} | |
bProxyIsJumpForceApplied = (JumpForceTimeRemaining > 0.0f); | |
ReplicatedMovementMode = CharacterMovement->PackNetworkMovementMode(); | |
ReplicatedBasedMovement = BasedMovement; | |
// Optimization: only update and replicate these values if they are actually going to be used. | |
if (BasedMovement.HasRelativeLocation()) | |
{ | |
// When velocity becomes zero, force replication so the position is updated to match the server (it may have moved due to simulation on the client). | |
ReplicatedBasedMovement.bServerHasVelocity = !CharacterMovement->Velocity.IsZero(); | |
// Make sure absolute rotations are updated in case rotation occurred after the base info was saved. | |
if (!BasedMovement.HasRelativeRotation()) | |
{ | |
ReplicatedBasedMovement.Rotation = GetActorRotation(); | |
} | |
} | |
// Save bandwidth by not replicating this value unless it is necessary, since it changes every update. | |
if ((CharacterMovement->NetworkSmoothingMode == ENetworkSmoothingMode::Linear) || CharacterMovement->bNetworkAlwaysReplicateTransformUpdateTimestamp) | |
{ | |
ReplicatedServerLastTransformUpdateTimeStamp = CharacterMovement->GetServerLastTransformUpdateTimeStamp(); | |
} | |
else | |
{ | |
ReplicatedServerLastTransformUpdateTimeStamp = 0.f; | |
} | |
} | |
void AMMOCharacter::PreReplicationForReplay(IRepChangedPropertyTracker& ChangedPropertyTracker) | |
{ | |
Super::PreReplicationForReplay(ChangedPropertyTracker); | |
const UWorld* World = GetWorld(); | |
if (World) | |
{ | |
// On client replays, our view pitch will be set to 0 as by default we do not replicate | |
// pitch for owners, just for simulated. So instead push our rotation into the sampler | |
if (World->IsRecordingClientReplay() && Controller != nullptr && GetLocalRole() == ROLE_AutonomousProxy && GetNetMode() == NM_Client) | |
{ | |
SetRemoteViewPitch(Controller->GetControlRotation().Pitch); | |
} | |
ReplayLastTransformUpdateTimeStamp = World->GetTimeSeconds(); | |
} | |
} | |
void AMMOCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const | |
{ | |
Super::GetLifetimeReplicatedProps(OutLifetimeProps); | |
DISABLE_REPLICATED_PROPERTY(AMMOCharacter, JumpMaxHoldTime); | |
DISABLE_REPLICATED_PROPERTY(AMMOCharacter, JumpMaxCount); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, RepRootMotion, COND_SimulatedOnly); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, ReplicatedBasedMovement, COND_SimulatedOnly); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, ReplicatedServerLastTransformUpdateTimeStamp, COND_SimulatedOnlyNoReplay); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, ReplicatedMovementMode, COND_SimulatedOnly); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, bIsCrouched, COND_SimulatedOnly); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, bProxyIsJumpForceApplied, COND_SimulatedOnly); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, AnimRootMotionTranslationScale, COND_SimulatedOnly); | |
DOREPLIFETIME_CONDITION(AMMOCharacter, ReplayLastTransformUpdateTimeStamp, COND_ReplayOnly); | |
} | |
bool AMMOCharacter::IsPlayingRootMotion() const | |
{ | |
if (Mesh) | |
{ | |
return Mesh->IsPlayingRootMotion(); | |
} | |
return false; | |
} | |
bool AMMOCharacter::HasAnyRootMotion() const | |
{ | |
return CharacterMovement ? CharacterMovement->HasRootMotionSources() : false; | |
} | |
bool AMMOCharacter::IsPlayingNetworkedRootMotionMontage() const | |
{ | |
if (Mesh) | |
{ | |
return Mesh->IsPlayingNetworkedRootMotionMontage(); | |
} | |
return false; | |
} | |
void AMMOCharacter::SetAnimRootMotionTranslationScale(float InAnimRootMotionTranslationScale) | |
{ | |
AnimRootMotionTranslationScale = InAnimRootMotionTranslationScale; | |
} | |
float AMMOCharacter::GetAnimRootMotionTranslationScale() const | |
{ | |
return AnimRootMotionTranslationScale; | |
} | |
float AMMOCharacter::PlayAnimMontage(class UAnimMontage* AnimMontage, float InPlayRate, FName StartSectionName) | |
{ | |
UAnimInstance* AnimInstance = (Mesh) ? Mesh->GetAnimInstance() : nullptr; | |
if (AnimMontage && AnimInstance) | |
{ | |
float const Duration = AnimInstance->Montage_Play(AnimMontage, InPlayRate); | |
if (Duration > 0.f) | |
{ | |
// Start at a given Section. | |
if (StartSectionName != NAME_None) | |
{ | |
AnimInstance->Montage_JumpToSection(StartSectionName, AnimMontage); | |
} | |
return Duration; | |
} | |
} | |
return 0.f; | |
} | |
void AMMOCharacter::StopAnimMontage(class UAnimMontage* AnimMontage) | |
{ | |
UAnimInstance* AnimInstance = (Mesh) ? Mesh->GetAnimInstance() : nullptr; | |
UAnimMontage* MontageToStop = (AnimMontage) ? AnimMontage : GetCurrentMontage(); | |
bool bShouldStopMontage = AnimInstance && MontageToStop && !AnimInstance->Montage_GetIsStopped(MontageToStop); | |
if (bShouldStopMontage) | |
{ | |
AnimInstance->Montage_Stop(MontageToStop->BlendOut.GetBlendTime(), MontageToStop); | |
} | |
} | |
class UAnimMontage* AMMOCharacter::GetCurrentMontage() | |
{ | |
UAnimInstance* AnimInstance = (Mesh) ? Mesh->GetAnimInstance() : nullptr; | |
if (AnimInstance) | |
{ | |
return AnimInstance->GetCurrentActiveMontage(); | |
} | |
return nullptr; | |
} | |
void AMMOCharacter::ClientCheatWalk_Implementation() | |
{ | |
#if !UE_BUILD_SHIPPING | |
SetActorEnableCollision(true); | |
if (CharacterMovement) | |
{ | |
CharacterMovement->bCheatFlying = false; | |
CharacterMovement->SetMovementMode(MOVE_Falling); | |
} | |
#endif | |
} | |
void AMMOCharacter::ClientCheatFly_Implementation() | |
{ | |
#if !UE_BUILD_SHIPPING | |
SetActorEnableCollision(true); | |
if (CharacterMovement) | |
{ | |
CharacterMovement->bCheatFlying = true; | |
CharacterMovement->SetMovementMode(MOVE_Flying); | |
} | |
#endif | |
} | |
void AMMOCharacter::ClientCheatGhost_Implementation() | |
{ | |
#if !UE_BUILD_SHIPPING | |
SetActorEnableCollision(false); | |
if (CharacterMovement) | |
{ | |
CharacterMovement->bCheatFlying = true; | |
CharacterMovement->SetMovementMode(MOVE_Flying); | |
} | |
#endif | |
} | |
void AMMOCharacter::RootMotionDebugClientPrintOnScreen_Implementation(const FString& InString) | |
{ | |
#if ROOT_MOTION_DEBUG | |
RootMotionSourceDebug::PrintOnScreenServerMsg(InString); | |
#endif | |
} | |
// ServerMove | |
void AMMOCharacter::ServerMove_Implementation(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
GetCharacterMovement()->ServerMove_Implementation(TimeStamp, InAccel, ClientLoc, CompressedMoveFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
bool AMMOCharacter::ServerMove_Validate(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
return GetCharacterMovement()->ServerMove_Validate(TimeStamp, InAccel, ClientLoc, CompressedMoveFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
// ServerMoveNoBase | |
void AMMOCharacter::ServerMoveNoBase_Implementation(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode) | |
{ | |
GetCharacterMovement()->ServerMove_Implementation(TimeStamp, InAccel, ClientLoc, CompressedMoveFlags, ClientRoll, View, /*ClientMovementBase=*/ nullptr, /*ClientBaseBoneName=*/ NAME_None, ClientMovementMode); | |
} | |
bool AMMOCharacter::ServerMoveNoBase_Validate(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode) | |
{ | |
return GetCharacterMovement()->ServerMove_Validate(TimeStamp, InAccel, ClientLoc, CompressedMoveFlags, ClientRoll, View, /*ClientMovementBase=*/ nullptr, /*ClientBaseBoneName=*/ NAME_None, ClientMovementMode); | |
} | |
// ServerMoveDual | |
void AMMOCharacter::ServerMoveDual_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
GetCharacterMovement()->ServerMoveDual_Implementation(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
bool AMMOCharacter::ServerMoveDual_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
return GetCharacterMovement()->ServerMoveDual_Validate(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
// ServerMoveDualNoBase | |
void AMMOCharacter::ServerMoveDualNoBase_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode) | |
{ | |
GetCharacterMovement()->ServerMoveDual_Implementation(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, /*ClientMovementBase=*/ nullptr, /*ClientBaseBoneName=*/ NAME_None, ClientMovementMode); | |
} | |
bool AMMOCharacter::ServerMoveDualNoBase_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode) | |
{ | |
return GetCharacterMovement()->ServerMoveDual_Validate(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, /*ClientMovementBase=*/ nullptr, /*ClientBaseBoneName=*/ NAME_None, ClientMovementMode); | |
} | |
// ServerMoveDualHybridRootMotion | |
void AMMOCharacter::ServerMoveDualHybridRootMotion_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
GetCharacterMovement()->ServerMoveDualHybridRootMotion_Implementation(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
bool AMMOCharacter::ServerMoveDualHybridRootMotion_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
return GetCharacterMovement()->ServerMoveDualHybridRootMotion_Validate(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
// ServerMoveOld | |
void AMMOCharacter::ServerMoveOld_Implementation(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags) | |
{ | |
GetCharacterMovement()->ServerMoveOld_Implementation(OldTimeStamp, OldAccel, OldMoveFlags); | |
} | |
bool AMMOCharacter::ServerMoveOld_Validate(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags) | |
{ | |
return GetCharacterMovement()->ServerMoveOld_Validate(OldTimeStamp, OldAccel, OldMoveFlags); | |
} | |
// ClientAckGoodMove | |
void AMMOCharacter::ClientAckGoodMove_Implementation(float TimeStamp) | |
{ | |
GetCharacterMovement()->ClientAckGoodMove_Implementation(TimeStamp); | |
} | |
// ClientAdjustPosition | |
void AMMOCharacter::ClientAdjustPosition_Implementation(float TimeStamp, FVector NewLoc, FVector NewVel, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
GetCharacterMovement()->ClientAdjustPosition_Implementation(TimeStamp, NewLoc, NewVel, NewBase, NewBaseBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
// ClientVeryShortAdjustPosition | |
void AMMOCharacter::ClientVeryShortAdjustPosition_Implementation(float TimeStamp, FVector NewLoc, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
GetCharacterMovement()->ClientVeryShortAdjustPosition_Implementation(TimeStamp, NewLoc, NewBase, NewBaseBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
// ClientAdjustRootMotionPosition | |
void AMMOCharacter::ClientAdjustRootMotionPosition_Implementation(float TimeStamp, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
GetCharacterMovement()->ClientAdjustRootMotionPosition_Implementation(TimeStamp, ServerMontageTrackPosition, ServerLoc, ServerRotation, ServerVelZ, ServerBase, ServerBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
// ClientAdjustRootMotionSourcePosition | |
void AMMOCharacter::ClientAdjustRootMotionSourcePosition_Implementation(float TimeStamp, FRootMotionSourceGroup ServerRootMotion, bool bHasAnimRootMotion, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
GetCharacterMovement()->ClientAdjustRootMotionSourcePosition_Implementation(TimeStamp, ServerRootMotion, bHasAnimRootMotion, ServerMontageTrackPosition, ServerLoc, ServerRotation, ServerVelZ, ServerBase, ServerBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Epic Games, Inc. All Rights Reserved. | |
#pragma once | |
#include "CoreMinimal.h" | |
#include "UObject/ObjectMacros.h" | |
#include "UObject/UObjectGlobals.h" | |
#include "Templates/SubclassOf.h" | |
#include "UObject/CoreNet.h" | |
#include "Engine/NetSerialization.h" | |
#include "Engine/EngineTypes.h" | |
#include "Components/ActorComponent.h" | |
#include "GameFramework/Actor.h" | |
#include "GameFramework/Pawn.h" | |
#include "Animation/AnimationAsset.h" | |
#include "GameFramework/RootMotionSource.h" | |
#include "BaseUnit.h" | |
#include "MMOCharacter.generated.h" | |
class AController; | |
class FDebugDisplayInfo; | |
class UAnimMontage; | |
class UArrowComponent; | |
class UCapsuleComponent; | |
class UMMOPlayerMovement; | |
class UPawnMovementComponent; | |
class UPrimitiveComponent; | |
class USkeletalMeshComponent; | |
struct FAnimMontageInstance; | |
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FMMOMovementModeChangedSignature, class AMMOCharacter*, Character, EMovementMode, PrevMovementMode, uint8, PreviousCustomMode); | |
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FMMOCharacterMovementUpdatedSignature, float, DeltaSeconds, FVector, OldLocation, FVector, OldVelocity); | |
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMMOCharacterReachedApexSignature); | |
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMMOLandedSignature, const FHitResult&, Hit); | |
/** Replicated data when playing a root motion montage. */ | |
USTRUCT() | |
struct FMMORepRootMotionMontage | |
{ | |
GENERATED_USTRUCT_BODY() | |
/** Whether this has useful/active data. */ | |
UPROPERTY() | |
bool bIsActive; | |
/** AnimMontage providing Root Motion */ | |
UPROPERTY() | |
UAnimMontage* AnimMontage; | |
/** Track position of Montage */ | |
UPROPERTY() | |
float Position; | |
/** Location */ | |
UPROPERTY() | |
FVector_NetQuantize100 Location; | |
/** Rotation */ | |
UPROPERTY() | |
FRotator Rotation; | |
/** Movement Relative to Base */ | |
UPROPERTY() | |
UPrimitiveComponent* MovementBase; | |
/** Bone on the MovementBase, if a skeletal mesh. */ | |
UPROPERTY() | |
FName MovementBaseBoneName; | |
/** Additional replicated flag, if MovementBase can't be resolved on the client. So we don't use wrong data. */ | |
UPROPERTY() | |
bool bRelativePosition; | |
/** Whether rotation is relative or absolute. */ | |
UPROPERTY() | |
bool bRelativeRotation; | |
/** State of Root Motion Sources on Authority */ | |
UPROPERTY() | |
FRootMotionSourceGroup AuthoritativeRootMotion; | |
/** Acceleration */ | |
UPROPERTY() | |
FVector_NetQuantize10 Acceleration; | |
/** Velocity */ | |
UPROPERTY() | |
FVector_NetQuantize10 LinearVelocity; | |
/** Clear root motion sources and root motion montage */ | |
void Clear() | |
{ | |
bIsActive = false; | |
AnimMontage = nullptr; | |
AuthoritativeRootMotion.Clear(); | |
} | |
/** Is Valid - animation root motion only */ | |
bool HasRootMotion() const | |
{ | |
return (AnimMontage != nullptr); | |
} | |
}; | |
USTRUCT() | |
struct FMMOSimulatedRootMotionReplicatedMove | |
{ | |
GENERATED_USTRUCT_BODY() | |
/** Local time when move was received on client and saved. */ | |
UPROPERTY() | |
float Time; | |
/** Root Motion information */ | |
UPROPERTY() | |
FMMORepRootMotionMontage RootMotion; | |
}; | |
/** MMOMovementBaseUtility provides utilities for working with movement bases, for which we may need relative positioning info. */ | |
namespace MMOMovementBaseUtility | |
{ | |
/** Determine whether MovementBase can possibly move. */ | |
MMOEY_API bool IsDynamicBase(const UPrimitiveComponent* MovementBase); | |
/** Determine whether MovementBase is simulating or attached to a simulating object. */ | |
MMOEY_API bool IsSimulatedBase(const UPrimitiveComponent* MovementBase); | |
/** Determine if we should use relative positioning when based on a component (because it may move). */ | |
FORCEINLINE bool UseRelativeLocation(const UPrimitiveComponent* MovementBase) | |
{ | |
return IsDynamicBase(MovementBase); | |
} | |
/** Ensure that BasedObjectTick ticks after NewBase */ | |
MMOEY_API void AddTickDependency(FTickFunction& BasedObjectTick, UPrimitiveComponent* NewBase); | |
/** Remove tick dependency of BasedObjectTick on OldBase */ | |
MMOEY_API void RemoveTickDependency(FTickFunction& BasedObjectTick, UPrimitiveComponent* OldBase); | |
/** Get the velocity of the given component, first checking the ComponentVelocity and falling back to the physics velocity if necessary. */ | |
MMOEY_API FVector GetMovementBaseVelocity(const UPrimitiveComponent* MovementBase, const FName BoneName); | |
/** Get the tangential velocity at WorldLocation for the given component. */ | |
MMOEY_API FVector GetMovementBaseTangentialVelocity(const UPrimitiveComponent* MovementBase, const FName BoneName, const FVector& WorldLocation); | |
/** Get the transforms for the given MovementBase, optionally at the location of a bone. Returns false if MovementBase is nullptr, or if BoneName is not a valid bone. */ | |
MMOEY_API bool GetMovementBaseTransform(const UPrimitiveComponent* MovementBase, const FName BoneName, FVector& OutLocation, FQuat& OutQuat); | |
} | |
/** Struct to hold information about the "base" object the character is standing on. */ | |
USTRUCT() | |
struct FMMOBasedMovementInfo | |
{ | |
GENERATED_USTRUCT_BODY() | |
/** Component we are based on */ | |
UPROPERTY() | |
UPrimitiveComponent* MovementBase; | |
/** Bone name on component, for skeletal meshes. NAME_None if not a skeletal mesh or if bone is invalid. */ | |
UPROPERTY() | |
FName BoneName; | |
/** Location relative to MovementBase. Only valid if HasRelativeLocation() is true. */ | |
UPROPERTY() | |
FVector_NetQuantize100 Location; | |
/** Rotation: relative to MovementBase if HasRelativeRotation() is true, absolute otherwise. */ | |
UPROPERTY() | |
FRotator Rotation; | |
/** Whether the server says that there is a base. On clients, the component may not have resolved yet. */ | |
UPROPERTY() | |
bool bServerHasBaseComponent; | |
/** Whether rotation is relative to the base or absolute. It can only be relative if location is also relative. */ | |
UPROPERTY() | |
bool bRelativeRotation; | |
/** Whether there is a velocity on the server. Used for forcing replication when velocity goes to zero. */ | |
UPROPERTY() | |
bool bServerHasVelocity; | |
/** Is location relative? */ | |
FORCEINLINE bool HasRelativeLocation() const | |
{ | |
return MMOMovementBaseUtility::UseRelativeLocation(MovementBase); | |
} | |
/** Is rotation relative or absolute? It can only be relative if location is also relative. */ | |
FORCEINLINE bool HasRelativeRotation() const | |
{ | |
return bRelativeRotation && HasRelativeLocation(); | |
} | |
/** Return true if the client should have MovementBase, but it hasn't replicated (possibly component has not streamed in). */ | |
FORCEINLINE bool IsBaseUnresolved() const | |
{ | |
return (MovementBase == nullptr) && bServerHasBaseComponent; | |
} | |
}; | |
/** | |
* Characters are Pawns that have a mesh, collision, and built-in movement logic. | |
* They are responsible for all physical interaction between the player or AI and the world, and also implement basic networking and input models. | |
* They are designed for a vertically-oriented player representation that can walk, jump, fly, and swim through the world using CharacterMovementComponent. | |
* | |
* @see APawn, UMMOPlayerMovementComponent | |
* @see https://docs.unrealengine.com/latest/INT/Gameplay/Framework/Pawn/Character/ | |
*/ | |
UCLASS(config = Game, BlueprintType, meta = (ShortTooltip = "A character is a type of Pawn that includes the ability to walk around.")) | |
class MMOEY_API AMMOCharacter : public ABaseUnit | |
{ | |
GENERATED_BODY() | |
public: | |
/** Default UObject constructor. */ | |
AMMOCharacter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); | |
void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; | |
private: | |
/** The main skeletal mesh associated with this Character (optional sub-object). */ | |
UPROPERTY(Category = Character, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) | |
USkeletalMeshComponent* Mesh; | |
/** Movement component used for movement logic in various movement modes (walking, falling, etc), containing relevant settings and functions to control movement. */ | |
UPROPERTY(Category = Character, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) | |
UMMOPlayerMovement* CharacterMovement; | |
/** The CapsuleComponent being used for movement collision (by CharacterMovement). Always treated as being vertically aligned in simple collision check functions. */ | |
UPROPERTY(Category = Character, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) | |
UCapsuleComponent* CapsuleComponent; | |
#if WITH_EDITORONLY_DATA | |
/** Component shown in the editor only to indicate character facing */ | |
UPROPERTY() | |
UArrowComponent* ArrowComponent; | |
#endif | |
public: | |
////////////////////////////////////////////////////////////////////////// | |
// Server RPCs that pass through to CharacterMovement (avoids RPC overhead for components). | |
// The base RPC function (eg 'ServerMove') is auto-generated for clients to trigger the call to the server function, | |
// eventually going to the _Implementation function (which we just pass to the CharacterMovementComponent). | |
////////////////////////////////////////////////////////////////////////// | |
/** Replicated function sent by client to server - contains client movement and view info. */ | |
UFUNCTION(unreliable, server, WithValidation) | |
void ServerMove(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
void ServerMove_Implementation(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
bool ServerMove_Validate(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/** | |
* Replicated function sent by client to server. Saves bandwidth over ServerMove() by implying that ClientMovementBase and ClientBaseBoneName are null. | |
* Passes through to CharacterMovement->ServerMove_Implementation() with null base params. | |
*/ | |
UFUNCTION(unreliable, server, WithValidation) | |
void ServerMoveNoBase(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode); | |
void ServerMoveNoBase_Implementation(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode); | |
bool ServerMoveNoBase_Validate(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode); | |
/** Replicated function sent by client to server - contains client movement and view info for two moves. */ | |
UFUNCTION(unreliable, server, WithValidation) | |
void ServerMoveDual(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
void ServerMoveDual_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
bool ServerMoveDual_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/** Replicated function sent by client to server - contains client movement and view info for two moves. */ | |
UFUNCTION(unreliable, server, WithValidation) | |
void ServerMoveDualNoBase(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode); | |
void ServerMoveDualNoBase_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode); | |
bool ServerMoveDualNoBase_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, uint8 ClientMovementMode); | |
/** Replicated function sent by client to server - contains client movement and view info for two moves. First move is non root motion, second is root motion. */ | |
UFUNCTION(unreliable, server, WithValidation) | |
void ServerMoveDualHybridRootMotion(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
void ServerMoveDualHybridRootMotion_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
bool ServerMoveDualHybridRootMotion_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/* Resending an (important) old move. Process it if not already processed. */ | |
UFUNCTION(unreliable, server, WithValidation) | |
void ServerMoveOld(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags); | |
void ServerMoveOld_Implementation(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags); | |
bool ServerMoveOld_Validate(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags); | |
////////////////////////////////////////////////////////////////////////// | |
// Client RPCS that pass through to CharacterMovement (avoids RPC overhead for components). | |
////////////////////////////////////////////////////////////////////////// | |
/** If no client adjustment is needed after processing received ServerMove(), ack the good move so client can remove it from SavedMoves */ | |
UFUNCTION(unreliable, client) | |
void ClientAckGoodMove(float TimeStamp); | |
void ClientAckGoodMove_Implementation(float TimeStamp); | |
/** Replicate position correction to client, associated with a timestamped servermove. Client will replay subsequent moves after applying adjustment. */ | |
UFUNCTION(unreliable, client) | |
void ClientAdjustPosition(float TimeStamp, FVector NewLoc, FVector NewVel, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
void ClientAdjustPosition_Implementation(float TimeStamp, FVector NewLoc, FVector NewVel, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
/* Bandwidth saving version, when velocity is zeroed */ | |
UFUNCTION(unreliable, client) | |
void ClientVeryShortAdjustPosition(float TimeStamp, FVector NewLoc, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
void ClientVeryShortAdjustPosition_Implementation(float TimeStamp, FVector NewLoc, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
/** Replicate position correction to client when using root motion for movement. (animation root motion specific) */ | |
UFUNCTION(unreliable, client) | |
void ClientAdjustRootMotionPosition(float TimeStamp, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
void ClientAdjustRootMotionPosition_Implementation(float TimeStamp, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
/** Replicate root motion source correction to client when using root motion for movement. */ | |
UFUNCTION(unreliable, client) | |
void ClientAdjustRootMotionSourcePosition(float TimeStamp, FRootMotionSourceGroup ServerRootMotion, bool bHasAnimRootMotion, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
void ClientAdjustRootMotionSourcePosition_Implementation(float TimeStamp, FRootMotionSourceGroup ServerRootMotion, bool bHasAnimRootMotion, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
public: | |
/** Returns Mesh subobject **/ | |
FORCEINLINE class USkeletalMeshComponent* GetMesh() const { return Mesh; } | |
/** Name of the MeshComponent. Use this name if you want to prevent creation of the component (with ObjectInitializer.DoNotCreateDefaultSubobject). */ | |
static FName MeshComponentName; | |
/** Returns CharacterMovement subobject **/ | |
FORCEINLINE class UMMOPlayerMovement* GetCharacterMovement() const | |
{ | |
if(!CharacterMovement) | |
UE_LOG(LogTemp, Error, TEXT("MMOCharacter: GetMovementComponent(): Character Movement is null!")) | |
return CharacterMovement; | |
} | |
/** Name of the CharacterMovement component. Use this name if you want to use a different class (with ObjectInitializer.SetDefaultSubobjectClass). */ | |
static FName CharacterMovementComponentName; | |
/** Returns CapsuleComponent subobject **/ | |
FORCEINLINE class UCapsuleComponent* GetCapsuleComponent() const { return CapsuleComponent; } | |
/** Name of the CapsuleComponent. */ | |
static FName CapsuleComponentName; | |
#if WITH_EDITORONLY_DATA | |
/** Returns ArrowComponent subobject **/ | |
class UArrowComponent* GetArrowComponent() const { return ArrowComponent; } | |
#endif | |
/** Sets the component the Character is walking on, used by CharacterMovement walking movement to be able to follow dynamic objects. */ | |
virtual void SetBase(UPrimitiveComponent* NewBase, const FName BoneName = NAME_None, bool bNotifyActor = true); | |
/** | |
* Cache mesh offset from capsule. This is used as the target for network smoothing interpolation, when the mesh is offset with lagged smoothing. | |
* This is automatically called during initialization; call this at runtime if you intend to change the default mesh offset from the capsule. | |
* @see GetBaseTranslationOffset(), GetBaseRotationOffset() | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
virtual void CacheInitialMeshOffset(FVector MeshRelativeLocation, FRotator MeshRelativeRotation); | |
protected: | |
/** Info about our current movement base (object we are standing on). */ | |
UPROPERTY() | |
struct FMMOBasedMovementInfo BasedMovement; | |
/** Replicated version of relative movement. Read-only on simulated proxies! */ | |
UPROPERTY(ReplicatedUsing = OnRep_ReplicatedBasedMovement) | |
struct FMMOBasedMovementInfo ReplicatedBasedMovement; | |
/** Scale to apply to root motion translation on this Character */ | |
UPROPERTY(Replicated) | |
float AnimRootMotionTranslationScale; | |
public: | |
/** Rep notify for ReplicatedBasedMovement */ | |
UFUNCTION() | |
virtual void OnRep_ReplicatedBasedMovement(); | |
/** Set whether this actor's movement replicates to network clients. */ | |
virtual void SetReplicateMovement(bool bInReplicateMovement) override; | |
protected: | |
/** Saved translation offset of mesh. */ | |
UPROPERTY() | |
FVector BaseTranslationOffset; | |
/** Saved rotation offset of mesh. */ | |
UPROPERTY() | |
FQuat BaseRotationOffset; | |
/** Event called after actor's base changes (if SetBase was requested to notify us with bNotifyPawn). */ | |
virtual void BaseChange(); | |
/** CharacterMovement ServerLastTransformUpdateTimeStamp value, replicated to simulated proxies. */ | |
UPROPERTY(Replicated) | |
float ReplicatedServerLastTransformUpdateTimeStamp; | |
UPROPERTY(ReplicatedUsing = OnRep_ReplayLastTransformUpdateTimeStamp) | |
float ReplayLastTransformUpdateTimeStamp; | |
/** CharacterMovement MovementMode (and custom mode) replicated for simulated proxies. Use CharacterMovementComponent::UnpackNetworkMovementMode() to translate it. */ | |
UPROPERTY(Replicated) | |
uint8 ReplicatedMovementMode; | |
/** Flag that we are receiving replication of the based movement. */ | |
UPROPERTY() | |
bool bInBaseReplication; | |
public: | |
UFUNCTION() | |
void OnRep_ReplayLastTransformUpdateTimeStamp(); | |
/** Accessor for ReplicatedServerLastTransformUpdateTimeStamp. */ | |
FORCEINLINE float GetReplicatedServerLastTransformUpdateTimeStamp() const { return ReplicatedServerLastTransformUpdateTimeStamp; } | |
/** Accessor for BasedMovement */ | |
FORCEINLINE const FMMOBasedMovementInfo& GetBasedMovement() const { return BasedMovement; } | |
/** Accessor for ReplicatedBasedMovement */ | |
FORCEINLINE const FMMOBasedMovementInfo& GetReplicatedBasedMovement() const { return ReplicatedBasedMovement; } | |
/** Save a new relative location in BasedMovement and a new rotation with is either relative or absolute. */ | |
void SaveRelativeBasedMovement(const FVector& NewRelativeLocation, const FRotator& NewRotation, bool bRelativeRotation); | |
/** Returns ReplicatedMovementMode */ | |
uint8 GetReplicatedMovementMode() const { return ReplicatedMovementMode; } | |
/** Get the saved translation offset of mesh. This is how much extra offset is applied from the center of the capsule. */ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
FVector GetBaseTranslationOffset() const { return BaseTranslationOffset; } | |
/** Get the saved rotation offset of mesh. This is how much extra rotation is applied from the capsule rotation. */ | |
virtual FQuat GetBaseRotationOffset() const { return BaseRotationOffset; } | |
/** Get the saved rotation offset of mesh. This is how much extra rotation is applied from the capsule rotation. */ | |
UFUNCTION(BlueprintCallable, Category = Character, meta = (DisplayName = "GetBaseRotationOffset", ScriptName = "GetBaseRotationOffset")) | |
FRotator GetBaseRotationOffsetRotator() const { return GetBaseRotationOffset().Rotator(); } | |
/** Default crouched eye height */ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Camera) | |
float CrouchedEyeHeight; | |
/** Set by character movement to specify that this Character is currently crouched. */ | |
UPROPERTY(BlueprintReadOnly, replicatedUsing = OnRep_IsCrouched, Category = Character) | |
uint32 bIsCrouched : 1; | |
/** Set to indicate that this Character is currently under the force of a jump (if JumpMaxHoldTime is non-zero). IsJumpProvidingForce() handles this as well. */ | |
UPROPERTY(Transient, Replicated) | |
uint32 bProxyIsJumpForceApplied : 1; | |
/** Handle Crouching replicated from server */ | |
UFUNCTION() | |
virtual void OnRep_IsCrouched(); | |
/** When true, player wants to jump */ | |
UPROPERTY(BlueprintReadOnly, Category = Character) | |
uint32 bPressedJump : 1; | |
/** When true, applying updates to network client (replaying saved moves for a locally controlled character) */ | |
UPROPERTY(Transient) | |
uint32 bClientUpdating : 1; | |
/** True if Pawn was initially falling when started to replay network moves. */ | |
UPROPERTY(Transient) | |
uint32 bClientWasFalling : 1; | |
/** If server disagrees with root motion track position, client has to resimulate root motion from last AckedMove. */ | |
UPROPERTY(Transient) | |
uint32 bClientResimulateRootMotion : 1; | |
/** If server disagrees with root motion state, client has to resimulate root motion from last AckedMove. */ | |
UPROPERTY(Transient) | |
uint32 bClientResimulateRootMotionSources : 1; | |
/** Disable simulated gravity (set when character encroaches geometry on client, to keep him from falling through floors) */ | |
UPROPERTY() | |
uint32 bSimGravityDisabled : 1; | |
UPROPERTY(Transient) | |
uint32 bClientCheckEncroachmentOnNetUpdate : 1; | |
/** Disable root motion on the server. When receiving a DualServerMove, where the first move is not root motion and the second is. */ | |
UPROPERTY(Transient) | |
uint32 bServerMoveIgnoreRootMotion : 1; | |
/** Tracks whether or not the character was already jumping last frame. */ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Transient, Category = Character) | |
uint32 bWasJumping : 1; | |
/** | |
* Jump key Held Time. | |
* This is the time that the player has held the jump key, in seconds. | |
*/ | |
UPROPERTY(Transient, BlueprintReadOnly, VisibleInstanceOnly, Category = Character) | |
float JumpKeyHoldTime; | |
/** Amount of jump force time remaining, if JumpMaxHoldTime > 0. */ | |
UPROPERTY(Transient, BlueprintReadOnly, VisibleInstanceOnly, Category = Character) | |
float JumpForceTimeRemaining; | |
/** Track last time a jump force started for a proxy. */ | |
UPROPERTY(Transient, BlueprintReadOnly, VisibleInstanceOnly, Category = Character) | |
float ProxyJumpForceStartedTime; | |
/** | |
* The max time the jump key can be held. | |
* Note that if StopJumping() is not called before the max jump hold time is reached, | |
* then the character will carry on receiving vertical velocity. Therefore it is usually | |
* best to call StopJumping() when jump input has ceased (such as a button up event). | |
*/ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = Character, Meta = (ClampMin = 0.0, UIMin = 0.0)) | |
float JumpMaxHoldTime; | |
/** | |
* The max number of jumps the character can perform. | |
* Note that if JumpMaxHoldTime is non zero and StopJumping is not called, the player | |
* may be able to perform and unlimited number of jumps. Therefore it is usually | |
* best to call StopJumping() when jump input has ceased (such as a button up event). | |
*/ | |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = Character) | |
int32 JumpMaxCount; | |
/** | |
* Tracks the current number of jumps performed. | |
* This is incremented in CheckJumpInput, used in CanJump_Implementation, and reset in OnMovementModeChanged. | |
* When providing overrides for these methods, it's recommended to either manually | |
* increment / reset this value, or call the Super:: method. | |
*/ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Character) | |
int32 JumpCurrentCount; | |
/** Incremented every time there is an Actor overlap event (start or stop) on this actor. */ | |
uint32 NumActorOverlapEventsCounter; | |
//~ Begin AActor Interface. | |
virtual void BeginPlay() override; | |
virtual void ClearCrossLevelReferences() override; | |
virtual void PreNetReceive() override; | |
virtual void PostNetReceive() override; | |
virtual void OnRep_ReplicatedMovement() override; | |
virtual void PostNetReceiveLocationAndRotation() override; | |
virtual void GetSimpleCollisionCylinder(float& CollisionRadius, float& CollisionHalfHeight) const override; | |
virtual UActorComponent* FindComponentByClass(const TSubclassOf<UActorComponent> ComponentClass) const override; | |
virtual void TornOff() override; | |
virtual void NotifyActorBeginOverlap(AActor* OtherActor); | |
virtual void NotifyActorEndOverlap(AActor* OtherActor); | |
//~ End AActor Interface | |
template<class T> | |
T* FindComponentByClass() const | |
{ | |
return AActor::FindComponentByClass<T>(); | |
} | |
//~ Begin INavAgentInterface Interface | |
virtual FVector GetNavAgentLocation() const override; | |
//~ End INavAgentInterface Interface | |
//~ Begin APawn Interface. | |
virtual void PostInitializeComponents() override; | |
virtual UPawnMovementComponent* GetMovementComponent() const override; | |
virtual UPrimitiveComponent* GetMovementBase() const override final { return BasedMovement.MovementBase; } | |
virtual float GetDefaultHalfHeight() const override; | |
virtual void TurnOff() override; | |
virtual void Restart() override; | |
virtual void PawnClientRestart() override; | |
virtual void PossessedBy(AController* NewController) override; | |
virtual void UnPossessed() override; | |
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; | |
virtual void DisplayDebug(class UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos) override; | |
virtual void RecalculateBaseEyeHeight() override; | |
virtual void UpdateNavigationRelevance() override; | |
//~ End APawn Interface | |
/** Apply momentum caused by damage. */ | |
virtual void ApplyDamageMomentum(float DamageTaken, FDamageEvent const& DamageEvent, APawn* PawnInstigator, AActor* DamageCauser); | |
/** | |
* Make the character jump on the next update. | |
* If you want your character to jump according to the time that the jump key is held, | |
* then you can set JumpKeyHoldTime to some non-zero value. Make sure in this case to | |
* call StopJumping() when you want the jump's z-velocity to stop being applied (such | |
* as on a button up event), otherwise the character will carry on receiving the | |
* velocity until JumpKeyHoldTime is reached. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
virtual void Jump(); | |
/** | |
* Stop the character from jumping on the next update. | |
* Call this from an input event (such as a button 'up' event) to cease applying | |
* jump Z-velocity. If this is not called, then jump z-velocity will be applied | |
* until JumpMaxHoldTime is reached. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
virtual void StopJumping(); | |
/** | |
* Check if the character can jump in the current state. | |
* | |
* The default implementation may be overridden or extended by implementing the custom CanJump event in Blueprints. | |
* | |
* @Return Whether the character can jump in the current state. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
bool CanJump() const; | |
protected: | |
/** | |
* Customizable event to check if the character can jump in the current state. | |
* Default implementation returns true if the character is on the ground and not crouching, | |
* has a valid CharacterMovementComponent and CanEverJump() returns true. | |
* Default implementation also allows for 'hold to jump higher' functionality: | |
* As well as returning true when on the ground, it also returns true when GetMaxJumpTime is more | |
* than zero and IsJumping returns true. | |
* | |
* | |
* @Return Whether the character can jump in the current state. | |
*/ | |
UFUNCTION(BlueprintNativeEvent, Category = Character, meta = (DisplayName = "CanJump")) | |
bool CanJumpInternal() const; | |
virtual bool CanJumpInternal_Implementation() const; | |
public: | |
/** Marks character as not trying to jump */ | |
virtual void ResetJumpState(); | |
/** | |
* True if jump is actively providing a force, such as when the jump key is held and the time it has been held is less than JumpMaxHoldTime. | |
* @see CharacterMovement->IsFalling | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
virtual bool IsJumpProvidingForce() const; | |
/** Play Animation Montage on the character mesh. Returns the length of the animation montage in seconds, or 0.f if failed to play. **/ | |
UFUNCTION(BlueprintCallable, Category = Animation) | |
virtual float PlayAnimMontage(class UAnimMontage* AnimMontage, float InPlayRate = 1.f, FName StartSectionName = NAME_None); | |
/** Stop Animation Montage. If nullptr, it will stop what's currently active. The Blend Out Time is taken from the montage asset that is being stopped. **/ | |
UFUNCTION(BlueprintCallable, Category = Animation) | |
virtual void StopAnimMontage(class UAnimMontage* AnimMontage = nullptr); | |
/** Return current playing Montage **/ | |
UFUNCTION(BlueprintCallable, Category = Animation) | |
class UAnimMontage* GetCurrentMontage(); | |
/** | |
* Set a pending launch velocity on the Character. This velocity will be processed on the next CharacterMovementComponent tick, | |
* and will set it to the "falling" state. Triggers the OnLaunched event. | |
* @PARAM LaunchVelocity is the velocity to impart to the Character | |
* @PARAM bXYOverride if true replace the XY part of the Character's velocity instead of adding to it. | |
* @PARAM bZOverride if true replace the Z component of the Character's velocity instead of adding to it. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
virtual void LaunchCharacter(FVector LaunchVelocity, bool bXYOverride, bool bZOverride); | |
/** Let blueprint know that we were launched */ | |
UFUNCTION(BlueprintImplementableEvent) | |
void OnLaunched(FVector LaunchVelocity, bool bXYOverride, bool bZOverride); | |
/** Event fired when the character has just started jumping */ | |
UFUNCTION(BlueprintNativeEvent, Category = Character) | |
void OnJumped(); | |
virtual void OnJumped_Implementation(); | |
/** Called when the character's movement enters falling */ | |
virtual void Falling() {} | |
/** Called when character's jump reaches Apex. Needs CharacterMovement->bNotifyApex = true */ | |
virtual void NotifyJumpApex(); | |
/** Broadcast when Character's jump reaches its apex. Needs CharacterMovement->bNotifyApex = true */ | |
UPROPERTY(BlueprintAssignable, Category = Character) | |
FMMOCharacterReachedApexSignature OnReachedJumpApex; | |
/** | |
* Called upon landing when falling, to perform actions based on the Hit result. Triggers the OnLanded event. | |
* Note that movement mode is still "Falling" during this event. Current Velocity value is the velocity at the time of landing. | |
* Consider OnMovementModeChanged() as well, as that can be used once the movement mode changes to the new mode (most likely Walking). | |
* | |
* @param Hit Result describing the landing that resulted in a valid landing spot. | |
* @see OnMovementModeChanged() | |
*/ | |
virtual void Landed(const FHitResult& Hit); | |
/** | |
* Called upon landing when falling, to perform actions based on the Hit result. | |
* Note that movement mode is still "Falling" during this event. Current Velocity value is the velocity at the time of landing. | |
* Consider OnMovementModeChanged() as well, as that can be used once the movement mode changes to the new mode (most likely Walking). | |
* | |
* @param Hit Result describing the landing that resulted in a valid landing spot. | |
* @see OnMovementModeChanged() | |
*/ | |
FMMOLandedSignature LandedDelegate; | |
/** | |
* Called upon landing when falling, to perform actions based on the Hit result. | |
* Note that movement mode is still "Falling" during this event. Current Velocity value is the velocity at the time of landing. | |
* Consider OnMovementModeChanged() as well, as that can be used once the movement mode changes to the new mode (most likely Walking). | |
* | |
* @param Hit Result describing the landing that resulted in a valid landing spot. | |
* @see OnMovementModeChanged() | |
*/ | |
UFUNCTION(BlueprintImplementableEvent) | |
void OnLanded(const FHitResult& Hit); | |
/** | |
* Event fired when the Character is walking off a surface and is about to fall because CharacterMovement->CurrentFloor became unwalkable. | |
* If CharacterMovement->MovementMode does not change during this event then the character will automatically start falling afterwards. | |
* @note Z velocity is zero during walking movement, and will be here as well. Another velocity can be computed here if desired and will be used when starting to fall. | |
* | |
* @param PreviousFloorImpactNormal Normal of the previous walkable floor. | |
* @param PreviousFloorContactNormal Normal of the contact with the previous walkable floor. | |
* @param PreviousLocation Previous character location before movement off the ledge. | |
* @param TimeTick Time delta of movement update resulting in moving off the ledge. | |
*/ | |
UFUNCTION(BlueprintNativeEvent, Category = Character) | |
void OnWalkingOffLedge(const FVector& PreviousFloorImpactNormal, const FVector& PreviousFloorContactNormal, const FVector& PreviousLocation, float TimeDelta); | |
virtual void OnWalkingOffLedge_Implementation(const FVector& PreviousFloorImpactNormal, const FVector& PreviousFloorContactNormal, const FVector& PreviousLocation, float TimeDelta); | |
/** | |
* Called when pawn's movement is blocked | |
* @param Impact describes the blocking hit. | |
*/ | |
virtual void MoveBlockedBy(const FHitResult& Impact) {}; | |
/** | |
* Request the character to start crouching. The request is processed on the next update of the CharacterMovementComponent. | |
* @see OnStartCrouch | |
* @see IsCrouched | |
* @see CharacterMovement->WantsToCrouch | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character, meta = (HidePin = "bClientSimulation")) | |
virtual void Crouch(bool bClientSimulation = false); | |
/** | |
* Request the character to stop crouching. The request is processed on the next update of the CharacterMovementComponent. | |
* @see OnEndCrouch | |
* @see IsCrouched | |
* @see CharacterMovement->WantsToCrouch | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Character, meta = (HidePin = "bClientSimulation")) | |
virtual void UnCrouch(bool bClientSimulation = false); | |
/** @return true if this character is currently able to crouch (and is not currently crouched) */ | |
UFUNCTION(BlueprintCallable, Category = Character) | |
virtual bool CanCrouch() const; | |
/** | |
* Called when Character stops crouching. Called on non-owned Characters through bIsCrouched replication. | |
* @param HalfHeightAdjust difference between default collision half-height, and actual crouched capsule half-height. | |
* @param ScaledHalfHeightAdjust difference after component scale is taken in to account. | |
*/ | |
virtual void OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust); | |
/** | |
* Event when Character stops crouching. | |
* @param HalfHeightAdjust difference between default collision half-height, and actual crouched capsule half-height. | |
* @param ScaledHalfHeightAdjust difference after component scale is taken in to account. | |
*/ | |
UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "OnEndCrouch", ScriptName = "OnEndCrouch")) | |
void K2_OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust); | |
/** | |
* Called when Character crouches. Called on non-owned Characters through bIsCrouched replication. | |
* @param HalfHeightAdjust difference between default collision half-height, and actual crouched capsule half-height. | |
* @param ScaledHalfHeightAdjust difference after component scale is taken in to account. | |
*/ | |
virtual void OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust); | |
/** | |
* Event when Character crouches. | |
* @param HalfHeightAdjust difference between default collision half-height, and actual crouched capsule half-height. | |
* @param ScaledHalfHeightAdjust difference after component scale is taken in to account. | |
*/ | |
UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "OnStartCrouch", ScriptName = "OnStartCrouch")) | |
void K2_OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust); | |
/** | |
* Called from CharacterMovementComponent to notify the character that the movement mode has changed. | |
* @param PrevMovementMode Movement mode before the change | |
* @param PrevCustomMode Custom mode before the change (applicable if PrevMovementMode is Custom) | |
*/ | |
virtual void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode = 0); | |
/** Multicast delegate for MovementMode changing. */ | |
UPROPERTY(BlueprintAssignable, Category = Character) | |
FMMOMovementModeChangedSignature MovementModeChangedDelegate; | |
/** | |
* Called from CharacterMovementComponent to notify the character that the movement mode has changed. | |
* @param PrevMovementMode Movement mode before the change | |
* @param NewMovementMode New movement mode | |
* @param PrevCustomMode Custom mode before the change (applicable if PrevMovementMode is Custom) | |
* @param NewCustomMode New custom mode (applicable if NewMovementMode is Custom) | |
*/ | |
UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "OnMovementModeChanged", ScriptName = "OnMovementModeChanged")) | |
void K2_OnMovementModeChanged(EMovementMode PrevMovementMode, EMovementMode NewMovementMode, uint8 PrevCustomMode, uint8 NewCustomMode); | |
/** | |
* Event for implementing custom character movement mode. Called by CharacterMovement if MovementMode is set to Custom. | |
* @note C++ code should override UMMOPlayerMovementComponent::PhysCustom() instead. | |
* @see UMMOPlayerMovementComponent::PhysCustom() | |
*/ | |
UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "UpdateCustomMovement", ScriptName = "UpdateCustomMovement")) | |
void K2_UpdateCustomMovement(float DeltaTime); | |
/** | |
* Event triggered at the end of a CharacterMovementComponent movement update. | |
* This is the preferred event to use rather than the Tick event when performing custom updates to CharacterMovement properties based on the current state. | |
* This is mainly due to the nature of network updates, where client corrections in position from the server can cause multiple iterations of a movement update, | |
* which allows this event to update as well, while a Tick event would not. | |
* | |
* @param DeltaSeconds Delta time in seconds for this update | |
* @param InitialLocation Location at the start of the update. May be different than the current location if movement occurred. | |
* @param InitialVelocity Velocity at the start of the update. May be different than the current velocity. | |
*/ | |
UPROPERTY(BlueprintAssignable, Category = Character) | |
FMMOCharacterMovementUpdatedSignature OnCharacterMovementUpdated; | |
/** Returns true if the Landed() event should be called. Used by CharacterMovement to prevent notifications while playing back network moves. */ | |
virtual bool ShouldNotifyLanded(const struct FHitResult& Hit); | |
/** Trigger jump if jump button has been pressed. */ | |
virtual void CheckJumpInput(float DeltaTime); | |
/** Update jump input state after having checked input. */ | |
virtual void ClearJumpInput(float DeltaTime); | |
/** | |
* Get the maximum jump time for the character. | |
* Note that if StopJumping() is not called before the max jump hold time is reached, | |
* then the character will carry on receiving vertical velocity. Therefore it is usually | |
* best to call StopJumping() when jump input has ceased (such as a button up event). | |
* | |
* @return Maximum jump time for the character | |
*/ | |
virtual float GetJumpMaxHoldTime() const; | |
UFUNCTION(Reliable, Client) | |
void ClientCheatWalk(); | |
virtual void ClientCheatWalk_Implementation(); | |
UFUNCTION(Reliable, Client) | |
void ClientCheatFly(); | |
virtual void ClientCheatFly_Implementation(); | |
UFUNCTION(Reliable, Client) | |
void ClientCheatGhost(); | |
virtual void ClientCheatGhost_Implementation(); | |
UFUNCTION(Reliable, Client) | |
void RootMotionDebugClientPrintOnScreen(const FString& InString); | |
virtual void RootMotionDebugClientPrintOnScreen_Implementation(const FString& InString); | |
// Root Motion | |
/** | |
* For LocallyControlled Autonomous clients. | |
* During a PerformMovement() after root motion is prepared, we save it off into this and | |
* then record it into our SavedMoves. | |
* During SavedMove playback we use it as our "Previous Move" SavedRootMotion which includes | |
* last received root motion from the Server | |
*/ | |
UPROPERTY(Transient) | |
FRootMotionSourceGroup SavedRootMotion; | |
/** For LocallyControlled Autonomous clients. Saved root motion data to be used by SavedMoves. */ | |
UPROPERTY(Transient) | |
FRootMotionMovementParams ClientRootMotionParams; | |
/** Array of previously received root motion moves from the server. */ | |
UPROPERTY(Transient) | |
TArray<FMMOSimulatedRootMotionReplicatedMove> RootMotionRepMoves; | |
/** Find usable root motion replicated move from our buffer. | |
* Goes through the buffer back in time, to find the first move that clears 'CanUseRootMotionRepMove' below. | |
* Returns index of that move or INDEX_NONE otherwise. | |
*/ | |
int32 FindRootMotionRepMove(const FAnimMontageInstance& ClientMontageInstance) const; | |
/** True if buffered move is usable to teleport client back to. */ | |
bool CanUseRootMotionRepMove(const FMMOSimulatedRootMotionReplicatedMove& RootMotionRepMove, const FAnimMontageInstance& ClientMontageInstance) const; | |
/** Restore actor to an old buffered move. */ | |
bool RestoreReplicatedMove(const FMMOSimulatedRootMotionReplicatedMove& RootMotionRepMove); | |
/** | |
* Called on client after position update is received to respond to the new location and rotation. | |
* Actual change in location is expected to occur in CharacterMovement->SmoothCorrection(), after which this occurs. | |
* Default behavior is to check for penetration in a blocking object if bClientCheckEncroachmentOnNetUpdate is enabled, and set bSimGravityDisabled=true if so. | |
*/ | |
virtual void OnUpdateSimulatedPosition(const FVector& OldLocation, const FQuat& OldRotation); | |
/** Replicated Root Motion montage */ | |
UPROPERTY(ReplicatedUsing = OnRep_RootMotion) | |
struct FMMORepRootMotionMontage RepRootMotion; | |
/** Handles replicated root motion properties on simulated proxies and position correction. */ | |
UFUNCTION() | |
void OnRep_RootMotion(); | |
/** Position fix up for Simulated Proxies playing Root Motion */ | |
void SimulatedRootMotionPositionFixup(float DeltaSeconds); | |
/** Get FAnimMontageInstance playing RootMotion */ | |
FAnimMontageInstance* GetRootMotionAnimMontageInstance() const; | |
/** True if we are playing Anim root motion right now */ | |
UFUNCTION(BlueprintCallable, Category = Animation, meta = (DisplayName = "IsPlayingAnimRootMotion")) | |
bool IsPlayingRootMotion() const; | |
/** True if we are playing root motion from any source right now (anim root motion, root motion source) */ | |
UFUNCTION(BlueprintCallable, Category = Animation) | |
bool HasAnyRootMotion() const; | |
/** | |
* True if we are playing Root Motion right now, through a Montage with RootMotionMode == ERootMotionMode::RootMotionFromMontagesOnly. | |
* This means code path for networked root motion is enabled. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = Animation) | |
bool IsPlayingNetworkedRootMotionMontage() const; | |
/** Sets scale to apply to root motion translation on this Character */ | |
void SetAnimRootMotionTranslationScale(float InAnimRootMotionTranslationScale = 1.f); | |
/** Returns current value of AnimRootMotionScale */ | |
UFUNCTION(BlueprintCallable, Category = Animation) | |
float GetAnimRootMotionTranslationScale() const; | |
/** | |
* Called on the actor right before replication occurs. | |
* Only called on Server, and for autonomous proxies if recording a Client Replay. | |
*/ | |
virtual void PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) override; | |
/** | |
* Called on the actor right before replication occurs. | |
* Called for everyone when recording a Client Replay, including Simulated Proxies. | |
*/ | |
virtual void PreReplicationForReplay(IRepChangedPropertyTracker& ChangedPropertyTracker) override; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Epic Games, Inc. All Rights Reserved. | |
/*============================================================================= | |
Movement.cpp: Character movement implementation | |
=============================================================================*/ | |
#include "MMOPlayerMovement.h" | |
#include "EngineStats.h" | |
#include "Components/PrimitiveComponent.h" | |
#include "AI/NavigationSystemBase.h" | |
#include "AI/Navigation/NavigationDataInterface.h" | |
#include "UObject/Package.h" | |
#include "GameFramework/PlayerController.h" | |
#include "GameFramework/PhysicsVolume.h" | |
#include "Components/SkeletalMeshComponent.h" | |
#include "Engine/NetDriver.h" | |
#include "DrawDebugHelpers.h" | |
#include "GameFramework/GameNetworkManager.h" | |
#include "GameFramework/Character.h" | |
#include "Components/CapsuleComponent.h" | |
#include "GameFramework/GameStateBase.h" | |
#include "Engine/Canvas.h" | |
#include "AI/Navigation/PathFollowingAgentInterface.h" | |
#include "AI/Navigation/AvoidanceManager.h" | |
#include "Components/BrushComponent.h" | |
#include "Misc/App.h" | |
#include "Engine/DemoNetDriver.h" | |
#include "Engine/NetworkObjectList.h" | |
#include "MMOCharacter.h" | |
// #include "Net/PerfCountersHelpers.h" // NOTE(Joey): commented out as UE can't find header file (internal ...module.h) for some reason | |
#include "ProfilingDebugging/CsvProfiler.h" | |
// NOTE(Joey): disable deprecated compile warning (for now, actually get rid of deprecated properties eventually) | |
#pragma warning( disable : 4996 ) | |
DEFINE_LOG_CATEGORY_STATIC(LogMMOCharacterMovement, Log, All); | |
DEFINE_LOG_CATEGORY_STATIC(LogMMONavMeshMovement, Log, All); | |
DEFINE_LOG_CATEGORY_STATIC(LogMMOCharacterNetSmoothing, Log, All); | |
// MAGIC NUMBERS | |
const float MAX_STEP_SIDE_Z = 0.08f; // maximum z value for the normal on the vertical side of steps | |
const float SWIMBOBSPEED = -80.f; | |
const float VERTICAL_SLOPE_NORMAL_Z = 0.001f; // Slope is vertical if Abs(Normal.Z) <= this threshold. Accounts for precision problems that sometimes angle normals slightly off horizontal for vertical surface. | |
const float UMMOPlayerMovement::MIN_TICK_TIME = 1e-6f; | |
const float UMMOPlayerMovement::MIN_FLOOR_DIST = 1.9f; | |
const float UMMOPlayerMovement::MAX_FLOOR_DIST = 2.4f; | |
const float UMMOPlayerMovement::BRAKE_TO_STOP_VELOCITY = 10.f; | |
const float UMMOPlayerMovement::SWEEP_EDGE_REJECT_DISTANCE = 0.15f; | |
// static const FString PerfCounter_NumServerMoves = TEXT("NumServerMoves"); | |
// static const FString PerfCounter_NumServerMoveCorrections = TEXT("NumServerMoveCorrections"); | |
// Defines for build configs | |
#if DO_CHECK && !UE_BUILD_SHIPPING // Disable even if checks in shipping are enabled. | |
#define devMMOCode( Code ) checkCode( Code ) | |
#else | |
#define devMMOCode(...) | |
#endif | |
// CVars | |
namespace MMOCharacterMovementCVars | |
{ | |
// Listen server smoothing | |
static int32 NetEnableListenServerSmoothing = 1; | |
FAutoConsoleVariableRef CVarNetEnableListenServerSmoothing( | |
TEXT("p.NetEnableListenServerSmoothing"), | |
NetEnableListenServerSmoothing, | |
TEXT("Whether to enable mesh smoothing on listen servers for the local view of remote clients.\n") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Default); | |
// Latent proxy prediction | |
static int32 NetEnableSkipProxyPredictionOnNetUpdate = 1; | |
FAutoConsoleVariableRef CVarNetEnableSkipProxyPredictionOnNetUpdate( | |
TEXT("p.NetEnableSkipProxyPredictionOnNetUpdate"), | |
NetEnableSkipProxyPredictionOnNetUpdate, | |
TEXT("Whether to allow proxies to skip prediction on frames with a network position update, if bNetworkSkipProxyPredictionOnNetUpdate is also true on the movement component.\n") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Default); | |
// Logging when character is stuck. Off by default in shipping. | |
#if UE_BUILD_SHIPPING | |
static float StuckWarningPeriod = -1.f; | |
#else | |
static float StuckWarningPeriod = 1.f; | |
#endif | |
FAutoConsoleVariableRef CVarStuckWarningPeriod( | |
TEXT("p.CharacterStuckWarningPeriod"), | |
StuckWarningPeriod, | |
TEXT("How often (in seconds) we are allowed to log a message about being stuck in geometry.\n") | |
TEXT("<0: Disable, >=0: Enable and log this often, in seconds."), | |
ECVF_Default); | |
static int32 NetEnableMoveCombining = 1; | |
FAutoConsoleVariableRef CVarNetEnableMoveCombining( | |
TEXT("p.NetEnableMoveCombining"), | |
NetEnableMoveCombining, | |
TEXT("Whether to enable move combining on the client to reduce bandwidth by combining similar moves.\n") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Default); | |
static int32 NetEnableMoveCombiningOnStaticBaseChange = 1; | |
FAutoConsoleVariableRef CVarNetEnableMoveCombiningOnStaticBaseChange( | |
TEXT("p.NetEnableMoveCombiningOnStaticBaseChange"), | |
NetEnableMoveCombiningOnStaticBaseChange, | |
TEXT("Whether to allow combining client moves when moving between static geometry.\n") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Default); | |
static float NetMoveCombiningAttachedLocationTolerance = 0.01f; | |
FAutoConsoleVariableRef CVarNetMoveCombiningAttachedLocationTolerance( | |
TEXT("p.NetMoveCombiningAttachedLocationTolerance"), | |
NetMoveCombiningAttachedLocationTolerance, | |
TEXT("Tolerance for relative location attachment change when combining moves. Small tolerances allow for very slight jitter due to transform updates."), | |
ECVF_Default); | |
static float NetMoveCombiningAttachedRotationTolerance = 0.01f; | |
FAutoConsoleVariableRef CVarNetMoveCombiningAttachedRotationTolerance( | |
TEXT("p.NetMoveCombiningAttachedRotationTolerance"), | |
NetMoveCombiningAttachedRotationTolerance, | |
TEXT("Tolerance for relative rotation attachment change when combining moves. Small tolerances allow for very slight jitter due to transform updates."), | |
ECVF_Default); | |
static float NetStationaryRotationTolerance = 0.1f; | |
FAutoConsoleVariableRef CVarNetStationaryRotationTolerance( | |
TEXT("p.NetStationaryRotationTolerance"), | |
NetStationaryRotationTolerance, | |
TEXT("Tolerance for GetClientNetSendDeltaTime() to remain throttled when small control rotation changes occur."), | |
ECVF_Default); | |
static int32 NetUseClientTimestampForReplicatedTransform = 1; | |
FAutoConsoleVariableRef CVarNetUseClientTimestampForReplicatedTransform( | |
TEXT("p.NetUseClientTimestampForReplicatedTransform"), | |
NetUseClientTimestampForReplicatedTransform, | |
TEXT("If enabled, use client timestamp changes to track the replicated transform timestamp, otherwise uses server tick time as the timestamp.\n") | |
TEXT("Game session usually needs to be restarted if this is changed at runtime.\n") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Default); | |
static int32 ReplayUseInterpolation = 0; | |
FAutoConsoleVariableRef CVarReplayUseInterpolation( | |
TEXT("p.ReplayUseInterpolation"), | |
ReplayUseInterpolation, | |
TEXT(""), | |
ECVF_Default); | |
static int32 ReplayLerpAcceleration = 0; | |
FAutoConsoleVariableRef CVarReplayLerpAcceleration( | |
TEXT("p.ReplayLerpAcceleration"), | |
ReplayLerpAcceleration, | |
TEXT(""), | |
ECVF_Default); | |
static int32 FixReplayOverSampling = 1; | |
FAutoConsoleVariableRef CVarFixReplayOverSampling( | |
TEXT("p.FixReplayOverSampling"), | |
FixReplayOverSampling, | |
TEXT("If 1, remove invalid replay samples that can occur due to oversampling (sampling at higher rate than physics is being ticked)"), | |
ECVF_Default); | |
static int32 ForceJumpPeakSubstep = 1; | |
FAutoConsoleVariableRef CVarForceJumpPeakSubstep( | |
TEXT("p.ForceJumpPeakSubstep"), | |
ForceJumpPeakSubstep, | |
TEXT("If 1, force a jump substep to always reach the peak position of a jump, which can often be cut off as framerate lowers."), | |
ECVF_Default); | |
static float NetServerMoveTimestampExpiredWarningThreshold = 1.0f; | |
FAutoConsoleVariableRef CVarNetServerMoveTimestampExpiredWarningThreshold( | |
TEXT("net.NetServerMoveTimestampExpiredWarningThreshold"), | |
NetServerMoveTimestampExpiredWarningThreshold, | |
TEXT("Tolerance for ServerMove() to warn when client moves are expired more than this time threshold behind the server."), | |
ECVF_Default); | |
#if !UE_BUILD_SHIPPING | |
int32 MMONetShowCorrections = 0; | |
FAutoConsoleVariableRef CVarMMONetShowCorrections( | |
TEXT("p.MMONetShowCorrections"), | |
MMONetShowCorrections, | |
TEXT("Whether to draw client position corrections (red is incorrect, green is corrected).\n") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Cheat); | |
float MMONetCorrectionLifetime = 4.f; | |
FAutoConsoleVariableRef CVarMMONetCorrectionLifetime( | |
TEXT("p.MMONetCorrectionLifetime"), | |
MMONetCorrectionLifetime, | |
TEXT("How long a visualized network correction persists.\n") | |
TEXT("Time in seconds each visualized network correction persists."), | |
ECVF_Cheat); | |
#endif // !UE_BUILD_SHIPPING | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
static float NetForceClientAdjustmentPercent = 0.f; | |
FAutoConsoleVariableRef CVarNetForceClientAdjustmentPercent( | |
TEXT("p.NetForceClientAdjustmentPercent"), | |
NetForceClientAdjustmentPercent, | |
TEXT("Percent of ServerCheckClientError checks to return true regardless of actual error.\n") | |
TEXT("Useful for testing client correction code.\n") | |
TEXT("<=0: Disable, 0.05: 5% of checks will return failed, 1.0: Always send client adjustments"), | |
ECVF_Cheat); | |
static float NetForceClientServerMoveLossPercent = 0.f; | |
FAutoConsoleVariableRef CVarNetForceClientServerMoveLossPercent( | |
TEXT("p.NetForceClientServerMoveLossPercent"), | |
NetForceClientServerMoveLossPercent, | |
TEXT("Percent of ServerMove calls for client to not send.\n") | |
TEXT("Useful for testing server force correction code.\n") | |
TEXT("<=0: Disable, 0.05: 5% of checks will return failed, 1.0: never send server moves"), | |
ECVF_Cheat); | |
static float NetForceClientServerMoveLossDuration = 0.f; | |
FAutoConsoleVariableRef CVarNetForceClientServerMoveLossDuration( | |
TEXT("p.NetForceClientServerMoveLossDuration"), | |
NetForceClientServerMoveLossDuration, | |
TEXT("Duration in seconds for client to drop ServerMove calls when NetForceClientServerMoveLossPercent check passes.\n") | |
TEXT("Useful for testing server force correction code.\n") | |
TEXT("Duration of zero means single frame loss."), | |
ECVF_Cheat); | |
static int32 VisualizeMovement = 0; | |
FAutoConsoleVariableRef CVarVisualizeMovement( | |
TEXT("p.VisualizeMovement"), | |
VisualizeMovement, | |
TEXT("Whether to draw in-world debug information for character movement.\n") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Cheat); | |
static int32 NetVisualizeSimulatedCorrections = 0; | |
FAutoConsoleVariableRef CVarNetVisualizeSimulatedCorrections( | |
TEXT("p.NetVisualizeSimulatedCorrections"), | |
NetVisualizeSimulatedCorrections, | |
TEXT("") | |
TEXT("0: Disable, 1: Enable"), | |
ECVF_Cheat); | |
static int32 DebugTimeDiscrepancy = 0; | |
FAutoConsoleVariableRef CVarDebugTimeDiscrepancy( | |
TEXT("p.DebugTimeDiscrepancy"), | |
DebugTimeDiscrepancy, | |
TEXT("Whether to log detailed Movement Time Discrepancy values for testing") | |
TEXT("0: Disable, 1: Enable Detection logging, 2: Enable Detection and Resolution logging"), | |
ECVF_Cheat); | |
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
} | |
/** | |
* Helper to change mesh bone updates within a scope. | |
* Example usage: | |
* { | |
* FScopedPreventMeshBoneUpdate ScopedNoMeshBoneUpdate(CharacterOwner->GetMesh(), EKinematicBonesUpdateToPhysics::SkipAllBones); | |
* // Do something to move mesh, bones will not update | |
* } | |
* // Movement of mesh at this point will use previous setting. | |
*/ | |
struct FScopedMeshBoneUpdateOverride | |
{ | |
FScopedMeshBoneUpdateOverride(USkeletalMeshComponent* Mesh, EKinematicBonesUpdateToPhysics::Type OverrideSetting) | |
: MeshRef(Mesh) | |
{ | |
if (MeshRef) | |
{ | |
// Save current state. | |
SavedUpdateSetting = MeshRef->KinematicBonesUpdateType; | |
// Override bone update setting. | |
MeshRef->KinematicBonesUpdateType = OverrideSetting; | |
} | |
} | |
~FScopedMeshBoneUpdateOverride() | |
{ | |
if (MeshRef) | |
{ | |
// Restore bone update flag. | |
MeshRef->KinematicBonesUpdateType = SavedUpdateSetting; | |
} | |
} | |
private: | |
USkeletalMeshComponent* MeshRef; | |
EKinematicBonesUpdateToPhysics::Type SavedUpdateSetting; | |
}; | |
void FMMOFindFloorResult::SetFromSweep(const FHitResult& InHit, const float InSweepFloorDist, const bool bIsWalkableFloor) | |
{ | |
bBlockingHit = InHit.IsValidBlockingHit(); | |
bWalkableFloor = bIsWalkableFloor; | |
bLineTrace = false; | |
FloorDist = InSweepFloorDist; | |
LineDist = 0.f; | |
HitResult = InHit; | |
} | |
void FMMOFindFloorResult::SetFromLineTrace(const FHitResult& InHit, const float InSweepFloorDist, const float InLineDist, const bool bIsWalkableFloor) | |
{ | |
// We require a sweep that hit if we are going to use a line result. | |
check(HitResult.bBlockingHit); | |
if (HitResult.bBlockingHit && InHit.bBlockingHit) | |
{ | |
// Override most of the sweep result with the line result, but save some values | |
FHitResult OldHit(HitResult); | |
HitResult = InHit; | |
// Restore some of the old values. We want the new normals and hit actor, however. | |
HitResult.Time = OldHit.Time; | |
HitResult.ImpactPoint = OldHit.ImpactPoint; | |
HitResult.Location = OldHit.Location; | |
HitResult.TraceStart = OldHit.TraceStart; | |
HitResult.TraceEnd = OldHit.TraceEnd; | |
bLineTrace = true; | |
FloorDist = InSweepFloorDist; | |
LineDist = InLineDist; | |
bWalkableFloor = bIsWalkableFloor; | |
} | |
} | |
void FMMOCharacterMovementComponentPostPhysicsTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) | |
{ | |
FActorComponentTickFunction::ExecuteTickHelper(Target, /*bTickInEditor=*/ false, DeltaTime, TickType, [this](float DilatedTime) | |
{ | |
Target->PostPhysicsTickComponent(DilatedTime, *this); | |
}); | |
} | |
FString FMMOCharacterMovementComponentPostPhysicsTickFunction::DiagnosticMessage() | |
{ | |
return Target->GetFullName() + TEXT("[UMMOPlayerMovement::PreClothTick]"); | |
} | |
FName FMMOCharacterMovementComponentPostPhysicsTickFunction::DiagnosticContext(bool bDetailed) | |
{ | |
if (bDetailed) | |
{ | |
return FName(*FString::Printf(TEXT("SkeletalMeshComponentClothTick/%s"), *GetFullNameSafe(Target))); | |
} | |
return FName(TEXT("SkeletalMeshComponentClothTick")); | |
} | |
UMMOPlayerMovement::UMMOPlayerMovement(const FObjectInitializer& ObjectInitializer) | |
: Super(ObjectInitializer) | |
{ | |
RandomStream.Initialize(FApp::bUseFixedSeed ? GetFName() : NAME_None); | |
PostPhysicsTickFunction.bCanEverTick = true; | |
PostPhysicsTickFunction.bStartWithTickEnabled = false; | |
PostPhysicsTickFunction.SetTickFunctionEnable(false); | |
PostPhysicsTickFunction.TickGroup = TG_PostPhysics; | |
bApplyGravityWhileJumping = true; | |
GravityScale = 1.f; | |
GroundFriction = 8.0f; | |
JumpZVelocity = 420.0f; | |
JumpOffJumpZFactor = 0.5f; | |
RotationRate = FRotator(0.f, 360.0f, 0.0f); | |
SetWalkableFloorZ(0.71f); | |
MaxStepHeight = 45.0f; | |
PerchRadiusThreshold = 0.0f; | |
PerchAdditionalHeight = 40.f; | |
MaxFlySpeed = 600.0f; | |
MaxWalkSpeed = 600.0f; | |
MaxSwimSpeed = 300.0f; | |
MaxCustomMovementSpeed = MaxWalkSpeed; | |
MaxSimulationTimeStep = 0.05f; | |
MaxSimulationIterations = 8; | |
MaxJumpApexAttemptsPerSimulation = 2; | |
NumJumpApexAttempts = 0; | |
MaxDepenetrationWithGeometry = 500.f; | |
MaxDepenetrationWithGeometryAsProxy = 100.f; | |
MaxDepenetrationWithPawn = 100.f; | |
MaxDepenetrationWithPawnAsProxy = 2.f; | |
// Set to match EVectorQuantization::RoundTwoDecimals | |
NetProxyShrinkRadius = 0.01f; | |
NetProxyShrinkHalfHeight = 0.01f; | |
NetworkSimulatedSmoothLocationTime = 0.100f; | |
NetworkSimulatedSmoothRotationTime = 0.050f; | |
ListenServerNetworkSimulatedSmoothLocationTime = 0.040f; | |
ListenServerNetworkSimulatedSmoothRotationTime = 0.033f; | |
NetworkMaxSmoothUpdateDistance = 256.f; | |
NetworkNoSmoothUpdateDistance = 384.f; | |
NetworkSmoothingMode = ENetworkSmoothingMode::Exponential; | |
ServerLastClientGoodMoveAckTime = -1.f; | |
ServerLastClientAdjustmentTime = -1.f; | |
NetworkMinTimeBetweenClientAckGoodMoves = 0.10f; | |
NetworkMinTimeBetweenClientAdjustments = 0.10f; | |
NetworkMinTimeBetweenClientAdjustmentsLargeCorrection = 0.05f; | |
NetworkLargeClientCorrectionDistance = 15.0f; | |
MaxWalkSpeedCrouched = MaxWalkSpeed * 0.5f; | |
MaxOutOfWaterStepHeight = 40.0f; | |
OutofWaterZ = 420.0f; | |
AirControl = 0.05f; | |
AirControlBoostMultiplier = 2.f; | |
AirControlBoostVelocityThreshold = 25.f; | |
FallingLateralFriction = 0.f; | |
MaxAcceleration = 2048.0f; | |
BrakingFrictionFactor = 2.0f; // Historical value, 1 would be more appropriate. | |
BrakingSubStepTime = 1.0f / 33.0f; | |
BrakingDecelerationWalking = MaxAcceleration; | |
BrakingDecelerationFalling = 0.f; | |
BrakingDecelerationFlying = 0.f; | |
BrakingDecelerationSwimming = 0.f; | |
LedgeCheckThreshold = 4.0f; | |
JumpOutOfWaterPitch = 11.25f; | |
#if WITH_EDITORONLY_DATA | |
CrouchedSpeedMultiplier_DEPRECATED = 0.5f; | |
UpperImpactNormalScale_DEPRECATED = 0.5f; | |
bForceBraking_DEPRECATED = false; | |
#endif | |
Mass = 100.0f; | |
bJustTeleported = true; | |
CrouchedHalfHeight = 40.0f; | |
Buoyancy = 1.0f; | |
LastUpdateRotation = FQuat::Identity; | |
LastUpdateVelocity = FVector::ZeroVector; | |
PendingImpulseToApply = FVector::ZeroVector; | |
PendingLaunchVelocity = FVector::ZeroVector; | |
DefaultWaterMovementMode = MOVE_Swimming; | |
DefaultLandMovementMode = MOVE_Walking; | |
GroundMovementMode = MOVE_Walking; | |
bForceNextFloorCheck = true; | |
bShrinkProxyCapsule = true; | |
bCanWalkOffLedges = true; | |
bCanWalkOffLedgesWhenCrouching = false; | |
bNetworkSmoothingComplete = true; // Initially true until we get a net update, so we don't try to smooth to an uninitialized value. | |
bWantsToLeaveNavWalking = false; | |
bIsNavWalkingOnServer = false; | |
bSweepWhileNavWalking = true; | |
bNeedsSweepWhileWalkingUpdate = false; | |
bEnablePhysicsInteraction = true; | |
StandingDownwardForceScale = 1.0f; | |
InitialPushForceFactor = 500.0f; | |
PushForceFactor = 750000.0f; | |
PushForcePointZOffsetFactor = -0.75f; | |
bPushForceUsingZOffset = false; | |
bPushForceScaledToMass = false; | |
bScalePushForceToVelocity = true; | |
TouchForceFactor = 1.0f; | |
bTouchForceScaledToMass = true; | |
MinTouchForce = -1.0f; | |
MaxTouchForce = 250.0f; | |
RepulsionForce = 2.5f; | |
bAllowPhysicsRotationDuringAnimRootMotion = false; // Old default behavior. | |
bUseControllerDesiredRotation = false; | |
bUseSeparateBrakingFriction = false; // Old default behavior. | |
bMaintainHorizontalGroundVelocity = true; | |
bImpartBaseVelocityX = true; | |
bImpartBaseVelocityY = true; | |
bImpartBaseVelocityZ = true; | |
bImpartBaseAngularVelocity = true; | |
bIgnoreClientMovementErrorChecksAndCorrection = false; | |
bServerAcceptClientAuthoritativePosition = false; | |
bAlwaysCheckFloor = true; | |
// default character can jump, walk, and swim | |
NavAgentProps.bCanJump = true; | |
NavAgentProps.bCanWalk = true; | |
NavAgentProps.bCanSwim = true; | |
ResetMoveState(); | |
ClientPredictionData = NULL; | |
ServerPredictionData = NULL; | |
// This should be greater than tolerated player timeout * 2. | |
MinTimeBetweenTimeStampResets = 4.f * 60.f; | |
LastTimeStampResetServerTime = 0.f; | |
bEnableScopedMovementUpdates = true; | |
// Disabled by default since it can be a subtle behavior change, you should opt in if you want to accept that. | |
bEnableServerDualMoveScopedMovementUpdates = false; | |
bRequestedMoveUseAcceleration = true; | |
bUseRVOAvoidance = false; | |
bUseRVOPostProcess = false; | |
AvoidanceLockVelocity = FVector::ZeroVector; | |
AvoidanceLockTimer = 0.0f; | |
AvoidanceGroup.bGroup0 = true; | |
GroupsToAvoid.Packed = 0xFFFFFFFF; | |
GroupsToIgnore.Packed = 0; | |
AvoidanceConsiderationRadius = 500.0f; | |
OldBaseQuat = FQuat::Identity; | |
OldBaseLocation = FVector::ZeroVector; | |
NavMeshProjectionInterval = 0.1f; | |
NavMeshProjectionInterpSpeed = 12.f; | |
NavMeshProjectionHeightScaleUp = 0.67f; | |
NavMeshProjectionHeightScaleDown = 1.0f; | |
NavWalkingFloorDistTolerance = 10.0f; | |
} | |
void UMMOPlayerMovement::PostLoad() | |
{ | |
Super::PostLoad(); | |
#if WITH_EDITORONLY_DATA | |
const int32 LinkerUE4Ver = GetLinkerUE4Version(); | |
if (LinkerUE4Ver < VER_UE4_CHARACTER_MOVEMENT_DECELERATION) | |
{ | |
BrakingDecelerationWalking = MaxAcceleration; | |
} | |
if (LinkerUE4Ver < VER_UE4_CHARACTER_BRAKING_REFACTOR) | |
{ | |
// This bool used to apply walking braking in flying and swimming modes. | |
if (bForceBraking_DEPRECATED) | |
{ | |
BrakingDecelerationFlying = BrakingDecelerationWalking; | |
BrakingDecelerationSwimming = BrakingDecelerationWalking; | |
} | |
} | |
if (LinkerUE4Ver < VER_UE4_CHARACTER_MOVEMENT_WALKABLE_FLOOR_REFACTOR) | |
{ | |
// Compute the walkable floor angle, since we have never done so yet. | |
UMMOPlayerMovement::SetWalkableFloorZ(WalkableFloorZ); | |
} | |
if (LinkerUE4Ver < VER_UE4_DEPRECATED_MOVEMENTCOMPONENT_MODIFIED_SPEEDS) | |
{ | |
MaxWalkSpeedCrouched = MaxWalkSpeed * CrouchedSpeedMultiplier_DEPRECATED; | |
MaxCustomMovementSpeed = MaxWalkSpeed; | |
} | |
#endif | |
CharacterOwner = Cast<AMMOCharacter>(PawnOwner); | |
} | |
#if WITH_EDITOR | |
void UMMOPlayerMovement::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) | |
{ | |
Super::PostEditChangeProperty(PropertyChangedEvent); | |
const FProperty* PropertyThatChanged = PropertyChangedEvent.MemberProperty; | |
if (PropertyThatChanged && PropertyThatChanged->GetFName() == GET_MEMBER_NAME_CHECKED(UMMOPlayerMovement, WalkableFloorAngle)) | |
{ | |
// Compute WalkableFloorZ from the Angle. | |
SetWalkableFloorAngle(WalkableFloorAngle); | |
} | |
} | |
#endif // WITH_EDITOR | |
void UMMOPlayerMovement::OnRegister() | |
{ | |
const ENetMode NetMode = GetNetMode(); | |
if (bUseRVOAvoidance && NetMode == NM_Client) | |
{ | |
bUseRVOAvoidance = false; | |
} | |
Super::OnRegister(); | |
#if WITH_EDITOR | |
// Compute WalkableFloorZ from the WalkableFloorAngle. | |
// This is only to respond to changes propagated by PostEditChangeProperty, so it's only done in the editor. | |
SetWalkableFloorAngle(WalkableFloorAngle); | |
#endif | |
// Force linear smoothing for replays. | |
const UWorld* MyWorld = GetWorld(); | |
const bool bIsReplay = (MyWorld && MyWorld->IsPlayingReplay()); | |
if (bIsReplay) | |
{ | |
// At least one of these conditions will be true | |
const bool bHasInterpolationData = MyWorld && MyWorld->DemoNetDriver && (MyWorld->DemoNetDriver->GetPlaybackDemoVersion() < HISTORY_CHARACTER_MOVEMENT_NOINTERP); | |
const bool bHasRepMovement = MyWorld && MyWorld->DemoNetDriver && (MyWorld->DemoNetDriver->GetPlaybackDemoVersion() >= HISTORY_CHARACTER_MOVEMENT); | |
if (MMOCharacterMovementCVars::ReplayUseInterpolation == 1) | |
{ | |
if (bHasInterpolationData) | |
{ | |
NetworkSmoothingMode = ENetworkSmoothingMode::Replay; | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("p.ReplayUseInterpolation is enabled, but the replay was not recorded with interpolation data.")); | |
ensure(bHasRepMovement); | |
NetworkSmoothingMode = ENetworkSmoothingMode::Linear; | |
} | |
} | |
else | |
{ | |
if (bHasRepMovement) | |
{ | |
NetworkSmoothingMode = ENetworkSmoothingMode::Linear; | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("p.ReplayUseInterpolation is disabled, but the replay was not recorded with rep movement data.")); | |
ensure(bHasInterpolationData); | |
NetworkSmoothingMode = ENetworkSmoothingMode::Replay; | |
} | |
} | |
} | |
else if (NetMode == NM_ListenServer) | |
{ | |
// Linear smoothing works on listen servers, but makes a lot less sense under the typical high update rate. | |
if (NetworkSmoothingMode == ENetworkSmoothingMode::Linear) | |
{ | |
NetworkSmoothingMode = ENetworkSmoothingMode::Exponential; | |
} | |
} | |
} | |
void UMMOPlayerMovement::BeginDestroy() | |
{ | |
if (ClientPredictionData) | |
{ | |
delete ClientPredictionData; | |
ClientPredictionData = NULL; | |
} | |
if (ServerPredictionData) | |
{ | |
delete ServerPredictionData; | |
ServerPredictionData = NULL; | |
} | |
Super::BeginDestroy(); | |
} | |
void UMMOPlayerMovement::Deactivate() | |
{ | |
bStopMovementAbortPaths = false; // Mirrors StopMovementKeepPathing(), because Super calls StopMovement() and we want that handled differently. | |
Super::Deactivate(); | |
if (!IsActive()) | |
{ | |
ClearAccumulatedForces(); | |
if (CharacterOwner) | |
{ | |
CharacterOwner->ResetJumpState(); | |
} | |
} | |
bStopMovementAbortPaths = true; | |
} | |
void UMMOPlayerMovement::SetUpdatedComponent(USceneComponent* NewUpdatedComponent) | |
{ | |
if (NewUpdatedComponent) | |
{ | |
const AMMOCharacter* NewCharacterOwner = Cast<AMMOCharacter>(NewUpdatedComponent->GetOwner()); | |
if (NewCharacterOwner == NULL) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Error, TEXT("%s owned by %s must update a component owned by a Character"), *GetName(), *GetNameSafe(NewUpdatedComponent->GetOwner())); | |
return; | |
} | |
// check that UpdatedComponent is a Capsule | |
if (Cast<UCapsuleComponent>(NewUpdatedComponent) == NULL) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Error, TEXT("%s owned by %s must update a capsule component"), *GetName(), *GetNameSafe(NewUpdatedComponent->GetOwner())); | |
return; | |
} | |
} | |
if (bMovementInProgress) | |
{ | |
// failsafe to avoid crashes in CharacterMovement. | |
bDeferUpdateMoveComponent = true; | |
DeferredUpdatedMoveComponent = NewUpdatedComponent; | |
return; | |
} | |
bDeferUpdateMoveComponent = false; | |
DeferredUpdatedMoveComponent = NULL; | |
USceneComponent* OldUpdatedComponent = UpdatedComponent; | |
UPrimitiveComponent* OldPrimitive = Cast<UPrimitiveComponent>(UpdatedComponent); | |
if (IsValid(OldPrimitive) && OldPrimitive->OnComponentBeginOverlap.IsBound()) | |
{ | |
OldPrimitive->OnComponentBeginOverlap.RemoveDynamic(this, &UMMOPlayerMovement::CapsuleTouched); | |
} | |
Super::SetUpdatedComponent(NewUpdatedComponent); | |
CharacterOwner = Cast<AMMOCharacter>(PawnOwner); | |
if (UpdatedComponent != OldUpdatedComponent) | |
{ | |
ClearAccumulatedForces(); | |
} | |
if (UpdatedComponent == NULL) | |
{ | |
StopActiveMovement(); | |
} | |
const bool bValidUpdatedPrimitive = IsValid(UpdatedPrimitive); | |
if (bValidUpdatedPrimitive && bEnablePhysicsInteraction) | |
{ | |
UpdatedPrimitive->OnComponentBeginOverlap.AddUniqueDynamic(this, &UMMOPlayerMovement::CapsuleTouched); | |
} | |
if (bNeedsSweepWhileWalkingUpdate) | |
{ | |
bSweepWhileNavWalking = bValidUpdatedPrimitive ? UpdatedPrimitive->GetGenerateOverlapEvents() : false; | |
bNeedsSweepWhileWalkingUpdate = false; | |
} | |
if (bUseRVOAvoidance && IsValid(NewUpdatedComponent)) | |
{ | |
UAvoidanceManager* AvoidanceManager = GetWorld()->GetAvoidanceManager(); | |
if (AvoidanceManager) | |
{ | |
AvoidanceManager->RegisterMovementComponent(this, AvoidanceWeight); | |
} | |
} | |
} | |
bool UMMOPlayerMovement::HasValidData() const | |
{ | |
const bool bIsValid = UpdatedComponent && IsValid(CharacterOwner); | |
#if ENABLE_NAN_DIAGNOSTIC | |
if (bIsValid) | |
{ | |
// NaN-checking updates | |
if (Velocity.ContainsNaN()) | |
{ | |
logOrEnsureNanError(TEXT("UMMOPlayerMovement::HasValidData() detected NaN/INF for (%s) in Velocity:\n%s"), *GetPathNameSafe(this), *Velocity.ToString()); | |
UMMOPlayerMovement* MutableThis = const_cast<UMMOPlayerMovement*>(this); | |
MutableThis->Velocity = FVector::ZeroVector; | |
} | |
if (!UpdatedComponent->GetComponentTransform().IsValid()) | |
{ | |
logOrEnsureNanError(TEXT("UMMOPlayerMovement::HasValidData() detected NaN/INF for (%s) in UpdatedComponent->ComponentTransform:\n%s"), *GetPathNameSafe(this), *UpdatedComponent->GetComponentTransform().ToHumanReadableString()); | |
} | |
if (UpdatedComponent->GetComponentRotation().ContainsNaN()) | |
{ | |
logOrEnsureNanError(TEXT("UMMOPlayerMovement::HasValidData() detected NaN/INF for (%s) in UpdatedComponent->GetComponentRotation():\n%s"), *GetPathNameSafe(this), *UpdatedComponent->GetComponentRotation().ToString()); | |
} | |
} | |
#endif | |
return bIsValid; | |
} | |
FCollisionShape UMMOPlayerMovement::GetPawnCapsuleCollisionShape(const EShrinkCapsuleExtent ShrinkMode, const float CustomShrinkAmount) const | |
{ | |
FVector Extent = GetPawnCapsuleExtent(ShrinkMode, CustomShrinkAmount); | |
return FCollisionShape::MakeCapsule(Extent); | |
} | |
FVector UMMOPlayerMovement::GetPawnCapsuleExtent(const EShrinkCapsuleExtent ShrinkMode, const float CustomShrinkAmount) const | |
{ | |
check(CharacterOwner); | |
float Radius, HalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(Radius, HalfHeight); | |
FVector CapsuleExtent(Radius, Radius, HalfHeight); | |
float RadiusEpsilon = 0.f; | |
float HeightEpsilon = 0.f; | |
switch (ShrinkMode) | |
{ | |
case SHRINK_None: | |
return CapsuleExtent; | |
case SHRINK_RadiusCustom: | |
RadiusEpsilon = CustomShrinkAmount; | |
break; | |
case SHRINK_HeightCustom: | |
HeightEpsilon = CustomShrinkAmount; | |
break; | |
case SHRINK_AllCustom: | |
RadiusEpsilon = CustomShrinkAmount; | |
HeightEpsilon = CustomShrinkAmount; | |
break; | |
default: | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("Unknown EShrinkCapsuleExtent in UMMOPlayerMovement::GetCapsuleExtent")); | |
break; | |
} | |
// Don't shrink to zero extent. | |
const float MinExtent = KINDA_SMALL_NUMBER * 10.f; | |
CapsuleExtent.X = FMath::Max(CapsuleExtent.X - RadiusEpsilon, MinExtent); | |
CapsuleExtent.Y = CapsuleExtent.X; | |
CapsuleExtent.Z = FMath::Max(CapsuleExtent.Z - HeightEpsilon, MinExtent); | |
return CapsuleExtent; | |
} | |
bool UMMOPlayerMovement::DoJump(bool bReplayingMoves) | |
{ | |
if (CharacterOwner && CharacterOwner->CanJump()) | |
{ | |
// Don't jump if we can't move up/down. | |
if (!bConstrainToPlane || FMath::Abs(PlaneConstraintNormal.Z) != 1.f) | |
{ | |
Velocity.Z = FMath::Max(Velocity.Z, JumpZVelocity); | |
SetMovementMode(MOVE_Falling); | |
return true; | |
} | |
} | |
return false; | |
} | |
bool UMMOPlayerMovement::CanAttemptJump() const | |
{ | |
return IsJumpAllowed() && | |
!bWantsToCrouch && | |
(IsMovingOnGround() || IsFalling()); // Falling included for double-jump and non-zero jump hold time, but validated by character. | |
} | |
FVector UMMOPlayerMovement::GetImpartedMovementBaseVelocity() const | |
{ | |
FVector Result = FVector::ZeroVector; | |
if (CharacterOwner) | |
{ | |
UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase(); | |
if (MovementBaseUtility::IsDynamicBase(MovementBase)) | |
{ | |
FVector BaseVelocity = MovementBaseUtility::GetMovementBaseVelocity(MovementBase, CharacterOwner->GetBasedMovement().BoneName); | |
if (bImpartBaseAngularVelocity) | |
{ | |
const FVector CharacterBasePosition = (UpdatedComponent->GetComponentLocation() - FVector(0.f, 0.f, CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleHalfHeight())); | |
const FVector BaseTangentialVel = MovementBaseUtility::GetMovementBaseTangentialVelocity(MovementBase, CharacterOwner->GetBasedMovement().BoneName, CharacterBasePosition); | |
BaseVelocity += BaseTangentialVel; | |
} | |
if (bImpartBaseVelocityX) | |
{ | |
Result.X = BaseVelocity.X; | |
} | |
if (bImpartBaseVelocityY) | |
{ | |
Result.Y = BaseVelocity.Y; | |
} | |
if (bImpartBaseVelocityZ) | |
{ | |
Result.Z = BaseVelocity.Z; | |
} | |
} | |
} | |
return Result; | |
} | |
void UMMOPlayerMovement::Launch(FVector const& LaunchVel) | |
{ | |
if ((MovementMode != MOVE_None) && IsActive() && HasValidData()) | |
{ | |
PendingLaunchVelocity = LaunchVel; | |
} | |
} | |
bool UMMOPlayerMovement::HandlePendingLaunch() | |
{ | |
if (!PendingLaunchVelocity.IsZero() && HasValidData()) | |
{ | |
Velocity = PendingLaunchVelocity; | |
SetMovementMode(MOVE_Falling); | |
PendingLaunchVelocity = FVector::ZeroVector; | |
bForceNextFloorCheck = true; | |
return true; | |
} | |
return false; | |
} | |
void UMMOPlayerMovement::JumpOff(AActor* MovementBaseActor) | |
{ | |
if (!bPerformingJumpOff) | |
{ | |
bPerformingJumpOff = true; | |
if (CharacterOwner) | |
{ | |
const float MaxSpeed = GetMaxSpeed() * 0.85f; | |
Velocity += MaxSpeed * GetBestDirectionOffActor(MovementBaseActor); | |
if (Velocity.Size2D() > MaxSpeed) | |
{ | |
Velocity = MaxSpeed * Velocity.GetSafeNormal(); | |
} | |
Velocity.Z = JumpOffJumpZFactor * JumpZVelocity; | |
SetMovementMode(MOVE_Falling); | |
} | |
bPerformingJumpOff = false; | |
} | |
} | |
FVector UMMOPlayerMovement::GetBestDirectionOffActor(AActor* BaseActor) const | |
{ | |
// By default, just pick a random direction. Derived character classes can choose to do more complex calculations, | |
// such as finding the shortest distance to move in based on the BaseActor's Bounding Volume. | |
const float RandAngle = FMath::DegreesToRadians(GetNetworkSafeRandomAngleDegrees()); | |
return FVector(FMath::Cos(RandAngle), FMath::Sin(RandAngle), 0.5f).GetSafeNormal(); | |
} | |
float UMMOPlayerMovement::GetNetworkSafeRandomAngleDegrees() const | |
{ | |
float Angle = RandomStream.FRand() * 360.f; | |
if (!IsNetMode(NM_Standalone)) | |
{ | |
// Networked game | |
// Get a timestamp that is relatively close between client and server (within ping). | |
FMMONetworkPredictionData_Server_Character const* ServerData = (HasPredictionData_Server() ? GetPredictionData_Server_Character() : NULL); | |
FMMONetworkPredictionData_Client_Character const* ClientData = (HasPredictionData_Client() ? GetPredictionData_Client_Character() : NULL); | |
float TimeStamp = Angle; | |
if (ServerData) | |
{ | |
TimeStamp = ServerData->CurrentClientTimeStamp; | |
} | |
else if (ClientData) | |
{ | |
TimeStamp = ClientData->CurrentTimeStamp; | |
} | |
// Convert to degrees with a faster period. | |
const float PeriodMult = 8.0f; | |
Angle = TimeStamp * PeriodMult; | |
Angle = FMath::Fmod(Angle, 360.f); | |
} | |
return Angle; | |
} | |
void UMMOPlayerMovement::SetDefaultMovementMode() | |
{ | |
// check for water volume | |
if (CanEverSwim() && IsInWater()) | |
{ | |
SetMovementMode(DefaultWaterMovementMode); | |
} | |
else if (!CharacterOwner || MovementMode != DefaultLandMovementMode) | |
{ | |
const float SavedVelocityZ = Velocity.Z; | |
SetMovementMode(DefaultLandMovementMode); | |
// Avoid 1-frame delay if trying to walk but walking fails at this location. | |
if (MovementMode == MOVE_Walking && GetMovementBase() == NULL) | |
{ | |
Velocity.Z = SavedVelocityZ; // Prevent temporary walking state from zeroing Z velocity. | |
SetMovementMode(MOVE_Falling); | |
} | |
} | |
} | |
void UMMOPlayerMovement::SetGroundMovementMode(EMovementMode NewGroundMovementMode) | |
{ | |
// Enforce restriction that it's either Walking or NavWalking. | |
if (NewGroundMovementMode != MOVE_Walking && NewGroundMovementMode != MOVE_NavWalking) | |
{ | |
return; | |
} | |
// Set new value | |
GroundMovementMode = NewGroundMovementMode; | |
// Possibly change movement modes if already on ground and choosing the other ground mode. | |
const bool bOnGround = (MovementMode == MOVE_Walking || MovementMode == MOVE_NavWalking); | |
if (bOnGround && MovementMode != NewGroundMovementMode) | |
{ | |
SetMovementMode(NewGroundMovementMode); | |
} | |
} | |
void UMMOPlayerMovement::SetMovementMode(EMovementMode NewMovementMode, uint8 NewCustomMode) | |
{ | |
if (NewMovementMode != MOVE_Custom) | |
{ | |
NewCustomMode = 0; | |
} | |
// If trying to use NavWalking but there is no navmesh, use walking instead. | |
if (NewMovementMode == MOVE_NavWalking) | |
{ | |
if (GetNavData() == nullptr) | |
{ | |
NewMovementMode = MOVE_Walking; | |
} | |
} | |
// Do nothing if nothing is changing. | |
if (MovementMode == NewMovementMode) | |
{ | |
// Allow changes in custom sub-mode. | |
if ((NewMovementMode != MOVE_Custom) || (NewCustomMode == CustomMovementMode)) | |
{ | |
return; | |
} | |
} | |
const EMovementMode PrevMovementMode = MovementMode; | |
const uint8 PrevCustomMode = CustomMovementMode; | |
MovementMode = NewMovementMode; | |
CustomMovementMode = NewCustomMode; | |
// We allow setting movement mode before we have a component to update, in case this happens at startup. | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// Handle change in movement mode | |
OnMovementModeChanged(PrevMovementMode, PrevCustomMode); | |
// @todo UE4 do we need to disable ragdoll physics here? Should this function do nothing if in ragdoll? | |
} | |
void UMMOPlayerMovement::OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// Update collision settings if needed | |
if (MovementMode == MOVE_NavWalking) | |
{ | |
GroundMovementMode = MovementMode; | |
// Walking uses only XY velocity | |
Velocity.Z = 0.f; | |
SetNavWalkingPhysics(true); | |
} | |
else if (PreviousMovementMode == MOVE_NavWalking) | |
{ | |
if (MovementMode == DefaultLandMovementMode || IsWalking()) | |
{ | |
const bool bSucceeded = TryToLeaveNavWalking(); | |
if (!bSucceeded) | |
{ | |
return; | |
} | |
} | |
else | |
{ | |
SetNavWalkingPhysics(false); | |
} | |
} | |
// React to changes in the movement mode. | |
if (MovementMode == MOVE_Walking) | |
{ | |
// Walking uses only XY velocity, and must be on a walkable floor, with a Base. | |
Velocity.Z = 0.f; | |
bCrouchMaintainsBaseLocation = true; | |
GroundMovementMode = MovementMode; | |
// make sure we update our new floor/base on initial entry of the walking physics | |
FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, false); | |
AdjustFloorHeight(); | |
SetBaseFromFloor(CurrentFloor); | |
} | |
else | |
{ | |
CurrentFloor.Clear(); | |
bCrouchMaintainsBaseLocation = false; | |
if (MovementMode == MOVE_Falling) | |
{ | |
Velocity += GetImpartedMovementBaseVelocity(); | |
CharacterOwner->Falling(); | |
} | |
SetBase(NULL); | |
if (MovementMode == MOVE_None) | |
{ | |
// Kill velocity and clear queued up events | |
StopMovementKeepPathing(); | |
CharacterOwner->ResetJumpState(); | |
ClearAccumulatedForces(); | |
} | |
} | |
if (MovementMode == MOVE_Falling && PreviousMovementMode != MOVE_Falling) | |
{ | |
IPathFollowingAgentInterface* PFAgent = GetPathFollowingAgent(); | |
if (PFAgent) | |
{ | |
PFAgent->OnStartedFalling(); | |
} | |
} | |
CharacterOwner->OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode); | |
ensureMsgf(GroundMovementMode == MOVE_Walking || GroundMovementMode == MOVE_NavWalking, TEXT("Invalid GroundMovementMode %d. MovementMode: %d, PreviousMovementMode: %d"), GroundMovementMode.GetValue(), MovementMode.GetValue(), PreviousMovementMode); | |
}; | |
namespace PackedMovementModeConstants | |
{ | |
const uint32 GroundShift = FMath::CeilLogTwo(MOVE_MAX); | |
const uint8 CustomModeThr = 2 * (1 << GroundShift); | |
const uint8 GroundMask = (1 << GroundShift) - 1; | |
} | |
uint8 UMMOPlayerMovement::PackNetworkMovementMode() const | |
{ | |
if (MovementMode != MOVE_Custom) | |
{ | |
ensureMsgf(GroundMovementMode == MOVE_Walking || GroundMovementMode == MOVE_NavWalking, TEXT("Invalid GroundMovementMode %d."), GroundMovementMode.GetValue()); | |
const uint8 GroundModeBit = (GroundMovementMode == MOVE_Walking ? 0 : 1); | |
return uint8(MovementMode.GetValue()) | (GroundModeBit << PackedMovementModeConstants::GroundShift); | |
} | |
else | |
{ | |
return CustomMovementMode + PackedMovementModeConstants::CustomModeThr; | |
} | |
} | |
void UMMOPlayerMovement::UnpackNetworkMovementMode(const uint8 ReceivedMode, TEnumAsByte<EMovementMode>& OutMode, uint8& OutCustomMode, TEnumAsByte<EMovementMode>& OutGroundMode) const | |
{ | |
if (ReceivedMode < PackedMovementModeConstants::CustomModeThr) | |
{ | |
OutMode = TEnumAsByte<EMovementMode>(ReceivedMode & PackedMovementModeConstants::GroundMask); | |
OutCustomMode = 0; | |
const uint8 GroundModeBit = (ReceivedMode >> PackedMovementModeConstants::GroundShift); | |
OutGroundMode = TEnumAsByte<EMovementMode>(GroundModeBit == 0 ? MOVE_Walking : MOVE_NavWalking); | |
} | |
else | |
{ | |
OutMode = MOVE_Custom; | |
OutCustomMode = ReceivedMode - PackedMovementModeConstants::CustomModeThr; | |
OutGroundMode = MOVE_Walking; | |
} | |
} | |
void UMMOPlayerMovement::ApplyNetworkMovementMode(const uint8 ReceivedMode) | |
{ | |
TEnumAsByte<EMovementMode> NetMovementMode(MOVE_None); | |
TEnumAsByte<EMovementMode> NetGroundMode(MOVE_None); | |
uint8 NetCustomMode(0); | |
UnpackNetworkMovementMode(ReceivedMode, NetMovementMode, NetCustomMode, NetGroundMode); | |
ensureMsgf(NetGroundMode == MOVE_Walking || NetGroundMode == MOVE_NavWalking, TEXT("Invalid NetGroundMode %d."), NetGroundMode.GetValue()); | |
// set additional flag, GroundMovementMode will be overwritten by SetMovementMode to match actual mode on client side | |
bIsNavWalkingOnServer = (NetGroundMode == MOVE_NavWalking); | |
GroundMovementMode = NetGroundMode; | |
SetMovementMode(NetMovementMode, NetCustomMode); | |
} | |
void UMMOPlayerMovement::PerformAirControlForPathFollowing(FVector Direction, float ZDiff) | |
{ | |
// use air control if low grav or above destination and falling towards it | |
if (CharacterOwner && Velocity.Z < 0.f && (ZDiff < 0.f || GetGravityZ() > 0.9f * GetWorld()->GetDefaultGravityZ())) | |
{ | |
if (ZDiff < 0.f) | |
{ | |
if ((Velocity.X == 0.f) && (Velocity.Y == 0.f)) | |
{ | |
Acceleration = FVector::ZeroVector; | |
} | |
else | |
{ | |
float Dist2D = Direction.Size2D(); | |
//Direction.Z = 0.f; | |
Acceleration = Direction.GetSafeNormal() * GetMaxAcceleration(); | |
if ((Dist2D < 0.5f * FMath::Abs(Direction.Z)) && ((Velocity | Direction) > 0.5f * FMath::Square(Dist2D))) | |
{ | |
Acceleration *= -1.f; | |
} | |
if (Dist2D < 1.5f * CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleRadius()) | |
{ | |
Velocity.X = 0.f; | |
Velocity.Y = 0.f; | |
Acceleration = FVector::ZeroVector; | |
} | |
else if ((Velocity | Direction) < 0.f) | |
{ | |
float M = FMath::Max(0.f, 0.2f - GetWorld()->DeltaTimeSeconds); | |
Velocity.X *= M; | |
Velocity.Y *= M; | |
} | |
} | |
} | |
} | |
} | |
void UMMOPlayerMovement::Serialize(FArchive& Archive) | |
{ | |
Super::Serialize(Archive); | |
if (Archive.IsLoading() && Archive.UE4Ver() < VER_UE4_ADDED_SWEEP_WHILE_WALKING_FLAG) | |
{ | |
// We need to update the bSweepWhileNavWalking flag to match the previous behavior. | |
// Since UpdatedComponent is transient, we'll have to wait until we're registered. | |
bNeedsSweepWhileWalkingUpdate = true; | |
} | |
} | |
void UMMOPlayerMovement::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) | |
{ | |
const FVector InputVector = ConsumeInputVector(); | |
if (!HasValidData() || ShouldSkipUpdate(DeltaTime)) | |
{ | |
return; | |
} | |
Super::TickComponent(DeltaTime, TickType, ThisTickFunction); | |
// Super tick may destroy/invalidate CharacterOwner or UpdatedComponent, so we need to re-check. | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// See if we fell out of the world. | |
const bool bIsSimulatingPhysics = UpdatedComponent->IsSimulatingPhysics(); | |
if (CharacterOwner->GetLocalRole() == ROLE_Authority && (!bCheatFlying || bIsSimulatingPhysics) && !CharacterOwner->CheckStillInWorld()) | |
{ | |
return; | |
} | |
// We don't update if simulating physics (eg ragdolls). | |
if (bIsSimulatingPhysics) | |
{ | |
// Update camera to ensure client gets updates even when physics move him far away from point where simulation started | |
if (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client)) | |
{ | |
MarkForClientCameraUpdate(); | |
} | |
ClearAccumulatedForces(); | |
return; | |
} | |
AvoidanceLockTimer -= DeltaTime; | |
if (CharacterOwner->GetLocalRole() > ROLE_SimulatedProxy) | |
{ | |
// If we are a client we might have received an update from the server. | |
const bool bIsClient = (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client)); | |
if (bIsClient) | |
{ | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (ClientData && ClientData->bUpdatePosition) | |
{ | |
ClientUpdatePositionAfterServerUpdate(); | |
} | |
} | |
// Allow root motion to move characters that have no controller. | |
if (CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion())) | |
{ | |
{ | |
// We need to check the jump state before adjusting input acceleration, to minimize latency | |
// and to make sure acceleration respects our potentially new falling state. | |
CharacterOwner->CheckJumpInput(DeltaTime); | |
// apply input to acceleration | |
Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector)); | |
AnalogInputModifier = ComputeAnalogInputModifier(); | |
} | |
if (CharacterOwner->GetLocalRole() == ROLE_Authority) | |
{ | |
PerformMovement(DeltaTime); | |
} | |
else if (bIsClient) | |
{ | |
ReplicateMoveToServer(DeltaTime, Acceleration); | |
} | |
} | |
else if (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy) | |
{ | |
// Server ticking for remote client. | |
// Between net updates from the client we need to update position if based on another object, | |
// otherwise the object will move on intermediate frames and we won't follow it. | |
MaybeUpdateBasedMovement(DeltaTime); | |
MaybeSaveBaseLocation(); | |
// Smooth on listen server for local view of remote clients. We may receive updates at a rate different than our own tick rate. | |
if (MMOCharacterMovementCVars::NetEnableListenServerSmoothing && !bNetworkSmoothingComplete && IsNetMode(NM_ListenServer)) | |
{ | |
SmoothClientPosition(DeltaTime); | |
} | |
} | |
} | |
else if (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) | |
{ | |
if (bShrinkProxyCapsule) | |
{ | |
AdjustProxyCapsuleSize(); | |
} | |
SimulatedTick(DeltaTime); | |
} | |
if (bUseRVOAvoidance) | |
{ | |
UpdateDefaultAvoidance(); | |
} | |
if (bEnablePhysicsInteraction) | |
{ | |
ApplyDownwardForce(DeltaTime); | |
ApplyRepulsionForce(DeltaTime); | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
const bool bMMOVisualizeMovement = MMOCharacterMovementCVars::VisualizeMovement > 0; | |
if (bMMOVisualizeMovement) | |
{ | |
VisualizeMovement(); | |
} | |
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
} | |
void UMMOPlayerMovement::PostPhysicsTickComponent(float DeltaTime, FMMOCharacterMovementComponentPostPhysicsTickFunction& ThisTickFunction) | |
{ | |
if (bDeferUpdateBasedMovement) | |
{ | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates); | |
UpdateBasedMovement(DeltaTime); | |
SaveBaseLocation(); | |
bDeferUpdateBasedMovement = false; | |
} | |
} | |
void UMMOPlayerMovement::AdjustProxyCapsuleSize() | |
{ | |
if (bShrinkProxyCapsule && CharacterOwner && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) | |
{ | |
bShrinkProxyCapsule = false; | |
float ShrinkRadius = FMath::Max(0.f, NetProxyShrinkRadius); | |
float ShrinkHalfHeight = FMath::Max(0.f, NetProxyShrinkHalfHeight); | |
if (ShrinkRadius == 0.f && ShrinkHalfHeight == 0.f) | |
{ | |
return; | |
} | |
float Radius, HalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetUnscaledCapsuleSize(Radius, HalfHeight); | |
const float ComponentScale = CharacterOwner->GetCapsuleComponent()->GetShapeScale(); | |
if (ComponentScale <= KINDA_SMALL_NUMBER) | |
{ | |
return; | |
} | |
const float NewRadius = FMath::Max(0.f, Radius - ShrinkRadius / ComponentScale); | |
const float NewHalfHeight = FMath::Max(0.f, HalfHeight - ShrinkHalfHeight / ComponentScale); | |
if (NewRadius == 0.f || NewHalfHeight == 0.f) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("Invalid attempt to shrink Proxy capsule for %s to zero dimension!"), *CharacterOwner->GetName()); | |
return; | |
} | |
UE_LOG(LogMMOCharacterMovement, Verbose, TEXT("Shrinking capsule for %s from (r=%.3f, h=%.3f) to (r=%.3f, h=%.3f)"), *CharacterOwner->GetName(), | |
Radius * ComponentScale, HalfHeight * ComponentScale, NewRadius * ComponentScale, NewHalfHeight * ComponentScale); | |
CharacterOwner->GetCapsuleComponent()->SetCapsuleSize(NewRadius, NewHalfHeight, true); | |
} | |
} | |
void UMMOPlayerMovement::SimulatedTick(float DeltaSeconds) | |
{ | |
checkSlow(CharacterOwner != nullptr); | |
if (NetworkSmoothingMode == ENetworkSmoothingMode::Replay) | |
{ | |
const FVector OldLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector; | |
const FVector OldVelocity = Velocity; | |
// Interpolate between appropriate samples | |
{ | |
SmoothClientPosition(DeltaSeconds); | |
} | |
// Update replicated movement mode | |
ApplyNetworkMovementMode(GetCharacterOwner()->GetReplicatedMovementMode()); | |
UpdateComponentVelocity(); | |
bJustTeleported = false; | |
if (CharacterOwner) | |
{ | |
CharacterOwner->RootMotionRepMoves.Empty(); | |
CurrentRootMotion.Clear(); | |
CharacterOwner->SavedRootMotion.Clear(); | |
} | |
// Note: we do not call the Super implementation, that runs prediction. | |
// We do still need to call these though | |
OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity); | |
CallMovementUpdateDelegate(DeltaSeconds, OldLocation, OldVelocity); | |
LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector; | |
LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity; | |
LastUpdateVelocity = Velocity; | |
//TickCharacterPose( DeltaSeconds ); | |
return; | |
} | |
// If we are playing a RootMotion AnimMontage. | |
if (CharacterOwner->IsPlayingNetworkedRootMotionMontage()) | |
{ | |
bWasSimulatingRootMotion = true; | |
UE_LOG(LogRootMotion, Verbose, TEXT("UMMOPlayerMovement::SimulatedTick")); | |
// Tick animations before physics. | |
if (CharacterOwner && CharacterOwner->GetMesh()) | |
{ | |
TickCharacterPose(DeltaSeconds); | |
// Make sure animation didn't trigger an event that destroyed us | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
} | |
if (RootMotionParams.bHasRootMotion) | |
{ | |
const FQuat OldRotationQuat = UpdatedComponent->GetComponentQuat(); | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
SimulateRootMotion(DeltaSeconds, RootMotionParams.GetRootMotionTransform()); | |
#if !(UE_BUILD_SHIPPING) | |
// debug | |
if (CharacterOwner && false) | |
{ | |
const FRotator OldRotation = OldRotationQuat.Rotator(); | |
const FRotator NewRotation = UpdatedComponent->GetComponentRotation(); | |
const FVector NewLocation = UpdatedComponent->GetComponentLocation(); | |
DrawDebugCoordinateSystem(GetWorld(), CharacterOwner->GetMesh()->GetComponentLocation() + FVector(0, 0, 1), NewRotation, 50.f, false); | |
DrawDebugLine(GetWorld(), OldLocation, NewLocation, FColor::Red, false, 10.f); | |
UE_LOG(LogRootMotion, Log, TEXT("UMMOPlayerMovement::SimulatedTick DeltaMovement Translation: %s, Rotation: %s, MovementBase: %s"), | |
*(NewLocation - OldLocation).ToCompactString(), *(NewRotation - OldRotation).GetNormalized().ToCompactString(), *GetNameSafe(CharacterOwner->GetMovementBase())); | |
} | |
#endif // !(UE_BUILD_SHIPPING) | |
} | |
// then, once our position is up to date with our animation, | |
// handle position correction if we have any pending updates received from the server. | |
if (CharacterOwner && (CharacterOwner->RootMotionRepMoves.Num() > 0)) | |
{ | |
CharacterOwner->SimulatedRootMotionPositionFixup(DeltaSeconds); | |
} | |
} | |
else if (CurrentRootMotion.HasActiveRootMotionSources()) | |
{ | |
// We have root motion sources and possibly animated root motion | |
bWasSimulatingRootMotion = true; | |
UE_LOG(LogRootMotion, Verbose, TEXT("UMMOPlayerMovement::SimulatedTick")); | |
// If we have RootMotionRepMoves, find the most recent important one and set position/rotation to it | |
bool bCorrectedToServer = false; | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
const FQuat OldRotation = UpdatedComponent->GetComponentQuat(); | |
if (CharacterOwner->RootMotionRepMoves.Num() > 0) | |
{ | |
// Move Actor back to position of that buffered move. (server replicated position). | |
FMMOSimulatedRootMotionReplicatedMove& RootMotionRepMove = CharacterOwner->RootMotionRepMoves.Last(); | |
if (CharacterOwner->RestoreReplicatedMove(RootMotionRepMove)) | |
{ | |
bCorrectedToServer = true; | |
} | |
Acceleration = RootMotionRepMove.RootMotion.Acceleration; | |
CharacterOwner->PostNetReceiveVelocity(RootMotionRepMove.RootMotion.LinearVelocity); | |
LastUpdateVelocity = RootMotionRepMove.RootMotion.LinearVelocity; | |
// Convert RootMotionSource Server IDs -> Local IDs in AuthoritativeRootMotion and cull invalid | |
// so that when we use this root motion it has the correct IDs | |
ConvertRootMotionServerIDsToLocalIDs(CurrentRootMotion, RootMotionRepMove.RootMotion.AuthoritativeRootMotion, RootMotionRepMove.Time); | |
RootMotionRepMove.RootMotion.AuthoritativeRootMotion.CullInvalidSources(); | |
// Set root motion states to that of repped in state | |
CurrentRootMotion.UpdateStateFrom(RootMotionRepMove.RootMotion.AuthoritativeRootMotion, true); | |
// Clear out existing RootMotionRepMoves since we've consumed the most recent | |
UE_LOG(LogRootMotion, Log, TEXT("\tClearing old moves in SimulatedTick (%d)"), CharacterOwner->RootMotionRepMoves.Num()); | |
CharacterOwner->RootMotionRepMoves.Reset(); | |
} | |
// Perform movement | |
PerformMovement(DeltaSeconds); | |
// After movement correction, smooth out error in position if any. | |
if (bCorrectedToServer) | |
{ | |
SmoothCorrection(OldLocation, OldRotation, UpdatedComponent->GetComponentLocation(), UpdatedComponent->GetComponentQuat()); | |
} | |
} | |
// Not playing RootMotion AnimMontage | |
else | |
{ | |
// if we were simulating root motion, we've been ignoring regular ReplicatedMovement updates. | |
// If we're not simulating root motion anymore, force us to sync our movement properties. | |
// (Root Motion could leave Velocity out of sync w/ ReplicatedMovement) | |
if (bWasSimulatingRootMotion) | |
{ | |
bWasSimulatingRootMotion = false; | |
CharacterOwner->RootMotionRepMoves.Empty(); | |
CharacterOwner->OnRep_ReplicatedMovement(); | |
CharacterOwner->OnRep_ReplicatedBasedMovement(); | |
ApplyNetworkMovementMode(GetCharacterOwner()->GetReplicatedMovementMode()); | |
} | |
if (CharacterOwner->IsReplicatingMovement() && UpdatedComponent) | |
{ | |
USkeletalMeshComponent* Mesh = CharacterOwner->GetMesh(); | |
const FVector SavedMeshRelativeLocation = Mesh ? Mesh->GetRelativeLocation() : FVector::ZeroVector; | |
const FQuat SavedCapsuleRotation = UpdatedComponent->GetComponentQuat(); | |
const bool bPreventMeshMovement = !bNetworkSmoothingComplete; | |
// Avoid moving the mesh during movement if SmoothClientPosition will take care of it. | |
{ | |
const FScopedPreventAttachedComponentMove PreventMeshMovement(bPreventMeshMovement ? Mesh : nullptr); | |
if (CharacterOwner->IsMatineeControlled() || CharacterOwner->IsPlayingRootMotion()) | |
{ | |
PerformMovement(DeltaSeconds); | |
} | |
else | |
{ | |
SimulateMovement(DeltaSeconds); | |
} | |
} | |
// With Linear smoothing we need to know if the rotation changes, since the mesh should follow along with that (if it was prevented above). | |
// This should be rare that rotation changes during simulation, but it can happen when ShouldRemainVertical() changes, or standing on a moving base. | |
const bool bValidateRotation = bPreventMeshMovement && (NetworkSmoothingMode == ENetworkSmoothingMode::Linear); | |
if (bValidateRotation && UpdatedComponent) | |
{ | |
// Same mesh with different rotation? | |
const FQuat NewCapsuleRotation = UpdatedComponent->GetComponentQuat(); | |
if (Mesh == CharacterOwner->GetMesh() && !NewCapsuleRotation.Equals(SavedCapsuleRotation, 1e-6f) && ClientPredictionData) | |
{ | |
// Smoothing should lerp toward this new rotation target, otherwise it will just try to go back toward the old rotation. | |
ClientPredictionData->MeshRotationTarget = NewCapsuleRotation; | |
Mesh->SetRelativeLocationAndRotation(SavedMeshRelativeLocation, CharacterOwner->GetBaseRotationOffset()); | |
} | |
} | |
} | |
} | |
// Smooth mesh location after moving the capsule above. | |
if (!bNetworkSmoothingComplete) | |
{ | |
SmoothClientPosition(DeltaSeconds); | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterNetSmoothing, Verbose, TEXT("Skipping network smoothing for %s."), *GetNameSafe(CharacterOwner)); | |
} | |
} | |
FTransform UMMOPlayerMovement::ConvertLocalRootMotionToWorld(const FTransform& LocalRootMotionTransform) | |
{ | |
const FTransform PreProcessedRootMotion = ProcessRootMotionPreConvertToWorld.IsBound() ? ProcessRootMotionPreConvertToWorld.Execute(LocalRootMotionTransform, this) : LocalRootMotionTransform; | |
const FTransform WorldSpaceRootMotion = CharacterOwner->GetMesh()->ConvertLocalRootMotionToWorld(PreProcessedRootMotion); | |
return ProcessRootMotionPostConvertToWorld.IsBound() ? ProcessRootMotionPostConvertToWorld.Execute(WorldSpaceRootMotion, this) : WorldSpaceRootMotion; | |
} | |
void UMMOPlayerMovement::SimulateRootMotion(float DeltaSeconds, const FTransform& LocalRootMotionTransform) | |
{ | |
if (CharacterOwner && CharacterOwner->GetMesh() && (DeltaSeconds > 0.f)) | |
{ | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates); | |
// Convert Local Space Root Motion to world space. Do it right before used by physics to make sure we use up to date transforms, as translation is relative to rotation. | |
const FTransform WorldSpaceRootMotionTransform = ConvertLocalRootMotionToWorld(LocalRootMotionTransform); | |
RootMotionParams.Set(WorldSpaceRootMotionTransform); | |
// Compute root motion velocity to be used by physics | |
AnimRootMotionVelocity = CalcAnimRootMotionVelocity(WorldSpaceRootMotionTransform.GetTranslation(), DeltaSeconds, Velocity); | |
Velocity = ConstrainAnimRootMotionVelocity(AnimRootMotionVelocity, Velocity); | |
// Update replicated movement mode. | |
if (bNetworkMovementModeChanged) | |
{ | |
ApplyNetworkMovementMode(CharacterOwner->GetReplicatedMovementMode()); | |
bNetworkMovementModeChanged = false; | |
} | |
NumJumpApexAttempts = 0; | |
StartNewPhysics(DeltaSeconds, 0); | |
// fixme laurent - simulate movement seems to have step up issues? investigate as that would be cheaper to use. | |
// SimulateMovement(DeltaSeconds); | |
// Apply Root Motion rotation after movement is complete. | |
const FQuat RootMotionRotationQuat = WorldSpaceRootMotionTransform.GetRotation(); | |
if (!RootMotionRotationQuat.IsIdentity()) | |
{ | |
const FQuat NewActorRotationQuat = RootMotionRotationQuat * UpdatedComponent->GetComponentQuat(); | |
MoveUpdatedComponent(FVector::ZeroVector, NewActorRotationQuat, true); | |
} | |
} | |
// Root Motion has been used, clear | |
RootMotionParams.Clear(); | |
} | |
// TODO: Deprecated, remove. | |
FVector UMMOPlayerMovement::CalcRootMotionVelocity(const FVector& RootMotionDeltaMove, float DeltaSeconds, const FVector& CurrentVelocity) const | |
{ | |
return CalcAnimRootMotionVelocity(RootMotionDeltaMove, DeltaSeconds, CurrentVelocity); | |
} | |
FVector UMMOPlayerMovement::CalcAnimRootMotionVelocity(const FVector& RootMotionDeltaMove, float DeltaSeconds, const FVector& CurrentVelocity) const | |
{ | |
if (ensure(DeltaSeconds > 0.f)) | |
{ | |
FVector RootMotionVelocity = RootMotionDeltaMove / DeltaSeconds; | |
return RootMotionVelocity; | |
} | |
else | |
{ | |
return CurrentVelocity; | |
} | |
} | |
FVector UMMOPlayerMovement::ConstrainAnimRootMotionVelocity(const FVector& RootMotionVelocity, const FVector& CurrentVelocity) const | |
{ | |
FVector Result = RootMotionVelocity; | |
// Do not override Velocity.Z if in falling physics, we want to keep the effect of gravity. | |
if (IsFalling()) | |
{ | |
Result.Z = CurrentVelocity.Z; | |
} | |
return Result; | |
} | |
void UMMOPlayerMovement::SimulateMovement(float DeltaSeconds) | |
{ | |
if (!HasValidData() || UpdatedComponent->Mobility != EComponentMobility::Movable || UpdatedComponent->IsSimulatingPhysics()) | |
{ | |
return; | |
} | |
const bool bIsSimulatedProxy = (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy); | |
const FRepMovement& ConstRepMovement = CharacterOwner->GetReplicatedMovement(); | |
// Workaround for replication not being updated initially | |
if (bIsSimulatedProxy && | |
ConstRepMovement.Location.IsZero() && | |
ConstRepMovement.Rotation.IsZero() && | |
ConstRepMovement.LinearVelocity.IsZero()) | |
{ | |
return; | |
} | |
// If base is not resolved on the client, we should not try to simulate at all | |
if (CharacterOwner->GetReplicatedBasedMovement().IsBaseUnresolved()) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Verbose, TEXT("Base for simulated character '%s' is not resolved on client, skipping SimulateMovement"), *CharacterOwner->GetName()); | |
return; | |
} | |
FVector OldVelocity; | |
FVector OldLocation; | |
// Scoped updates can improve performance of multiple MoveComponent calls. | |
{ | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates); | |
bool bHandledNetUpdate = false; | |
if (bIsSimulatedProxy) | |
{ | |
// Handle network changes | |
if (bNetworkUpdateReceived) | |
{ | |
bNetworkUpdateReceived = false; | |
bHandledNetUpdate = true; | |
UE_LOG(LogMMOCharacterMovement, Verbose, TEXT("Proxy %s received net update"), *CharacterOwner->GetName()); | |
if (bNetworkMovementModeChanged) | |
{ | |
ApplyNetworkMovementMode(CharacterOwner->GetReplicatedMovementMode()); | |
bNetworkMovementModeChanged = false; | |
} | |
else if (bJustTeleported || bForceNextFloorCheck) | |
{ | |
// Make sure floor is current. We will continue using the replicated base, if there was one. | |
bJustTeleported = false; | |
UpdateFloorFromAdjustment(); | |
} | |
} | |
else if (bForceNextFloorCheck) | |
{ | |
UpdateFloorFromAdjustment(); | |
} | |
} | |
UpdateCharacterStateBeforeMovement(DeltaSeconds); | |
if (MovementMode != MOVE_None) | |
{ | |
//TODO: Also ApplyAccumulatedForces()? | |
HandlePendingLaunch(); | |
} | |
ClearAccumulatedForces(); | |
if (MovementMode == MOVE_None) | |
{ | |
return; | |
} | |
const bool bSimGravityDisabled = (bIsSimulatedProxy && CharacterOwner->bSimGravityDisabled); | |
const bool bZeroReplicatedGroundVelocity = (bIsSimulatedProxy && IsMovingOnGround() && ConstRepMovement.LinearVelocity.IsZero()); | |
// bSimGravityDisabled means velocity was zero when replicated and we were stuck in something. Avoid external changes in velocity as well. | |
// Being in ground movement with zero velocity, we cannot simulate proxy velocities safely because we might not get any further updates from the server. | |
if (bSimGravityDisabled || bZeroReplicatedGroundVelocity) | |
{ | |
Velocity = FVector::ZeroVector; | |
} | |
MaybeUpdateBasedMovement(DeltaSeconds); | |
// simulated pawns predict location | |
OldVelocity = Velocity; | |
OldLocation = UpdatedComponent->GetComponentLocation(); | |
UpdateProxyAcceleration(); | |
// May only need to simulate forward on frames where we haven't just received a new position update. | |
if (!bHandledNetUpdate || !bNetworkSkipProxyPredictionOnNetUpdate || !MMOCharacterMovementCVars::NetEnableSkipProxyPredictionOnNetUpdate) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Verbose, TEXT("Proxy %s simulating movement"), *GetNameSafe(CharacterOwner)); | |
FStepDownResult StepDownResult; | |
MoveSmooth(Velocity, DeltaSeconds, &StepDownResult); | |
// find floor and check if falling | |
if (IsMovingOnGround() || MovementMode == MOVE_Falling) | |
{ | |
if (StepDownResult.bComputedFloor) | |
{ | |
CurrentFloor = StepDownResult.FloorResult; | |
} | |
else if (Velocity.Z <= 0.f) | |
{ | |
FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, Velocity.IsZero(), NULL); | |
} | |
else | |
{ | |
CurrentFloor.Clear(); | |
} | |
if (!CurrentFloor.IsWalkableFloor()) | |
{ | |
if (!bSimGravityDisabled) | |
{ | |
// No floor, must fall. | |
if (Velocity.Z <= 0.f || bApplyGravityWhileJumping || !CharacterOwner->IsJumpProvidingForce()) | |
{ | |
Velocity = NewFallVelocity(Velocity, FVector(0.f, 0.f, GetGravityZ()), DeltaSeconds); | |
} | |
} | |
SetMovementMode(MOVE_Falling); | |
} | |
else | |
{ | |
// Walkable floor | |
if (IsMovingOnGround()) | |
{ | |
AdjustFloorHeight(); | |
SetBase(CurrentFloor.HitResult.Component.Get(), CurrentFloor.HitResult.BoneName); | |
} | |
else if (MovementMode == MOVE_Falling) | |
{ | |
if (CurrentFloor.FloorDist <= MIN_FLOOR_DIST || (bSimGravityDisabled && CurrentFloor.FloorDist <= MAX_FLOOR_DIST)) | |
{ | |
// Landed | |
SetPostLandedPhysics(CurrentFloor.HitResult); | |
} | |
else | |
{ | |
if (!bSimGravityDisabled) | |
{ | |
// Continue falling. | |
Velocity = NewFallVelocity(Velocity, FVector(0.f, 0.f, GetGravityZ()), DeltaSeconds); | |
} | |
CurrentFloor.Clear(); | |
} | |
} | |
} | |
} | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Verbose, TEXT("Proxy %s SKIPPING simulate movement"), *GetNameSafe(CharacterOwner)); | |
} | |
UpdateCharacterStateAfterMovement(DeltaSeconds); | |
// consume path following requested velocity | |
bHasRequestedVelocity = false; | |
OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity); | |
} // End scoped movement update | |
// Call custom post-movement events. These happen after the scoped movement completes in case the events want to use the current state of overlaps etc. | |
CallMovementUpdateDelegate(DeltaSeconds, OldLocation, OldVelocity); | |
SaveBaseLocation(); | |
UpdateComponentVelocity(); | |
bJustTeleported = false; | |
LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector; | |
LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity; | |
LastUpdateVelocity = Velocity; | |
} | |
UPrimitiveComponent* UMMOPlayerMovement::GetMovementBase() const | |
{ | |
return CharacterOwner ? CharacterOwner->GetMovementBase() : NULL; | |
} | |
void UMMOPlayerMovement::SetBase(UPrimitiveComponent* NewBase, FName BoneName, bool bNotifyActor) | |
{ | |
// prevent from changing Base while server is NavWalking (no Base in that mode), so both sides are in sync | |
// otherwise it will cause problems with position smoothing | |
if (CharacterOwner && !bIsNavWalkingOnServer) | |
{ | |
CharacterOwner->SetBase(NewBase, NewBase ? BoneName : NAME_None, bNotifyActor); | |
} | |
} | |
void UMMOPlayerMovement::SetBaseFromFloor(const FMMOFindFloorResult& FloorResult) | |
{ | |
if (FloorResult.IsWalkableFloor()) | |
{ | |
SetBase(FloorResult.HitResult.GetComponent(), FloorResult.HitResult.BoneName); | |
} | |
else | |
{ | |
SetBase(nullptr); | |
} | |
} | |
void UMMOPlayerMovement::MaybeUpdateBasedMovement(float DeltaSeconds) | |
{ | |
bDeferUpdateBasedMovement = false; | |
UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase(); | |
if (MovementBaseUtility::UseRelativeLocation(MovementBase)) | |
{ | |
// Need to see if anything we're on is simulating physics or has a parent that is. | |
if (!MovementBaseUtility::IsSimulatedBase(MovementBase)) | |
{ | |
bDeferUpdateBasedMovement = false; | |
UpdateBasedMovement(DeltaSeconds); | |
// If previously simulated, go back to using normal tick dependencies. | |
if (PostPhysicsTickFunction.IsTickFunctionEnabled()) | |
{ | |
PostPhysicsTickFunction.SetTickFunctionEnable(false); | |
MovementBaseUtility::AddTickDependency(PrimaryComponentTick, MovementBase); | |
} | |
} | |
else | |
{ | |
// defer movement base update until after physics | |
bDeferUpdateBasedMovement = true; | |
// If previously not simulating, remove tick dependencies and use post physics tick function. | |
if (!PostPhysicsTickFunction.IsTickFunctionEnabled()) | |
{ | |
PostPhysicsTickFunction.SetTickFunctionEnable(true); | |
MovementBaseUtility::RemoveTickDependency(PrimaryComponentTick, MovementBase); | |
} | |
} | |
} | |
else | |
{ | |
// Remove any previous physics tick dependencies. SetBase() takes care of the other dependencies. | |
if (PostPhysicsTickFunction.IsTickFunctionEnabled()) | |
{ | |
PostPhysicsTickFunction.SetTickFunctionEnable(false); | |
} | |
} | |
} | |
void UMMOPlayerMovement::MaybeSaveBaseLocation() | |
{ | |
if (!bDeferUpdateBasedMovement) | |
{ | |
SaveBaseLocation(); | |
} | |
} | |
// @todo UE4 - handle lift moving up and down through encroachment | |
void UMMOPlayerMovement::UpdateBasedMovement(float DeltaSeconds) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
const UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase(); | |
if (!MovementBaseUtility::UseRelativeLocation(MovementBase)) | |
{ | |
return; | |
} | |
if (!IsValid(MovementBase) || !IsValid(MovementBase->GetOwner())) | |
{ | |
SetBase(NULL); | |
return; | |
} | |
// Ignore collision with bases during these movements. | |
TGuardValue<EMoveComponentFlags> ScopedFlagRestore(MoveComponentFlags, MoveComponentFlags | MOVECOMP_IgnoreBases); | |
FQuat DeltaQuat = FQuat::Identity; | |
FVector DeltaPosition = FVector::ZeroVector; | |
FQuat NewBaseQuat; | |
FVector NewBaseLocation; | |
if (!MovementBaseUtility::GetMovementBaseTransform(MovementBase, CharacterOwner->GetBasedMovement().BoneName, NewBaseLocation, NewBaseQuat)) | |
{ | |
return; | |
} | |
// Find change in rotation | |
const bool bRotationChanged = !OldBaseQuat.Equals(NewBaseQuat, 1e-8f); | |
if (bRotationChanged) | |
{ | |
DeltaQuat = NewBaseQuat * OldBaseQuat.Inverse(); | |
} | |
// only if base moved | |
if (bRotationChanged || (OldBaseLocation != NewBaseLocation)) | |
{ | |
// Calculate new transform matrix of base actor (ignoring scale). | |
const FQuatRotationTranslationMatrix OldLocalToWorld(OldBaseQuat, OldBaseLocation); | |
const FQuatRotationTranslationMatrix NewLocalToWorld(NewBaseQuat, NewBaseLocation); | |
if (CharacterOwner->IsMatineeControlled()) | |
{ | |
FRotationTranslationMatrix HardRelMatrix(CharacterOwner->GetBasedMovement().Rotation, CharacterOwner->GetBasedMovement().Location); | |
const FMatrix NewWorldTM = HardRelMatrix * NewLocalToWorld; | |
const FQuat NewWorldRot = bIgnoreBaseRotation ? UpdatedComponent->GetComponentQuat() : NewWorldTM.ToQuat(); | |
MoveUpdatedComponent(NewWorldTM.GetOrigin() - UpdatedComponent->GetComponentLocation(), NewWorldRot, true); | |
} | |
else | |
{ | |
FQuat FinalQuat = UpdatedComponent->GetComponentQuat(); | |
if (bRotationChanged && !bIgnoreBaseRotation) | |
{ | |
// Apply change in rotation and pipe through FaceRotation to maintain axis restrictions | |
const FQuat PawnOldQuat = UpdatedComponent->GetComponentQuat(); | |
const FQuat TargetQuat = DeltaQuat * FinalQuat; | |
FRotator TargetRotator(TargetQuat); | |
CharacterOwner->FaceRotation(TargetRotator, 0.f); | |
FinalQuat = UpdatedComponent->GetComponentQuat(); | |
if (PawnOldQuat.Equals(FinalQuat, 1e-6f)) | |
{ | |
// Nothing changed. This means we probably are using another rotation mechanism (bOrientToMovement etc). We should still follow the base object. | |
// @todo: This assumes only Yaw is used, currently a valid assumption. This is the only reason FaceRotation() is used above really, aside from being a virtual hook. | |
if (bOrientRotationToMovement || (bUseControllerDesiredRotation && CharacterOwner->Controller)) | |
{ | |
TargetRotator.Pitch = 0.f; | |
TargetRotator.Roll = 0.f; | |
MoveUpdatedComponent(FVector::ZeroVector, TargetRotator, false); | |
FinalQuat = UpdatedComponent->GetComponentQuat(); | |
} | |
} | |
// Pipe through ControlRotation, to affect camera. | |
if (CharacterOwner->Controller) | |
{ | |
const FQuat PawnDeltaRotation = FinalQuat * PawnOldQuat.Inverse(); | |
FRotator FinalRotation = FinalQuat.Rotator(); | |
UpdateBasedRotation(FinalRotation, PawnDeltaRotation.Rotator()); | |
FinalQuat = UpdatedComponent->GetComponentQuat(); | |
} | |
} | |
// We need to offset the base of the character here, not its origin, so offset by half height | |
float HalfHeight, Radius; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(Radius, HalfHeight); | |
FVector const BaseOffset(0.0f, 0.0f, HalfHeight); | |
FVector const LocalBasePos = OldLocalToWorld.InverseTransformPosition(UpdatedComponent->GetComponentLocation() - BaseOffset); | |
FVector const NewWorldPos = ConstrainLocationToPlane(NewLocalToWorld.TransformPosition(LocalBasePos) + BaseOffset); | |
DeltaPosition = ConstrainDirectionToPlane(NewWorldPos - UpdatedComponent->GetComponentLocation()); | |
// move attached actor | |
if (bFastAttachedMove) | |
{ | |
// we're trusting no other obstacle can prevent the move here | |
UpdatedComponent->SetWorldLocationAndRotation(NewWorldPos, FinalQuat, false); | |
} | |
else | |
{ | |
// hack - transforms between local and world space introducing slight error FIXMESTEVE - discuss with engine team: just skip the transforms if no rotation? | |
FVector BaseMoveDelta = NewBaseLocation - OldBaseLocation; | |
if (!bRotationChanged && (BaseMoveDelta.X == 0.f) && (BaseMoveDelta.Y == 0.f)) | |
{ | |
DeltaPosition.X = 0.f; | |
DeltaPosition.Y = 0.f; | |
} | |
FHitResult MoveOnBaseHit(1.f); | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
MoveUpdatedComponent(DeltaPosition, FinalQuat, true, &MoveOnBaseHit); | |
if ((UpdatedComponent->GetComponentLocation() - (OldLocation + DeltaPosition)).IsNearlyZero() == false) | |
{ | |
OnUnableToFollowBaseMove(DeltaPosition, OldLocation, MoveOnBaseHit); | |
} | |
} | |
} | |
if (MovementBase->IsSimulatingPhysics() && CharacterOwner->GetMesh()) | |
{ | |
CharacterOwner->GetMesh()->ApplyDeltaToAllPhysicsTransforms(DeltaPosition, DeltaQuat); | |
} | |
} | |
} | |
void UMMOPlayerMovement::OnUnableToFollowBaseMove(const FVector& DeltaPosition, const FVector& OldLocation, const FHitResult& MoveOnBaseHit) | |
{ | |
// no default implementation, left for subclasses to override. | |
} | |
void UMMOPlayerMovement::UpdateBasedRotation(FRotator& FinalRotation, const FRotator& ReducedRotation) | |
{ | |
AController* Controller = CharacterOwner ? CharacterOwner->Controller : NULL; | |
float ControllerRoll = 0.f; | |
if (Controller && !bIgnoreBaseRotation) | |
{ | |
FRotator const ControllerRot = Controller->GetControlRotation(); | |
ControllerRoll = ControllerRot.Roll; | |
Controller->SetControlRotation(ControllerRot + ReducedRotation); | |
} | |
// Remove roll | |
FinalRotation.Roll = 0.f; | |
if (Controller) | |
{ | |
FinalRotation.Roll = UpdatedComponent->GetComponentRotation().Roll; | |
FRotator NewRotation = Controller->GetControlRotation(); | |
NewRotation.Roll = ControllerRoll; | |
Controller->SetControlRotation(NewRotation); | |
} | |
} | |
void UMMOPlayerMovement::DisableMovement() | |
{ | |
if (CharacterOwner) | |
{ | |
SetMovementMode(MOVE_None); | |
} | |
else | |
{ | |
MovementMode = MOVE_None; | |
CustomMovementMode = 0; | |
} | |
} | |
void UMMOPlayerMovement::PerformMovement(float DeltaSeconds) | |
{ | |
const UWorld* MyWorld = GetWorld(); | |
if (!HasValidData() || MyWorld == nullptr) | |
{ | |
return; | |
} | |
// no movement if we can't move, or if currently doing physical simulation on UpdatedComponent | |
if (MovementMode == MOVE_None || UpdatedComponent->Mobility != EComponentMobility::Movable || UpdatedComponent->IsSimulatingPhysics()) | |
{ | |
if (!CharacterOwner->bClientUpdating && !CharacterOwner->bServerMoveIgnoreRootMotion) | |
{ | |
// Consume root motion | |
if (CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh()) | |
{ | |
TickCharacterPose(DeltaSeconds); | |
RootMotionParams.Clear(); | |
} | |
if (CurrentRootMotion.HasActiveRootMotionSources()) | |
{ | |
CurrentRootMotion.Clear(); | |
} | |
} | |
// Clear pending physics forces | |
ClearAccumulatedForces(); | |
return; | |
} | |
// Force floor update if we've moved outside of CharacterMovement since last update. | |
bForceNextFloorCheck |= (IsMovingOnGround() && UpdatedComponent->GetComponentLocation() != LastUpdateLocation); | |
FVector OldVelocity; | |
FVector OldLocation; | |
// Scoped updates can improve performance of multiple MoveComponent calls. | |
{ | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates); | |
MaybeUpdateBasedMovement(DeltaSeconds); | |
OldVelocity = Velocity; | |
OldLocation = UpdatedComponent->GetComponentLocation(); | |
ApplyAccumulatedForces(DeltaSeconds); | |
// Update the character state before we do our movement | |
UpdateCharacterStateBeforeMovement(DeltaSeconds); | |
if (MovementMode == MOVE_NavWalking && bWantsToLeaveNavWalking) | |
{ | |
TryToLeaveNavWalking(); | |
} | |
// Character::LaunchCharacter() has been deferred until now. | |
HandlePendingLaunch(); | |
ClearAccumulatedForces(); | |
// NaN tracking | |
devMMOCode(ensureMsgf(!Velocity.ContainsNaN(), TEXT("UMMOPlayerMovement::PerformMovement: Velocity contains NaN (%s)\n%s"), *GetPathNameSafe(this), *Velocity.ToString())); | |
// Clear jump input now, to allow movement events to trigger it for next update. | |
CharacterOwner->ClearJumpInput(DeltaSeconds); | |
NumJumpApexAttempts = 0; | |
// change position | |
StartNewPhysics(DeltaSeconds, 0); | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// Update character state based on change from movement | |
UpdateCharacterStateAfterMovement(DeltaSeconds); | |
if ((bAllowPhysicsRotationDuringAnimRootMotion || !HasAnimRootMotion()) && !CharacterOwner->IsMatineeControlled()) | |
{ | |
PhysicsRotation(DeltaSeconds); | |
} | |
// consume path following requested velocity | |
bHasRequestedVelocity = false; | |
OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity); | |
} // End scoped movement update | |
// Call external post-movement events. These happen after the scoped movement completes in case the events want to use the current state of overlaps etc. | |
CallMovementUpdateDelegate(DeltaSeconds, OldLocation, OldVelocity); | |
SaveBaseLocation(); | |
UpdateComponentVelocity(); | |
const bool bHasAuthority = CharacterOwner && CharacterOwner->HasAuthority(); | |
// If we move we want to avoid a long delay before replication catches up to notice this change, especially if it's throttling our rate. | |
if (bHasAuthority && UNetDriver::IsAdaptiveNetUpdateFrequencyEnabled() && UpdatedComponent) | |
{ | |
UNetDriver* NetDriver = MyWorld->GetNetDriver(); | |
if (NetDriver && NetDriver->IsServer()) | |
{ | |
FNetworkObjectInfo* NetActor = NetDriver->FindOrAddNetworkObjectInfo(CharacterOwner); | |
if (NetActor && MyWorld->GetTimeSeconds() <= NetActor->NextUpdateTime && NetDriver->IsNetworkActorUpdateFrequencyThrottled(*NetActor)) | |
{ | |
if (ShouldCancelAdaptiveReplication()) | |
{ | |
NetDriver->CancelAdaptiveReplication(*NetActor); | |
} | |
} | |
} | |
} | |
const FVector NewLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector; | |
const FQuat NewRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity; | |
if (bHasAuthority && UpdatedComponent && !IsNetMode(NM_Client)) | |
{ | |
const bool bLocationChanged = (NewLocation != LastUpdateLocation); | |
const bool bRotationChanged = (NewRotation != LastUpdateRotation); | |
if (bLocationChanged || bRotationChanged) | |
{ | |
// Update ServerLastTransformUpdateTimeStamp. This is used by Linear smoothing on clients to interpolate positions with the correct delta time, | |
// so the timestamp should be based on the client's move delta (ServerAccumulatedClientTimeStamp), not the server time when receiving the RPC. | |
const bool bIsRemotePlayer = (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy); | |
const FMMONetworkPredictionData_Server_Character* ServerData = bIsRemotePlayer ? GetPredictionData_Server_Character() : nullptr; | |
if (bIsRemotePlayer && ServerData && MMOCharacterMovementCVars::NetUseClientTimestampForReplicatedTransform) | |
{ | |
ServerLastTransformUpdateTimeStamp = float(ServerData->ServerAccumulatedClientTimeStamp); | |
} | |
else | |
{ | |
ServerLastTransformUpdateTimeStamp = MyWorld->GetTimeSeconds(); | |
} | |
} | |
} | |
LastUpdateLocation = NewLocation; | |
LastUpdateRotation = NewRotation; | |
LastUpdateVelocity = Velocity; | |
} | |
bool UMMOPlayerMovement::ShouldCancelAdaptiveReplication() const | |
{ | |
// Update sooner if important properties changed. | |
const bool bVelocityChanged = (Velocity != LastUpdateVelocity); | |
const bool bLocationChanged = (UpdatedComponent->GetComponentLocation() != LastUpdateLocation); | |
const bool bRotationChanged = (UpdatedComponent->GetComponentQuat() != LastUpdateRotation); | |
return (bVelocityChanged || bLocationChanged || bRotationChanged); | |
} | |
void UMMOPlayerMovement::CallMovementUpdateDelegate(float DeltaTime, const FVector& OldLocation, const FVector& OldVelocity) | |
{ | |
// Update component velocity in case events want to read it | |
UpdateComponentVelocity(); | |
// Delegate (for blueprints) | |
if (CharacterOwner) | |
{ | |
CharacterOwner->OnCharacterMovementUpdated.Broadcast(DeltaTime, OldLocation, OldVelocity); | |
} | |
} | |
void UMMOPlayerMovement::OnMovementUpdated(float DeltaTime, const FVector& OldLocation, const FVector& OldVelocity) | |
{ | |
// empty base implementation, intended for derived classes to override. | |
} | |
void UMMOPlayerMovement::SaveBaseLocation() | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
const UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase(); | |
if (MovementBaseUtility::UseRelativeLocation(MovementBase) && !CharacterOwner->IsMatineeControlled()) | |
{ | |
// Read transforms into OldBaseLocation, OldBaseQuat | |
MovementBaseUtility::GetMovementBaseTransform(MovementBase, CharacterOwner->GetBasedMovement().BoneName, OldBaseLocation, OldBaseQuat); | |
// Location | |
const FVector RelativeLocation = UpdatedComponent->GetComponentLocation() - OldBaseLocation; | |
// Rotation | |
if (bIgnoreBaseRotation) | |
{ | |
// Absolute rotation | |
CharacterOwner->SaveRelativeBasedMovement(RelativeLocation, UpdatedComponent->GetComponentRotation(), false); | |
} | |
else | |
{ | |
// Relative rotation | |
const FRotator RelativeRotation = (FQuatRotationMatrix(UpdatedComponent->GetComponentQuat()) * FQuatRotationMatrix(OldBaseQuat).GetTransposed()).Rotator(); | |
CharacterOwner->SaveRelativeBasedMovement(RelativeLocation, RelativeRotation, true); | |
} | |
} | |
} | |
bool UMMOPlayerMovement::CanCrouchInCurrentState() const | |
{ | |
if (!CanEverCrouch()) | |
{ | |
return false; | |
} | |
return (IsFalling() || IsMovingOnGround()) && UpdatedComponent && !UpdatedComponent->IsSimulatingPhysics(); | |
} | |
void UMMOPlayerMovement::Crouch(bool bClientSimulation) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
if (!bClientSimulation && !CanCrouchInCurrentState()) | |
{ | |
return; | |
} | |
// See if collision is already at desired size. | |
if (CharacterOwner->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight() == CrouchedHalfHeight) | |
{ | |
if (!bClientSimulation) | |
{ | |
CharacterOwner->bIsCrouched = true; | |
} | |
CharacterOwner->OnStartCrouch(0.f, 0.f); | |
return; | |
} | |
if (bClientSimulation && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) | |
{ | |
// restore collision size before crouching | |
AMMOCharacter* DefaultCharacter = CharacterOwner->GetClass()->GetDefaultObject<AMMOCharacter>(); | |
CharacterOwner->GetCapsuleComponent()->SetCapsuleSize(DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleRadius(), DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight()); | |
bShrinkProxyCapsule = true; | |
} | |
// Change collision size to crouching dimensions | |
const float ComponentScale = CharacterOwner->GetCapsuleComponent()->GetShapeScale(); | |
const float OldUnscaledHalfHeight = CharacterOwner->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight(); | |
const float OldUnscaledRadius = CharacterOwner->GetCapsuleComponent()->GetUnscaledCapsuleRadius(); | |
// Height is not allowed to be smaller than radius. | |
const float ClampedCrouchedHalfHeight = FMath::Max3(0.f, OldUnscaledRadius, CrouchedHalfHeight); | |
CharacterOwner->GetCapsuleComponent()->SetCapsuleSize(OldUnscaledRadius, ClampedCrouchedHalfHeight); | |
float HalfHeightAdjust = (OldUnscaledHalfHeight - ClampedCrouchedHalfHeight); | |
float ScaledHalfHeightAdjust = HalfHeightAdjust * ComponentScale; | |
if (!bClientSimulation) | |
{ | |
// Crouching to a larger height? (this is rare) | |
if (ClampedCrouchedHalfHeight > OldUnscaledHalfHeight) | |
{ | |
FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CrouchTrace), false, CharacterOwner); | |
FCollisionResponseParams ResponseParam; | |
InitCollisionParams(CapsuleParams, ResponseParam); | |
const bool bEncroached = GetWorld()->OverlapBlockingTestByChannel(UpdatedComponent->GetComponentLocation() - FVector(0.f, 0.f, ScaledHalfHeightAdjust), FQuat::Identity, | |
UpdatedComponent->GetCollisionObjectType(), GetPawnCapsuleCollisionShape(SHRINK_None), CapsuleParams, ResponseParam); | |
// If encroached, cancel | |
if (bEncroached) | |
{ | |
CharacterOwner->GetCapsuleComponent()->SetCapsuleSize(OldUnscaledRadius, OldUnscaledHalfHeight); | |
return; | |
} | |
} | |
if (bCrouchMaintainsBaseLocation) | |
{ | |
// Intentionally not using MoveUpdatedComponent, where a horizontal plane constraint would prevent the base of the capsule from staying at the same spot. | |
UpdatedComponent->MoveComponent(FVector(0.f, 0.f, -ScaledHalfHeightAdjust), UpdatedComponent->GetComponentQuat(), true, nullptr, EMoveComponentFlags::MOVECOMP_NoFlags, ETeleportType::TeleportPhysics); | |
} | |
CharacterOwner->bIsCrouched = true; | |
} | |
bForceNextFloorCheck = true; | |
// OnStartCrouch takes the change from the Default size, not the current one (though they are usually the same). | |
const float MeshAdjust = ScaledHalfHeightAdjust; | |
AMMOCharacter* DefaultCharacter = CharacterOwner->GetClass()->GetDefaultObject<AMMOCharacter>(); | |
HalfHeightAdjust = (DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight() - ClampedCrouchedHalfHeight); | |
ScaledHalfHeightAdjust = HalfHeightAdjust * ComponentScale; | |
AdjustProxyCapsuleSize(); | |
CharacterOwner->OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust); | |
// Don't smooth this change in mesh position | |
if ((bClientSimulation && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) || (IsNetMode(NM_ListenServer) && CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)) | |
{ | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (ClientData) | |
{ | |
ClientData->MeshTranslationOffset -= FVector(0.f, 0.f, MeshAdjust); | |
ClientData->OriginalMeshTranslationOffset = ClientData->MeshTranslationOffset; | |
} | |
} | |
} | |
void UMMOPlayerMovement::UnCrouch(bool bClientSimulation) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
AMMOCharacter* DefaultCharacter = CharacterOwner->GetClass()->GetDefaultObject<AMMOCharacter>(); | |
// See if collision is already at desired size. | |
if (CharacterOwner->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight() == DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight()) | |
{ | |
if (!bClientSimulation) | |
{ | |
CharacterOwner->bIsCrouched = false; | |
} | |
CharacterOwner->OnEndCrouch(0.f, 0.f); | |
return; | |
} | |
const float CurrentCrouchedHalfHeight = CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); | |
const float ComponentScale = CharacterOwner->GetCapsuleComponent()->GetShapeScale(); | |
const float OldUnscaledHalfHeight = CharacterOwner->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight(); | |
const float HalfHeightAdjust = DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight() - OldUnscaledHalfHeight; | |
const float ScaledHalfHeightAdjust = HalfHeightAdjust * ComponentScale; | |
const FVector PawnLocation = UpdatedComponent->GetComponentLocation(); | |
// Grow to uncrouched size. | |
check(CharacterOwner->GetCapsuleComponent()); | |
if (!bClientSimulation) | |
{ | |
// Try to stay in place and see if the larger capsule fits. We use a slightly taller capsule to avoid penetration. | |
const UWorld* MyWorld = GetWorld(); | |
const float SweepInflation = KINDA_SMALL_NUMBER * 10.f; | |
FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CrouchTrace), false, CharacterOwner); | |
FCollisionResponseParams ResponseParam; | |
InitCollisionParams(CapsuleParams, ResponseParam); | |
// Compensate for the difference between current capsule size and standing size | |
const FCollisionShape StandingCapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_HeightCustom, -SweepInflation - ScaledHalfHeightAdjust); // Shrink by negative amount, so actually grow it. | |
const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); | |
bool bEncroached = true; | |
if (!bCrouchMaintainsBaseLocation) | |
{ | |
// Expand in place | |
bEncroached = MyWorld->OverlapBlockingTestByChannel(PawnLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); | |
if (bEncroached) | |
{ | |
// Try adjusting capsule position to see if we can avoid encroachment. | |
if (ScaledHalfHeightAdjust > 0.f) | |
{ | |
// Shrink to a short capsule, sweep down to base to find where that would hit something, and then try to stand up from there. | |
float PawnRadius, PawnHalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); | |
const float ShrinkHalfHeight = PawnHalfHeight - PawnRadius; | |
const float TraceDist = PawnHalfHeight - ShrinkHalfHeight; | |
const FVector Down = FVector(0.f, 0.f, -TraceDist); | |
FHitResult Hit(1.f); | |
const FCollisionShape ShortCapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_HeightCustom, ShrinkHalfHeight); | |
const bool bBlockingHit = MyWorld->SweepSingleByChannel(Hit, PawnLocation, PawnLocation + Down, FQuat::Identity, CollisionChannel, ShortCapsuleShape, CapsuleParams); | |
if (Hit.bStartPenetrating) | |
{ | |
bEncroached = true; | |
} | |
else | |
{ | |
// Compute where the base of the sweep ended up, and see if we can stand there | |
const float DistanceToBase = (Hit.Time * TraceDist) + ShortCapsuleShape.Capsule.HalfHeight; | |
const FVector NewLoc = FVector(PawnLocation.X, PawnLocation.Y, PawnLocation.Z - DistanceToBase + StandingCapsuleShape.Capsule.HalfHeight + SweepInflation + MIN_FLOOR_DIST / 2.f); | |
bEncroached = MyWorld->OverlapBlockingTestByChannel(NewLoc, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); | |
if (!bEncroached) | |
{ | |
// Intentionally not using MoveUpdatedComponent, where a horizontal plane constraint would prevent the base of the capsule from staying at the same spot. | |
UpdatedComponent->MoveComponent(NewLoc - PawnLocation, UpdatedComponent->GetComponentQuat(), false, nullptr, EMoveComponentFlags::MOVECOMP_NoFlags, ETeleportType::TeleportPhysics); | |
} | |
} | |
} | |
} | |
} | |
else | |
{ | |
// Expand while keeping base location the same. | |
FVector StandingLocation = PawnLocation + FVector(0.f, 0.f, StandingCapsuleShape.GetCapsuleHalfHeight() - CurrentCrouchedHalfHeight); | |
bEncroached = MyWorld->OverlapBlockingTestByChannel(StandingLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); | |
if (bEncroached) | |
{ | |
if (IsMovingOnGround()) | |
{ | |
// Something might be just barely overhead, try moving down closer to the floor to avoid it. | |
const float MinFloorDist = KINDA_SMALL_NUMBER * 10.f; | |
if (CurrentFloor.bBlockingHit && CurrentFloor.FloorDist > MinFloorDist) | |
{ | |
StandingLocation.Z -= CurrentFloor.FloorDist - MinFloorDist; | |
bEncroached = MyWorld->OverlapBlockingTestByChannel(StandingLocation, FQuat::Identity, CollisionChannel, StandingCapsuleShape, CapsuleParams, ResponseParam); | |
} | |
} | |
} | |
if (!bEncroached) | |
{ | |
// Commit the change in location. | |
UpdatedComponent->MoveComponent(StandingLocation - PawnLocation, UpdatedComponent->GetComponentQuat(), false, nullptr, EMoveComponentFlags::MOVECOMP_NoFlags, ETeleportType::TeleportPhysics); | |
bForceNextFloorCheck = true; | |
} | |
} | |
// If still encroached then abort. | |
if (bEncroached) | |
{ | |
return; | |
} | |
CharacterOwner->bIsCrouched = false; | |
} | |
else | |
{ | |
bShrinkProxyCapsule = true; | |
} | |
// Now call SetCapsuleSize() to cause touch/untouch events and actually grow the capsule | |
CharacterOwner->GetCapsuleComponent()->SetCapsuleSize(DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleRadius(), DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight(), true); | |
const float MeshAdjust = ScaledHalfHeightAdjust; | |
AdjustProxyCapsuleSize(); | |
CharacterOwner->OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust); | |
// Don't smooth this change in mesh position | |
if ((bClientSimulation && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy) || (IsNetMode(NM_ListenServer) && CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)) | |
{ | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (ClientData) | |
{ | |
ClientData->MeshTranslationOffset += FVector(0.f, 0.f, MeshAdjust); | |
ClientData->OriginalMeshTranslationOffset = ClientData->MeshTranslationOffset; | |
} | |
} | |
} | |
void UMMOPlayerMovement::UpdateCharacterStateBeforeMovement(float DeltaSeconds) | |
{ | |
// Proxies get replicated crouch state. | |
if (CharacterOwner->GetLocalRole() != ROLE_SimulatedProxy) | |
{ | |
// Check for a change in crouch state. Players toggle crouch by changing bWantsToCrouch. | |
const bool bIsCrouching = IsCrouching(); | |
if (bIsCrouching && (!bWantsToCrouch || !CanCrouchInCurrentState())) | |
{ | |
UnCrouch(false); | |
} | |
else if (!bIsCrouching && bWantsToCrouch && CanCrouchInCurrentState()) | |
{ | |
Crouch(false); | |
} | |
} | |
} | |
void UMMOPlayerMovement::UpdateCharacterStateAfterMovement(float DeltaSeconds) | |
{ | |
// Proxies get replicated crouch state. | |
if (CharacterOwner->GetLocalRole() != ROLE_SimulatedProxy) | |
{ | |
// Uncrouch if no longer allowed to be crouched | |
if (IsCrouching() && !CanCrouchInCurrentState()) | |
{ | |
UnCrouch(false); | |
} | |
} | |
} | |
void UMMOPlayerMovement::StartNewPhysics(float deltaTime, int32 Iterations) | |
{ | |
if ((deltaTime < MIN_TICK_TIME) || (Iterations >= MaxSimulationIterations) || !HasValidData()) | |
{ | |
return; | |
} | |
if (UpdatedComponent->IsSimulatingPhysics()) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Log, TEXT("UMMOPlayerMovement::StartNewPhysics: UpdateComponent (%s) is simulating physics - aborting."), *UpdatedComponent->GetPathName()); | |
return; | |
} | |
const bool bSavedMovementInProgress = bMovementInProgress; | |
bMovementInProgress = true; | |
switch (MovementMode) | |
{ | |
case MOVE_None: | |
break; | |
case MOVE_Walking: | |
PhysWalking(deltaTime, Iterations); | |
break; | |
case MOVE_NavWalking: | |
PhysNavWalking(deltaTime, Iterations); | |
break; | |
case MOVE_Falling: | |
PhysFalling(deltaTime, Iterations); | |
break; | |
case MOVE_Flying: | |
PhysFlying(deltaTime, Iterations); | |
break; | |
case MOVE_Swimming: | |
PhysSwimming(deltaTime, Iterations); | |
break; | |
case MOVE_Custom: | |
PhysCustom(deltaTime, Iterations); | |
break; | |
default: | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("%s has unsupported movement mode %d"), *CharacterOwner->GetName(), int32(MovementMode)); | |
SetMovementMode(MOVE_None); | |
break; | |
} | |
bMovementInProgress = bSavedMovementInProgress; | |
if (bDeferUpdateMoveComponent) | |
{ | |
SetUpdatedComponent(DeferredUpdatedMoveComponent); | |
} | |
} | |
float UMMOPlayerMovement::GetGravityZ() const | |
{ | |
return Super::GetGravityZ() * GravityScale; | |
} | |
float UMMOPlayerMovement::GetMaxSpeed() const | |
{ | |
switch (MovementMode) | |
{ | |
case MOVE_Walking: | |
case MOVE_NavWalking: | |
return IsCrouching() ? MaxWalkSpeedCrouched : MaxWalkSpeed; | |
case MOVE_Falling: | |
return MaxWalkSpeed; | |
case MOVE_Swimming: | |
return MaxSwimSpeed; | |
case MOVE_Flying: | |
return MaxFlySpeed; | |
case MOVE_Custom: | |
return MaxCustomMovementSpeed; | |
case MOVE_None: | |
default: | |
return 0.f; | |
} | |
} | |
float UMMOPlayerMovement::GetMinAnalogSpeed() const | |
{ | |
switch (MovementMode) | |
{ | |
case MOVE_Walking: | |
case MOVE_NavWalking: | |
case MOVE_Falling: | |
return MinAnalogWalkSpeed; | |
default: | |
return 0.f; | |
} | |
} | |
FVector UMMOPlayerMovement::GetPenetrationAdjustment(const FHitResult& Hit) const | |
{ | |
FVector Result = Super::GetPenetrationAdjustment(Hit); | |
if (CharacterOwner) | |
{ | |
const bool bIsProxy = (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy); | |
float MaxDistance = bIsProxy ? MaxDepenetrationWithGeometryAsProxy : MaxDepenetrationWithGeometry; | |
const AActor* HitActor = Hit.GetActor(); | |
if (Cast<APawn>(HitActor)) | |
{ | |
MaxDistance = bIsProxy ? MaxDepenetrationWithPawnAsProxy : MaxDepenetrationWithPawn; | |
} | |
Result = Result.GetClampedToMaxSize(MaxDistance); | |
} | |
return Result; | |
} | |
bool UMMOPlayerMovement::ResolvePenetrationImpl(const FVector& Adjustment, const FHitResult& Hit, const FQuat& NewRotation) | |
{ | |
// If movement occurs, mark that we teleported, so we don't incorrectly adjust velocity based on a potentially very different movement than our movement direction. | |
bJustTeleported |= Super::ResolvePenetrationImpl(Adjustment, Hit, NewRotation); | |
return bJustTeleported; | |
} | |
float UMMOPlayerMovement::SlideAlongSurface(const FVector& Delta, float Time, const FVector& InNormal, FHitResult& Hit, bool bHandleImpact) | |
{ | |
if (!Hit.bBlockingHit) | |
{ | |
return 0.f; | |
} | |
FVector Normal(InNormal); | |
if (IsMovingOnGround()) | |
{ | |
// We don't want to be pushed up an unwalkable surface. | |
if (Normal.Z > 0.f) | |
{ | |
if (!IsWalkable(Hit)) | |
{ | |
Normal = Normal.GetSafeNormal2D(); | |
} | |
} | |
else if (Normal.Z < -KINDA_SMALL_NUMBER) | |
{ | |
// Don't push down into the floor when the impact is on the upper portion of the capsule. | |
if (CurrentFloor.FloorDist < MIN_FLOOR_DIST && CurrentFloor.bBlockingHit) | |
{ | |
const FVector FloorNormal = CurrentFloor.HitResult.Normal; | |
const bool bFloorOpposedToMovement = (Delta | FloorNormal) < 0.f && (FloorNormal.Z < 1.f - DELTA); | |
if (bFloorOpposedToMovement) | |
{ | |
Normal = FloorNormal; | |
} | |
Normal = Normal.GetSafeNormal2D(); | |
} | |
} | |
} | |
return Super::SlideAlongSurface(Delta, Time, Normal, Hit, bHandleImpact); | |
} | |
void UMMOPlayerMovement::TwoWallAdjust(FVector& Delta, const FHitResult& Hit, const FVector& OldHitNormal) const | |
{ | |
const FVector InDelta = Delta; | |
Super::TwoWallAdjust(Delta, Hit, OldHitNormal); | |
if (IsMovingOnGround()) | |
{ | |
// Allow slides up walkable surfaces, but not unwalkable ones (treat those as vertical barriers). | |
if (Delta.Z > 0.f) | |
{ | |
if ((Hit.Normal.Z >= WalkableFloorZ || IsWalkable(Hit)) && Hit.Normal.Z > KINDA_SMALL_NUMBER) | |
{ | |
// Maintain horizontal velocity | |
const float Time = (1.f - Hit.Time); | |
const FVector ScaledDelta = Delta.GetSafeNormal() * InDelta.Size(); | |
Delta = FVector(InDelta.X, InDelta.Y, ScaledDelta.Z / Hit.Normal.Z) * Time; | |
// Should never exceed MaxStepHeight in vertical component, so rescale if necessary. | |
// This should be rare (Hit.Normal.Z above would have been very small) but we'd rather lose horizontal velocity than go too high. | |
if (Delta.Z > MaxStepHeight) | |
{ | |
const float Rescale = MaxStepHeight / Delta.Z; | |
Delta *= Rescale; | |
} | |
} | |
else | |
{ | |
Delta.Z = 0.f; | |
} | |
} | |
else if (Delta.Z < 0.f) | |
{ | |
// Don't push down into the floor. | |
if (CurrentFloor.FloorDist < MIN_FLOOR_DIST && CurrentFloor.bBlockingHit) | |
{ | |
Delta.Z = 0.f; | |
} | |
} | |
} | |
} | |
FVector UMMOPlayerMovement::ComputeSlideVector(const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const | |
{ | |
FVector Result = Super::ComputeSlideVector(Delta, Time, Normal, Hit); | |
// prevent boosting up slopes | |
if (IsFalling()) | |
{ | |
Result = HandleSlopeBoosting(Result, Delta, Time, Normal, Hit); | |
} | |
return Result; | |
} | |
FVector UMMOPlayerMovement::HandleSlopeBoosting(const FVector& SlideResult, const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const | |
{ | |
FVector Result = SlideResult; | |
if (Result.Z > 0.f) | |
{ | |
// Don't move any higher than we originally intended. | |
const float ZLimit = Delta.Z * Time; | |
if (Result.Z - ZLimit > KINDA_SMALL_NUMBER) | |
{ | |
if (ZLimit > 0.f) | |
{ | |
// Rescale the entire vector (not just the Z component) otherwise we change the direction and likely head right back into the impact. | |
const float UpPercent = ZLimit / Result.Z; | |
Result *= UpPercent; | |
} | |
else | |
{ | |
// We were heading down but were going to deflect upwards. Just make the deflection horizontal. | |
Result = FVector::ZeroVector; | |
} | |
// Make remaining portion of original result horizontal and parallel to impact normal. | |
const FVector RemainderXY = (SlideResult - Result) * FVector(1.f, 1.f, 0.f); | |
const FVector NormalXY = Normal.GetSafeNormal2D(); | |
const FVector Adjust = Super::ComputeSlideVector(RemainderXY, 1.f, NormalXY, Hit); | |
Result += Adjust; | |
} | |
} | |
return Result; | |
} | |
FVector UMMOPlayerMovement::NewFallVelocity(const FVector& InitialVelocity, const FVector& Gravity, float DeltaTime) const | |
{ | |
FVector Result = InitialVelocity; | |
if (DeltaTime > 0.f) | |
{ | |
// Apply gravity. | |
Result += Gravity * DeltaTime; | |
// Don't exceed terminal velocity. | |
const float TerminalLimit = FMath::Abs(GetPhysicsVolume()->TerminalVelocity); | |
if (Result.SizeSquared() > FMath::Square(TerminalLimit)) | |
{ | |
const FVector GravityDir = Gravity.GetSafeNormal(); | |
if ((Result | GravityDir) > TerminalLimit) | |
{ | |
Result = FVector::PointPlaneProject(Result, FVector::ZeroVector, GravityDir) + GravityDir * TerminalLimit; | |
} | |
} | |
} | |
return Result; | |
} | |
float UMMOPlayerMovement::ImmersionDepth() const | |
{ | |
float depth = 0.f; | |
if (CharacterOwner && GetPhysicsVolume()->bWaterVolume) | |
{ | |
const float CollisionHalfHeight = CharacterOwner->GetSimpleCollisionHalfHeight(); | |
if ((CollisionHalfHeight == 0.f) || (Buoyancy == 0.f)) | |
{ | |
depth = 1.f; | |
} | |
else | |
{ | |
UBrushComponent* VolumeBrushComp = GetPhysicsVolume()->GetBrushComponent(); | |
FHitResult Hit(1.f); | |
if (VolumeBrushComp) | |
{ | |
const FVector TraceStart = UpdatedComponent->GetComponentLocation() + FVector(0.f, 0.f, CollisionHalfHeight); | |
const FVector TraceEnd = UpdatedComponent->GetComponentLocation() - FVector(0.f, 0.f, CollisionHalfHeight); | |
FCollisionQueryParams NewTraceParams(SCENE_QUERY_STAT(ImmersionDepth), true); | |
VolumeBrushComp->LineTraceComponent(Hit, TraceStart, TraceEnd, NewTraceParams); | |
} | |
depth = (Hit.Time == 1.f) ? 1.f : (1.f - Hit.Time); | |
} | |
} | |
return depth; | |
} | |
bool UMMOPlayerMovement::IsFlying() const | |
{ | |
return (MovementMode == MOVE_Flying) && UpdatedComponent; | |
} | |
bool UMMOPlayerMovement::IsMovingOnGround() const | |
{ | |
return ((MovementMode == MOVE_Walking) || (MovementMode == MOVE_NavWalking)) && UpdatedComponent; | |
} | |
bool UMMOPlayerMovement::IsFalling() const | |
{ | |
return (MovementMode == MOVE_Falling) && UpdatedComponent; | |
} | |
bool UMMOPlayerMovement::IsSwimming() const | |
{ | |
return (MovementMode == MOVE_Swimming) && UpdatedComponent; | |
} | |
bool UMMOPlayerMovement::IsCrouching() const | |
{ | |
return CharacterOwner && CharacterOwner->bIsCrouched; | |
} | |
void UMMOPlayerMovement::CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration) | |
{ | |
// Do not update velocity when using root motion or when SimulatedProxy and not simulating root motion - SimulatedProxy are repped their Velocity | |
if (!HasValidData() || HasAnimRootMotion() || DeltaTime < MIN_TICK_TIME || (CharacterOwner && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy && !bWasSimulatingRootMotion)) | |
{ | |
return; | |
} | |
Friction = FMath::Max(0.f, Friction); | |
const float MaxAccel = GetMaxAcceleration(); | |
float MaxSpeed = GetMaxSpeed(); | |
// Check if path following requested movement | |
bool bZeroRequestedAcceleration = true; | |
FVector RequestedAcceleration = FVector::ZeroVector; | |
float RequestedSpeed = 0.0f; | |
if (ApplyRequestedMove(DeltaTime, MaxAccel, MaxSpeed, Friction, BrakingDeceleration, RequestedAcceleration, RequestedSpeed)) | |
{ | |
bZeroRequestedAcceleration = false; | |
} | |
if (bForceMaxAccel) | |
{ | |
// Force acceleration at full speed. | |
// In consideration order for direction: Acceleration, then Velocity, then Pawn's rotation. | |
if (Acceleration.SizeSquared() > SMALL_NUMBER) | |
{ | |
Acceleration = Acceleration.GetSafeNormal() * MaxAccel; | |
} | |
else | |
{ | |
Acceleration = MaxAccel * (Velocity.SizeSquared() < SMALL_NUMBER ? UpdatedComponent->GetForwardVector() : Velocity.GetSafeNormal()); | |
} | |
AnalogInputModifier = 1.f; | |
} | |
// Path following above didn't care about the analog modifier, but we do for everything else below, so get the fully modified value. | |
// Use max of requested speed and max speed if we modified the speed in ApplyRequestedMove above. | |
const float MaxInputSpeed = FMath::Max(MaxSpeed * AnalogInputModifier, GetMinAnalogSpeed()); | |
MaxSpeed = FMath::Max(RequestedSpeed, MaxInputSpeed); | |
// Apply braking or deceleration | |
const bool bZeroAcceleration = Acceleration.IsZero(); | |
const bool bVelocityOverMax = IsExceedingMaxSpeed(MaxSpeed); | |
// Only apply braking if there is no acceleration, or we are over our max speed and need to slow down to it. | |
if ((bZeroAcceleration && bZeroRequestedAcceleration) || bVelocityOverMax) | |
{ | |
const FVector OldVelocity = Velocity; | |
const float ActualBrakingFriction = (bUseSeparateBrakingFriction ? BrakingFriction : Friction); | |
ApplyVelocityBraking(DeltaTime, ActualBrakingFriction, BrakingDeceleration); | |
// Don't allow braking to lower us below max speed if we started above it. | |
if (bVelocityOverMax && Velocity.SizeSquared() < FMath::Square(MaxSpeed) && FVector::DotProduct(Acceleration, OldVelocity) > 0.0f) | |
{ | |
Velocity = OldVelocity.GetSafeNormal() * MaxSpeed; | |
} | |
} | |
else if (!bZeroAcceleration) | |
{ | |
// Friction affects our ability to change direction. This is only done for input acceleration, not path following. | |
const FVector AccelDir = Acceleration.GetSafeNormal(); | |
const float VelSize = Velocity.Size(); | |
Velocity = Velocity - (Velocity - AccelDir * VelSize) * FMath::Min(DeltaTime * Friction, 1.f); | |
} | |
// Apply fluid friction | |
if (bFluid) | |
{ | |
Velocity = Velocity * (1.f - FMath::Min(Friction * DeltaTime, 1.f)); | |
} | |
// Apply input acceleration | |
if (!bZeroAcceleration) | |
{ | |
const float NewMaxInputSpeed = IsExceedingMaxSpeed(MaxInputSpeed) ? Velocity.Size() : MaxInputSpeed; | |
Velocity += Acceleration * DeltaTime; | |
Velocity = Velocity.GetClampedToMaxSize(NewMaxInputSpeed); | |
} | |
// Apply additional requested acceleration | |
if (!bZeroRequestedAcceleration) | |
{ | |
const float NewMaxRequestedSpeed = IsExceedingMaxSpeed(RequestedSpeed) ? Velocity.Size() : RequestedSpeed; | |
Velocity += RequestedAcceleration * DeltaTime; | |
Velocity = Velocity.GetClampedToMaxSize(NewMaxRequestedSpeed); | |
} | |
if (bUseRVOAvoidance) | |
{ | |
CalcAvoidanceVelocity(DeltaTime); | |
} | |
} | |
bool UMMOPlayerMovement::ShouldComputeAccelerationToReachRequestedVelocity(const float RequestedSpeed) const | |
{ | |
// Compute acceleration if accelerating toward requested speed, 1% buffer. | |
return bRequestedMoveUseAcceleration && Velocity.SizeSquared() < FMath::Square(RequestedSpeed * 1.01f); | |
} | |
bool UMMOPlayerMovement::ApplyRequestedMove(float DeltaTime, float MaxAccel, float MaxSpeed, float Friction, float BrakingDeceleration, FVector& OutAcceleration, float& OutRequestedSpeed) | |
{ | |
if (bHasRequestedVelocity) | |
{ | |
const float RequestedSpeedSquared = RequestedVelocity.SizeSquared(); | |
if (RequestedSpeedSquared < KINDA_SMALL_NUMBER) | |
{ | |
return false; | |
} | |
// Compute requested speed from path following | |
float RequestedSpeed = FMath::Sqrt(RequestedSpeedSquared); | |
const FVector RequestedMoveDir = RequestedVelocity / RequestedSpeed; | |
RequestedSpeed = (bRequestedMoveWithMaxSpeed ? MaxSpeed : FMath::Min(MaxSpeed, RequestedSpeed)); | |
// Compute actual requested velocity | |
const FVector MoveVelocity = RequestedMoveDir * RequestedSpeed; | |
// Compute acceleration. Use MaxAccel to limit speed increase, 1% buffer. | |
FVector NewAcceleration = FVector::ZeroVector; | |
const float CurrentSpeedSq = Velocity.SizeSquared(); | |
if (ShouldComputeAccelerationToReachRequestedVelocity(RequestedSpeed)) | |
{ | |
// Turn in the same manner as with input acceleration. | |
const float VelSize = FMath::Sqrt(CurrentSpeedSq); | |
Velocity = Velocity - (Velocity - RequestedMoveDir * VelSize) * FMath::Min(DeltaTime * Friction, 1.f); | |
// How much do we need to accelerate to get to the new velocity? | |
NewAcceleration = ((MoveVelocity - Velocity) / DeltaTime); | |
NewAcceleration = NewAcceleration.GetClampedToMaxSize(MaxAccel); | |
} | |
else | |
{ | |
// Just set velocity directly. | |
// If decelerating we do so instantly, so we don't slide through the destination if we can't brake fast enough. | |
Velocity = MoveVelocity; | |
} | |
// Copy to out params | |
OutRequestedSpeed = RequestedSpeed; | |
OutAcceleration = NewAcceleration; | |
return true; | |
} | |
return false; | |
} | |
void UMMOPlayerMovement::RequestDirectMove(const FVector& MoveVelocity, bool bForceMaxSpeed) | |
{ | |
if (MoveVelocity.SizeSquared() < KINDA_SMALL_NUMBER) | |
{ | |
return; | |
} | |
if (ShouldPerformAirControlForPathFollowing()) | |
{ | |
const FVector FallVelocity = MoveVelocity.GetClampedToMaxSize(GetMaxSpeed()); | |
PerformAirControlForPathFollowing(FallVelocity, FallVelocity.Z); | |
return; | |
} | |
RequestedVelocity = MoveVelocity; | |
bHasRequestedVelocity = true; | |
bRequestedMoveWithMaxSpeed = bForceMaxSpeed; | |
if (IsMovingOnGround()) | |
{ | |
RequestedVelocity.Z = 0.0f; | |
} | |
} | |
bool UMMOPlayerMovement::ShouldPerformAirControlForPathFollowing() const | |
{ | |
return IsFalling(); | |
} | |
void UMMOPlayerMovement::RequestPathMove(const FVector& MoveInput) | |
{ | |
FVector AdjustedMoveInput(MoveInput); | |
// preserve magnitude when moving on ground/falling and requested input has Z component | |
// see ConstrainInputAcceleration for details | |
if (MoveInput.Z != 0.f && (IsMovingOnGround() || IsFalling())) | |
{ | |
const float Mag = MoveInput.Size(); | |
AdjustedMoveInput = MoveInput.GetSafeNormal2D() * Mag; | |
} | |
Super::RequestPathMove(AdjustedMoveInput); | |
} | |
bool UMMOPlayerMovement::CanStartPathFollowing() const | |
{ | |
if (!HasValidData() || HasAnimRootMotion()) | |
{ | |
return false; | |
} | |
if (CharacterOwner) | |
{ | |
if (CharacterOwner->GetRootComponent() && CharacterOwner->GetRootComponent()->IsSimulatingPhysics()) | |
{ | |
return false; | |
} | |
else if (CharacterOwner->IsMatineeControlled()) | |
{ | |
return false; | |
} | |
} | |
return Super::CanStartPathFollowing(); | |
} | |
bool UMMOPlayerMovement::CanStopPathFollowing() const | |
{ | |
return !IsFalling(); | |
} | |
float UMMOPlayerMovement::GetPathFollowingBrakingDistance(float MaxSpeed) const | |
{ | |
if (bUseFixedBrakingDistanceForPaths) | |
{ | |
return FixedPathBrakingDistance; | |
} | |
const float BrakingDeceleration = FMath::Abs(GetMaxBrakingDeceleration()); | |
// character won't be able to stop with negative or nearly zero deceleration, use MaxSpeed for path length calculations | |
const float BrakingDistance = (BrakingDeceleration < SMALL_NUMBER) ? MaxSpeed : (FMath::Square(MaxSpeed) / (2.f * BrakingDeceleration)); | |
return BrakingDistance; | |
} | |
void UMMOPlayerMovement::CalcAvoidanceVelocity(float DeltaTime) | |
{ | |
UAvoidanceManager* AvoidanceManager = GetWorld()->GetAvoidanceManager(); | |
if (AvoidanceWeight >= 1.0f || AvoidanceManager == NULL || GetCharacterOwner() == NULL) | |
{ | |
return; | |
} | |
if (GetCharacterOwner()->GetLocalRole() != ROLE_Authority) | |
{ | |
return; | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
const bool bMMOShowDebug = AvoidanceManager->IsDebugEnabled(AvoidanceUID); | |
#endif | |
//Adjust velocity only if we're in "Walking" mode. We should also check if we're dazed, being knocked around, maybe off-navmesh, etc. | |
UCapsuleComponent* OurCapsule = GetCharacterOwner()->GetCapsuleComponent(); | |
if (!Velocity.IsZero() && IsMovingOnGround() && OurCapsule) | |
{ | |
//See if we're doing a locked avoidance move already, and if so, skip the testing and just do the move. | |
if (AvoidanceLockTimer > 0.0f) | |
{ | |
Velocity = AvoidanceLockVelocity; | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (bMMOShowDebug) | |
{ | |
DrawDebugLine(GetWorld(), GetActorFeetLocation(), GetActorFeetLocation() + Velocity, FColor::Blue, false, 0.5f, SDPG_MAX); | |
} | |
#endif | |
} | |
else | |
{ | |
FVector NewVelocity = AvoidanceManager->GetAvoidanceVelocityForComponent(this); | |
if (bUseRVOPostProcess) | |
{ | |
PostProcessAvoidanceVelocity(NewVelocity); | |
} | |
if (!NewVelocity.Equals(Velocity)) //Really want to branch hint that this will probably not pass | |
{ | |
//Had to divert course, lock this avoidance move in for a short time. This will make us a VO, so unlocked others will know to avoid us. | |
Velocity = NewVelocity; | |
SetAvoidanceVelocityLock(AvoidanceManager, AvoidanceManager->LockTimeAfterAvoid); | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (bMMOShowDebug) | |
{ | |
DrawDebugLine(GetWorld(), GetActorFeetLocation(), GetActorFeetLocation() + Velocity, FColor::Red, false, 0.05f, SDPG_MAX, 10.0f); | |
} | |
#endif | |
} | |
else | |
{ | |
//Although we didn't divert course, our velocity for this frame is decided. We will not reciprocate anything further, so treat as a VO for the remainder of this frame. | |
SetAvoidanceVelocityLock(AvoidanceManager, AvoidanceManager->LockTimeAfterClean); //10 ms of lock time should be adequate. | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (bMMOShowDebug) | |
{ | |
//DrawDebugLine(GetWorld(), GetActorLocation(), GetActorLocation() + Velocity, FColor::Green, false, 0.05f, SDPG_MAX, 10.0f); | |
} | |
#endif | |
} | |
} | |
//RickH - We might do better to do this later in our update | |
AvoidanceManager->UpdateRVO(this); | |
bWasAvoidanceUpdated = true; | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
else if (bMMOShowDebug) | |
{ | |
DrawDebugLine(GetWorld(), GetActorFeetLocation(), GetActorFeetLocation() + Velocity, FColor::Yellow, false, 0.05f, SDPG_MAX); | |
} | |
if (bMMOShowDebug) | |
{ | |
FVector UpLine(0, 0, 500); | |
DrawDebugLine(GetWorld(), GetActorFeetLocation(), GetActorFeetLocation() + UpLine, (AvoidanceLockTimer > 0.01f) ? FColor::Red : FColor::Blue, false, 0.05f, SDPG_MAX, 5.0f); | |
} | |
#endif | |
} | |
void UMMOPlayerMovement::PostProcessAvoidanceVelocity(FVector& NewVelocity) | |
{ | |
// empty in base class | |
} | |
void UMMOPlayerMovement::UpdateDefaultAvoidance() | |
{ | |
if (!bUseRVOAvoidance) | |
{ | |
return; | |
} | |
UAvoidanceManager* AvoidanceManager = GetWorld()->GetAvoidanceManager(); | |
if (AvoidanceManager && !bWasAvoidanceUpdated && GetCharacterOwner()->GetCapsuleComponent()) | |
{ | |
AvoidanceManager->UpdateRVO(this); | |
//Consider this a clean move because we didn't even try to avoid. | |
SetAvoidanceVelocityLock(AvoidanceManager, AvoidanceManager->LockTimeAfterClean); | |
} | |
bWasAvoidanceUpdated = false; //Reset for next frame | |
} | |
void UMMOPlayerMovement::SetRVOAvoidanceUID(int32 UID) | |
{ | |
AvoidanceUID = UID; | |
} | |
int32 UMMOPlayerMovement::GetRVOAvoidanceUID() | |
{ | |
return AvoidanceUID; | |
} | |
void UMMOPlayerMovement::SetRVOAvoidanceWeight(float Weight) | |
{ | |
AvoidanceWeight = Weight; | |
} | |
float UMMOPlayerMovement::GetRVOAvoidanceWeight() | |
{ | |
return AvoidanceWeight; | |
} | |
FVector UMMOPlayerMovement::GetRVOAvoidanceOrigin() | |
{ | |
return GetActorFeetLocation(); | |
} | |
float UMMOPlayerMovement::GetRVOAvoidanceRadius() | |
{ | |
UCapsuleComponent* CapsuleComp = GetCharacterOwner()->GetCapsuleComponent(); | |
return CapsuleComp ? CapsuleComp->GetScaledCapsuleRadius() : 0.0f; | |
} | |
float UMMOPlayerMovement::GetRVOAvoidanceConsiderationRadius() | |
{ | |
return AvoidanceConsiderationRadius; | |
} | |
float UMMOPlayerMovement::GetRVOAvoidanceHeight() | |
{ | |
UCapsuleComponent* CapsuleComp = GetCharacterOwner()->GetCapsuleComponent(); | |
return CapsuleComp ? CapsuleComp->GetScaledCapsuleHalfHeight() : 0.0f; | |
} | |
FVector UMMOPlayerMovement::GetVelocityForRVOConsideration() | |
{ | |
return Velocity; | |
} | |
int32 UMMOPlayerMovement::GetAvoidanceGroupMask() | |
{ | |
return AvoidanceGroup.Packed; | |
} | |
int32 UMMOPlayerMovement::GetGroupsToAvoidMask() | |
{ | |
return GroupsToAvoid.Packed; | |
} | |
int32 UMMOPlayerMovement::GetGroupsToIgnoreMask() | |
{ | |
return GroupsToIgnore.Packed; | |
} | |
void UMMOPlayerMovement::SetAvoidanceVelocityLock(class UAvoidanceManager* Avoidance, float Duration) | |
{ | |
Avoidance->OverrideToMaxWeight(AvoidanceUID, Duration); | |
AvoidanceLockVelocity = Velocity; | |
AvoidanceLockTimer = Duration; | |
} | |
void UMMOPlayerMovement::NotifyBumpedPawn(APawn* BumpedPawn) | |
{ | |
Super::NotifyBumpedPawn(BumpedPawn); | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
UAvoidanceManager* Avoidance = GetWorld()->GetAvoidanceManager(); | |
const bool bMMOShowDebug = Avoidance && Avoidance->IsDebugEnabled(AvoidanceUID); | |
if (bMMOShowDebug) | |
{ | |
DrawDebugLine(GetWorld(), GetActorFeetLocation(), GetActorFeetLocation() + FVector(0, 0, 500), (AvoidanceLockTimer > 0) ? FColor(255, 64, 64) : FColor(64, 64, 255), false, 2.0f, SDPG_MAX, 20.0f); | |
} | |
#endif | |
// Unlock avoidance move. This mostly happens when two pawns who are locked into avoidance moves collide with each other. | |
AvoidanceLockTimer = 0.0f; | |
} | |
float UMMOPlayerMovement::GetMaxJumpHeight() const | |
{ | |
const float Gravity = GetGravityZ(); | |
if (FMath::Abs(Gravity) > KINDA_SMALL_NUMBER) | |
{ | |
return FMath::Square(JumpZVelocity) / (-2.f * Gravity); | |
} | |
else | |
{ | |
return 0.f; | |
} | |
} | |
float UMMOPlayerMovement::GetMaxJumpHeightWithJumpTime() const | |
{ | |
const float MaxJumpHeight = GetMaxJumpHeight(); | |
if (CharacterOwner) | |
{ | |
// When bApplyGravityWhileJumping is true, the actual max height will be lower than this. | |
// However, it will also be dependent on framerate (and substep iterations) so just return this | |
// to avoid expensive calculations. | |
// This can be imagined as the character being displaced to some height, then jumping from that height. | |
return (CharacterOwner->JumpMaxHoldTime * JumpZVelocity) + MaxJumpHeight; | |
} | |
return MaxJumpHeight; | |
} | |
// TODO: deprecated, remove. | |
float UMMOPlayerMovement::GetModifiedMaxAcceleration() const | |
{ | |
// Allow calling old deprecated function to maintain old behavior until it is removed. | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS | |
return CharacterOwner ? MaxAcceleration * GetMaxSpeedModifier() : 0.f; | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS | |
} | |
// TODO: deprecated, remove. | |
float UMMOPlayerMovement::K2_GetModifiedMaxAcceleration() const | |
{ | |
// Allow calling old deprecated function to maintain old behavior until it is removed. | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS | |
return GetModifiedMaxAcceleration(); | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS | |
} | |
float UMMOPlayerMovement::GetMaxAcceleration() const | |
{ | |
return MaxAcceleration; | |
} | |
float UMMOPlayerMovement::GetMaxBrakingDeceleration() const | |
{ | |
switch (MovementMode) | |
{ | |
case MOVE_Walking: | |
case MOVE_NavWalking: | |
return BrakingDecelerationWalking; | |
case MOVE_Falling: | |
return BrakingDecelerationFalling; | |
case MOVE_Swimming: | |
return BrakingDecelerationSwimming; | |
case MOVE_Flying: | |
return BrakingDecelerationFlying; | |
case MOVE_Custom: | |
return 0.f; | |
case MOVE_None: | |
default: | |
return 0.f; | |
} | |
} | |
FVector UMMOPlayerMovement::GetCurrentAcceleration() const | |
{ | |
return Acceleration; | |
} | |
void UMMOPlayerMovement::ApplyVelocityBraking(float DeltaTime, float Friction, float BrakingDeceleration) | |
{ | |
if (Velocity.IsZero() || !HasValidData() || HasAnimRootMotion() || DeltaTime < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
const float FrictionFactor = FMath::Max(0.f, BrakingFrictionFactor); | |
Friction = FMath::Max(0.f, Friction * FrictionFactor); | |
BrakingDeceleration = FMath::Max(0.f, BrakingDeceleration); | |
const bool bZeroFriction = (Friction == 0.f); | |
const bool bZeroBraking = (BrakingDeceleration == 0.f); | |
if (bZeroFriction && bZeroBraking) | |
{ | |
return; | |
} | |
const FVector OldVel = Velocity; | |
// subdivide braking to get reasonably consistent results at lower frame rates | |
// (important for packet loss situations w/ networking) | |
float RemainingTime = DeltaTime; | |
const float MaxTimeStep = FMath::Clamp(BrakingSubStepTime, 1.0f / 75.0f, 1.0f / 20.0f); | |
// Decelerate to brake to a stop | |
const FVector RevAccel = (bZeroBraking ? FVector::ZeroVector : (-BrakingDeceleration * Velocity.GetSafeNormal())); | |
while (RemainingTime >= MIN_TICK_TIME) | |
{ | |
// Zero friction uses constant deceleration, so no need for iteration. | |
const float dt = ((RemainingTime > MaxTimeStep && !bZeroFriction) ? FMath::Min(MaxTimeStep, RemainingTime * 0.5f) : RemainingTime); | |
RemainingTime -= dt; | |
// apply friction and braking | |
Velocity = Velocity + ((-Friction) * Velocity + RevAccel) * dt; | |
// Don't reverse direction | |
if ((Velocity | OldVel) <= 0.f) | |
{ | |
Velocity = FVector::ZeroVector; | |
return; | |
} | |
} | |
// Clamp to zero if nearly zero, or if below min threshold and braking. | |
const float VSizeSq = Velocity.SizeSquared(); | |
if (VSizeSq <= KINDA_SMALL_NUMBER || (!bZeroBraking && VSizeSq <= FMath::Square(BRAKE_TO_STOP_VELOCITY))) | |
{ | |
Velocity = FVector::ZeroVector; | |
} | |
} | |
void UMMOPlayerMovement::PhysFlying(float deltaTime, int32 Iterations) | |
{ | |
if (deltaTime < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
RestorePreAdditiveRootMotionVelocity(); | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) | |
{ | |
if (bCheatFlying && Acceleration.IsZero()) | |
{ | |
Velocity = FVector::ZeroVector; | |
} | |
const float Friction = 0.5f * GetPhysicsVolume()->FluidFriction; | |
CalcVelocity(deltaTime, Friction, true, GetMaxBrakingDeceleration()); | |
} | |
ApplyRootMotionToVelocity(deltaTime); | |
Iterations++; | |
bJustTeleported = false; | |
FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
const FVector Adjusted = Velocity * deltaTime; | |
FHitResult Hit(1.f); | |
SafeMoveUpdatedComponent(Adjusted, UpdatedComponent->GetComponentQuat(), true, Hit); | |
if (Hit.Time < 1.f) | |
{ | |
const FVector GravDir = FVector(0.f, 0.f, -1.f); | |
const FVector VelDir = Velocity.GetSafeNormal(); | |
const float UpDown = GravDir | VelDir; | |
bool bSteppedUp = false; | |
if ((FMath::Abs(Hit.ImpactNormal.Z) < 0.2f) && (UpDown < 0.5f) && (UpDown > -0.2f) && CanStepUp(Hit)) | |
{ | |
float stepZ = UpdatedComponent->GetComponentLocation().Z; | |
bSteppedUp = StepUp(GravDir, Adjusted * (1.f - Hit.Time), Hit); | |
if (bSteppedUp) | |
{ | |
OldLocation.Z = UpdatedComponent->GetComponentLocation().Z + (OldLocation.Z - stepZ); | |
} | |
} | |
if (!bSteppedUp) | |
{ | |
//adjust and try again | |
HandleImpact(Hit, deltaTime, Adjusted); | |
SlideAlongSurface(Adjusted, (1.f - Hit.Time), Hit.Normal, Hit, true); | |
} | |
} | |
if (!bJustTeleported && !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) | |
{ | |
Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / deltaTime; | |
} | |
} | |
void UMMOPlayerMovement::RestorePreAdditiveRootMotionVelocity() | |
{ | |
} | |
void UMMOPlayerMovement::ApplyRootMotionToVelocity(float deltaTime) | |
{ | |
return; | |
} | |
void UMMOPlayerMovement::HandleSwimmingWallHit(const FHitResult& Hit, float DeltaTime) | |
{ | |
} | |
void UMMOPlayerMovement::PhysSwimming(float deltaTime, int32 Iterations) | |
{ | |
if (deltaTime < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
float NetFluidFriction = 0.f; | |
float Depth = ImmersionDepth(); | |
float NetBuoyancy = Buoyancy * Depth; | |
float OriginalAccelZ = Acceleration.Z; | |
bool bLimitedUpAccel = false; | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && (Velocity.Z > 0.33f * MaxSwimSpeed) && (NetBuoyancy != 0.f)) | |
{ | |
//damp positive Z out of water | |
Velocity.Z = FMath::Max(0.33f * MaxSwimSpeed, Velocity.Z * Depth * Depth); | |
} | |
else if (Depth < 0.65f) | |
{ | |
bLimitedUpAccel = (Acceleration.Z > 0.f); | |
Acceleration.Z = FMath::Min(0.1f, Acceleration.Z); | |
} | |
Iterations++; | |
FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
bJustTeleported = false; | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) | |
{ | |
const float Friction = 0.5f * GetPhysicsVolume()->FluidFriction * Depth; | |
CalcVelocity(deltaTime, Friction, true, GetMaxBrakingDeceleration()); | |
Velocity.Z += GetGravityZ() * deltaTime * (1.f - NetBuoyancy); | |
} | |
FVector Adjusted = Velocity * deltaTime; | |
FHitResult Hit(1.f); | |
float remainingTime = deltaTime * Swim(Adjusted, Hit); | |
//may have left water - if so, script might have set new physics mode | |
if (!IsSwimming()) | |
{ | |
StartNewPhysics(remainingTime, Iterations); | |
return; | |
} | |
if (Hit.Time < 1.f && CharacterOwner) | |
{ | |
HandleSwimmingWallHit(Hit, deltaTime); | |
if (bLimitedUpAccel && (Velocity.Z >= 0.f)) | |
{ | |
// allow upward velocity at surface if against obstacle | |
Velocity.Z += OriginalAccelZ * deltaTime; | |
Adjusted = Velocity * (1.f - Hit.Time) * deltaTime; | |
Swim(Adjusted, Hit); | |
if (!IsSwimming()) | |
{ | |
StartNewPhysics(remainingTime, Iterations); | |
return; | |
} | |
} | |
const FVector GravDir = FVector(0.f, 0.f, -1.f); | |
const FVector VelDir = Velocity.GetSafeNormal(); | |
const float UpDown = GravDir | VelDir; | |
bool bSteppedUp = false; | |
if ((FMath::Abs(Hit.ImpactNormal.Z) < 0.2f) && (UpDown < 0.5f) && (UpDown > -0.2f) && CanStepUp(Hit)) | |
{ | |
float stepZ = UpdatedComponent->GetComponentLocation().Z; | |
const FVector RealVelocity = Velocity; | |
Velocity.Z = 1.f; // HACK: since will be moving up, in case pawn leaves the water | |
bSteppedUp = StepUp(GravDir, Adjusted * (1.f - Hit.Time), Hit); | |
if (bSteppedUp) | |
{ | |
//may have left water - if so, script might have set new physics mode | |
if (!IsSwimming()) | |
{ | |
StartNewPhysics(remainingTime, Iterations); | |
return; | |
} | |
OldLocation.Z = UpdatedComponent->GetComponentLocation().Z + (OldLocation.Z - stepZ); | |
} | |
Velocity = RealVelocity; | |
} | |
if (!bSteppedUp) | |
{ | |
//adjust and try again | |
HandleImpact(Hit, deltaTime, Adjusted); | |
SlideAlongSurface(Adjusted, (1.f - Hit.Time), Hit.Normal, Hit, true); | |
} | |
} | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && !bJustTeleported && ((deltaTime - remainingTime) > KINDA_SMALL_NUMBER) && CharacterOwner) | |
{ | |
bool bWaterJump = !GetPhysicsVolume()->bWaterVolume; | |
float velZ = Velocity.Z; | |
Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / (deltaTime - remainingTime); | |
if (bWaterJump) | |
{ | |
Velocity.Z = velZ; | |
} | |
} | |
if (!GetPhysicsVolume()->bWaterVolume && IsSwimming()) | |
{ | |
SetMovementMode(MOVE_Falling); //in case script didn't change it (w/ zone change) | |
} | |
//may have left water - if so, script might have set new physics mode | |
if (!IsSwimming()) | |
{ | |
StartNewPhysics(remainingTime, Iterations); | |
} | |
} | |
void UMMOPlayerMovement::StartSwimming(FVector OldLocation, FVector OldVelocity, float timeTick, float remainingTime, int32 Iterations) | |
{ | |
if (remainingTime < MIN_TICK_TIME || timeTick < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && !bJustTeleported) | |
{ | |
Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / timeTick; //actual average velocity | |
Velocity = 2.f * Velocity - OldVelocity; //end velocity has 2* accel of avg | |
Velocity = Velocity.GetClampedToMaxSize(GetPhysicsVolume()->TerminalVelocity); | |
} | |
const FVector End = FindWaterLine(UpdatedComponent->GetComponentLocation(), OldLocation); | |
float waterTime = 0.f; | |
if (End != UpdatedComponent->GetComponentLocation()) | |
{ | |
const float ActualDist = (UpdatedComponent->GetComponentLocation() - OldLocation).Size(); | |
if (ActualDist > KINDA_SMALL_NUMBER) | |
{ | |
waterTime = timeTick * (End - UpdatedComponent->GetComponentLocation()).Size() / ActualDist; | |
remainingTime += waterTime; | |
} | |
MoveUpdatedComponent(End - UpdatedComponent->GetComponentLocation(), UpdatedComponent->GetComponentQuat(), true); | |
} | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && (Velocity.Z > 2.f * SWIMBOBSPEED) && (Velocity.Z < 0.f)) //allow for falling out of water | |
{ | |
Velocity.Z = SWIMBOBSPEED - Velocity.Size2D() * 0.7f; //smooth bobbing | |
} | |
if ((remainingTime >= MIN_TICK_TIME) && (Iterations < MaxSimulationIterations)) | |
{ | |
PhysSwimming(remainingTime, Iterations); | |
} | |
} | |
float UMMOPlayerMovement::Swim(FVector Delta, FHitResult& Hit) | |
{ | |
FVector Start = UpdatedComponent->GetComponentLocation(); | |
float airTime = 0.f; | |
SafeMoveUpdatedComponent(Delta, UpdatedComponent->GetComponentQuat(), true, Hit); | |
if (!GetPhysicsVolume()->bWaterVolume) //then left water | |
{ | |
const FVector End = FindWaterLine(Start, UpdatedComponent->GetComponentLocation()); | |
const float DesiredDist = Delta.Size(); | |
if (End != UpdatedComponent->GetComponentLocation() && DesiredDist > KINDA_SMALL_NUMBER) | |
{ | |
airTime = (End - UpdatedComponent->GetComponentLocation()).Size() / DesiredDist; | |
if (((UpdatedComponent->GetComponentLocation() - Start) | (End - UpdatedComponent->GetComponentLocation())) > 0.f) | |
{ | |
airTime = 0.f; | |
} | |
SafeMoveUpdatedComponent(End - UpdatedComponent->GetComponentLocation(), UpdatedComponent->GetComponentQuat(), true, Hit); | |
} | |
} | |
return airTime; | |
} | |
FVector UMMOPlayerMovement::FindWaterLine(FVector InWater, FVector OutofWater) | |
{ | |
FVector Result = OutofWater; | |
TArray<FHitResult> Hits; | |
GetWorld()->LineTraceMultiByChannel(Hits, OutofWater, InWater, UpdatedComponent->GetCollisionObjectType(), FCollisionQueryParams(SCENE_QUERY_STAT(FindWaterLine), true, CharacterOwner)); | |
for (int32 HitIdx = 0; HitIdx < Hits.Num(); HitIdx++) | |
{ | |
const FHitResult& Check = Hits[HitIdx]; | |
if (!CharacterOwner->IsOwnedBy(Check.GetActor()) && !Check.Component.Get()->IsWorldGeometry()) | |
{ | |
APhysicsVolume* W = Cast<APhysicsVolume>(Check.GetActor()); | |
if (W && W->bWaterVolume) | |
{ | |
FVector Dir = (InWater - OutofWater).GetSafeNormal(); | |
Result = Check.Location; | |
if (W == GetPhysicsVolume()) | |
Result += 0.1f * Dir; | |
else | |
Result -= 0.1f * Dir; | |
break; | |
} | |
} | |
} | |
return Result; | |
} | |
void UMMOPlayerMovement::NotifyJumpApex() | |
{ | |
if (CharacterOwner) | |
{ | |
CharacterOwner->NotifyJumpApex(); | |
} | |
} | |
FVector UMMOPlayerMovement::GetFallingLateralAcceleration(float DeltaTime) | |
{ | |
// No acceleration in Z | |
FVector FallAcceleration = FVector(Acceleration.X, Acceleration.Y, 0.f); | |
// bound acceleration, falling object has minimal ability to impact acceleration | |
if (!HasAnimRootMotion() && FallAcceleration.SizeSquared2D() > 0.f) | |
{ | |
FallAcceleration = GetAirControl(DeltaTime, AirControl, FallAcceleration); | |
FallAcceleration = FallAcceleration.GetClampedToMaxSize(GetMaxAcceleration()); | |
} | |
return FallAcceleration; | |
} | |
bool UMMOPlayerMovement::ShouldLimitAirControl(float DeltaTime, const FVector& FallAcceleration) const | |
{ | |
return (FallAcceleration.SizeSquared2D() > 0.f); | |
} | |
FVector UMMOPlayerMovement::GetAirControl(float DeltaTime, float TickAirControl, const FVector& FallAcceleration) | |
{ | |
// Boost | |
if (TickAirControl != 0.f) | |
{ | |
TickAirControl = BoostAirControl(DeltaTime, TickAirControl, FallAcceleration); | |
} | |
return TickAirControl * FallAcceleration; | |
} | |
float UMMOPlayerMovement::BoostAirControl(float DeltaTime, float TickAirControl, const FVector& FallAcceleration) | |
{ | |
// Allow a burst of initial acceleration | |
if (AirControlBoostMultiplier > 0.f && Velocity.SizeSquared2D() < FMath::Square(AirControlBoostVelocityThreshold)) | |
{ | |
TickAirControl = FMath::Min(1.f, AirControlBoostMultiplier * TickAirControl); | |
} | |
return TickAirControl; | |
} | |
void UMMOPlayerMovement::PhysFalling(float deltaTime, int32 Iterations) | |
{ | |
if (deltaTime < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
FVector FallAcceleration = GetFallingLateralAcceleration(deltaTime); | |
FallAcceleration.Z = 0.f; | |
const bool bHasLimitedAirControl = ShouldLimitAirControl(deltaTime, FallAcceleration); | |
float remainingTime = deltaTime; | |
while ((remainingTime >= MIN_TICK_TIME) && (Iterations < MaxSimulationIterations)) | |
{ | |
Iterations++; | |
float timeTick = GetSimulationTimeStep(remainingTime, Iterations); | |
remainingTime -= timeTick; | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
const FQuat PawnRotation = UpdatedComponent->GetComponentQuat(); | |
bJustTeleported = false; | |
RestorePreAdditiveRootMotionVelocity(); | |
const FVector OldVelocity = Velocity; | |
// Apply input | |
const float MaxDecel = GetMaxBrakingDeceleration(); | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) | |
{ | |
// Compute Velocity | |
{ | |
// Acceleration = FallAcceleration for CalcVelocity(), but we restore it after using it. | |
TGuardValue<FVector> RestoreAcceleration(Acceleration, FallAcceleration); | |
Velocity.Z = 0.f; | |
CalcVelocity(timeTick, FallingLateralFriction, false, MaxDecel); | |
Velocity.Z = OldVelocity.Z; | |
} | |
} | |
// Compute current gravity | |
const FVector Gravity(0.f, 0.f, GetGravityZ()); | |
float GravityTime = timeTick; | |
// If jump is providing force, gravity may be affected. | |
bool bEndingJumpForce = false; | |
if (CharacterOwner->JumpForceTimeRemaining > 0.0f) | |
{ | |
// Consume some of the force time. Only the remaining time (if any) is affected by gravity when bApplyGravityWhileJumping=false. | |
const float JumpForceTime = FMath::Min(CharacterOwner->JumpForceTimeRemaining, timeTick); | |
GravityTime = bApplyGravityWhileJumping ? timeTick : FMath::Max(0.0f, timeTick - JumpForceTime); | |
// Update Character state | |
CharacterOwner->JumpForceTimeRemaining -= JumpForceTime; | |
if (CharacterOwner->JumpForceTimeRemaining <= 0.0f) | |
{ | |
CharacterOwner->ResetJumpState(); | |
bEndingJumpForce = true; | |
} | |
} | |
// Apply gravity | |
Velocity = NewFallVelocity(Velocity, Gravity, GravityTime); | |
// See if we need to sub-step to exactly reach the apex. This is important for avoiding "cutting off the top" of the trajectory as framerate varies. | |
if (MMOCharacterMovementCVars::ForceJumpPeakSubstep && OldVelocity.Z > 0.f && Velocity.Z <= 0.f && NumJumpApexAttempts < MaxJumpApexAttemptsPerSimulation) | |
{ | |
const FVector DerivedAccel = (Velocity - OldVelocity) / timeTick; | |
if (!FMath::IsNearlyZero(DerivedAccel.Z)) | |
{ | |
const float TimeToApex = -OldVelocity.Z / DerivedAccel.Z; | |
// The time-to-apex calculation should be precise, and we want to avoid adding a substep when we are basically already at the apex from the previous iteration's work. | |
const float ApexTimeMinimum = 0.0001f; | |
if (TimeToApex >= ApexTimeMinimum && TimeToApex < timeTick) | |
{ | |
const FVector ApexVelocity = OldVelocity + DerivedAccel * TimeToApex; | |
Velocity = ApexVelocity; | |
Velocity.Z = 0.f; // Should be nearly zero anyway, but this makes apex notifications consistent. | |
// We only want to move the amount of time it takes to reach the apex, and refund the unused time for next iteration. | |
remainingTime += (timeTick - TimeToApex); | |
timeTick = TimeToApex; | |
Iterations--; | |
NumJumpApexAttempts++; | |
} | |
} | |
} | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("dt=(%.6f) OldLocation=(%s) OldVelocity=(%s) NewVelocity=(%s)"), timeTick, *(UpdatedComponent->GetComponentLocation()).ToString(), *OldVelocity.ToString(), *Velocity.ToString()); | |
ApplyRootMotionToVelocity(timeTick); | |
if (bNotifyApex && (Velocity.Z < 0.f)) | |
{ | |
// Just passed jump apex since now going down | |
bNotifyApex = false; | |
NotifyJumpApex(); | |
} | |
// Compute change in position (using midpoint integration method). | |
FVector Adjusted = 0.5f * (OldVelocity + Velocity) * timeTick; | |
// Special handling if ending the jump force where we didn't apply gravity during the jump. | |
if (bEndingJumpForce && !bApplyGravityWhileJumping) | |
{ | |
// We had a portion of the time at constant speed then a portion with acceleration due to gravity. | |
// Account for that here with a more correct change in position. | |
const float NonGravityTime = FMath::Max(0.f, timeTick - GravityTime); | |
Adjusted = (OldVelocity * NonGravityTime) + (0.5f * (OldVelocity + Velocity) * GravityTime); | |
} | |
// Move | |
FHitResult Hit(1.f); | |
SafeMoveUpdatedComponent(Adjusted, PawnRotation, true, Hit); | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
float LastMoveTimeSlice = timeTick; | |
float subTimeTickRemaining = timeTick * (1.f - Hit.Time); | |
if (IsSwimming()) //just entered water | |
{ | |
remainingTime += subTimeTickRemaining; | |
StartSwimming(OldLocation, OldVelocity, timeTick, remainingTime, Iterations); | |
return; | |
} | |
else if (Hit.bBlockingHit) | |
{ | |
if (IsValidLandingSpot(UpdatedComponent->GetComponentLocation(), Hit)) | |
{ | |
remainingTime += subTimeTickRemaining; | |
ProcessLanded(Hit, remainingTime, Iterations); | |
return; | |
} | |
else | |
{ | |
// Compute impact deflection based on final velocity, not integration step. | |
// This allows us to compute a new velocity from the deflected vector, and ensures the full gravity effect is included in the slide result. | |
Adjusted = Velocity * timeTick; | |
// See if we can convert a normally invalid landing spot (based on the hit result) to a usable one. | |
if (!Hit.bStartPenetrating && ShouldCheckForValidLandingSpot(timeTick, Adjusted, Hit)) | |
{ | |
const FVector PawnLocation = UpdatedComponent->GetComponentLocation(); | |
FMMOFindFloorResult FloorResult; | |
FindFloor(PawnLocation, FloorResult, false); | |
if (FloorResult.IsWalkableFloor() && IsValidLandingSpot(PawnLocation, FloorResult.HitResult)) | |
{ | |
remainingTime += subTimeTickRemaining; | |
ProcessLanded(FloorResult.HitResult, remainingTime, Iterations); | |
return; | |
} | |
} | |
HandleImpact(Hit, LastMoveTimeSlice, Adjusted); | |
// If we've changed physics mode, abort. | |
if (!HasValidData() || !IsFalling()) | |
{ | |
return; | |
} | |
// Limit air control based on what we hit. | |
// We moved to the impact point using air control, but may want to deflect from there based on a limited air control acceleration. | |
FVector VelocityNoAirControl = OldVelocity; | |
FVector AirControlAccel = Acceleration; | |
if (bHasLimitedAirControl) | |
{ | |
// Compute VelocityNoAirControl | |
{ | |
// Find velocity *without* acceleration. | |
TGuardValue<FVector> RestoreAcceleration(Acceleration, FVector::ZeroVector); | |
TGuardValue<FVector> RestoreVelocity(Velocity, OldVelocity); | |
Velocity.Z = 0.f; | |
CalcVelocity(timeTick, FallingLateralFriction, false, MaxDecel); | |
VelocityNoAirControl = FVector(Velocity.X, Velocity.Y, OldVelocity.Z); | |
VelocityNoAirControl = NewFallVelocity(VelocityNoAirControl, Gravity, GravityTime); | |
} | |
const bool bCheckLandingSpot = false; // we already checked above. | |
AirControlAccel = (Velocity - VelocityNoAirControl) / timeTick; | |
const FVector AirControlDeltaV = LimitAirControl(LastMoveTimeSlice, AirControlAccel, Hit, bCheckLandingSpot) * LastMoveTimeSlice; | |
Adjusted = (VelocityNoAirControl + AirControlDeltaV) * LastMoveTimeSlice; | |
} | |
const FVector OldHitNormal = Hit.Normal; | |
const FVector OldHitImpactNormal = Hit.ImpactNormal; | |
FVector Delta = ComputeSlideVector(Adjusted, 1.f - Hit.Time, OldHitNormal, Hit); | |
// Compute velocity after deflection (only gravity component for RootMotion) | |
if (subTimeTickRemaining > KINDA_SMALL_NUMBER && !bJustTeleported) | |
{ | |
const FVector NewVelocity = (Delta / subTimeTickRemaining); | |
Velocity = HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocityWithIgnoreZAccumulate() ? FVector(Velocity.X, Velocity.Y, NewVelocity.Z) : NewVelocity; | |
} | |
if (subTimeTickRemaining > KINDA_SMALL_NUMBER && (Delta | Adjusted) > 0.f) | |
{ | |
// Move in deflected direction. | |
SafeMoveUpdatedComponent(Delta, PawnRotation, true, Hit); | |
if (Hit.bBlockingHit) | |
{ | |
// hit second wall | |
LastMoveTimeSlice = subTimeTickRemaining; | |
subTimeTickRemaining = subTimeTickRemaining * (1.f - Hit.Time); | |
if (IsValidLandingSpot(UpdatedComponent->GetComponentLocation(), Hit)) | |
{ | |
remainingTime += subTimeTickRemaining; | |
ProcessLanded(Hit, remainingTime, Iterations); | |
return; | |
} | |
HandleImpact(Hit, LastMoveTimeSlice, Delta); | |
// If we've changed physics mode, abort. | |
if (!HasValidData() || !IsFalling()) | |
{ | |
return; | |
} | |
// Act as if there was no air control on the last move when computing new deflection. | |
if (bHasLimitedAirControl && Hit.Normal.Z > VERTICAL_SLOPE_NORMAL_Z) | |
{ | |
const FVector LastMoveNoAirControl = VelocityNoAirControl * LastMoveTimeSlice; | |
Delta = ComputeSlideVector(LastMoveNoAirControl, 1.f, OldHitNormal, Hit); | |
} | |
FVector PreTwoWallDelta = Delta; | |
TwoWallAdjust(Delta, Hit, OldHitNormal); | |
// Limit air control, but allow a slide along the second wall. | |
if (bHasLimitedAirControl) | |
{ | |
const bool bCheckLandingSpot = false; // we already checked above. | |
const FVector AirControlDeltaV = LimitAirControl(subTimeTickRemaining, AirControlAccel, Hit, bCheckLandingSpot) * subTimeTickRemaining; | |
// Only allow if not back in to first wall | |
if (FVector::DotProduct(AirControlDeltaV, OldHitNormal) > 0.f) | |
{ | |
Delta += (AirControlDeltaV * subTimeTickRemaining); | |
} | |
} | |
// Compute velocity after deflection (only gravity component for RootMotion) | |
if (subTimeTickRemaining > KINDA_SMALL_NUMBER && !bJustTeleported) | |
{ | |
const FVector NewVelocity = (Delta / subTimeTickRemaining); | |
Velocity = HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocityWithIgnoreZAccumulate() ? FVector(Velocity.X, Velocity.Y, NewVelocity.Z) : NewVelocity; | |
} | |
// bDitch=true means that pawn is straddling two slopes, neither of which he can stand on | |
bool bDitch = ((OldHitImpactNormal.Z > 0.f) && (Hit.ImpactNormal.Z > 0.f) && (FMath::Abs(Delta.Z) <= KINDA_SMALL_NUMBER) && ((Hit.ImpactNormal | OldHitImpactNormal) < 0.f)); | |
SafeMoveUpdatedComponent(Delta, PawnRotation, true, Hit); | |
if (Hit.Time == 0.f) | |
{ | |
// if we are stuck then try to side step | |
FVector SideDelta = (OldHitNormal + Hit.ImpactNormal).GetSafeNormal2D(); | |
if (SideDelta.IsNearlyZero()) | |
{ | |
SideDelta = FVector(OldHitNormal.Y, -OldHitNormal.X, 0).GetSafeNormal(); | |
} | |
SafeMoveUpdatedComponent(SideDelta, PawnRotation, true, Hit); | |
} | |
if (bDitch || IsValidLandingSpot(UpdatedComponent->GetComponentLocation(), Hit) || Hit.Time == 0.f) | |
{ | |
remainingTime = 0.f; | |
ProcessLanded(Hit, remainingTime, Iterations); | |
return; | |
} | |
else if (GetPerchRadiusThreshold() > 0.f && Hit.Time == 1.f && OldHitImpactNormal.Z >= WalkableFloorZ) | |
{ | |
// We might be in a virtual 'ditch' within our perch radius. This is rare. | |
const FVector PawnLocation = UpdatedComponent->GetComponentLocation(); | |
const float ZMovedDist = FMath::Abs(PawnLocation.Z - OldLocation.Z); | |
const float MovedDist2DSq = (PawnLocation - OldLocation).SizeSquared2D(); | |
if (ZMovedDist <= 0.2f * timeTick && MovedDist2DSq <= 4.f * timeTick) | |
{ | |
Velocity.X += 0.25f * GetMaxSpeed() * (RandomStream.FRand() - 0.5f); | |
Velocity.Y += 0.25f * GetMaxSpeed() * (RandomStream.FRand() - 0.5f); | |
Velocity.Z = FMath::Max<float>(JumpZVelocity * 0.25f, 1.f); | |
Delta = Velocity * timeTick; | |
SafeMoveUpdatedComponent(Delta, PawnRotation, true, Hit); | |
} | |
} | |
} | |
} | |
} | |
} | |
if (Velocity.SizeSquared2D() <= KINDA_SMALL_NUMBER * 10.f) | |
{ | |
Velocity.X = 0.f; | |
Velocity.Y = 0.f; | |
} | |
} | |
} | |
FVector UMMOPlayerMovement::LimitAirControl(float DeltaTime, const FVector& FallAcceleration, const FHitResult& HitResult, bool bCheckForValidLandingSpot) | |
{ | |
FVector Result(FallAcceleration); | |
if (HitResult.IsValidBlockingHit() && HitResult.Normal.Z > VERTICAL_SLOPE_NORMAL_Z) | |
{ | |
if (!bCheckForValidLandingSpot || !IsValidLandingSpot(HitResult.Location, HitResult)) | |
{ | |
// If acceleration is into the wall, limit contribution. | |
if (FVector::DotProduct(FallAcceleration, HitResult.Normal) < 0.f) | |
{ | |
// Allow movement parallel to the wall, but not into it because that may push us up. | |
const FVector Normal2D = HitResult.Normal.GetSafeNormal2D(); | |
Result = FVector::VectorPlaneProject(FallAcceleration, Normal2D); | |
} | |
} | |
} | |
else if (HitResult.bStartPenetrating) | |
{ | |
// Allow movement out of penetration. | |
return (FVector::DotProduct(Result, HitResult.Normal) > 0.f ? Result : FVector::ZeroVector); | |
} | |
return Result; | |
} | |
bool UMMOPlayerMovement::CheckLedgeDirection(const FVector& OldLocation, const FVector& SideStep, const FVector& GravDir) const | |
{ | |
const FVector SideDest = OldLocation + SideStep; | |
FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CheckLedgeDirection), false, CharacterOwner); | |
FCollisionResponseParams ResponseParam; | |
InitCollisionParams(CapsuleParams, ResponseParam); | |
const FCollisionShape CapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_None); | |
const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); | |
FHitResult Result(1.f); | |
GetWorld()->SweepSingleByChannel(Result, OldLocation, SideDest, FQuat::Identity, CollisionChannel, CapsuleShape, CapsuleParams, ResponseParam); | |
if (!Result.bBlockingHit || IsWalkable(Result)) | |
{ | |
if (!Result.bBlockingHit) | |
{ | |
GetWorld()->SweepSingleByChannel(Result, SideDest, SideDest + GravDir * (MaxStepHeight + LedgeCheckThreshold), FQuat::Identity, CollisionChannel, CapsuleShape, CapsuleParams, ResponseParam); | |
} | |
if ((Result.Time < 1.f) && IsWalkable(Result)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
FVector UMMOPlayerMovement::GetLedgeMove(const FVector& OldLocation, const FVector& Delta, const FVector& GravDir) const | |
{ | |
if (!HasValidData() || Delta.IsZero()) | |
{ | |
return FVector::ZeroVector; | |
} | |
FVector SideDir(Delta.Y, -1.f * Delta.X, 0.f); | |
// try left | |
if (CheckLedgeDirection(OldLocation, SideDir, GravDir)) | |
{ | |
return SideDir; | |
} | |
// try right | |
SideDir *= -1.f; | |
if (CheckLedgeDirection(OldLocation, SideDir, GravDir)) | |
{ | |
return SideDir; | |
} | |
return FVector::ZeroVector; | |
} | |
bool UMMOPlayerMovement::CanWalkOffLedges() const | |
{ | |
if (!bCanWalkOffLedgesWhenCrouching && IsCrouching()) | |
{ | |
return false; | |
} | |
return bCanWalkOffLedges; | |
} | |
bool UMMOPlayerMovement::CheckFall(const FMMOFindFloorResult& OldFloor, const FHitResult& Hit, const FVector& Delta, const FVector& OldLocation, float remainingTime, float timeTick, int32 Iterations, bool bMustJump) | |
{ | |
if (!HasValidData()) | |
{ | |
return false; | |
} | |
if (bMustJump || CanWalkOffLedges()) | |
{ | |
HandleWalkingOffLedge(OldFloor.HitResult.ImpactNormal, OldFloor.HitResult.Normal, OldLocation, timeTick); | |
if (IsMovingOnGround()) | |
{ | |
// If still walking, then fall. If not, assume the user set a different mode they want to keep. | |
StartFalling(Iterations, remainingTime, timeTick, Delta, OldLocation); | |
} | |
return true; | |
} | |
return false; | |
} | |
void UMMOPlayerMovement::StartFalling(int32 Iterations, float remainingTime, float timeTick, const FVector& Delta, const FVector& subLoc) | |
{ | |
// start falling | |
const float DesiredDist = Delta.Size(); | |
const float ActualDist = (UpdatedComponent->GetComponentLocation() - subLoc).Size2D(); | |
remainingTime = (DesiredDist < KINDA_SMALL_NUMBER) | |
? 0.f | |
: remainingTime + timeTick * (1.f - FMath::Min(1.f, ActualDist / DesiredDist)); | |
if (IsMovingOnGround()) | |
{ | |
// This is to catch cases where the first frame of PIE is executed, and the | |
// level is not yet visible. In those cases, the player will fall out of the | |
// world... So, don't set MOVE_Falling straight away. | |
if (!GIsEditor || (GetWorld()->HasBegunPlay() && (GetWorld()->GetTimeSeconds() >= 1.f))) | |
{ | |
SetMovementMode(MOVE_Falling); //default behavior if script didn't change physics | |
} | |
else | |
{ | |
// Make sure that the floor check code continues processing during this delay. | |
bForceNextFloorCheck = true; | |
} | |
} | |
StartNewPhysics(remainingTime, Iterations); | |
} | |
void UMMOPlayerMovement::RevertMove(const FVector& OldLocation, UPrimitiveComponent* OldBase, const FVector& PreviousBaseLocation, const FMMOFindFloorResult& OldFloor, bool bFailMove) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("RevertMove from %f %f %f to %f %f %f"), CharacterOwner->Location.X, CharacterOwner->Location.Y, CharacterOwner->Location.Z, OldLocation.X, OldLocation.Y, OldLocation.Z); | |
UpdatedComponent->SetWorldLocation(OldLocation, false, nullptr, GetTeleportType()); | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("Now at %f %f %f"), CharacterOwner->Location.X, CharacterOwner->Location.Y, CharacterOwner->Location.Z); | |
bJustTeleported = false; | |
// if our previous base couldn't have moved or changed in any physics-affecting way, restore it | |
if (IsValid(OldBase) && | |
(!MovementBaseUtility::IsDynamicBase(OldBase) || | |
(OldBase->Mobility == EComponentMobility::Static) || | |
(OldBase->GetComponentLocation() == PreviousBaseLocation) | |
) | |
) | |
{ | |
CurrentFloor = OldFloor; | |
SetBase(OldBase, OldFloor.HitResult.BoneName); | |
} | |
else | |
{ | |
SetBase(NULL); | |
} | |
if (bFailMove) | |
{ | |
// end movement now | |
Velocity = FVector::ZeroVector; | |
Acceleration = FVector::ZeroVector; | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("%s FAILMOVE RevertMove"), *CharacterOwner->GetName()); | |
} | |
} | |
FVector UMMOPlayerMovement::ComputeGroundMovementDelta(const FVector& Delta, const FHitResult& RampHit, const bool bHitFromLineTrace) const | |
{ | |
const FVector FloorNormal = RampHit.ImpactNormal; | |
const FVector ContactNormal = RampHit.Normal; | |
if (FloorNormal.Z < (1.f - KINDA_SMALL_NUMBER) && FloorNormal.Z > KINDA_SMALL_NUMBER && ContactNormal.Z > KINDA_SMALL_NUMBER && !bHitFromLineTrace && IsWalkable(RampHit)) | |
{ | |
// Compute a vector that moves parallel to the surface, by projecting the horizontal movement direction onto the ramp. | |
const float FloorDotDelta = (FloorNormal | Delta); | |
FVector RampMovement(Delta.X, Delta.Y, -FloorDotDelta / FloorNormal.Z); | |
if (bMaintainHorizontalGroundVelocity) | |
{ | |
return RampMovement; | |
} | |
else | |
{ | |
return RampMovement.GetSafeNormal() * Delta.Size(); | |
} | |
} | |
return Delta; | |
} | |
void UMMOPlayerMovement::OnCharacterStuckInGeometry(const FHitResult* Hit) | |
{ | |
if (MMOCharacterMovementCVars::StuckWarningPeriod >= 0) | |
{ | |
const UWorld* MyWorld = GetWorld(); | |
const float RealTimeSeconds = MyWorld->GetRealTimeSeconds(); | |
if ((RealTimeSeconds - LastStuckWarningTime) >= MMOCharacterMovementCVars::StuckWarningPeriod) | |
{ | |
LastStuckWarningTime = RealTimeSeconds; | |
if (Hit == nullptr) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Log, TEXT("%s is stuck and failed to move! (%d other events since notify)"), *CharacterOwner->GetName(), StuckWarningCountSinceNotify); | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Log, TEXT("%s is stuck and failed to move! Velocity: X=%3.2f Y=%3.2f Z=%3.2f Location: X=%3.2f Y=%3.2f Z=%3.2f Normal: X=%3.2f Y=%3.2f Z=%3.2f PenetrationDepth:%.3f Actor:%s Component:%s BoneName:%s (%d other events since notify)"), | |
*GetNameSafe(CharacterOwner), | |
Velocity.X, Velocity.Y, Velocity.Z, | |
Hit->Location.X, Hit->Location.Y, Hit->Location.Z, | |
Hit->Normal.X, Hit->Normal.Y, Hit->Normal.Z, | |
Hit->PenetrationDepth, | |
*GetNameSafe(Hit->GetActor()), | |
*GetNameSafe(Hit->GetComponent()), | |
Hit->BoneName.IsValid() ? *Hit->BoneName.ToString() : TEXT("None"), | |
StuckWarningCountSinceNotify | |
); | |
} | |
StuckWarningCountSinceNotify = 0; | |
} | |
else | |
{ | |
StuckWarningCountSinceNotify += 1; | |
} | |
} | |
// Don't update velocity based on our (failed) change in position this update since we're stuck. | |
bJustTeleported = true; | |
} | |
void UMMOPlayerMovement::MoveAlongFloor(const FVector& InVelocity, float DeltaSeconds, FStepDownResult* OutStepDownResult) | |
{ | |
if (!CurrentFloor.IsWalkableFloor()) | |
{ | |
return; | |
} | |
// Move along the current floor | |
const FVector Delta = FVector(InVelocity.X, InVelocity.Y, 0.f) * DeltaSeconds; | |
FHitResult Hit(1.f); | |
FVector RampVector = ComputeGroundMovementDelta(Delta, CurrentFloor.HitResult, CurrentFloor.bLineTrace); | |
SafeMoveUpdatedComponent(RampVector, UpdatedComponent->GetComponentQuat(), true, Hit); | |
float LastMoveTimeSlice = DeltaSeconds; | |
if (Hit.bStartPenetrating) | |
{ | |
// Allow this hit to be used as an impact we can deflect off, otherwise we do nothing the rest of the update and appear to hitch. | |
HandleImpact(Hit); | |
SlideAlongSurface(Delta, 1.f, Hit.Normal, Hit, true); | |
if (Hit.bStartPenetrating) | |
{ | |
OnCharacterStuckInGeometry(&Hit); | |
} | |
} | |
else if (Hit.IsValidBlockingHit()) | |
{ | |
// We impacted something (most likely another ramp, but possibly a barrier). | |
float PercentTimeApplied = Hit.Time; | |
if ((Hit.Time > 0.f) && (Hit.Normal.Z > KINDA_SMALL_NUMBER) && IsWalkable(Hit)) | |
{ | |
// Another walkable ramp. | |
const float InitialPercentRemaining = 1.f - PercentTimeApplied; | |
RampVector = ComputeGroundMovementDelta(Delta * InitialPercentRemaining, Hit, false); | |
LastMoveTimeSlice = InitialPercentRemaining * LastMoveTimeSlice; | |
SafeMoveUpdatedComponent(RampVector, UpdatedComponent->GetComponentQuat(), true, Hit); | |
const float SecondHitPercent = Hit.Time * InitialPercentRemaining; | |
PercentTimeApplied = FMath::Clamp(PercentTimeApplied + SecondHitPercent, 0.f, 1.f); | |
} | |
if (Hit.IsValidBlockingHit()) | |
{ | |
if (CanStepUp(Hit) || (CharacterOwner->GetMovementBase() != NULL && CharacterOwner->GetMovementBase()->GetOwner() == Hit.GetActor())) | |
{ | |
// hit a barrier, try to step up | |
const FVector GravDir(0.f, 0.f, -1.f); | |
if (!StepUp(GravDir, Delta * (1.f - PercentTimeApplied), Hit, OutStepDownResult)) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Verbose, TEXT("- StepUp (ImpactNormal %s, Normal %s"), *Hit.ImpactNormal.ToString(), *Hit.Normal.ToString()); | |
HandleImpact(Hit, LastMoveTimeSlice, RampVector); | |
SlideAlongSurface(Delta, 1.f - PercentTimeApplied, Hit.Normal, Hit, true); | |
} | |
else | |
{ | |
// Don't recalculate velocity based on this height adjustment, if considering vertical adjustments. | |
UE_LOG(LogMMOCharacterMovement, Verbose, TEXT("+ StepUp (ImpactNormal %s, Normal %s"), *Hit.ImpactNormal.ToString(), *Hit.Normal.ToString()); | |
bJustTeleported |= !bMaintainHorizontalGroundVelocity; | |
} | |
} | |
else if (Hit.Component.IsValid() && !Hit.Component.Get()->CanCharacterStepUp(CharacterOwner)) | |
{ | |
HandleImpact(Hit, LastMoveTimeSlice, RampVector); | |
SlideAlongSurface(Delta, 1.f - PercentTimeApplied, Hit.Normal, Hit, true); | |
} | |
} | |
} | |
} | |
void UMMOPlayerMovement::MaintainHorizontalGroundVelocity() | |
{ | |
if (Velocity.Z != 0.f) | |
{ | |
if (bMaintainHorizontalGroundVelocity) | |
{ | |
// Ramp movement already maintained the velocity, so we just want to remove the vertical component. | |
Velocity.Z = 0.f; | |
} | |
else | |
{ | |
// Rescale velocity to be horizontal but maintain magnitude of last update. | |
Velocity = Velocity.GetSafeNormal2D() * Velocity.Size(); | |
} | |
} | |
} | |
void UMMOPlayerMovement::PhysWalking(float deltaTime, int32 Iterations) | |
{ | |
if (deltaTime < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
if (!CharacterOwner || (!CharacterOwner->Controller && !bRunPhysicsWithNoController && !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && (CharacterOwner->GetLocalRole() != ROLE_SimulatedProxy))) | |
{ | |
Acceleration = FVector::ZeroVector; | |
Velocity = FVector::ZeroVector; | |
return; | |
} | |
if (!UpdatedComponent->IsQueryCollisionEnabled()) | |
{ | |
SetMovementMode(MOVE_Walking); | |
return; | |
} | |
devMMOCode(ensureMsgf(!Velocity.ContainsNaN(), TEXT("PhysWalking: Velocity contains NaN before Iteration (%s)\n%s"), *GetPathNameSafe(this), *Velocity.ToString())); | |
bJustTeleported = false; | |
bool bCheckedFall = false; | |
bool bTriedLedgeMove = false; | |
float remainingTime = deltaTime; | |
// Perform the move | |
while ((remainingTime >= MIN_TICK_TIME) && (Iterations < MaxSimulationIterations) && CharacterOwner && (CharacterOwner->Controller || bRunPhysicsWithNoController || HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocity() || (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy))) | |
{ | |
Iterations++; | |
bJustTeleported = false; | |
const float timeTick = GetSimulationTimeStep(remainingTime, Iterations); | |
remainingTime -= timeTick; | |
// Save current values | |
UPrimitiveComponent* const OldBase = GetMovementBase(); | |
const FVector PreviousBaseLocation = (OldBase != NULL) ? OldBase->GetComponentLocation() : FVector::ZeroVector; | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
const FMMOFindFloorResult OldFloor = CurrentFloor; | |
RestorePreAdditiveRootMotionVelocity(); | |
// Ensure velocity is horizontal. | |
MaintainHorizontalGroundVelocity(); | |
const FVector OldVelocity = Velocity; | |
Acceleration.Z = 0.f; | |
// Apply acceleration | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) | |
{ | |
CalcVelocity(timeTick, GroundFriction, false, GetMaxBrakingDeceleration()); | |
devMMOCode(ensureMsgf(!Velocity.ContainsNaN(), TEXT("PhysWalking: Velocity contains NaN after CalcVelocity (%s)\n%s"), *GetPathNameSafe(this), *Velocity.ToString())); | |
} | |
ApplyRootMotionToVelocity(timeTick); | |
devMMOCode(ensureMsgf(!Velocity.ContainsNaN(), TEXT("PhysWalking: Velocity contains NaN after Root Motion application (%s)\n%s"), *GetPathNameSafe(this), *Velocity.ToString())); | |
if (IsFalling()) | |
{ | |
// Root motion could have put us into Falling. | |
// No movement has taken place this movement tick so we pass on full time/past iteration count | |
StartNewPhysics(remainingTime + timeTick, Iterations - 1); | |
return; | |
} | |
// Compute move parameters | |
const FVector MoveVelocity = Velocity; | |
const FVector Delta = timeTick * MoveVelocity; | |
const bool bZeroDelta = Delta.IsNearlyZero(); | |
FStepDownResult StepDownResult; | |
if (bZeroDelta) | |
{ | |
remainingTime = 0.f; | |
} | |
else | |
{ | |
// try to move forward | |
MoveAlongFloor(MoveVelocity, timeTick, &StepDownResult); | |
if (IsFalling()) | |
{ | |
// pawn decided to jump up | |
const float DesiredDist = Delta.Size(); | |
if (DesiredDist > KINDA_SMALL_NUMBER) | |
{ | |
const float ActualDist = (UpdatedComponent->GetComponentLocation() - OldLocation).Size2D(); | |
remainingTime += timeTick * (1.f - FMath::Min(1.f, ActualDist / DesiredDist)); | |
} | |
StartNewPhysics(remainingTime, Iterations); | |
return; | |
} | |
else if (IsSwimming()) //just entered water | |
{ | |
StartSwimming(OldLocation, OldVelocity, timeTick, remainingTime, Iterations); | |
return; | |
} | |
} | |
// Update floor. | |
// StepUp might have already done it for us. | |
if (StepDownResult.bComputedFloor) | |
{ | |
CurrentFloor = StepDownResult.FloorResult; | |
} | |
else | |
{ | |
FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, bZeroDelta, NULL); | |
} | |
// check for ledges here | |
const bool bCheckLedges = !CanWalkOffLedges(); | |
if (bCheckLedges && !CurrentFloor.IsWalkableFloor()) | |
{ | |
// calculate possible alternate movement | |
const FVector GravDir = FVector(0.f, 0.f, -1.f); | |
const FVector NewDelta = bTriedLedgeMove ? FVector::ZeroVector : GetLedgeMove(OldLocation, Delta, GravDir); | |
if (!NewDelta.IsZero()) | |
{ | |
// first revert this move | |
RevertMove(OldLocation, OldBase, PreviousBaseLocation, OldFloor, false); | |
// avoid repeated ledge moves if the first one fails | |
bTriedLedgeMove = true; | |
// Try new movement direction | |
Velocity = NewDelta / timeTick; | |
remainingTime += timeTick; | |
continue; | |
} | |
else | |
{ | |
// see if it is OK to jump | |
// @todo collision : only thing that can be problem is that oldbase has world collision on | |
bool bMustJump = bZeroDelta || (OldBase == NULL || (!OldBase->IsQueryCollisionEnabled() && MovementBaseUtility::IsDynamicBase(OldBase))); | |
if ((bMustJump || !bCheckedFall) && CheckFall(OldFloor, CurrentFloor.HitResult, Delta, OldLocation, remainingTime, timeTick, Iterations, bMustJump)) | |
{ | |
return; | |
} | |
bCheckedFall = true; | |
// revert this move | |
RevertMove(OldLocation, OldBase, PreviousBaseLocation, OldFloor, true); | |
remainingTime = 0.f; | |
break; | |
} | |
} | |
else | |
{ | |
// Validate the floor check | |
if (CurrentFloor.IsWalkableFloor()) | |
{ | |
if (ShouldCatchAir(OldFloor, CurrentFloor)) | |
{ | |
HandleWalkingOffLedge(OldFloor.HitResult.ImpactNormal, OldFloor.HitResult.Normal, OldLocation, timeTick); | |
if (IsMovingOnGround()) | |
{ | |
// If still walking, then fall. If not, assume the user set a different mode they want to keep. | |
StartFalling(Iterations, remainingTime, timeTick, Delta, OldLocation); | |
} | |
return; | |
} | |
AdjustFloorHeight(); | |
SetBase(CurrentFloor.HitResult.Component.Get(), CurrentFloor.HitResult.BoneName); | |
} | |
else if (CurrentFloor.HitResult.bStartPenetrating && remainingTime <= 0.f) | |
{ | |
// The floor check failed because it started in penetration | |
// We do not want to try to move downward because the downward sweep failed, rather we'd like to try to pop out of the floor. | |
FHitResult Hit(CurrentFloor.HitResult); | |
Hit.TraceEnd = Hit.TraceStart + FVector(0.f, 0.f, MAX_FLOOR_DIST); | |
const FVector RequestedAdjustment = GetPenetrationAdjustment(Hit); | |
ResolvePenetration(RequestedAdjustment, Hit, UpdatedComponent->GetComponentQuat()); | |
bForceNextFloorCheck = true; | |
} | |
// check if just entered water | |
if (IsSwimming()) | |
{ | |
StartSwimming(OldLocation, Velocity, timeTick, remainingTime, Iterations); | |
return; | |
} | |
// See if we need to start falling. | |
if (!CurrentFloor.IsWalkableFloor() && !CurrentFloor.HitResult.bStartPenetrating) | |
{ | |
const bool bMustJump = bJustTeleported || bZeroDelta || (OldBase == NULL || (!OldBase->IsQueryCollisionEnabled() && MovementBaseUtility::IsDynamicBase(OldBase))); | |
if ((bMustJump || !bCheckedFall) && CheckFall(OldFloor, CurrentFloor.HitResult, Delta, OldLocation, remainingTime, timeTick, Iterations, bMustJump)) | |
{ | |
return; | |
} | |
bCheckedFall = true; | |
} | |
} | |
// Allow overlap events and such to change physics state and velocity | |
if (IsMovingOnGround()) | |
{ | |
// Make velocity reflect actual move | |
if (!bJustTeleported && !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && timeTick >= MIN_TICK_TIME) | |
{ | |
// TODO-RootMotionSource: Allow this to happen during partial override Velocity, but only set allowed axes? | |
Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / timeTick; | |
} | |
} | |
// If we didn't move at all this iteration then abort (since future iterations will also be stuck). | |
if (UpdatedComponent->GetComponentLocation() == OldLocation) | |
{ | |
remainingTime = 0.f; | |
break; | |
} | |
} | |
if (IsMovingOnGround()) | |
{ | |
MaintainHorizontalGroundVelocity(); | |
} | |
} | |
void UMMOPlayerMovement::PhysNavWalking(float deltaTime, int32 Iterations) | |
{ | |
if (deltaTime < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
if ((!CharacterOwner || !CharacterOwner->Controller) && !bRunPhysicsWithNoController && !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) | |
{ | |
Acceleration = FVector::ZeroVector; | |
Velocity = FVector::ZeroVector; | |
return; | |
} | |
RestorePreAdditiveRootMotionVelocity(); | |
// Ensure velocity is horizontal. | |
MaintainHorizontalGroundVelocity(); | |
devMMOCode(ensureMsgf(!Velocity.ContainsNaN(), TEXT("PhysNavWalking: Velocity contains NaN before CalcVelocity (%s)\n%s"), *GetPathNameSafe(this), *Velocity.ToString())); | |
//bound acceleration | |
Acceleration.Z = 0.f; | |
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) | |
{ | |
CalcVelocity(deltaTime, GroundFriction, false, GetMaxBrakingDeceleration()); | |
devMMOCode(ensureMsgf(!Velocity.ContainsNaN(), TEXT("PhysNavWalking: Velocity contains NaN after CalcVelocity (%s)\n%s"), *GetPathNameSafe(this), *Velocity.ToString())); | |
} | |
ApplyRootMotionToVelocity(deltaTime); | |
if (IsFalling()) | |
{ | |
// Root motion could have put us into Falling | |
StartNewPhysics(deltaTime, Iterations); | |
return; | |
} | |
Iterations++; | |
FVector DesiredMove = Velocity; | |
DesiredMove.Z = 0.f; | |
const FVector OldLocation = GetActorFeetLocation(); | |
const FVector DeltaMove = DesiredMove * deltaTime; | |
const bool bDeltaMoveNearlyZero = DeltaMove.IsNearlyZero(); | |
FVector AdjustedDest = OldLocation + DeltaMove; | |
FNavLocation DestNavLocation; | |
bool bSameNavLocation = false; | |
if (CachedNavLocation.NodeRef != INVALID_NAVNODEREF) | |
{ | |
if (bProjectNavMeshWalking) | |
{ | |
const float DistSq2D = (OldLocation - CachedNavLocation.Location).SizeSquared2D(); | |
const float DistZ = FMath::Abs(OldLocation.Z - CachedNavLocation.Location.Z); | |
const float TotalCapsuleHeight = CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() * 2.0f; | |
const float ProjectionScale = (OldLocation.Z > CachedNavLocation.Location.Z) ? NavMeshProjectionHeightScaleUp : NavMeshProjectionHeightScaleDown; | |
const float DistZThr = TotalCapsuleHeight * FMath::Max(0.f, ProjectionScale); | |
bSameNavLocation = (DistSq2D <= KINDA_SMALL_NUMBER) && (DistZ < DistZThr); | |
} | |
else | |
{ | |
bSameNavLocation = CachedNavLocation.Location.Equals(OldLocation); | |
} | |
if (bDeltaMoveNearlyZero && bSameNavLocation) | |
{ | |
if (const INavigationDataInterface* NavData = GetNavData()) | |
{ | |
if (!NavData->IsNodeRefValid(CachedNavLocation.NodeRef)) | |
{ | |
CachedNavLocation.NodeRef = INVALID_NAVNODEREF; | |
bSameNavLocation = false; | |
} | |
} | |
} | |
} | |
if (bDeltaMoveNearlyZero && bSameNavLocation) | |
{ | |
DestNavLocation = CachedNavLocation; | |
UE_LOG(LogMMONavMeshMovement, VeryVerbose, TEXT("%s using cached navmesh location! (bProjectNavMeshWalking = %d)"), *GetNameSafe(CharacterOwner), bProjectNavMeshWalking); | |
} | |
else | |
{ | |
// Start the trace from the Z location of the last valid trace. | |
// Otherwise if we are projecting our location to the underlying geometry and it's far above or below the navmesh, | |
// we'll follow that geometry's plane out of range of valid navigation. | |
if (bSameNavLocation && bProjectNavMeshWalking) | |
{ | |
AdjustedDest.Z = CachedNavLocation.Location.Z; | |
} | |
// Find the point on the NavMesh | |
const bool bHasNavigationData = FindNavFloor(AdjustedDest, DestNavLocation); | |
if (!bHasNavigationData) | |
{ | |
SetMovementMode(MOVE_Walking); | |
return; | |
} | |
CachedNavLocation = DestNavLocation; | |
} | |
if (DestNavLocation.NodeRef != INVALID_NAVNODEREF) | |
{ | |
FVector NewLocation(AdjustedDest.X, AdjustedDest.Y, DestNavLocation.Location.Z); | |
if (bProjectNavMeshWalking) | |
{ | |
const float TotalCapsuleHeight = CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() * 2.0f; | |
const float UpOffset = TotalCapsuleHeight * FMath::Max(0.f, NavMeshProjectionHeightScaleUp); | |
const float DownOffset = TotalCapsuleHeight * FMath::Max(0.f, NavMeshProjectionHeightScaleDown); | |
NewLocation = ProjectLocationFromNavMesh(deltaTime, OldLocation, NewLocation, UpOffset, DownOffset); | |
} | |
FVector AdjustedDelta = NewLocation - OldLocation; | |
if (!AdjustedDelta.IsNearlyZero()) | |
{ | |
FHitResult HitResult; | |
SafeMoveUpdatedComponent(AdjustedDelta, UpdatedComponent->GetComponentQuat(), bSweepWhileNavWalking, HitResult); | |
} | |
// Update velocity to reflect actual move | |
if (!bJustTeleported && !HasAnimRootMotion() && !CurrentRootMotion.HasVelocity()) | |
{ | |
Velocity = (GetActorFeetLocation() - OldLocation) / deltaTime; | |
MaintainHorizontalGroundVelocity(); | |
} | |
bJustTeleported = false; | |
} | |
else | |
{ | |
StartFalling(Iterations, deltaTime, deltaTime, DeltaMove, OldLocation); | |
} | |
} | |
bool UMMOPlayerMovement::FindNavFloor(const FVector& TestLocation, FNavLocation& NavFloorLocation) const | |
{ | |
const INavigationDataInterface* NavData = GetNavData(); | |
if (NavData == nullptr || CharacterOwner == nullptr) | |
{ | |
return false; | |
} | |
const FNavAgentProperties& AgentProps = CharacterOwner->GetNavAgentPropertiesRef(); | |
const float SearchRadius = AgentProps.AgentRadius * 2.0f; | |
const float SearchHeight = AgentProps.AgentHeight * AgentProps.NavWalkingSearchHeightScale; | |
return NavData->ProjectPoint(TestLocation, NavFloorLocation, FVector(SearchRadius, SearchRadius, SearchHeight)); | |
} | |
FVector UMMOPlayerMovement::ProjectLocationFromNavMesh(float DeltaSeconds, const FVector& CurrentFeetLocation, const FVector& TargetNavLocation, float UpOffset, float DownOffset) | |
{ | |
FVector NewLocation = TargetNavLocation; | |
const float ZOffset = -(DownOffset + UpOffset); | |
if (ZOffset > -SMALL_NUMBER) | |
{ | |
return NewLocation; | |
} | |
const FVector TraceStart = FVector(TargetNavLocation.X, TargetNavLocation.Y, TargetNavLocation.Z + UpOffset); | |
const FVector TraceEnd = FVector(TargetNavLocation.X, TargetNavLocation.Y, TargetNavLocation.Z - DownOffset); | |
// We can skip this trace if we are checking at the same location as the last trace (ie, we haven't moved). | |
const bool bCachedLocationStillValid = (CachedProjectedNavMeshHitResult.bBlockingHit && | |
CachedProjectedNavMeshHitResult.TraceStart == TraceStart && | |
CachedProjectedNavMeshHitResult.TraceEnd == TraceEnd); | |
NavMeshProjectionTimer -= DeltaSeconds; | |
if (NavMeshProjectionTimer <= 0.0f) | |
{ | |
if (!bCachedLocationStillValid || bAlwaysCheckFloor) | |
{ | |
UE_LOG(LogMMONavMeshMovement, VeryVerbose, TEXT("ProjectLocationFromNavMesh(): %s interval: %.3f velocity: %s"), *GetNameSafe(CharacterOwner), NavMeshProjectionInterval, *Velocity.ToString()); | |
FHitResult HitResult; | |
FindBestNavMeshLocation(TraceStart, TraceEnd, CurrentFeetLocation, TargetNavLocation, HitResult); | |
// discard result if we were already inside something | |
if (HitResult.bStartPenetrating || !HitResult.bBlockingHit) | |
{ | |
CachedProjectedNavMeshHitResult.Reset(); | |
} | |
else | |
{ | |
CachedProjectedNavMeshHitResult = HitResult; | |
} | |
} | |
else | |
{ | |
UE_LOG(LogMMONavMeshMovement, VeryVerbose, TEXT("ProjectLocationFromNavMesh(): %s interval: %.3f velocity: %s [SKIP TRACE]"), *GetNameSafe(CharacterOwner), NavMeshProjectionInterval, *Velocity.ToString()); | |
} | |
// Wrap around to maintain same relative offset to tick time changes. | |
// Prevents large framerate spikes from aligning multiple characters to the same frame (if they start staggered, they will now remain staggered). | |
float ModTime = 0.f; | |
if (NavMeshProjectionInterval > SMALL_NUMBER) | |
{ | |
ModTime = FMath::Fmod(-NavMeshProjectionTimer, NavMeshProjectionInterval); | |
} | |
NavMeshProjectionTimer = NavMeshProjectionInterval - ModTime; | |
} | |
// Project to last plane we found. | |
if (CachedProjectedNavMeshHitResult.bBlockingHit) | |
{ | |
if (bCachedLocationStillValid && FMath::IsNearlyEqual(CurrentFeetLocation.Z, CachedProjectedNavMeshHitResult.ImpactPoint.Z, 0.01f)) | |
{ | |
// Already at destination. | |
NewLocation.Z = CurrentFeetLocation.Z; | |
} | |
else | |
{ | |
//const FVector ProjectedPoint = FMath::LinePlaneIntersection(TraceStart, TraceEnd, CachedProjectedNavMeshHitResult.ImpactPoint, CachedProjectedNavMeshHitResult.Normal); | |
//float ProjectedZ = ProjectedPoint.Z; | |
// Optimized assuming we only care about Z coordinate of result. | |
const FVector& PlaneOrigin = CachedProjectedNavMeshHitResult.ImpactPoint; | |
const FVector& PlaneNormal = CachedProjectedNavMeshHitResult.Normal; | |
float ProjectedZ = TraceStart.Z + ZOffset * (((PlaneOrigin - TraceStart) | PlaneNormal) / (ZOffset * PlaneNormal.Z)); | |
// Limit to not be too far above or below NavMesh location | |
ProjectedZ = FMath::Clamp(ProjectedZ, TraceEnd.Z, TraceStart.Z); | |
// Interp for smoother updates (less "pop" when trace hits something new). 0 interp speed is instant. | |
const float InterpSpeed = FMath::Max(0.f, NavMeshProjectionInterpSpeed); | |
ProjectedZ = FMath::FInterpTo(CurrentFeetLocation.Z, ProjectedZ, DeltaSeconds, InterpSpeed); | |
ProjectedZ = FMath::Clamp(ProjectedZ, TraceEnd.Z, TraceStart.Z); | |
// Final result | |
NewLocation.Z = ProjectedZ; | |
} | |
} | |
return NewLocation; | |
} | |
void UMMOPlayerMovement::FindBestNavMeshLocation(const FVector& TraceStart, const FVector& TraceEnd, const FVector& CurrentFeetLocation, const FVector& TargetNavLocation, FHitResult& OutHitResult) const | |
{ | |
// raycast to underlying mesh to allow us to more closely follow geometry | |
// we use static objects here as a best approximation to accept only objects that | |
// influence navmesh generation | |
FCollisionQueryParams Params(SCENE_QUERY_STAT(ProjectLocation), false); | |
// blocked by world static and optionally world dynamic | |
FCollisionResponseParams ResponseParams(ECR_Ignore); | |
ResponseParams.CollisionResponse.SetResponse(ECC_WorldStatic, ECR_Overlap); | |
ResponseParams.CollisionResponse.SetResponse(ECC_WorldDynamic, bProjectNavMeshOnBothWorldChannels ? ECR_Overlap : ECR_Ignore); | |
TArray<FHitResult> MultiTraceHits; | |
GetWorld()->LineTraceMultiByChannel(MultiTraceHits, TraceStart, TraceEnd, ECC_WorldStatic, Params, ResponseParams); | |
struct FCompareFHitResultNavMeshTrace | |
{ | |
explicit FCompareFHitResultNavMeshTrace(const FVector& inSourceLocation) : SourceLocation(inSourceLocation) | |
{ | |
} | |
FORCEINLINE bool operator()(const FHitResult& A, const FHitResult& B) const | |
{ | |
const float ADistSqr = (SourceLocation - A.ImpactPoint).SizeSquared(); | |
const float BDistSqr = (SourceLocation - B.ImpactPoint).SizeSquared(); | |
return (ADistSqr < BDistSqr); | |
} | |
const FVector& SourceLocation; | |
}; | |
struct FRemoveNotBlockingResponseNavMeshTrace | |
{ | |
FRemoveNotBlockingResponseNavMeshTrace(bool bInCheckOnlyWorldStatic) : bCheckOnlyWorldStatic(bInCheckOnlyWorldStatic) {} | |
FORCEINLINE bool operator()(const FHitResult& TestHit) const | |
{ | |
UPrimitiveComponent* PrimComp = TestHit.GetComponent(); | |
const bool bBlockOnWorldStatic = PrimComp && (PrimComp->GetCollisionResponseToChannel(ECC_WorldStatic) == ECR_Block); | |
const bool bBlockOnWorldDynamic = PrimComp && (PrimComp->GetCollisionResponseToChannel(ECC_WorldDynamic) == ECR_Block); | |
return !bBlockOnWorldStatic && (!bBlockOnWorldDynamic || bCheckOnlyWorldStatic); | |
} | |
bool bCheckOnlyWorldStatic; | |
}; | |
MultiTraceHits.RemoveAllSwap(FRemoveNotBlockingResponseNavMeshTrace(!bProjectNavMeshOnBothWorldChannels), /*bAllowShrinking*/false); | |
if (MultiTraceHits.Num() > 0) | |
{ | |
// Sort the hits by the closest to our origin. | |
MultiTraceHits.Sort(FCompareFHitResultNavMeshTrace(TargetNavLocation)); | |
// Cache the closest hit and treat it as a blocking hit (we used an overlap to get all the world static hits so we could sort them ourselves) | |
OutHitResult = MultiTraceHits[0]; | |
OutHitResult.bBlockingHit = true; | |
} | |
} | |
const INavigationDataInterface* UMMOPlayerMovement::GetNavData() const | |
{ | |
const UWorld* World = GetWorld(); | |
if (World == nullptr || World->GetNavigationSystem() == nullptr | |
|| !HasValidData() | |
|| CharacterOwner == nullptr) | |
{ | |
return nullptr; | |
} | |
const INavigationDataInterface* NavData = FNavigationSystem::GetNavDataForActor(*CharacterOwner); | |
return NavData; | |
} | |
void UMMOPlayerMovement::PhysCustom(float deltaTime, int32 Iterations) | |
{ | |
if (CharacterOwner) | |
{ | |
CharacterOwner->K2_UpdateCustomMovement(deltaTime); | |
} | |
} | |
bool UMMOPlayerMovement::ShouldCatchAir(const FMMOFindFloorResult& OldFloor, const FMMOFindFloorResult& NewFloor) | |
{ | |
return false; | |
} | |
void UMMOPlayerMovement::HandleWalkingOffLedge(const FVector& PreviousFloorImpactNormal, const FVector& PreviousFloorContactNormal, const FVector& PreviousLocation, float TimeDelta) | |
{ | |
if (CharacterOwner) | |
{ | |
CharacterOwner->OnWalkingOffLedge(PreviousFloorImpactNormal, PreviousFloorContactNormal, PreviousLocation, TimeDelta); | |
} | |
} | |
void UMMOPlayerMovement::AdjustFloorHeight() | |
{ | |
// If we have a floor check that hasn't hit anything, don't adjust height. | |
if (!CurrentFloor.IsWalkableFloor()) | |
{ | |
return; | |
} | |
float OldFloorDist = CurrentFloor.FloorDist; | |
if (CurrentFloor.bLineTrace) | |
{ | |
if (OldFloorDist < MIN_FLOOR_DIST && CurrentFloor.LineDist >= MIN_FLOOR_DIST) | |
{ | |
// This would cause us to scale unwalkable walls | |
UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("Adjust floor height aborting due to line trace with small floor distance (line: %.2f, sweep: %.2f)"), CurrentFloor.LineDist, CurrentFloor.FloorDist); | |
return; | |
} | |
else | |
{ | |
// Falling back to a line trace means the sweep was unwalkable (or in penetration). Use the line distance for the vertical adjustment. | |
OldFloorDist = CurrentFloor.LineDist; | |
} | |
} | |
// Move up or down to maintain floor height. | |
if (OldFloorDist < MIN_FLOOR_DIST || OldFloorDist > MAX_FLOOR_DIST) | |
{ | |
FHitResult AdjustHit(1.f); | |
const float InitialZ = UpdatedComponent->GetComponentLocation().Z; | |
const float AvgFloorDist = (MIN_FLOOR_DIST + MAX_FLOOR_DIST) * 0.5f; | |
const float MoveDist = AvgFloorDist - OldFloorDist; | |
SafeMoveUpdatedComponent(FVector(0.f, 0.f, MoveDist), UpdatedComponent->GetComponentQuat(), true, AdjustHit); | |
UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("Adjust floor height %.3f (Hit = %d)"), MoveDist, AdjustHit.bBlockingHit); | |
if (!AdjustHit.IsValidBlockingHit()) | |
{ | |
CurrentFloor.FloorDist += MoveDist; | |
} | |
else if (MoveDist > 0.f) | |
{ | |
const float CurrentZ = UpdatedComponent->GetComponentLocation().Z; | |
CurrentFloor.FloorDist += CurrentZ - InitialZ; | |
} | |
else | |
{ | |
checkSlow(MoveDist < 0.f); | |
const float CurrentZ = UpdatedComponent->GetComponentLocation().Z; | |
CurrentFloor.FloorDist = CurrentZ - AdjustHit.Location.Z; | |
if (IsWalkable(AdjustHit)) | |
{ | |
CurrentFloor.SetFromSweep(AdjustHit, CurrentFloor.FloorDist, true); | |
} | |
} | |
// Don't recalculate velocity based on this height adjustment, if considering vertical adjustments. | |
// Also avoid it if we moved out of penetration | |
bJustTeleported |= !bMaintainHorizontalGroundVelocity || (OldFloorDist < 0.f); | |
// If something caused us to adjust our height (especially a depentration) we should ensure another check next frame or we will keep a stale result. | |
bForceNextFloorCheck = true; | |
} | |
} | |
void UMMOPlayerMovement::StopActiveMovement() | |
{ | |
Super::StopActiveMovement(); | |
Acceleration = FVector::ZeroVector; | |
bHasRequestedVelocity = false; | |
RequestedVelocity = FVector::ZeroVector; | |
} | |
void UMMOPlayerMovement::ProcessLanded(const FHitResult& Hit, float remainingTime, int32 Iterations) | |
{ | |
if (CharacterOwner && CharacterOwner->ShouldNotifyLanded(Hit)) | |
{ | |
CharacterOwner->Landed(Hit); | |
} | |
if (IsFalling()) | |
{ | |
if (GroundMovementMode == MOVE_NavWalking) | |
{ | |
// verify navmesh projection and current floor | |
// otherwise movement will be stuck in infinite loop: | |
// navwalking -> (no navmesh) -> falling -> (standing on something) -> navwalking -> .... | |
const FVector TestLocation = GetActorFeetLocation(); | |
FNavLocation NavLocation; | |
const bool bHasNavigationData = FindNavFloor(TestLocation, NavLocation); | |
if (!bHasNavigationData || NavLocation.NodeRef == INVALID_NAVNODEREF) | |
{ | |
GroundMovementMode = MOVE_Walking; | |
UE_LOG(LogMMONavMeshMovement, Verbose, TEXT("ProcessLanded(): %s tried to go to NavWalking but couldn't find NavMesh! Using Walking instead."), *GetNameSafe(CharacterOwner)); | |
} | |
} | |
SetPostLandedPhysics(Hit); | |
} | |
IPathFollowingAgentInterface* PFAgent = GetPathFollowingAgent(); | |
if (PFAgent) | |
{ | |
PFAgent->OnLanded(); | |
} | |
StartNewPhysics(remainingTime, Iterations); | |
} | |
void UMMOPlayerMovement::SetPostLandedPhysics(const FHitResult& Hit) | |
{ | |
if (CharacterOwner) | |
{ | |
if (CanEverSwim() && IsInWater()) | |
{ | |
SetMovementMode(MOVE_Swimming); | |
} | |
else | |
{ | |
const FVector PreImpactAccel = Acceleration + (IsFalling() ? FVector(0.f, 0.f, GetGravityZ()) : FVector::ZeroVector); | |
const FVector PreImpactVelocity = Velocity; | |
if (DefaultLandMovementMode == MOVE_Walking || | |
DefaultLandMovementMode == MOVE_NavWalking || | |
DefaultLandMovementMode == MOVE_Falling) | |
{ | |
SetMovementMode(GroundMovementMode); | |
} | |
else | |
{ | |
SetDefaultMovementMode(); | |
} | |
ApplyImpactPhysicsForces(Hit, PreImpactAccel, PreImpactVelocity); | |
} | |
} | |
} | |
void UMMOPlayerMovement::SetNavWalkingPhysics(bool bEnable) | |
{ | |
if (UpdatedPrimitive) | |
{ | |
if (bEnable) | |
{ | |
UpdatedPrimitive->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Ignore); | |
UpdatedPrimitive->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Ignore); | |
CachedProjectedNavMeshHitResult.Reset(); | |
// Stagger timed updates so many different characters spawned at the same time don't update on the same frame. | |
// Initially we want an immediate update though, so set time to a negative randomized range. | |
NavMeshProjectionTimer = (NavMeshProjectionInterval > 0.f) ? FMath::FRandRange(-NavMeshProjectionInterval, 0.f) : 0.f; | |
} | |
else | |
{ | |
UPrimitiveComponent* DefaultCapsule = nullptr; | |
if (CharacterOwner && CharacterOwner->GetCapsuleComponent() == UpdatedComponent) | |
{ | |
AMMOCharacter* DefaultCharacter = CharacterOwner->GetClass()->GetDefaultObject<AMMOCharacter>(); | |
DefaultCapsule = DefaultCharacter ? DefaultCharacter->GetCapsuleComponent() : nullptr; | |
} | |
if (DefaultCapsule) | |
{ | |
UpdatedPrimitive->SetCollisionResponseToChannel(ECC_WorldStatic, DefaultCapsule->GetCollisionResponseToChannel(ECC_WorldStatic)); | |
UpdatedPrimitive->SetCollisionResponseToChannel(ECC_WorldDynamic, DefaultCapsule->GetCollisionResponseToChannel(ECC_WorldDynamic)); | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("Can't revert NavWalking collision settings for %s.%s"), | |
*GetNameSafe(CharacterOwner), *GetNameSafe(UpdatedComponent)); | |
} | |
} | |
} | |
} | |
bool UMMOPlayerMovement::TryToLeaveNavWalking() | |
{ | |
SetNavWalkingPhysics(false); | |
bool bSucceeded = true; | |
if (CharacterOwner) | |
{ | |
FVector CollisionFreeLocation = UpdatedComponent->GetComponentLocation(); | |
bSucceeded = GetWorld()->FindTeleportSpot(CharacterOwner, CollisionFreeLocation, UpdatedComponent->GetComponentRotation()); | |
if (bSucceeded) | |
{ | |
CharacterOwner->SetActorLocation(CollisionFreeLocation); | |
} | |
else | |
{ | |
SetNavWalkingPhysics(true); | |
} | |
} | |
if (MovementMode == MOVE_NavWalking && bSucceeded) | |
{ | |
SetMovementMode(DefaultLandMovementMode != MOVE_NavWalking ? DefaultLandMovementMode.GetValue() : MOVE_Walking); | |
} | |
else if (MovementMode != MOVE_NavWalking && !bSucceeded) | |
{ | |
SetMovementMode(MOVE_NavWalking); | |
} | |
bWantsToLeaveNavWalking = !bSucceeded; | |
return bSucceeded; | |
} | |
void UMMOPlayerMovement::OnTeleported() | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
Super::OnTeleported(); | |
bJustTeleported = true; | |
// Find floor at current location | |
UpdateFloorFromAdjustment(); | |
// Validate it. We don't want to pop down to walking mode from very high off the ground, but we'd like to keep walking if possible. | |
UPrimitiveComponent* OldBase = CharacterOwner->GetMovementBase(); | |
UPrimitiveComponent* NewBase = NULL; | |
if (OldBase && CurrentFloor.IsWalkableFloor() && CurrentFloor.FloorDist <= MAX_FLOOR_DIST && Velocity.Z <= 0.f) | |
{ | |
// Close enough to land or just keep walking. | |
NewBase = CurrentFloor.HitResult.Component.Get(); | |
} | |
else | |
{ | |
CurrentFloor.Clear(); | |
} | |
const bool bWasFalling = (MovementMode == MOVE_Falling); | |
const bool bWasSwimming = (MovementMode == DefaultWaterMovementMode) || (MovementMode == MOVE_Swimming); | |
if (CanEverSwim() && IsInWater()) | |
{ | |
if (!bWasSwimming) | |
{ | |
SetMovementMode(DefaultWaterMovementMode); | |
} | |
} | |
else if (!CurrentFloor.IsWalkableFloor() || (OldBase && !NewBase)) | |
{ | |
if (!bWasFalling && MovementMode != MOVE_Flying && MovementMode != MOVE_Custom) | |
{ | |
SetMovementMode(MOVE_Falling); | |
} | |
} | |
else if (NewBase) | |
{ | |
if (bWasSwimming) | |
{ | |
SetMovementMode(DefaultLandMovementMode); | |
} | |
else if (bWasFalling) | |
{ | |
ProcessLanded(CurrentFloor.HitResult, 0.f, 0); | |
} | |
} | |
SaveBaseLocation(); | |
} | |
float Local_GetAxisDeltaRotation(float InAxisRotationRate, float DeltaTime) | |
{ | |
// Values over 360 don't do anything, see FMath::FixedTurn. However we are trying to avoid giant floats from overflowing other calculations. | |
return (InAxisRotationRate >= 0.f) ? FMath::Min(InAxisRotationRate * DeltaTime, 360.f) : 360.f; | |
} | |
FRotator UMMOPlayerMovement::GetDeltaRotation(float DeltaTime) const | |
{ | |
return FRotator(Local_GetAxisDeltaRotation(RotationRate.Pitch, DeltaTime), Local_GetAxisDeltaRotation(RotationRate.Yaw, DeltaTime), Local_GetAxisDeltaRotation(RotationRate.Roll, DeltaTime)); | |
} | |
FRotator UMMOPlayerMovement::ComputeOrientToMovementRotation(const FRotator& CurrentRotation, float DeltaTime, FRotator& DeltaRotation) const | |
{ | |
if (Acceleration.SizeSquared() < KINDA_SMALL_NUMBER) | |
{ | |
// AI path following request can orient us in that direction (it's effectively an acceleration) | |
if (bHasRequestedVelocity && RequestedVelocity.SizeSquared() > KINDA_SMALL_NUMBER) | |
{ | |
return RequestedVelocity.GetSafeNormal().Rotation(); | |
} | |
// Don't change rotation if there is no acceleration. | |
return CurrentRotation; | |
} | |
// Rotate toward direction of acceleration. | |
return Acceleration.GetSafeNormal().Rotation(); | |
} | |
bool UMMOPlayerMovement::ShouldRemainVertical() const | |
{ | |
// Always remain vertical when walking or falling. | |
return IsMovingOnGround() || IsFalling(); | |
} | |
void UMMOPlayerMovement::PhysicsRotation(float DeltaTime) | |
{ | |
if (!(bOrientRotationToMovement || bUseControllerDesiredRotation)) | |
{ | |
return; | |
} | |
if (!HasValidData() || (!CharacterOwner->Controller && !bRunPhysicsWithNoController)) | |
{ | |
return; | |
} | |
FRotator CurrentRotation = UpdatedComponent->GetComponentRotation(); // Normalized | |
CurrentRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): CurrentRotation")); | |
FRotator DeltaRot = GetDeltaRotation(DeltaTime); | |
DeltaRot.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): GetDeltaRotation")); | |
FRotator DesiredRotation = CurrentRotation; | |
if (bOrientRotationToMovement) | |
{ | |
DesiredRotation = ComputeOrientToMovementRotation(CurrentRotation, DeltaTime, DeltaRot); | |
} | |
else if (CharacterOwner->Controller && bUseControllerDesiredRotation) | |
{ | |
DesiredRotation = CharacterOwner->Controller->GetDesiredRotation(); | |
} | |
else | |
{ | |
return; | |
} | |
if (ShouldRemainVertical()) | |
{ | |
DesiredRotation.Pitch = 0.f; | |
DesiredRotation.Yaw = FRotator::NormalizeAxis(DesiredRotation.Yaw); | |
DesiredRotation.Roll = 0.f; | |
} | |
else | |
{ | |
DesiredRotation.Normalize(); | |
} | |
// Accumulate a desired new rotation. | |
const float AngleTolerance = 1e-3f; | |
if (!CurrentRotation.Equals(DesiredRotation, AngleTolerance)) | |
{ | |
// PITCH | |
if (!FMath::IsNearlyEqual(CurrentRotation.Pitch, DesiredRotation.Pitch, AngleTolerance)) | |
{ | |
DesiredRotation.Pitch = FMath::FixedTurn(CurrentRotation.Pitch, DesiredRotation.Pitch, DeltaRot.Pitch); | |
} | |
// YAW | |
if (!FMath::IsNearlyEqual(CurrentRotation.Yaw, DesiredRotation.Yaw, AngleTolerance)) | |
{ | |
DesiredRotation.Yaw = FMath::FixedTurn(CurrentRotation.Yaw, DesiredRotation.Yaw, DeltaRot.Yaw); | |
} | |
// ROLL | |
if (!FMath::IsNearlyEqual(CurrentRotation.Roll, DesiredRotation.Roll, AngleTolerance)) | |
{ | |
DesiredRotation.Roll = FMath::FixedTurn(CurrentRotation.Roll, DesiredRotation.Roll, DeltaRot.Roll); | |
} | |
// Set the new rotation. | |
DesiredRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): DesiredRotation")); | |
MoveUpdatedComponent(FVector::ZeroVector, DesiredRotation, /*bSweep*/ false); | |
} | |
} | |
void UMMOPlayerMovement::PhysicsVolumeChanged(APhysicsVolume* NewVolume) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
if (NewVolume && NewVolume->bWaterVolume) | |
{ | |
// just entered water | |
if (!CanEverSwim()) | |
{ | |
// AI needs to stop any current moves | |
IPathFollowingAgentInterface* PFAgent = GetPathFollowingAgent(); | |
if (PFAgent) | |
{ | |
//PathFollowingComp->AbortMove(*this, FPathFollowingResultFlags::MovementStop); | |
PFAgent->OnUnableToMove(*this); | |
} | |
} | |
else if (!IsSwimming()) | |
{ | |
SetMovementMode(MOVE_Swimming); | |
} | |
} | |
else if (IsSwimming()) | |
{ | |
// just left the water - check if should jump out | |
SetMovementMode(MOVE_Falling); | |
FVector JumpDir(0.f); | |
FVector WallNormal(0.f); | |
if (Acceleration.Z > 0.f && ShouldJumpOutOfWater(JumpDir) | |
&& ((JumpDir | Acceleration) > 0.f) && CheckWaterJump(JumpDir, WallNormal)) | |
{ | |
JumpOutOfWater(WallNormal); | |
Velocity.Z = OutofWaterZ; //set here so physics uses this for remainder of tick | |
} | |
} | |
} | |
bool UMMOPlayerMovement::ShouldJumpOutOfWater(FVector& JumpDir) | |
{ | |
AController* OwnerController = CharacterOwner->GetController(); | |
if (OwnerController) | |
{ | |
const FRotator ControllerRot = OwnerController->GetControlRotation(); | |
if ((Velocity.Z > 0.0f) && (ControllerRot.Pitch > JumpOutOfWaterPitch)) | |
{ | |
// if Pawn is going up and looking up, then make him jump | |
JumpDir = ControllerRot.Vector(); | |
return true; | |
} | |
} | |
return false; | |
} | |
void UMMOPlayerMovement::JumpOutOfWater(FVector WallNormal) {} | |
bool UMMOPlayerMovement::CheckWaterJump(FVector CheckPoint, FVector& WallNormal) | |
{ | |
if (!HasValidData()) | |
{ | |
return false; | |
} | |
// check if there is a wall directly in front of the swimming pawn | |
CheckPoint.Z = 0.f; | |
FVector CheckNorm = CheckPoint.GetSafeNormal(); | |
float PawnCapsuleRadius, PawnCapsuleHalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnCapsuleRadius, PawnCapsuleHalfHeight); | |
CheckPoint = UpdatedComponent->GetComponentLocation() + 1.2f * PawnCapsuleRadius * CheckNorm; | |
FVector Extent(PawnCapsuleRadius, PawnCapsuleRadius, PawnCapsuleHalfHeight); | |
FHitResult HitInfo(1.f); | |
FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CheckWaterJump), false, CharacterOwner); | |
FCollisionResponseParams ResponseParam; | |
InitCollisionParams(CapsuleParams, ResponseParam); | |
FCollisionShape CapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_None); | |
const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); | |
bool bHit = GetWorld()->SweepSingleByChannel(HitInfo, UpdatedComponent->GetComponentLocation(), CheckPoint, FQuat::Identity, CollisionChannel, CapsuleShape, CapsuleParams, ResponseParam); | |
if (bHit && !Cast<APawn>(HitInfo.GetActor())) | |
{ | |
// hit a wall - check if it is low enough | |
WallNormal = -1.f * HitInfo.ImpactNormal; | |
FVector Start = UpdatedComponent->GetComponentLocation(); | |
Start.Z += MaxOutOfWaterStepHeight; | |
CheckPoint = Start + 3.2f * PawnCapsuleRadius * WallNormal; | |
FCollisionQueryParams LineParams(SCENE_QUERY_STAT(CheckWaterJump), true, CharacterOwner); | |
FCollisionResponseParams LineResponseParam; | |
InitCollisionParams(LineParams, LineResponseParam); | |
bHit = GetWorld()->LineTraceSingleByChannel(HitInfo, Start, CheckPoint, CollisionChannel, LineParams, LineResponseParam); | |
// if no high obstruction, or it's a valid floor, then pawn can jump out of water | |
return !bHit || IsWalkable(HitInfo); | |
} | |
return false; | |
} | |
void UMMOPlayerMovement::AddImpulse(FVector Impulse, bool bVelocityChange) | |
{ | |
if (!Impulse.IsZero() && (MovementMode != MOVE_None) && IsActive() && HasValidData()) | |
{ | |
// handle scaling by mass | |
FVector FinalImpulse = Impulse; | |
if (!bVelocityChange) | |
{ | |
if (Mass > SMALL_NUMBER) | |
{ | |
FinalImpulse = FinalImpulse / Mass; | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("Attempt to apply impulse to zero or negative Mass in CharacterMovement")); | |
} | |
} | |
PendingImpulseToApply += FinalImpulse; | |
} | |
} | |
void UMMOPlayerMovement::AddForce(FVector Force) | |
{ | |
if (!Force.IsZero() && (MovementMode != MOVE_None) && IsActive() && HasValidData()) | |
{ | |
if (Mass > SMALL_NUMBER) | |
{ | |
PendingForceToApply += Force / Mass; | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("Attempt to apply force to zero or negative Mass in CharacterMovement")); | |
} | |
} | |
} | |
void UMMOPlayerMovement::MoveSmooth(const FVector& InVelocity, const float DeltaSeconds, FStepDownResult* OutStepDownResult) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// Custom movement mode. | |
// Custom movement may need an update even if there is zero velocity. | |
if (MovementMode == MOVE_Custom) | |
{ | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates); | |
PhysCustom(DeltaSeconds, 0); | |
return; | |
} | |
FVector Delta = InVelocity * DeltaSeconds; | |
if (Delta.IsZero()) | |
{ | |
return; | |
} | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates); | |
if (IsMovingOnGround()) | |
{ | |
MoveAlongFloor(InVelocity, DeltaSeconds, OutStepDownResult); | |
} | |
else | |
{ | |
FHitResult Hit(1.f); | |
SafeMoveUpdatedComponent(Delta, UpdatedComponent->GetComponentQuat(), true, Hit); | |
if (Hit.IsValidBlockingHit()) | |
{ | |
bool bSteppedUp = false; | |
if (IsFlying()) | |
{ | |
if (CanStepUp(Hit)) | |
{ | |
OutStepDownResult = NULL; // No need for a floor when not walking. | |
if (FMath::Abs(Hit.ImpactNormal.Z) < 0.2f) | |
{ | |
const FVector GravDir = FVector(0.f, 0.f, -1.f); | |
const FVector DesiredDir = Delta.GetSafeNormal(); | |
const float UpDown = GravDir | DesiredDir; | |
if ((UpDown < 0.5f) && (UpDown > -0.2f)) | |
{ | |
bSteppedUp = StepUp(GravDir, Delta * (1.f - Hit.Time), Hit, OutStepDownResult); | |
} | |
} | |
} | |
} | |
// If StepUp failed, try sliding. | |
if (!bSteppedUp) | |
{ | |
SlideAlongSurface(Delta, 1.f - Hit.Time, Hit.Normal, Hit, false); | |
} | |
} | |
} | |
} | |
void UMMOPlayerMovement::UpdateProxyAcceleration() | |
{ | |
// Not currently replicated for simulated movement, but make it non-zero for animations that may want it, based on velocity. | |
Acceleration = Velocity.GetSafeNormal(); | |
AnalogInputModifier = 1.0f; | |
} | |
bool UMMOPlayerMovement::IsWalkable(const FHitResult& Hit) const | |
{ | |
if (!Hit.IsValidBlockingHit()) | |
{ | |
// No hit, or starting in penetration | |
return false; | |
} | |
// Never walk up vertical surfaces. | |
if (Hit.ImpactNormal.Z < KINDA_SMALL_NUMBER) | |
{ | |
return false; | |
} | |
float TestWalkableZ = WalkableFloorZ; | |
// See if this component overrides the walkable floor z. | |
const UPrimitiveComponent* HitComponent = Hit.Component.Get(); | |
if (HitComponent) | |
{ | |
const FWalkableSlopeOverride& SlopeOverride = HitComponent->GetWalkableSlopeOverride(); | |
TestWalkableZ = SlopeOverride.ModifyWalkableFloorZ(TestWalkableZ); | |
} | |
// Can't walk on this surface if it is too steep. | |
if (Hit.ImpactNormal.Z < TestWalkableZ) | |
{ | |
return false; | |
} | |
return true; | |
} | |
void UMMOPlayerMovement::SetWalkableFloorAngle(float InWalkableFloorAngle) | |
{ | |
WalkableFloorAngle = FMath::Clamp(InWalkableFloorAngle, 0.f, 90.0f); | |
WalkableFloorZ = FMath::Cos(FMath::DegreesToRadians(WalkableFloorAngle)); | |
} | |
void UMMOPlayerMovement::SetWalkableFloorZ(float InWalkableFloorZ) | |
{ | |
WalkableFloorZ = FMath::Clamp(InWalkableFloorZ, 0.f, 1.f); | |
WalkableFloorAngle = FMath::RadiansToDegrees(FMath::Acos(WalkableFloorZ)); | |
} | |
float UMMOPlayerMovement::K2_GetWalkableFloorAngle() const | |
{ | |
return GetWalkableFloorAngle(); | |
} | |
float UMMOPlayerMovement::K2_GetWalkableFloorZ() const | |
{ | |
return GetWalkableFloorZ(); | |
} | |
bool UMMOPlayerMovement::IsWithinEdgeTolerance(const FVector& CapsuleLocation, const FVector& TestImpactPoint, const float CapsuleRadius) const | |
{ | |
const float DistFromCenterSq = (TestImpactPoint - CapsuleLocation).SizeSquared2D(); | |
const float ReducedRadiusSq = FMath::Square(FMath::Max(SWEEP_EDGE_REJECT_DISTANCE + KINDA_SMALL_NUMBER, CapsuleRadius - SWEEP_EDGE_REJECT_DISTANCE)); | |
return DistFromCenterSq < ReducedRadiusSq; | |
} | |
void UMMOPlayerMovement::ComputeFloorDist(const FVector& CapsuleLocation, float LineDistance, float SweepDistance, FMMOFindFloorResult& OutFloorResult, float SweepRadius, const FHitResult* DownwardSweepResult) const | |
{ | |
UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("[Role:%d] ComputeFloorDist: %s at location %s"), (int32)CharacterOwner->GetLocalRole(), *GetNameSafe(CharacterOwner), *CapsuleLocation.ToString()); | |
OutFloorResult.Clear(); | |
float PawnRadius, PawnHalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); | |
bool bSkipSweep = false; | |
if (DownwardSweepResult != NULL && DownwardSweepResult->IsValidBlockingHit()) | |
{ | |
// Only if the supplied sweep was vertical and downward. | |
if ((DownwardSweepResult->TraceStart.Z > DownwardSweepResult->TraceEnd.Z) && | |
(DownwardSweepResult->TraceStart - DownwardSweepResult->TraceEnd).SizeSquared2D() <= KINDA_SMALL_NUMBER) | |
{ | |
// Reject hits that are barely on the cusp of the radius of the capsule | |
if (IsWithinEdgeTolerance(DownwardSweepResult->Location, DownwardSweepResult->ImpactPoint, PawnRadius)) | |
{ | |
// Don't try a redundant sweep, regardless of whether this sweep is usable. | |
bSkipSweep = true; | |
const bool bIsWalkable = IsWalkable(*DownwardSweepResult); | |
const float FloorDist = (CapsuleLocation.Z - DownwardSweepResult->Location.Z); | |
OutFloorResult.SetFromSweep(*DownwardSweepResult, FloorDist, bIsWalkable); | |
if (bIsWalkable) | |
{ | |
// Use the supplied downward sweep as the floor hit result. | |
return; | |
} | |
} | |
} | |
} | |
// We require the sweep distance to be >= the line distance, otherwise the HitResult can't be interpreted as the sweep result. | |
if (SweepDistance < LineDistance) | |
{ | |
ensure(SweepDistance >= LineDistance); | |
return; | |
} | |
bool bBlockingHit = false; | |
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(ComputeFloorDist), false, CharacterOwner); | |
FCollisionResponseParams ResponseParam; | |
InitCollisionParams(QueryParams, ResponseParam); | |
const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); | |
// Sweep test | |
if (!bSkipSweep && SweepDistance > 0.f && SweepRadius > 0.f) | |
{ | |
// Use a shorter height to avoid sweeps giving weird results if we start on a surface. | |
// This also allows us to adjust out of penetrations. | |
const float ShrinkScale = 0.9f; | |
const float ShrinkScaleOverlap = 0.1f; | |
float ShrinkHeight = (PawnHalfHeight - PawnRadius) * (1.f - ShrinkScale); | |
float TraceDist = SweepDistance + ShrinkHeight; | |
FCollisionShape CapsuleShape = FCollisionShape::MakeCapsule(SweepRadius, PawnHalfHeight - ShrinkHeight); | |
FHitResult Hit(1.f); | |
bBlockingHit = FloorSweepTest(Hit, CapsuleLocation, CapsuleLocation + FVector(0.f, 0.f, -TraceDist), CollisionChannel, CapsuleShape, QueryParams, ResponseParam); | |
if (bBlockingHit) | |
{ | |
// Reject hits adjacent to us, we only care about hits on the bottom portion of our capsule. | |
// Check 2D distance to impact point, reject if within a tolerance from radius. | |
if (Hit.bStartPenetrating || !IsWithinEdgeTolerance(CapsuleLocation, Hit.ImpactPoint, CapsuleShape.Capsule.Radius)) | |
{ | |
// Use a capsule with a slightly smaller radius and shorter height to avoid the adjacent object. | |
// Capsule must not be nearly zero or the trace will fall back to a line trace from the start point and have the wrong length. | |
CapsuleShape.Capsule.Radius = FMath::Max(0.f, CapsuleShape.Capsule.Radius - SWEEP_EDGE_REJECT_DISTANCE - KINDA_SMALL_NUMBER); | |
if (!CapsuleShape.IsNearlyZero()) | |
{ | |
ShrinkHeight = (PawnHalfHeight - PawnRadius) * (1.f - ShrinkScaleOverlap); | |
TraceDist = SweepDistance + ShrinkHeight; | |
CapsuleShape.Capsule.HalfHeight = FMath::Max(PawnHalfHeight - ShrinkHeight, CapsuleShape.Capsule.Radius); | |
Hit.Reset(1.f, false); | |
bBlockingHit = FloorSweepTest(Hit, CapsuleLocation, CapsuleLocation + FVector(0.f, 0.f, -TraceDist), CollisionChannel, CapsuleShape, QueryParams, ResponseParam); | |
} | |
} | |
// Reduce hit distance by ShrinkHeight because we shrank the capsule for the trace. | |
// We allow negative distances here, because this allows us to pull out of penetrations. | |
const float MaxPenetrationAdjust = FMath::Max(MAX_FLOOR_DIST, PawnRadius); | |
const float SweepResult = FMath::Max(-MaxPenetrationAdjust, Hit.Time * TraceDist - ShrinkHeight); | |
OutFloorResult.SetFromSweep(Hit, SweepResult, false); | |
if (Hit.IsValidBlockingHit() && IsWalkable(Hit)) | |
{ | |
if (SweepResult <= SweepDistance) | |
{ | |
// Hit within test distance. | |
OutFloorResult.bWalkableFloor = true; | |
return; | |
} | |
} | |
} | |
} | |
// Since we require a longer sweep than line trace, we don't want to run the line trace if the sweep missed everything. | |
// We do however want to try a line trace if the sweep was stuck in penetration. | |
if (!OutFloorResult.bBlockingHit && !OutFloorResult.HitResult.bStartPenetrating) | |
{ | |
OutFloorResult.FloorDist = SweepDistance; | |
return; | |
} | |
// Line trace | |
if (LineDistance > 0.f) | |
{ | |
const float ShrinkHeight = PawnHalfHeight; | |
const FVector LineTraceStart = CapsuleLocation; | |
const float TraceDist = LineDistance + ShrinkHeight; | |
const FVector Down = FVector(0.f, 0.f, -TraceDist); | |
FHitResult Hit(1.f); | |
bBlockingHit = GetWorld()->LineTraceSingleByChannel(Hit, LineTraceStart, LineTraceStart + Down, CollisionChannel, QueryParams, ResponseParam); | |
if (bBlockingHit) | |
{ | |
if (Hit.Time > 0.f) | |
{ | |
// Reduce hit distance by ShrinkHeight because we started the trace higher than the base. | |
// We allow negative distances here, because this allows us to pull out of penetrations. | |
const float MaxPenetrationAdjust = FMath::Max(MAX_FLOOR_DIST, PawnRadius); | |
const float LineResult = FMath::Max(-MaxPenetrationAdjust, Hit.Time * TraceDist - ShrinkHeight); | |
OutFloorResult.bBlockingHit = true; | |
if (LineResult <= LineDistance && IsWalkable(Hit)) | |
{ | |
OutFloorResult.SetFromLineTrace(Hit, OutFloorResult.FloorDist, LineResult, true); | |
return; | |
} | |
} | |
} | |
} | |
// No hits were acceptable. | |
OutFloorResult.bWalkableFloor = false; | |
} | |
void UMMOPlayerMovement::FindFloor(const FVector& CapsuleLocation, FMMOFindFloorResult& OutFloorResult, bool bCanUseCachedLocation, const FHitResult* DownwardSweepResult) const | |
{ | |
// No collision, no floor... | |
if (!HasValidData() || !UpdatedComponent->IsQueryCollisionEnabled()) | |
{ | |
OutFloorResult.Clear(); | |
return; | |
} | |
UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("[Role:%d] FindFloor: %s at location %s"), (int32)CharacterOwner->GetLocalRole(), *GetNameSafe(CharacterOwner), *CapsuleLocation.ToString()); | |
check(CharacterOwner->GetCapsuleComponent()); | |
// Increase height check slightly if walking, to prevent floor height adjustment from later invalidating the floor result. | |
const float HeightCheckAdjust = (IsMovingOnGround() ? MAX_FLOOR_DIST + KINDA_SMALL_NUMBER : -MAX_FLOOR_DIST); | |
float FloorSweepTraceDist = FMath::Max(MAX_FLOOR_DIST, MaxStepHeight + HeightCheckAdjust); | |
float FloorLineTraceDist = FloorSweepTraceDist; | |
bool bNeedToValidateFloor = true; | |
// Sweep floor | |
if (FloorLineTraceDist > 0.f || FloorSweepTraceDist > 0.f) | |
{ | |
UMMOPlayerMovement* MutableThis = const_cast<UMMOPlayerMovement*>(this); | |
if (bAlwaysCheckFloor || !bCanUseCachedLocation || bForceNextFloorCheck || bJustTeleported) | |
{ | |
MutableThis->bForceNextFloorCheck = false; | |
ComputeFloorDist(CapsuleLocation, FloorLineTraceDist, FloorSweepTraceDist, OutFloorResult, CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleRadius(), DownwardSweepResult); | |
} | |
else | |
{ | |
// Force floor check if base has collision disabled or if it does not block us. | |
UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase(); | |
const AActor* BaseActor = MovementBase ? MovementBase->GetOwner() : NULL; | |
const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType(); | |
if (MovementBase != NULL) | |
{ | |
MutableThis->bForceNextFloorCheck = !MovementBase->IsQueryCollisionEnabled() | |
|| MovementBase->GetCollisionResponseToChannel(CollisionChannel) != ECR_Block | |
|| MovementBaseUtility::IsDynamicBase(MovementBase); | |
} | |
const bool IsActorBasePendingKill = BaseActor && BaseActor->IsPendingKill(); | |
if (!bForceNextFloorCheck && !IsActorBasePendingKill && MovementBase) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("%s SKIP check for floor"), *CharacterOwner->GetName()); | |
OutFloorResult = CurrentFloor; | |
bNeedToValidateFloor = false; | |
} | |
else | |
{ | |
MutableThis->bForceNextFloorCheck = false; | |
ComputeFloorDist(CapsuleLocation, FloorLineTraceDist, FloorSweepTraceDist, OutFloorResult, CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleRadius(), DownwardSweepResult); | |
} | |
} | |
} | |
// OutFloorResult.HitResult is now the result of the vertical floor check. | |
// See if we should try to "perch" at this location. | |
if (bNeedToValidateFloor && OutFloorResult.bBlockingHit && !OutFloorResult.bLineTrace) | |
{ | |
const bool bCheckRadius = true; | |
if (ShouldComputePerchResult(OutFloorResult.HitResult, bCheckRadius)) | |
{ | |
float MaxPerchFloorDist = FMath::Max(MAX_FLOOR_DIST, MaxStepHeight + HeightCheckAdjust); | |
if (IsMovingOnGround()) | |
{ | |
MaxPerchFloorDist += FMath::Max(0.f, PerchAdditionalHeight); | |
} | |
FMMOFindFloorResult PerchFloorResult; | |
if (ComputePerchResult(GetValidPerchRadius(), OutFloorResult.HitResult, MaxPerchFloorDist, PerchFloorResult)) | |
{ | |
// Don't allow the floor distance adjustment to push us up too high, or we will move beyond the perch distance and fall next time. | |
const float AvgFloorDist = (MIN_FLOOR_DIST + MAX_FLOOR_DIST) * 0.5f; | |
const float MoveUpDist = (AvgFloorDist - OutFloorResult.FloorDist); | |
if (MoveUpDist + PerchFloorResult.FloorDist >= MaxPerchFloorDist) | |
{ | |
OutFloorResult.FloorDist = AvgFloorDist; | |
} | |
// If the regular capsule is on an unwalkable surface but the perched one would allow us to stand, override the normal to be one that is walkable. | |
if (!OutFloorResult.bWalkableFloor) | |
{ | |
// Floor distances are used as the distance of the regular capsule to the point of collision, to make sure AdjustFloorHeight() behaves correctly. | |
OutFloorResult.SetFromLineTrace(PerchFloorResult.HitResult, OutFloorResult.FloorDist, FMath::Max(OutFloorResult.FloorDist, MIN_FLOOR_DIST), true); | |
} | |
} | |
else | |
{ | |
// We had no floor (or an invalid one because it was unwalkable), and couldn't perch here, so invalidate floor (which will cause us to start falling). | |
OutFloorResult.bWalkableFloor = false; | |
} | |
} | |
} | |
} | |
void UMMOPlayerMovement::K2_FindFloor(FVector CapsuleLocation, FMMOFindFloorResult& FloorResult) const | |
{ | |
const bool SavedForceNextFloorCheck(bForceNextFloorCheck); | |
FindFloor(CapsuleLocation, FloorResult, false); | |
// FindFloor clears this, but this is only a test not done during normal movement. | |
UMMOPlayerMovement* MutableThis = const_cast<UMMOPlayerMovement*>(this); | |
MutableThis->bForceNextFloorCheck = SavedForceNextFloorCheck; | |
} | |
void UMMOPlayerMovement::K2_ComputeFloorDist(FVector CapsuleLocation, float LineDistance, float SweepDistance, float SweepRadius, FMMOFindFloorResult& FloorResult) const | |
{ | |
if (HasValidData()) | |
{ | |
SweepDistance = FMath::Max(SweepDistance, 0.f); | |
LineDistance = FMath::Clamp(LineDistance, 0.f, SweepDistance); | |
SweepRadius = FMath::Max(SweepRadius, 0.f); | |
ComputeFloorDist(CapsuleLocation, LineDistance, SweepDistance, FloorResult, SweepRadius); | |
} | |
} | |
bool UMMOPlayerMovement::FloorSweepTest( | |
FHitResult& OutHit, | |
const FVector& Start, | |
const FVector& End, | |
ECollisionChannel TraceChannel, | |
const struct FCollisionShape& CollisionShape, | |
const struct FCollisionQueryParams& Params, | |
const struct FCollisionResponseParams& ResponseParam | |
) const | |
{ | |
bool bBlockingHit = false; | |
if (!bUseFlatBaseForFloorChecks) | |
{ | |
bBlockingHit = GetWorld()->SweepSingleByChannel(OutHit, Start, End, FQuat::Identity, TraceChannel, CollisionShape, Params, ResponseParam); | |
} | |
else | |
{ | |
// Test with a box that is enclosed by the capsule. | |
const float CapsuleRadius = CollisionShape.GetCapsuleRadius(); | |
const float CapsuleHeight = CollisionShape.GetCapsuleHalfHeight(); | |
const FCollisionShape BoxShape = FCollisionShape::MakeBox(FVector(CapsuleRadius * 0.707f, CapsuleRadius * 0.707f, CapsuleHeight)); | |
// First test with the box rotated so the corners are along the major axes (ie rotated 45 degrees). | |
bBlockingHit = GetWorld()->SweepSingleByChannel(OutHit, Start, End, FQuat(FVector(0.f, 0.f, -1.f), PI * 0.25f), TraceChannel, BoxShape, Params, ResponseParam); | |
if (!bBlockingHit) | |
{ | |
// Test again with the same box, not rotated. | |
OutHit.Reset(1.f, false); | |
bBlockingHit = GetWorld()->SweepSingleByChannel(OutHit, Start, End, FQuat::Identity, TraceChannel, BoxShape, Params, ResponseParam); | |
} | |
} | |
return bBlockingHit; | |
} | |
bool UMMOPlayerMovement::IsValidLandingSpot(const FVector& CapsuleLocation, const FHitResult& Hit) const | |
{ | |
if (!Hit.bBlockingHit) | |
{ | |
return false; | |
} | |
// Skip some checks if penetrating. Penetration will be handled by the FindFloor call (using a smaller capsule) | |
if (!Hit.bStartPenetrating) | |
{ | |
// Reject unwalkable floor normals. | |
if (!IsWalkable(Hit)) | |
{ | |
return false; | |
} | |
float PawnRadius, PawnHalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); | |
// Reject hits that are above our lower hemisphere (can happen when sliding down a vertical surface). | |
const float LowerHemisphereZ = Hit.Location.Z - PawnHalfHeight + PawnRadius; | |
if (Hit.ImpactPoint.Z >= LowerHemisphereZ) | |
{ | |
return false; | |
} | |
// Reject hits that are barely on the cusp of the radius of the capsule | |
if (!IsWithinEdgeTolerance(Hit.Location, Hit.ImpactPoint, PawnRadius)) | |
{ | |
return false; | |
} | |
} | |
else | |
{ | |
// Penetrating | |
if (Hit.Normal.Z < KINDA_SMALL_NUMBER) | |
{ | |
// Normal is nearly horizontal or downward, that's a penetration adjustment next to a vertical or overhanging wall. Don't pop to the floor. | |
return false; | |
} | |
} | |
FMMOFindFloorResult FloorResult; | |
FindFloor(CapsuleLocation, FloorResult, false, &Hit); | |
if (!FloorResult.IsWalkableFloor()) | |
{ | |
return false; | |
} | |
return true; | |
} | |
bool UMMOPlayerMovement::ShouldCheckForValidLandingSpot(float DeltaTime, const FVector& Delta, const FHitResult& Hit) const | |
{ | |
// See if we hit an edge of a surface on the lower portion of the capsule. | |
// In this case the normal will not equal the impact normal, and a downward sweep may find a walkable surface on top of the edge. | |
if (Hit.Normal.Z > KINDA_SMALL_NUMBER && !Hit.Normal.Equals(Hit.ImpactNormal)) | |
{ | |
const FVector PawnLocation = UpdatedComponent->GetComponentLocation(); | |
if (IsWithinEdgeTolerance(PawnLocation, Hit.ImpactPoint, CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleRadius())) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
float UMMOPlayerMovement::GetPerchRadiusThreshold() const | |
{ | |
// Don't allow negative values. | |
return FMath::Max(0.f, PerchRadiusThreshold); | |
} | |
float UMMOPlayerMovement::GetValidPerchRadius() const | |
{ | |
if (CharacterOwner) | |
{ | |
const float PawnRadius = CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleRadius(); | |
return FMath::Clamp(PawnRadius - GetPerchRadiusThreshold(), 0.11f, PawnRadius); | |
} | |
return 0.f; | |
} | |
bool UMMOPlayerMovement::ShouldComputePerchResult(const FHitResult& InHit, bool bCheckRadius) const | |
{ | |
if (!InHit.IsValidBlockingHit()) | |
{ | |
return false; | |
} | |
// Don't try to perch if the edge radius is very small. | |
if (GetPerchRadiusThreshold() <= SWEEP_EDGE_REJECT_DISTANCE) | |
{ | |
return false; | |
} | |
if (bCheckRadius) | |
{ | |
const float DistFromCenterSq = (InHit.ImpactPoint - InHit.Location).SizeSquared2D(); | |
const float StandOnEdgeRadius = GetValidPerchRadius(); | |
if (DistFromCenterSq <= FMath::Square(StandOnEdgeRadius)) | |
{ | |
// Already within perch radius. | |
return false; | |
} | |
} | |
return true; | |
} | |
bool UMMOPlayerMovement::ComputePerchResult(const float TestRadius, const FHitResult& InHit, const float InMaxFloorDist, FMMOFindFloorResult& OutPerchFloorResult) const | |
{ | |
if (InMaxFloorDist <= 0.f) | |
{ | |
return false; | |
} | |
// Sweep further than actual requested distance, because a reduced capsule radius means we could miss some hits that the normal radius would contact. | |
float PawnRadius, PawnHalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); | |
const float InHitAboveBase = FMath::Max(0.f, InHit.ImpactPoint.Z - (InHit.Location.Z - PawnHalfHeight)); | |
const float PerchLineDist = FMath::Max(0.f, InMaxFloorDist - InHitAboveBase); | |
const float PerchSweepDist = FMath::Max(0.f, InMaxFloorDist); | |
const float ActualSweepDist = PerchSweepDist + PawnRadius; | |
ComputeFloorDist(InHit.Location, PerchLineDist, ActualSweepDist, OutPerchFloorResult, TestRadius); | |
if (!OutPerchFloorResult.IsWalkableFloor()) | |
{ | |
return false; | |
} | |
else if (InHitAboveBase + OutPerchFloorResult.FloorDist > InMaxFloorDist) | |
{ | |
// Hit something past max distance | |
OutPerchFloorResult.bWalkableFloor = false; | |
return false; | |
} | |
return true; | |
} | |
bool UMMOPlayerMovement::CanStepUp(const FHitResult& Hit) const | |
{ | |
if (!Hit.IsValidBlockingHit() || !HasValidData() || MovementMode == MOVE_Falling) | |
{ | |
return false; | |
} | |
// No component for "fake" hits when we are on a known good base. | |
const UPrimitiveComponent* HitComponent = Hit.Component.Get(); | |
if (!HitComponent) | |
{ | |
return true; | |
} | |
if (!HitComponent->CanCharacterStepUp(CharacterOwner)) | |
{ | |
return false; | |
} | |
// No actor for "fake" hits when we are on a known good base. | |
const AActor* HitActor = Hit.GetActor(); | |
if (!HitActor) | |
{ | |
return true; | |
} | |
if (!HitActor->CanBeBaseForCharacter(CharacterOwner)) | |
{ | |
return false; | |
} | |
return true; | |
} | |
bool UMMOPlayerMovement::StepUp(const FVector& GravDir, const FVector& Delta, const FHitResult& InHit, FStepDownResult* OutStepDownResult) | |
{ | |
if (!CanStepUp(InHit) || MaxStepHeight <= 0.f) | |
{ | |
return false; | |
} | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
float PawnRadius, PawnHalfHeight; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(PawnRadius, PawnHalfHeight); | |
// Don't bother stepping up if top of capsule is hitting something. | |
const float InitialImpactZ = InHit.ImpactPoint.Z; | |
if (InitialImpactZ > OldLocation.Z + (PawnHalfHeight - PawnRadius)) | |
{ | |
return false; | |
} | |
if (GravDir.IsZero()) | |
{ | |
return false; | |
} | |
// Gravity should be a normalized direction | |
ensure(GravDir.IsNormalized()); | |
float StepTravelUpHeight = MaxStepHeight; | |
float StepTravelDownHeight = StepTravelUpHeight; | |
const float StepSideZ = -1.f * FVector::DotProduct(InHit.ImpactNormal, GravDir); | |
float PawnInitialFloorBaseZ = OldLocation.Z - PawnHalfHeight; | |
float PawnFloorPointZ = PawnInitialFloorBaseZ; | |
if (IsMovingOnGround() && CurrentFloor.IsWalkableFloor()) | |
{ | |
// Since we float a variable amount off the floor, we need to enforce max step height off the actual point of impact with the floor. | |
const float FloorDist = FMath::Max(0.f, CurrentFloor.GetDistanceToFloor()); | |
PawnInitialFloorBaseZ -= FloorDist; | |
StepTravelUpHeight = FMath::Max(StepTravelUpHeight - FloorDist, 0.f); | |
StepTravelDownHeight = (MaxStepHeight + MAX_FLOOR_DIST * 2.f); | |
const bool bHitVerticalFace = !IsWithinEdgeTolerance(InHit.Location, InHit.ImpactPoint, PawnRadius); | |
if (!CurrentFloor.bLineTrace && !bHitVerticalFace) | |
{ | |
PawnFloorPointZ = CurrentFloor.HitResult.ImpactPoint.Z; | |
} | |
else | |
{ | |
// Base floor point is the base of the capsule moved down by how far we are hovering over the surface we are hitting. | |
PawnFloorPointZ -= CurrentFloor.FloorDist; | |
} | |
} | |
// Don't step up if the impact is below us, accounting for distance from floor. | |
if (InitialImpactZ <= PawnInitialFloorBaseZ) | |
{ | |
return false; | |
} | |
// Scope our movement updates, and do not apply them until all intermediate moves are completed. | |
FScopedMovementUpdate ScopedStepUpMovement(UpdatedComponent, EScopedUpdate::DeferredUpdates); | |
// step up - treat as vertical wall | |
FHitResult SweepUpHit(1.f); | |
const FQuat PawnRotation = UpdatedComponent->GetComponentQuat(); | |
MoveUpdatedComponent(-GravDir * StepTravelUpHeight, PawnRotation, true, &SweepUpHit); | |
if (SweepUpHit.bStartPenetrating) | |
{ | |
// Undo movement | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
// step fwd | |
FHitResult Hit(1.f); | |
MoveUpdatedComponent(Delta, PawnRotation, true, &Hit); | |
// Check result of forward movement | |
if (Hit.bBlockingHit) | |
{ | |
if (Hit.bStartPenetrating) | |
{ | |
// Undo movement | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
// If we hit something above us and also something ahead of us, we should notify about the upward hit as well. | |
// The forward hit will be handled later (in the bSteppedOver case below). | |
// In the case of hitting something above but not forward, we are not blocked from moving so we don't need the notification. | |
if (SweepUpHit.bBlockingHit && Hit.bBlockingHit) | |
{ | |
HandleImpact(SweepUpHit); | |
} | |
// pawn ran into a wall | |
HandleImpact(Hit); | |
if (IsFalling()) | |
{ | |
return true; | |
} | |
// adjust and try again | |
const float ForwardHitTime = Hit.Time; | |
const float ForwardSlideAmount = SlideAlongSurface(Delta, 1.f - Hit.Time, Hit.Normal, Hit, true); | |
if (IsFalling()) | |
{ | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
// If both the forward hit and the deflection got us nowhere, there is no point in this step up. | |
if (ForwardHitTime == 0.f && ForwardSlideAmount == 0.f) | |
{ | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
} | |
// Step down | |
MoveUpdatedComponent(GravDir * StepTravelDownHeight, UpdatedComponent->GetComponentQuat(), true, &Hit); | |
// If step down was initially penetrating abort the step up | |
if (Hit.bStartPenetrating) | |
{ | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
FStepDownResult StepDownResult; | |
if (Hit.IsValidBlockingHit()) | |
{ | |
// See if this step sequence would have allowed us to travel higher than our max step height allows. | |
const float DeltaZ = Hit.ImpactPoint.Z - PawnFloorPointZ; | |
if (DeltaZ > MaxStepHeight) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("- Reject StepUp (too high Height %.3f) up from floor base %f to %f"), DeltaZ, PawnInitialFloorBaseZ, NewLocation.Z); | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
// Reject unwalkable surface normals here. | |
if (!IsWalkable(Hit)) | |
{ | |
// Reject if normal opposes movement direction | |
const bool bNormalTowardsMe = (Delta | Hit.ImpactNormal) < 0.f; | |
if (bNormalTowardsMe) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("- Reject StepUp (unwalkable normal %s opposed to movement)"), *Hit.ImpactNormal.ToString()); | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
// Also reject if we would end up being higher than our starting location by stepping down. | |
// It's fine to step down onto an unwalkable normal below us, we will just slide off. Rejecting those moves would prevent us from being able to walk off the edge. | |
if (Hit.Location.Z > OldLocation.Z) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("- Reject StepUp (unwalkable normal %s above old position)"), *Hit.ImpactNormal.ToString()); | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
} | |
// Reject moves where the downward sweep hit something very close to the edge of the capsule. This maintains consistency with FindFloor as well. | |
if (!IsWithinEdgeTolerance(Hit.Location, Hit.ImpactPoint, PawnRadius)) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("- Reject StepUp (outside edge tolerance)")); | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
// Don't step up onto invalid surfaces if traveling higher. | |
if (DeltaZ > 0.f && !CanStepUp(Hit)) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, VeryVerbose, TEXT("- Reject StepUp (up onto surface with !CanStepUp())")); | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
// See if we can validate the floor as a result of this step down. In almost all cases this should succeed, and we can avoid computing the floor outside this method. | |
if (OutStepDownResult != NULL) | |
{ | |
FindFloor(UpdatedComponent->GetComponentLocation(), StepDownResult.FloorResult, false, &Hit); | |
// Reject unwalkable normals if we end up higher than our initial height. | |
// It's fine to walk down onto an unwalkable surface, don't reject those moves. | |
if (Hit.Location.Z > OldLocation.Z) | |
{ | |
// We should reject the floor result if we are trying to step up an actual step where we are not able to perch (this is rare). | |
// In those cases we should instead abort the step up and try to slide along the stair. | |
if (!StepDownResult.FloorResult.bBlockingHit && StepSideZ < MAX_STEP_SIDE_Z) | |
{ | |
ScopedStepUpMovement.RevertMove(); | |
return false; | |
} | |
} | |
StepDownResult.bComputedFloor = true; | |
} | |
} | |
// Copy step down result. | |
if (OutStepDownResult != NULL) | |
{ | |
*OutStepDownResult = StepDownResult; | |
} | |
// Don't recalculate velocity based on this height adjustment, if considering vertical adjustments. | |
bJustTeleported |= !bMaintainHorizontalGroundVelocity; | |
return true; | |
} | |
void UMMOPlayerMovement::HandleImpact(const FHitResult& Impact, float TimeSlice, const FVector& MoveDelta) | |
{ | |
if (CharacterOwner) | |
{ | |
CharacterOwner->MoveBlockedBy(Impact); | |
} | |
IPathFollowingAgentInterface* PFAgent = GetPathFollowingAgent(); | |
if (PFAgent) | |
{ | |
// Also notify path following! | |
PFAgent->OnMoveBlockedBy(Impact); | |
} | |
APawn* OtherPawn = Cast<APawn>(Impact.GetActor()); | |
if (OtherPawn) | |
{ | |
NotifyBumpedPawn(OtherPawn); | |
} | |
if (bEnablePhysicsInteraction) | |
{ | |
const FVector ForceAccel = Acceleration + (IsFalling() ? FVector(0.f, 0.f, GetGravityZ()) : FVector::ZeroVector); | |
ApplyImpactPhysicsForces(Impact, ForceAccel, Velocity); | |
} | |
} | |
void UMMOPlayerMovement::ApplyImpactPhysicsForces(const FHitResult& Impact, const FVector& ImpactAcceleration, const FVector& ImpactVelocity) | |
{ | |
if (bEnablePhysicsInteraction && Impact.bBlockingHit) | |
{ | |
if (UPrimitiveComponent* ImpactComponent = Impact.GetComponent()) | |
{ | |
FBodyInstance* BI = ImpactComponent->GetBodyInstance(Impact.BoneName); | |
if (BI != nullptr && BI->IsInstanceSimulatingPhysics()) | |
{ | |
FVector ForcePoint = Impact.ImpactPoint; | |
const float BodyMass = FMath::Max(BI->GetBodyMass(), 1.0f); | |
if (bPushForceUsingZOffset) | |
{ | |
FBox Bounds = BI->GetBodyBounds(); | |
FVector Center, Extents; | |
Bounds.GetCenterAndExtents(Center, Extents); | |
if (!Extents.IsNearlyZero()) | |
{ | |
ForcePoint.Z = Center.Z + Extents.Z * PushForcePointZOffsetFactor; | |
} | |
} | |
FVector Force = Impact.ImpactNormal * -1.0f; | |
float PushForceModificator = 1.0f; | |
const FVector ComponentVelocity = ImpactComponent->GetPhysicsLinearVelocity(); | |
const FVector VirtualVelocity = ImpactAcceleration.IsZero() ? ImpactVelocity : ImpactAcceleration.GetSafeNormal() * GetMaxSpeed(); | |
float Dot = 0.0f; | |
if (bScalePushForceToVelocity && !ComponentVelocity.IsNearlyZero()) | |
{ | |
Dot = ComponentVelocity | VirtualVelocity; | |
if (Dot > 0.0f && Dot < 1.0f) | |
{ | |
PushForceModificator *= Dot; | |
} | |
} | |
if (bPushForceScaledToMass) | |
{ | |
PushForceModificator *= BodyMass; | |
} | |
Force *= PushForceModificator; | |
if (ComponentVelocity.IsNearlyZero()) | |
{ | |
Force *= InitialPushForceFactor; | |
ImpactComponent->AddImpulseAtLocation(Force, ForcePoint, Impact.BoneName); | |
} | |
else | |
{ | |
Force *= PushForceFactor; | |
ImpactComponent->AddForceAtLocation(Force, ForcePoint, Impact.BoneName); | |
} | |
} | |
} | |
} | |
} | |
FString UMMOPlayerMovement::GetMovementName() const | |
{ | |
if (CharacterOwner) | |
{ | |
if (CharacterOwner->GetRootComponent() && CharacterOwner->GetRootComponent()->IsSimulatingPhysics()) | |
{ | |
return TEXT("Rigid Body"); | |
} | |
else if (CharacterOwner->IsMatineeControlled()) | |
{ | |
return TEXT("Matinee"); | |
} | |
} | |
// Using character movement | |
switch (MovementMode) | |
{ | |
case MOVE_None: return TEXT("NULL"); break; | |
case MOVE_Walking: return TEXT("Walking"); break; | |
case MOVE_NavWalking: return TEXT("NavWalking"); break; | |
case MOVE_Falling: return TEXT("Falling"); break; | |
case MOVE_Swimming: return TEXT("Swimming"); break; | |
case MOVE_Flying: return TEXT("Flying"); break; | |
case MOVE_Custom: return TEXT("Custom"); break; | |
default: break; | |
} | |
return TEXT("Unknown"); | |
} | |
void UMMOPlayerMovement::DisplayDebug(UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos) | |
{ | |
if (CharacterOwner == NULL) | |
{ | |
return; | |
} | |
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager; | |
DisplayDebugManager.SetDrawColor(FColor::White); | |
FString T = FString::Printf(TEXT("CHARACTER MOVEMENT Floor %s Crouched %i"), *CurrentFloor.HitResult.ImpactNormal.ToString(), IsCrouching()); | |
DisplayDebugManager.DrawString(T); | |
T = FString::Printf(TEXT("Updated Component: %s"), *UpdatedComponent->GetName()); | |
DisplayDebugManager.DrawString(T); | |
T = FString::Printf(TEXT("Acceleration: %s"), *Acceleration.ToCompactString()); | |
DisplayDebugManager.DrawString(T); | |
T = FString::Printf(TEXT("bForceMaxAccel: %i"), bForceMaxAccel); | |
DisplayDebugManager.DrawString(T); | |
T = FString::Printf(TEXT("RootMotionSources: %d active"), CurrentRootMotion.RootMotionSources.Num()); | |
DisplayDebugManager.DrawString(T); | |
APhysicsVolume* PhysicsVolume = GetPhysicsVolume(); | |
const UPrimitiveComponent* BaseComponent = CharacterOwner->GetMovementBase(); | |
const AActor* BaseActor = BaseComponent ? BaseComponent->GetOwner() : NULL; | |
T = FString::Printf(TEXT("%s In physicsvolume %s on base %s component %s gravity %f"), *GetMovementName(), (PhysicsVolume ? *PhysicsVolume->GetName() : TEXT("None")), | |
(BaseActor ? *BaseActor->GetName() : TEXT("None")), (BaseComponent ? *BaseComponent->GetName() : TEXT("None")), GetGravityZ()); | |
DisplayDebugManager.DrawString(T); | |
} | |
float UMMOPlayerMovement::VisualizeMovement() const | |
{ | |
float HeightOffset = 0.f; | |
const float OffsetPerElement = 10.0f; | |
if (CharacterOwner == nullptr) | |
{ | |
return HeightOffset; | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
const FVector TopOfCapsule = GetActorLocation() + FVector(0.f, 0.f, CharacterOwner->GetSimpleCollisionHalfHeight()); | |
// Position | |
{ | |
const FColor DebugColor = FColor::White; | |
const FVector DebugLocation = TopOfCapsule + FVector(0.f, 0.f, HeightOffset); | |
FString DebugText = FString::Printf(TEXT("Position: %s"), *GetActorLocation().ToCompactString()); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
} | |
// Velocity | |
{ | |
const FColor DebugColor = FColor::Green; | |
HeightOffset += OffsetPerElement; | |
const FVector DebugLocation = TopOfCapsule + FVector(0.f, 0.f, HeightOffset); | |
DrawDebugDirectionalArrow(GetWorld(), DebugLocation - FVector(0.f, 0.f, 5.0f), DebugLocation - FVector(0.f, 0.f, 5.0f) + Velocity, | |
100.f, DebugColor, false, -1.f, (uint8)'\000', 10.f); | |
FString DebugText = FString::Printf(TEXT("Velocity: %s (Speed: %.2f) (Max: %.2f)"), *Velocity.ToCompactString(), Velocity.Size(), GetMaxSpeed()); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
} | |
// Acceleration | |
{ | |
const FColor DebugColor = FColor::Yellow; | |
HeightOffset += OffsetPerElement; | |
const float MaxAccelerationLineLength = 200.f; | |
const float CurrentMaxAccel = GetMaxAcceleration(); | |
const float CurrentAccelAsPercentOfMaxAccel = CurrentMaxAccel > 0.f ? Acceleration.Size() / CurrentMaxAccel : 1.f; | |
const FVector DebugLocation = TopOfCapsule + FVector(0.f, 0.f, HeightOffset); | |
DrawDebugDirectionalArrow(GetWorld(), DebugLocation - FVector(0.f, 0.f, 5.0f), | |
DebugLocation - FVector(0.f, 0.f, 5.0f) + Acceleration.GetSafeNormal(SMALL_NUMBER) * CurrentAccelAsPercentOfMaxAccel * MaxAccelerationLineLength, | |
25.f, DebugColor, false, -1.f, (uint8)'\000', 8.f); | |
FString DebugText = FString::Printf(TEXT("Acceleration: %s"), *Acceleration.ToCompactString()); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
} | |
// Movement Mode | |
{ | |
const FColor DebugColor = FColor::Blue; | |
HeightOffset += OffsetPerElement; | |
FVector DebugLocation = TopOfCapsule + FVector(0.f, 0.f, HeightOffset); | |
FString DebugText = FString::Printf(TEXT("MovementMode: %s"), *GetMovementName()); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
if (IsInWater()) | |
{ | |
HeightOffset += OffsetPerElement; | |
DebugLocation = TopOfCapsule + FVector(0.f, 0.f, HeightOffset); | |
DebugText = FString::Printf(TEXT("ImmersionDepth: %.2f"), ImmersionDepth()); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
} | |
} | |
// Jump | |
{ | |
const FColor DebugColor = FColor::Blue; | |
HeightOffset += OffsetPerElement; | |
FVector DebugLocation = TopOfCapsule + FVector(0.f, 0.f, HeightOffset); | |
FString DebugText = FString::Printf(TEXT("bIsJumping: %d Count: %d HoldTime: %.2f"), CharacterOwner->bPressedJump, CharacterOwner->JumpCurrentCount, CharacterOwner->JumpKeyHoldTime); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
} | |
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
return HeightOffset; | |
} | |
void UMMOPlayerMovement::ForceReplicationUpdate() | |
{ | |
if (HasPredictionData_Server()) | |
{ | |
GetPredictionData_Server_Character()->LastUpdateTime = GetWorld()->TimeSeconds - 10.f; | |
} | |
} | |
void UMMOPlayerMovement::ForceClientAdjustment() | |
{ | |
ServerLastClientAdjustmentTime = -1.f; | |
} | |
FVector UMMOPlayerMovement::ConstrainInputAcceleration(const FVector& InputAcceleration) const | |
{ | |
// walking or falling pawns ignore up/down sliding | |
if (InputAcceleration.Z != 0.f && (IsMovingOnGround() || IsFalling())) | |
{ | |
return FVector(InputAcceleration.X, InputAcceleration.Y, 0.f); | |
} | |
return InputAcceleration; | |
} | |
FVector UMMOPlayerMovement::ScaleInputAcceleration(const FVector& InputAcceleration) const | |
{ | |
return GetMaxAcceleration() * InputAcceleration.GetClampedToMaxSize(1.0f); | |
} | |
FVector UMMOPlayerMovement::RoundAcceleration(FVector InAccel) const | |
{ | |
// Match FVector_NetQuantize10 (1 decimal place of precision). | |
InAccel.X = FMath::RoundToFloat(InAccel.X * 10.f) / 10.f; | |
InAccel.Y = FMath::RoundToFloat(InAccel.Y * 10.f) / 10.f; | |
InAccel.Z = FMath::RoundToFloat(InAccel.Z * 10.f) / 10.f; | |
return InAccel; | |
} | |
float UMMOPlayerMovement::ComputeAnalogInputModifier() const | |
{ | |
const float MaxAccel = GetMaxAcceleration(); | |
if (Acceleration.SizeSquared() > 0.f && MaxAccel > SMALL_NUMBER) | |
{ | |
return FMath::Clamp(Acceleration.Size() / MaxAccel, 0.f, 1.f); | |
} | |
return 0.f; | |
} | |
float UMMOPlayerMovement::GetAnalogInputModifier() const | |
{ | |
return AnalogInputModifier; | |
} | |
float UMMOPlayerMovement::GetSimulationTimeStep(float RemainingTime, int32 Iterations) const | |
{ | |
static uint32 s_WarningCount = 0; | |
if (RemainingTime > MaxSimulationTimeStep) | |
{ | |
if (Iterations < MaxSimulationIterations) | |
{ | |
// Subdivide moves to be no longer than MaxSimulationTimeStep seconds | |
RemainingTime = FMath::Min(MaxSimulationTimeStep, RemainingTime * 0.5f); | |
} | |
else | |
{ | |
// If this is the last iteration, just use all the remaining time. This is usually better than cutting things short, as the simulation won't move far enough otherwise. | |
// Print a throttled warning. | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if ((s_WarningCount++ < 100) || (GFrameCounter & 15) == 0) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("GetSimulationTimeStep() - Max iterations %d hit while remaining time %.6f > MaxSimulationTimeStep (%.3f) for '%s', movement '%s'"), MaxSimulationIterations, RemainingTime, MaxSimulationTimeStep, *GetNameSafe(CharacterOwner), *GetMovementName()); | |
} | |
#endif | |
} | |
} | |
// no less than MIN_TICK_TIME (to avoid potential divide-by-zero during simulation). | |
return FMath::Max(MIN_TICK_TIME, RemainingTime); | |
} | |
void UMMOPlayerMovement::SmoothCorrection(const FVector& OldLocation, const FQuat& OldRotation, const FVector& NewLocation, const FQuat& NewRotation) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// We shouldn't be running this on a server that is not a listen server. | |
checkSlow(GetNetMode() != NM_DedicatedServer); | |
checkSlow(GetNetMode() != NM_Standalone); | |
// Only client proxies or remote clients on a listen server should run this code. | |
const bool bIsSimulatedProxy = (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy); | |
const bool bIsRemoteAutoProxy = (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy); | |
ensure(bIsSimulatedProxy || bIsRemoteAutoProxy); | |
// Getting a correction means new data, so smoothing needs to run. | |
bNetworkSmoothingComplete = false; | |
// Handle selected smoothing mode. | |
if (NetworkSmoothingMode == ENetworkSmoothingMode::Replay) | |
{ | |
// Replays use pure interpolation in this mode, all of the work is done in SmoothClientPosition_Interpolate | |
return; | |
} | |
else if (NetworkSmoothingMode == ENetworkSmoothingMode::Disabled) | |
{ | |
UpdatedComponent->SetWorldLocationAndRotation(NewLocation, NewRotation, false, nullptr, ETeleportType::TeleportPhysics); | |
bNetworkSmoothingComplete = true; | |
} | |
else if (FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character()) | |
{ | |
const UWorld* MyWorld = GetWorld(); | |
if (!ensure(MyWorld != nullptr)) | |
{ | |
return; | |
} | |
// The mesh doesn't move, but the capsule does so we have a new offset. | |
FVector NewToOldVector = (OldLocation - NewLocation); | |
if (bIsNavWalkingOnServer && FMath::Abs(NewToOldVector.Z) < NavWalkingFloorDistTolerance) | |
{ | |
// ignore smoothing on Z axis | |
// don't modify new location (local simulation result), since it's probably more accurate than server data | |
// and shouldn't matter as long as difference is relatively small | |
NewToOldVector.Z = 0; | |
} | |
const float DistSq = NewToOldVector.SizeSquared(); | |
if (DistSq > FMath::Square(ClientData->MaxSmoothNetUpdateDist)) | |
{ | |
ClientData->MeshTranslationOffset = (DistSq > FMath::Square(ClientData->NoSmoothNetUpdateDist)) | |
? FVector::ZeroVector | |
: ClientData->MeshTranslationOffset + ClientData->MaxSmoothNetUpdateDist * NewToOldVector.GetSafeNormal(); | |
} | |
else | |
{ | |
ClientData->MeshTranslationOffset = ClientData->MeshTranslationOffset + NewToOldVector; | |
} | |
UE_LOG(LogMMOCharacterNetSmoothing, Verbose, TEXT("Proxy %s SmoothCorrection(%.2f)"), *GetNameSafe(CharacterOwner), FMath::Sqrt(DistSq)); | |
if (NetworkSmoothingMode == ENetworkSmoothingMode::Linear) | |
{ | |
ClientData->OriginalMeshTranslationOffset = ClientData->MeshTranslationOffset; | |
// Remember the current and target rotation, we're going to lerp between them | |
ClientData->OriginalMeshRotationOffset = OldRotation; | |
ClientData->MeshRotationOffset = OldRotation; | |
ClientData->MeshRotationTarget = NewRotation; | |
// Move the capsule, but not the mesh. | |
// Note: we don't change rotation, we lerp towards it in SmoothClientPosition. | |
if (NewLocation != OldLocation) | |
{ | |
const FScopedPreventAttachedComponentMove PreventMeshMove(CharacterOwner->GetMesh()); | |
UpdatedComponent->SetWorldLocation(NewLocation, false, nullptr, GetTeleportType()); | |
} | |
} | |
else | |
{ | |
// Calc rotation needed to keep current world rotation after UpdatedComponent moves. | |
// Take difference between where we were rotated before, and where we're going | |
ClientData->MeshRotationOffset = (NewRotation.Inverse() * OldRotation) * ClientData->MeshRotationOffset; | |
ClientData->MeshRotationTarget = FQuat::Identity; | |
const FScopedPreventAttachedComponentMove PreventMeshMove(CharacterOwner->GetMesh()); | |
UpdatedComponent->SetWorldLocationAndRotation(NewLocation, NewRotation, false, nullptr, GetTeleportType()); | |
} | |
////////////////////////////////////////////////////////////////////////// | |
// Update smoothing timestamps | |
// If running ahead, pull back slightly. This will cause the next delta to seem slightly longer, and cause us to lerp to it slightly slower. | |
if (ClientData->SmoothingClientTimeStamp > ClientData->SmoothingServerTimeStamp) | |
{ | |
const double OldClientTimeStamp = ClientData->SmoothingClientTimeStamp; | |
ClientData->SmoothingClientTimeStamp = FMath::LerpStable(ClientData->SmoothingServerTimeStamp, OldClientTimeStamp, 0.5); | |
UE_LOG(LogMMOCharacterNetSmoothing, VeryVerbose, TEXT("SmoothCorrection: Pull back client from ClientTimeStamp: %.6f to %.6f, ServerTimeStamp: %.6f for %s"), | |
OldClientTimeStamp, ClientData->SmoothingClientTimeStamp, ClientData->SmoothingServerTimeStamp, *GetNameSafe(CharacterOwner)); | |
} | |
// Using server timestamp lets us know how much time actually elapsed, regardless of packet lag variance. | |
double OldServerTimeStamp = ClientData->SmoothingServerTimeStamp; | |
if (bIsSimulatedProxy) | |
{ | |
// This value is normally only updated on the server, however some code paths might try to read it instead of the replicated value so copy it for proxies as well. | |
ServerLastTransformUpdateTimeStamp = CharacterOwner->GetReplicatedServerLastTransformUpdateTimeStamp(); | |
} | |
ClientData->SmoothingServerTimeStamp = ServerLastTransformUpdateTimeStamp; | |
// Initial update has no delta. | |
if (ClientData->LastCorrectionTime == 0) | |
{ | |
ClientData->SmoothingClientTimeStamp = ClientData->SmoothingServerTimeStamp; | |
OldServerTimeStamp = ClientData->SmoothingServerTimeStamp; | |
} | |
// Don't let the client fall too far behind or run ahead of new server time. | |
const double ServerDeltaTime = ClientData->SmoothingServerTimeStamp - OldServerTimeStamp; | |
const double MaxOffset = ClientData->MaxClientSmoothingDeltaTime; | |
const double MinOffset = FMath::Min(double(ClientData->SmoothNetUpdateTime), MaxOffset); | |
// MaxDelta is the farthest behind we're allowed to be after receiving a new server time. | |
const double MaxDelta = FMath::Clamp(ServerDeltaTime * 1.25, MinOffset, MaxOffset); | |
ClientData->SmoothingClientTimeStamp = FMath::Clamp(ClientData->SmoothingClientTimeStamp, ClientData->SmoothingServerTimeStamp - MaxDelta, ClientData->SmoothingServerTimeStamp); | |
// Compute actual delta between new server timestamp and client simulation. | |
ClientData->LastCorrectionDelta = ClientData->SmoothingServerTimeStamp - ClientData->SmoothingClientTimeStamp; | |
ClientData->LastCorrectionTime = MyWorld->GetTimeSeconds(); | |
UE_LOG(LogMMOCharacterNetSmoothing, VeryVerbose, TEXT("SmoothCorrection: WorldTime: %.6f, ServerTimeStamp: %.6f, ClientTimeStamp: %.6f, Delta: %.6f for %s"), | |
MyWorld->GetTimeSeconds(), ClientData->SmoothingServerTimeStamp, ClientData->SmoothingClientTimeStamp, ClientData->LastCorrectionDelta, *GetNameSafe(CharacterOwner)); | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 2) | |
{ | |
const float Radius = 4.0f; | |
const bool bPersist = false; | |
const float Lifetime = 10.0f; | |
const int32 Sides = 8; | |
const float ArrowSize = 4.0f; | |
const FVector SimulatedLocation = OldLocation; | |
const FVector ServerLocation = NewLocation + FVector(0, 0, 0.5f); | |
const FVector SmoothLocation = CharacterOwner->GetMesh()->GetComponentLocation() - CharacterOwner->GetBaseTranslationOffset() + FVector(0, 0, 1.0f); | |
//DrawDebugCoordinateSystem( GetWorld(), ServerLocation + FVector( 0, 0, 300.0f ), UpdatedComponent->GetComponentRotation(), 45.0f, bPersist, Lifetime ); | |
// Draw simulated location | |
DrawCircle(GetWorld(), SimulatedLocation, FVector(1, 0, 0), FVector(0, 1, 0), FColor(255, 0, 0, 255), Radius, Sides, bPersist, Lifetime); | |
// Draw server (corrected location) | |
DrawCircle(GetWorld(), ServerLocation, FVector(1, 0, 0), FVector(0, 1, 0), FColor(0, 255, 0, 255), Radius, Sides, bPersist, Lifetime); | |
// Draw smooth simulated location | |
FRotationMatrix SmoothMatrix(CharacterOwner->GetMesh()->GetComponentRotation()); | |
DrawDebugDirectionalArrow(GetWorld(), SmoothLocation, SmoothLocation + SmoothMatrix.GetScaledAxis(EAxis::Y) * 5, ArrowSize, FColor(255, 255, 0, 255), bPersist, Lifetime); | |
DrawCircle(GetWorld(), SmoothLocation, FVector(1, 0, 0), FVector(0, 1, 0), FColor(0, 0, 255, 255), Radius, Sides, bPersist, Lifetime); | |
if (ClientData->LastServerLocation != FVector::ZeroVector) | |
{ | |
// Arrow showing simulated line | |
DrawDebugDirectionalArrow(GetWorld(), ClientData->LastServerLocation, SimulatedLocation, ArrowSize, FColor(255, 0, 0, 255), bPersist, Lifetime); | |
// Arrow showing server line | |
DrawDebugDirectionalArrow(GetWorld(), ClientData->LastServerLocation, ServerLocation, ArrowSize, FColor(0, 255, 0, 255), bPersist, Lifetime); | |
// Arrow showing smooth location plot | |
DrawDebugDirectionalArrow(GetWorld(), ClientData->LastSmoothLocation, SmoothLocation, ArrowSize, FColor(0, 0, 255, 255), bPersist, Lifetime); | |
// Line showing correction | |
DrawDebugDirectionalArrow(GetWorld(), SimulatedLocation, ServerLocation, ArrowSize, FColor(128, 0, 0, 255), bPersist, Lifetime); | |
// Line showing smooth vector | |
DrawDebugDirectionalArrow(GetWorld(), ServerLocation, SmoothLocation, ArrowSize, FColor(0, 0, 128, 255), bPersist, Lifetime); | |
} | |
ClientData->LastServerLocation = ServerLocation; | |
ClientData->LastSmoothLocation = SmoothLocation; | |
} | |
#endif | |
} | |
} | |
FArchive& operator<<(FArchive& Ar, FMMOCharacterReplaySample& V) | |
{ | |
SerializePackedVector<10, 24>(V.Location, Ar); | |
SerializePackedVector<10, 24>(V.Velocity, Ar); | |
SerializePackedVector<10, 24>(V.Acceleration, Ar); | |
V.Rotation.SerializeCompressed(Ar); | |
Ar << V.RemoteViewPitch; | |
if (Ar.IsSaving() || (!Ar.AtEnd() && !Ar.IsError())) | |
{ | |
Ar << V.Time; | |
} | |
return Ar; | |
} | |
void UMMOPlayerMovement::SmoothClientPosition(float DeltaSeconds) | |
{ | |
if (!HasValidData() || NetworkSmoothingMode == ENetworkSmoothingMode::Disabled) | |
{ | |
return; | |
} | |
// We shouldn't be running this on a server that is not a listen server. | |
checkSlow(GetNetMode() != NM_DedicatedServer); | |
checkSlow(GetNetMode() != NM_Standalone); | |
// Only client proxies or remote clients on a listen server should run this code. | |
const bool bIsSimulatedProxy = (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy); | |
const bool bIsRemoteAutoProxy = (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy); | |
if (!ensure(bIsSimulatedProxy || bIsRemoteAutoProxy)) | |
{ | |
return; | |
} | |
SmoothClientPosition_Interpolate(DeltaSeconds); | |
SmoothClientPosition_UpdateVisuals(); | |
} | |
void UMMOPlayerMovement::SmoothClientPosition_Interpolate(float DeltaSeconds) | |
{ | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (ClientData) | |
{ | |
if (NetworkSmoothingMode == ENetworkSmoothingMode::Linear) | |
{ | |
const UWorld* MyWorld = GetWorld(); | |
// Increment client position. | |
ClientData->SmoothingClientTimeStamp += DeltaSeconds; | |
float LerpPercent = 0.f; | |
const float LerpLimit = 1.15f; | |
const float TargetDelta = ClientData->LastCorrectionDelta; | |
if (TargetDelta > SMALL_NUMBER) | |
{ | |
// Don't let the client get too far ahead (happens on spikes). But we do want a buffer for variable network conditions. | |
const float MaxClientTimeAheadPercent = 0.15f; | |
const float MaxTimeAhead = TargetDelta * MaxClientTimeAheadPercent; | |
ClientData->SmoothingClientTimeStamp = FMath::Min<float>(ClientData->SmoothingClientTimeStamp, ClientData->SmoothingServerTimeStamp + MaxTimeAhead); | |
// Compute interpolation alpha based on our client position within the server delta. We should take TargetDelta seconds to reach alpha of 1. | |
const float RemainingTime = ClientData->SmoothingServerTimeStamp - ClientData->SmoothingClientTimeStamp; | |
const float CurrentSmoothTime = TargetDelta - RemainingTime; | |
LerpPercent = FMath::Clamp(CurrentSmoothTime / TargetDelta, 0.0f, LerpLimit); | |
UE_LOG(LogMMOCharacterNetSmoothing, VeryVerbose, TEXT("Interpolate: WorldTime: %.6f, ServerTimeStamp: %.6f, ClientTimeStamp: %.6f, Elapsed: %.6f, Alpha: %.6f for %s"), | |
MyWorld->GetTimeSeconds(), ClientData->SmoothingServerTimeStamp, ClientData->SmoothingClientTimeStamp, CurrentSmoothTime, LerpPercent, *GetNameSafe(CharacterOwner)); | |
} | |
else | |
{ | |
LerpPercent = 1.0f; | |
} | |
if (LerpPercent >= 1.0f - KINDA_SMALL_NUMBER) | |
{ | |
if (Velocity.IsNearlyZero()) | |
{ | |
ClientData->MeshTranslationOffset = FVector::ZeroVector; | |
ClientData->SmoothingClientTimeStamp = ClientData->SmoothingServerTimeStamp; | |
bNetworkSmoothingComplete = true; | |
} | |
else | |
{ | |
// Allow limited forward prediction. | |
ClientData->MeshTranslationOffset = FMath::LerpStable(ClientData->OriginalMeshTranslationOffset, FVector::ZeroVector, LerpPercent); | |
bNetworkSmoothingComplete = (LerpPercent >= LerpLimit); | |
} | |
ClientData->MeshRotationOffset = ClientData->MeshRotationTarget; | |
} | |
else | |
{ | |
ClientData->MeshTranslationOffset = FMath::LerpStable(ClientData->OriginalMeshTranslationOffset, FVector::ZeroVector, LerpPercent); | |
ClientData->MeshRotationOffset = FQuat::FastLerp(ClientData->OriginalMeshRotationOffset, ClientData->MeshRotationTarget, LerpPercent).GetNormalized(); | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
// Show lerp percent | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 1) | |
{ | |
const FColor DebugColor = FColor::White; | |
const FVector DebugLocation = CharacterOwner->GetMesh()->GetComponentLocation() + FVector(0.f, 0.f, 130.0f) - CharacterOwner->GetBaseTranslationOffset(); | |
FString DebugText = FString::Printf(TEXT("Lerp: %2.2f"), LerpPercent); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
FString TimeText = FString::Printf(TEXT("ClientTime: %2.2f ServerTime: %2.2f"), ClientData->SmoothingClientTimeStamp, ClientData->SmoothingServerTimeStamp); | |
DrawDebugString(GetWorld(), DebugLocation + 25.f, TimeText, nullptr, DebugColor, 0.f, true); | |
} | |
#endif | |
} | |
else if (NetworkSmoothingMode == ENetworkSmoothingMode::Exponential) | |
{ | |
// Smooth interpolation of mesh translation to avoid popping of other client pawns unless under a low tick rate. | |
// Faster interpolation if stopped. | |
const float SmoothLocationTime = Velocity.IsZero() ? 0.5f * ClientData->SmoothNetUpdateTime : ClientData->SmoothNetUpdateTime; | |
if (DeltaSeconds < SmoothLocationTime) | |
{ | |
// Slowly decay translation offset | |
ClientData->MeshTranslationOffset = (ClientData->MeshTranslationOffset * (1.f - DeltaSeconds / SmoothLocationTime)); | |
} | |
else | |
{ | |
ClientData->MeshTranslationOffset = FVector::ZeroVector; | |
} | |
// Smooth rotation | |
const FQuat MeshRotationTarget = ClientData->MeshRotationTarget; | |
if (DeltaSeconds < ClientData->SmoothNetUpdateRotationTime) | |
{ | |
// Slowly decay rotation offset | |
ClientData->MeshRotationOffset = FQuat::FastLerp(ClientData->MeshRotationOffset, MeshRotationTarget, DeltaSeconds / ClientData->SmoothNetUpdateRotationTime).GetNormalized(); | |
} | |
else | |
{ | |
ClientData->MeshRotationOffset = MeshRotationTarget; | |
} | |
// Check if lerp is complete | |
if (ClientData->MeshTranslationOffset.IsNearlyZero(1e-2f) && ClientData->MeshRotationOffset.Equals(MeshRotationTarget, 1e-5f)) | |
{ | |
bNetworkSmoothingComplete = true; | |
// Make sure to snap exactly to target values. | |
ClientData->MeshTranslationOffset = FVector::ZeroVector; | |
ClientData->MeshRotationOffset = MeshRotationTarget; | |
} | |
} | |
else if (NetworkSmoothingMode == ENetworkSmoothingMode::Replay) | |
{ | |
const UWorld* MyWorld = GetWorld(); | |
if (!MyWorld || !MyWorld->DemoNetDriver) | |
{ | |
return; | |
} | |
const float CurrentTime = MyWorld->DemoNetDriver->DemoCurrentTime; | |
// Remove old samples | |
while (ClientData->ReplaySamples.Num() > 0) | |
{ | |
if (ClientData->ReplaySamples[0].Time > CurrentTime - 1.0f) | |
{ | |
break; | |
} | |
ClientData->ReplaySamples.RemoveAt(0); | |
} | |
FReplayExternalDataArray* ExternalReplayData = MyWorld->DemoNetDriver->GetExternalDataArrayForObject(CharacterOwner); | |
// Grab any samples available, deserialize them, then clear originals | |
if (ExternalReplayData && ExternalReplayData->Num() > 0) | |
{ | |
for (int i = 0; i < ExternalReplayData->Num(); i++) | |
{ | |
FMMOCharacterReplaySample ReplaySample; | |
(*ExternalReplayData)[i].Reader << ReplaySample; | |
if (FMath::IsNearlyZero(ReplaySample.Time)) | |
{ | |
ReplaySample.Time = (*ExternalReplayData)[i].TimeSeconds; | |
} | |
ClientData->ReplaySamples.Add(ReplaySample); | |
} | |
if (MMOCharacterMovementCVars::FixReplayOverSampling > 0) | |
{ | |
// Remove invalid replay samples that can occur due to oversampling (sampling at higher rate than physics is being ticked) | |
// We detect this by finding samples that have the same location but have a velocity that says the character should be moving | |
// If we don't do this, then characters will look like they are skipping around, which looks really bad | |
for (int i = 1; i < ClientData->ReplaySamples.Num(); i++) | |
{ | |
if (ClientData->ReplaySamples[i].Location.Equals(ClientData->ReplaySamples[i - 1].Location, KINDA_SMALL_NUMBER)) | |
{ | |
if (ClientData->ReplaySamples[i - 1].Velocity.SizeSquared() > FMath::Square(KINDA_SMALL_NUMBER) && ClientData->ReplaySamples[i].Velocity.SizeSquared() > FMath::Square(KINDA_SMALL_NUMBER)) | |
{ | |
ClientData->ReplaySamples.RemoveAt(i); | |
i--; | |
} | |
} | |
} | |
} | |
ExternalReplayData->Empty(); | |
} | |
bool bFoundSample = false; | |
for (int i = 0; i < ClientData->ReplaySamples.Num() - 1; i++) | |
{ | |
if (CurrentTime >= ClientData->ReplaySamples[i].Time && CurrentTime <= ClientData->ReplaySamples[i + 1].Time) | |
{ | |
const float EPSILON = SMALL_NUMBER; | |
const float Delta = (ClientData->ReplaySamples[i + 1].Time - ClientData->ReplaySamples[i].Time); | |
const float LerpPercent = Delta > EPSILON ? FMath::Clamp<float>((float)(CurrentTime - ClientData->ReplaySamples[i].Time) / Delta, 0.0f, 1.0f) : 1.0f; | |
const FMMOCharacterReplaySample& ReplaySample1 = ClientData->ReplaySamples[i]; | |
const FMMOCharacterReplaySample& ReplaySample2 = ClientData->ReplaySamples[i + 1]; | |
const FVector Location = FMath::Lerp(ReplaySample1.Location, ReplaySample2.Location, LerpPercent); | |
const FQuat Rotation = FQuat::FastLerp(FQuat(ReplaySample1.Rotation), FQuat(ReplaySample2.Rotation), LerpPercent).GetNormalized(); | |
Velocity = FMath::Lerp(ReplaySample1.Velocity, ReplaySample2.Velocity, LerpPercent); | |
if (MMOCharacterMovementCVars::ReplayLerpAcceleration) | |
{ | |
Acceleration = FMath::Lerp(ReplaySample1.Acceleration, ReplaySample2.Acceleration, LerpPercent); | |
} | |
else | |
{ | |
Acceleration = ReplaySample2.Acceleration; | |
} | |
const FRotator Rotator1(FRotator::DecompressAxisFromByte(ReplaySample1.RemoteViewPitch), 0.0f, 0.0f); | |
const FRotator Rotator2(FRotator::DecompressAxisFromByte(ReplaySample2.RemoteViewPitch), 0.0f, 0.0f); | |
const FRotator FinalPitch = FQuat::FastLerp(FQuat(Rotator1), FQuat(Rotator2), LerpPercent).GetNormalized().Rotator(); | |
CharacterOwner->BlendedReplayViewPitch = FinalPitch.Pitch; | |
UpdateComponentVelocity(); | |
USkeletalMeshComponent* Mesh = CharacterOwner->GetMesh(); | |
if (Mesh) | |
{ | |
Mesh->SetRelativeLocation_Direct(CharacterOwner->GetBaseTranslationOffset()); | |
Mesh->SetRelativeRotation_Direct(CharacterOwner->GetBaseRotationOffset().Rotator()); | |
} | |
ClientData->MeshTranslationOffset = Location; | |
ClientData->MeshRotationOffset = Rotation; | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 1) | |
{ | |
const float Radius = 4.0f; | |
const int32 Sides = 8; | |
const float ArrowSize = 4.0f; | |
const FColor DebugColor = FColor::White; | |
const FVector DebugLocation = CharacterOwner->GetMesh()->GetComponentLocation() + FVector(0.f, 0.f, 300.0f) - CharacterOwner->GetBaseTranslationOffset(); | |
FString DebugText = FString::Printf(TEXT("Lerp: %2.2f, %2.2f"), LerpPercent, CharacterOwner->BlendedReplayViewPitch); | |
DrawDebugString(GetWorld(), DebugLocation, DebugText, nullptr, DebugColor, 0.f, true); | |
DrawDebugBox(GetWorld(), DebugLocation, FVector(45, 45, 45), CharacterOwner->GetMesh()->GetComponentQuat(), FColor(0, 255, 0)); | |
DrawDebugDirectionalArrow(GetWorld(), DebugLocation, DebugLocation + Velocity, 20.0f, FColor(255, 0, 0, 255)); | |
} | |
#endif | |
bFoundSample = true; | |
break; | |
} | |
} | |
if (!bFoundSample && ClientData->ReplaySamples.Num() > 0) | |
{ | |
int32 BestSample = 0; | |
float BestDelta = FMath::Abs(ClientData->ReplaySamples[BestSample].Time - CurrentTime); | |
for (int i = 1; i < ClientData->ReplaySamples.Num(); i++) | |
{ | |
const float Delta = FMath::Abs(ClientData->ReplaySamples[i].Time - CurrentTime); | |
if (Delta < BestDelta) | |
{ | |
BestDelta = Delta; | |
BestSample = i; | |
} | |
} | |
const FMMOCharacterReplaySample& ReplaySample = ClientData->ReplaySamples[BestSample]; | |
Velocity = ReplaySample.Velocity; | |
Acceleration = ReplaySample.Acceleration; | |
CharacterOwner->BlendedReplayViewPitch = FRotator::DecompressAxisFromByte(ReplaySample.RemoteViewPitch); | |
UpdateComponentVelocity(); | |
USkeletalMeshComponent* const Mesh = CharacterOwner->GetMesh(); | |
if (Mesh) | |
{ | |
Mesh->SetRelativeLocation_Direct(CharacterOwner->GetBaseTranslationOffset()); | |
Mesh->SetRelativeRotation_Direct(CharacterOwner->GetBaseRotationOffset().Rotator()); | |
} | |
ClientData->MeshTranslationOffset = ReplaySample.Location; | |
ClientData->MeshRotationOffset = ReplaySample.Rotation.Quaternion(); | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (Mesh && MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 1) | |
{ | |
const float ArrowSize = 4.0f; | |
const FVector DebugLocation = Mesh->GetComponentLocation() + FVector(0.f, 0.f, 300.0f) - CharacterOwner->GetBaseTranslationOffset(); | |
DrawDebugBox(GetWorld(), DebugLocation, FVector(45, 45, 45), Mesh->GetComponentQuat(), FColor(255, 0, 0)); | |
DrawDebugDirectionalArrow(GetWorld(), DebugLocation, DebugLocation + Velocity, 20.0f, FColor(255, 0, 0, 255)); | |
} | |
#endif | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
// Show future samples | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 1) | |
{ | |
const float Radius = 4.0f; | |
const int32 Sides = 8; | |
const float ArrowSize = 4.0f; | |
const FColor DebugColor = FColor::White; | |
// Draw points ahead up to a few seconds | |
for (int i = 0; i < ClientData->ReplaySamples.Num(); i++) | |
{ | |
const bool bHasMorePoints = i < ClientData->ReplaySamples.Num() - 1; | |
const bool bActiveSamples = (bHasMorePoints && CurrentTime >= ClientData->ReplaySamples[i].Time && CurrentTime <= ClientData->ReplaySamples[i + 1].Time); | |
if (ClientData->ReplaySamples[i].Time >= CurrentTime || bActiveSamples) | |
{ | |
//const FVector Adjust = FVector( 0.f, 0.f, 300.0f + i * 15.0f ); | |
const FVector Adjust = FVector(0.f, 0.f, 300.0f); | |
const FVector Location = ClientData->ReplaySamples[i].Location + Adjust; | |
if (bHasMorePoints) | |
{ | |
const FVector NextLocation = ClientData->ReplaySamples[i + 1].Location + Adjust; | |
DrawDebugDirectionalArrow(GetWorld(), Location, NextLocation, 4.0f, FColor(0, 255, 0, 255)); | |
} | |
DrawCircle(GetWorld(), Location, FVector(1, 0, 0), FVector(0, 1, 0), FColor(255, 0, 0, 255), Radius, Sides, false, 0.0f); | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 2) | |
{ | |
DrawDebugDirectionalArrow(GetWorld(), Location, Location + ClientData->ReplaySamples[i].Velocity, 20.0f, FColor(255, 0, 0, 255)); | |
} | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 3) | |
{ | |
DrawDebugDirectionalArrow(GetWorld(), Location, Location + ClientData->ReplaySamples[i].Acceleration, 20.0f, FColor(255, 255, 255, 255)); | |
} | |
} | |
if (ClientData->ReplaySamples[i].Time - CurrentTime > 2.0f) | |
{ | |
break; | |
} | |
} | |
} | |
#endif | |
bNetworkSmoothingComplete = false; | |
} | |
else | |
{ | |
// Unhandled mode | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
//UE_LOG(LogMMOCharacterNetSmoothing, VeryVerbose, TEXT("SmoothClientPosition_Interpolate %s: Translation: %s Rotation: %s"), | |
// *GetNameSafe(CharacterOwner), *ClientData->MeshTranslationOffset.ToString(), *ClientData->MeshRotationOffset.ToString()); | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 1 && NetworkSmoothingMode != ENetworkSmoothingMode::Replay) | |
{ | |
const FVector DebugLocation = CharacterOwner->GetMesh()->GetComponentLocation() + FVector(0.f, 0.f, 300.0f) - CharacterOwner->GetBaseTranslationOffset(); | |
DrawDebugBox(GetWorld(), DebugLocation, FVector(45, 45, 45), CharacterOwner->GetMesh()->GetComponentQuat(), FColor(0, 255, 0)); | |
//DrawDebugCoordinateSystem( GetWorld(), UpdatedComponent->GetComponentLocation() + FVector( 0, 0, 300.0f ), UpdatedComponent->GetComponentRotation(), 45.0f ); | |
//DrawDebugBox( GetWorld(), UpdatedComponent->GetComponentLocation() + FVector( 0, 0, 300.0f ), FVector( 45, 45, 45 ), UpdatedComponent->GetComponentQuat(), FColor( 0, 255, 0 ) ); | |
if (MMOCharacterMovementCVars::NetVisualizeSimulatedCorrections >= 3) | |
{ | |
ClientData->SimulatedDebugDrawTime += DeltaSeconds; | |
if (ClientData->SimulatedDebugDrawTime >= 1.0f / 60.0f) | |
{ | |
const float Radius = 2.0f; | |
const bool bPersist = false; | |
const float Lifetime = 10.0f; | |
const int32 Sides = 8; | |
const FVector SmoothLocation = CharacterOwner->GetMesh()->GetComponentLocation() - CharacterOwner->GetBaseTranslationOffset(); | |
const FVector SimulatedLocation = UpdatedComponent->GetComponentLocation(); | |
DrawCircle(GetWorld(), SmoothLocation + FVector(0, 0, 1.5f), FVector(1, 0, 0), FVector(0, 1, 0), FColor(0, 0, 255, 255), Radius, Sides, bPersist, Lifetime); | |
DrawCircle(GetWorld(), SimulatedLocation + FVector(0, 0, 2.0f), FVector(1, 0, 0), FVector(0, 1, 0), FColor(255, 0, 0, 255), Radius, Sides, bPersist, Lifetime); | |
ClientData->SimulatedDebugDrawTime = 0.0f; | |
} | |
} | |
} | |
#endif | |
} | |
} | |
void UMMOPlayerMovement::SmoothClientPosition_UpdateVisuals() | |
{ | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
USkeletalMeshComponent* Mesh = CharacterOwner->GetMesh(); | |
if (ClientData && Mesh && !Mesh->IsSimulatingPhysics()) | |
{ | |
if (NetworkSmoothingMode == ENetworkSmoothingMode::Linear) | |
{ | |
// Adjust capsule rotation and mesh location. Optimized to trigger only one transform chain update. | |
// If we know the rotation is changing that will update children, so it's sufficient to set RelativeLocation directly on the mesh. | |
const FVector NewRelLocation = ClientData->MeshRotationOffset.UnrotateVector(ClientData->MeshTranslationOffset) + CharacterOwner->GetBaseTranslationOffset(); | |
if (!UpdatedComponent->GetComponentQuat().Equals(ClientData->MeshRotationOffset, 1e-6f)) | |
{ | |
const FVector OldLocation = Mesh->GetRelativeLocation(); | |
const FRotator OldRotation = UpdatedComponent->GetRelativeRotation(); | |
Mesh->SetRelativeLocation_Direct(NewRelLocation); | |
UpdatedComponent->SetWorldRotation(ClientData->MeshRotationOffset); | |
// If we did not move from SetWorldRotation, we need to at least call SetRelativeLocation since we were relying on the UpdatedComponent to update the transform of the mesh | |
if (UpdatedComponent->GetRelativeRotation() == OldRotation) | |
{ | |
Mesh->SetRelativeLocation_Direct(OldLocation); | |
Mesh->SetRelativeLocation(NewRelLocation, false, nullptr, GetTeleportType()); | |
} | |
} | |
else | |
{ | |
Mesh->SetRelativeLocation(NewRelLocation, false, nullptr, GetTeleportType()); | |
} | |
} | |
else if (NetworkSmoothingMode == ENetworkSmoothingMode::Exponential) | |
{ | |
// Adjust mesh location and rotation | |
const FVector NewRelTranslation = UpdatedComponent->GetComponentToWorld().InverseTransformVectorNoScale(ClientData->MeshTranslationOffset) + CharacterOwner->GetBaseTranslationOffset(); | |
const FQuat NewRelRotation = ClientData->MeshRotationOffset * CharacterOwner->GetBaseRotationOffset(); | |
Mesh->SetRelativeLocationAndRotation(NewRelTranslation, NewRelRotation, false, nullptr, GetTeleportType()); | |
} | |
else if (NetworkSmoothingMode == ENetworkSmoothingMode::Replay) | |
{ | |
if (!UpdatedComponent->GetComponentQuat().Equals(ClientData->MeshRotationOffset, SCENECOMPONENT_QUAT_TOLERANCE) || !UpdatedComponent->GetComponentLocation().Equals(ClientData->MeshTranslationOffset, KINDA_SMALL_NUMBER)) | |
{ | |
UpdatedComponent->SetWorldLocationAndRotation(ClientData->MeshTranslationOffset, ClientData->MeshRotationOffset, false, nullptr, GetTeleportType()); | |
} | |
} | |
else | |
{ | |
// Unhandled mode | |
} | |
} | |
} | |
bool UMMOPlayerMovement::ClientUpdatePositionAfterServerUpdate() | |
{ | |
if (!HasValidData()) | |
{ | |
return false; | |
} | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
check(ClientData); | |
if (!ClientData->bUpdatePosition) | |
{ | |
return false; | |
} | |
ClientData->bUpdatePosition = false; | |
// Don't do any network position updates on things running PHYS_RigidBody | |
if (CharacterOwner->GetRootComponent() && CharacterOwner->GetRootComponent()->IsSimulatingPhysics()) | |
{ | |
return false; | |
} | |
if (ClientData->SavedMoves.Num() == 0) | |
{ | |
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ClientUpdatePositionAfterServerUpdate No saved moves to replay"), ClientData->SavedMoves.Num()); | |
// With no saved moves to resimulate, the move the server updated us with is the last move we've done, no resimulation needed. | |
CharacterOwner->bClientResimulateRootMotion = false; | |
if (CharacterOwner->bClientResimulateRootMotionSources) | |
{ | |
// With no resimulation, we just update our current root motion to what the server sent us | |
UE_LOG(LogRootMotion, VeryVerbose, TEXT("CurrentRootMotion getting updated to ServerUpdate state: %s"), *CharacterOwner->GetName()); | |
CurrentRootMotion.UpdateStateFrom(CharacterOwner->SavedRootMotion); | |
CharacterOwner->bClientResimulateRootMotionSources = false; | |
} | |
return false; | |
} | |
// Save important values that might get affected by the replay. | |
const float SavedAnalogInputModifier = AnalogInputModifier; | |
const FRootMotionMovementParams BackupRootMotionParams = RootMotionParams; // For animation root motion | |
const FRootMotionSourceGroup BackupRootMotion = CurrentRootMotion; | |
const bool bRealJump = CharacterOwner->bPressedJump; | |
const bool bRealCrouch = bWantsToCrouch; | |
const bool bRealForceMaxAccel = bForceMaxAccel; | |
CharacterOwner->bClientWasFalling = (MovementMode == MOVE_Falling); | |
CharacterOwner->bClientUpdating = true; | |
bForceNextFloorCheck = true; | |
// Replay moves that have not yet been acked. | |
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ClientUpdatePositionAfterServerUpdate Replaying %d Moves, starting at Timestamp %f"), ClientData->SavedMoves.Num(), ClientData->SavedMoves[0]->TimeStamp); | |
for (int32 i = 0; i < ClientData->SavedMoves.Num(); i++) | |
{ | |
FMMOSavedMove_Character* const CurrentMove = ClientData->SavedMoves[i].Get(); | |
checkSlow(CurrentMove != nullptr); | |
CurrentMove->PrepMoveFor(CharacterOwner); | |
MoveAutonomous(CurrentMove->TimeStamp, CurrentMove->DeltaTime, CurrentMove->GetCompressedFlags(), CurrentMove->Acceleration); | |
CurrentMove->PostUpdate(CharacterOwner, FMMOSavedMove_Character::PostUpdate_Replay); | |
} | |
if (FMMOSavedMove_Character* const PendingMove = ClientData->PendingMove.Get()) | |
{ | |
PendingMove->bForceNoCombine = true; | |
} | |
// Restore saved values. | |
AnalogInputModifier = SavedAnalogInputModifier; | |
RootMotionParams = BackupRootMotionParams; | |
CurrentRootMotion = BackupRootMotion; | |
if (CharacterOwner->bClientResimulateRootMotionSources) | |
{ | |
// If we were resimulating root motion sources, it's because we had mismatched state | |
// with the server - we just resimulated our SavedMoves and now need to restore | |
// CurrentRootMotion with the latest "good state" | |
UE_LOG(LogRootMotion, VeryVerbose, TEXT("CurrentRootMotion getting updated after ServerUpdate replays: %s"), *CharacterOwner->GetName()); | |
CurrentRootMotion.UpdateStateFrom(CharacterOwner->SavedRootMotion); | |
CharacterOwner->bClientResimulateRootMotionSources = false; | |
} | |
CharacterOwner->SavedRootMotion.Clear(); | |
CharacterOwner->bClientResimulateRootMotion = false; | |
CharacterOwner->bClientUpdating = false; | |
CharacterOwner->bPressedJump = bRealJump; | |
bWantsToCrouch = bRealCrouch; | |
bForceMaxAccel = bRealForceMaxAccel; | |
bForceNextFloorCheck = true; | |
return (ClientData->SavedMoves.Num() > 0); | |
} | |
bool UMMOPlayerMovement::ForcePositionUpdate(float DeltaTime) | |
{ | |
if (!HasValidData() || MovementMode == MOVE_None || UpdatedComponent->Mobility != EComponentMobility::Movable) | |
{ | |
return false; | |
} | |
check(CharacterOwner->GetLocalRole() == ROLE_Authority); | |
check(CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy); | |
// TODO: this is basically copied from MoveAutonmous, should consolidate. Or consider moving CheckJumpInput inside PerformMovement(). | |
{ | |
CharacterOwner->CheckJumpInput(DeltaTime); | |
// Acceleration constraints could change after jump input. | |
Acceleration = ConstrainInputAcceleration(Acceleration); | |
Acceleration = Acceleration.GetClampedToMaxSize(GetMaxAcceleration()); | |
AnalogInputModifier = ComputeAnalogInputModifier(); | |
} | |
FMMONetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character(); | |
// Increment client timestamp so we reject client moves after this new simulated time position. | |
ServerData->CurrentClientTimeStamp += DeltaTime; | |
// Increment server timestamp so ServerLastTransformUpdateTimeStamp gets changed if there is an actual movement. | |
const double SavedServerTimestamp = ServerData->ServerAccumulatedClientTimeStamp; | |
ServerData->ServerAccumulatedClientTimeStamp += DeltaTime; | |
const bool bServerMoveHasOccurred = (ServerData->ServerTimeStampLastServerMove != 0.f); | |
if (bServerMoveHasOccurred) | |
{ | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("ForcePositionUpdate: %s (DeltaTime %.2f -> ServerTimeStamp %.2f)"), *GetNameSafe(CharacterOwner), DeltaTime, ServerData->CurrentClientTimeStamp); | |
} | |
// Force movement update. | |
PerformMovement(DeltaTime); | |
// TODO: smooth correction on listen server? | |
return true; | |
} | |
FNetworkPredictionData_Client* UMMOPlayerMovement::GetPredictionData_Client() const | |
{ | |
if (ClientPredictionData == nullptr) | |
{ | |
UMMOPlayerMovement* MutableThis = const_cast<UMMOPlayerMovement*>(this); | |
MutableThis->ClientPredictionData = new FMMONetworkPredictionData_Client_Character(*this); | |
} | |
return ClientPredictionData; | |
} | |
FNetworkPredictionData_Server* UMMOPlayerMovement::GetPredictionData_Server() const | |
{ | |
if (ServerPredictionData == nullptr) | |
{ | |
UMMOPlayerMovement* MutableThis = const_cast<UMMOPlayerMovement*>(this); | |
MutableThis->ServerPredictionData = new FMMONetworkPredictionData_Server_Character(*this); | |
} | |
return ServerPredictionData; | |
} | |
FMMONetworkPredictionData_Client_Character* UMMOPlayerMovement::GetPredictionData_Client_Character() const | |
{ | |
// Should only be called on client or listen server (for remote clients) in network games | |
checkSlow(CharacterOwner != NULL); | |
checkSlow(CharacterOwner->GetLocalRole() < ROLE_Authority || (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy && GetNetMode() == NM_ListenServer)); | |
checkSlow(GetNetMode() == NM_Client || GetNetMode() == NM_ListenServer); | |
if (ClientPredictionData == nullptr) | |
{ | |
UMMOPlayerMovement* MutableThis = const_cast<UMMOPlayerMovement*>(this); | |
MutableThis->ClientPredictionData = static_cast<class FMMONetworkPredictionData_Client_Character*>(GetPredictionData_Client()); | |
} | |
return ClientPredictionData; | |
} | |
FMMONetworkPredictionData_Server_Character* UMMOPlayerMovement::GetPredictionData_Server_Character() const | |
{ | |
// Should only be called on server in network games | |
checkSlow(CharacterOwner != NULL); | |
checkSlow(CharacterOwner->GetLocalRole() == ROLE_Authority); | |
checkSlow(GetNetMode() < NM_Client); | |
if (ServerPredictionData == nullptr) | |
{ | |
UMMOPlayerMovement* MutableThis = const_cast<UMMOPlayerMovement*>(this); | |
MutableThis->ServerPredictionData = static_cast<class FMMONetworkPredictionData_Server_Character*>(GetPredictionData_Server()); | |
} | |
return ServerPredictionData; | |
} | |
bool UMMOPlayerMovement::HasPredictionData_Client() const | |
{ | |
return (ClientPredictionData != nullptr) && HasValidData(); | |
} | |
bool UMMOPlayerMovement::HasPredictionData_Server() const | |
{ | |
return (ServerPredictionData != nullptr) && HasValidData(); | |
} | |
void UMMOPlayerMovement::ResetPredictionData_Client() | |
{ | |
ForceClientAdjustment(); | |
if (ClientPredictionData) | |
{ | |
delete ClientPredictionData; | |
ClientPredictionData = nullptr; | |
} | |
} | |
void UMMOPlayerMovement::ResetPredictionData_Server() | |
{ | |
ForceClientAdjustment(); | |
if (ServerPredictionData) | |
{ | |
delete ServerPredictionData; | |
ServerPredictionData = nullptr; | |
} | |
} | |
float FMMONetworkPredictionData_Client_Character::UpdateTimeStampAndDeltaTime(float DeltaTime, class AMMOCharacter& CharacterOwner, class UMMOPlayerMovement& CharacterMovementComponent) | |
{ | |
// Reset TimeStamp regularly to combat float accuracy decreasing over time. | |
if (CurrentTimeStamp > CharacterMovementComponent.MinTimeBetweenTimeStampResets) | |
{ | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("Resetting Client's TimeStamp %f"), CurrentTimeStamp); | |
CurrentTimeStamp -= CharacterMovementComponent.MinTimeBetweenTimeStampResets; | |
// Mark all buffered moves as having old time stamps, so we make sure to not resend them. | |
// That would confuse the server. | |
for (int32 MoveIndex = 0; MoveIndex < SavedMoves.Num(); MoveIndex++) | |
{ | |
const FMMOSavedMovePtr& CurrentMove = SavedMoves[MoveIndex]; | |
SavedMoves[MoveIndex]->bOldTimeStampBeforeReset = true; | |
} | |
// Do LastAckedMove as well. No need to do PendingMove as that move is part of the SavedMoves array. | |
if (LastAckedMove.IsValid()) | |
{ | |
LastAckedMove->bOldTimeStampBeforeReset = true; | |
} | |
// Also apply the reset to any active root motions. | |
CharacterMovementComponent.CurrentRootMotion.ApplyTimeStampReset(CharacterMovementComponent.MinTimeBetweenTimeStampResets); | |
} | |
// Update Current TimeStamp. | |
CurrentTimeStamp += DeltaTime; | |
float ClientDeltaTime = DeltaTime; | |
// Server uses TimeStamps to derive DeltaTime which introduces some rounding errors. | |
// Make sure we do the same, so MoveAutonomous uses the same inputs and is deterministic!! | |
if (SavedMoves.Num() > 0) | |
{ | |
const FMMOSavedMovePtr& PreviousMove = SavedMoves.Last(); | |
if (!PreviousMove->bOldTimeStampBeforeReset) | |
{ | |
// How server will calculate its deltatime to update physics. | |
const float ServerDeltaTime = CurrentTimeStamp - PreviousMove->TimeStamp; | |
// Have client always use the Server's DeltaTime. Otherwise our physics simulation will differ and we'll trigger too many position corrections and increase our network traffic. | |
ClientDeltaTime = ServerDeltaTime; | |
} | |
} | |
return FMath::Min(ClientDeltaTime, MaxMoveDeltaTime * CharacterOwner.GetActorTimeDilation()); | |
} | |
void UMMOPlayerMovement::ReplicateMoveToServer(float DeltaTime, const FVector& NewAcceleration) | |
{ | |
check(CharacterOwner != NULL); | |
// Can only start sending moves if our controllers are synced up over the network, otherwise we flood the reliable buffer. | |
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController()); | |
if (PC && PC->AcknowledgedPawn != CharacterOwner) | |
{ | |
return; | |
} | |
// Bail out if our character's controller doesn't have a Player. This may be the case when the local player | |
// has switched to another controller, such as a debug camera controller. | |
if (PC && PC->Player == nullptr) | |
{ | |
return; | |
} | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (!ClientData) | |
{ | |
return; | |
} | |
// Update our delta time for physics simulation. | |
DeltaTime = ClientData->UpdateTimeStampAndDeltaTime(DeltaTime, *CharacterOwner, *this); | |
// Find the oldest (unacknowledged) important move (OldMove). | |
// Don't include the last move because it may be combined with the next new move. | |
// A saved move is interesting if it differs significantly from the last acknowledged move | |
FMMOSavedMovePtr OldMove = NULL; | |
if (ClientData->LastAckedMove.IsValid()) | |
{ | |
const int32 NumSavedMoves = ClientData->SavedMoves.Num(); | |
for (int32 i = 0; i < NumSavedMoves - 1; i++) | |
{ | |
const FMMOSavedMovePtr& CurrentMove = ClientData->SavedMoves[i]; | |
if (CurrentMove->IsImportantMove(ClientData->LastAckedMove)) | |
{ | |
OldMove = CurrentMove; | |
break; | |
} | |
} | |
} | |
// Get a SavedMove object to store the movement in. | |
FMMOSavedMovePtr NewMovePtr = ClientData->CreateSavedMove(); | |
FMMOSavedMove_Character* const NewMove = NewMovePtr.Get(); | |
if (NewMove == nullptr) | |
{ | |
return; | |
} | |
NewMove->SetMoveFor(CharacterOwner, DeltaTime, NewAcceleration, *ClientData); | |
const UWorld* MyWorld = GetWorld(); | |
// see if the two moves could be combined | |
// do not combine moves which have different TimeStamps (before and after reset). | |
if (const FMMOSavedMove_Character* PendingMove = ClientData->PendingMove.Get()) | |
{ | |
if (PendingMove->CanCombineWith(NewMovePtr, CharacterOwner, ClientData->MaxMoveDeltaTime * CharacterOwner->GetActorTimeDilation(*MyWorld))) | |
{ | |
// Only combine and move back to the start location if we don't move back in to a spot that would make us collide with something new. | |
const FVector OldStartLocation = PendingMove->GetRevertedLocation(); | |
const bool bAttachedToObject = (NewMovePtr->StartAttachParent != nullptr); | |
if (bAttachedToObject || !OverlapTest(OldStartLocation, PendingMove->StartRotation.Quaternion(), UpdatedComponent->GetCollisionObjectType(), GetPawnCapsuleCollisionShape(SHRINK_None), CharacterOwner)) | |
{ | |
// Avoid updating Mesh bones to physics during the teleport back, since PerformMovement() will update it right away anyway below. | |
// Note: this must be before the FScopedMovementUpdate below, since that scope is what actually moves the character and mesh. | |
FScopedMeshBoneUpdateOverride ScopedNoMeshBoneUpdate(CharacterOwner->GetMesh(), EKinematicBonesUpdateToPhysics::SkipAllBones); | |
// Accumulate multiple transform updates until scope ends. | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, EScopedUpdate::DeferredUpdates); | |
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("CombineMove: add delta %f + %f and revert from %f %f to %f %f"), DeltaTime, PendingMove->DeltaTime, UpdatedComponent->GetComponentLocation().X, UpdatedComponent->GetComponentLocation().Y, OldStartLocation.X, OldStartLocation.Y); | |
NewMove->CombineWith(PendingMove, CharacterOwner, PC, OldStartLocation); | |
if (PC) | |
{ | |
// We reverted position to that at the start of the pending move (above), however some code paths expect rotation to be set correctly | |
// before character movement occurs (via FaceRotation), so try that now. The bOrientRotationToMovement path happens later as part of PerformMovement() and PhysicsRotation(). | |
CharacterOwner->FaceRotation(PC->GetControlRotation(), NewMove->DeltaTime); | |
} | |
SaveBaseLocation(); | |
NewMove->SetInitialPosition(CharacterOwner); | |
// Remove pending move from move list. It would have to be the last move on the list. | |
if (ClientData->SavedMoves.Num() > 0 && ClientData->SavedMoves.Last() == ClientData->PendingMove) | |
{ | |
const bool bAllowShrinking = false; | |
ClientData->SavedMoves.Pop(bAllowShrinking); | |
} | |
ClientData->FreeMove(ClientData->PendingMove); | |
ClientData->PendingMove = nullptr; | |
PendingMove = nullptr; // Avoid dangling reference, it's deleted above. | |
} | |
else | |
{ | |
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("Not combining move [would collide at start location]")); | |
} | |
} | |
else | |
{ | |
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("Not combining move [not allowed by CanCombineWith()]")); | |
} | |
} | |
// Acceleration should match what we send to the server, plus any other restrictions the server also enforces (see MoveAutonomous). | |
Acceleration = NewMove->Acceleration.GetClampedToMaxSize(GetMaxAcceleration()); | |
AnalogInputModifier = ComputeAnalogInputModifier(); // recompute since acceleration may have changed. | |
// Perform the move locally | |
CharacterOwner->ClientRootMotionParams.Clear(); | |
CharacterOwner->SavedRootMotion.Clear(); | |
PerformMovement(NewMove->DeltaTime); | |
NewMove->PostUpdate(CharacterOwner, FMMOSavedMove_Character::PostUpdate_Record); | |
// Add NewMove to the list | |
if (CharacterOwner->IsReplicatingMovement()) | |
{ | |
check(NewMove == NewMovePtr.Get()); | |
ClientData->SavedMoves.Push(NewMovePtr); | |
const bool bCanDelayMove = (MMOCharacterMovementCVars::NetEnableMoveCombining != 0) && CanDelaySendingMove(NewMovePtr); | |
if (bCanDelayMove && ClientData->PendingMove.IsValid() == false) | |
{ | |
// Decide whether to hold off on move | |
const float NetMoveDelta = FMath::Clamp(GetClientNetSendDeltaTime(PC, ClientData, NewMovePtr), 1.f / 120.f, 1.f / 5.f); | |
if ((MyWorld->TimeSeconds - ClientData->ClientUpdateTime) * MyWorld->GetWorldSettings()->GetEffectiveTimeDilation() < NetMoveDelta) | |
{ | |
// Delay sending this move. | |
ClientData->PendingMove = NewMovePtr; | |
return; | |
} | |
} | |
ClientData->ClientUpdateTime = MyWorld->TimeSeconds; | |
UE_CLOG(CharacterOwner && UpdatedComponent, LogNetPlayerMovement, VeryVerbose, TEXT("ClientMove Time %f Acceleration %s Velocity %s Position %s Rotation %s DeltaTime %f Mode %s MovementBase %s.%s (Dynamic:%d) DualMove? %d"), | |
NewMove->TimeStamp, *NewMove->Acceleration.ToString(), *Velocity.ToString(), *UpdatedComponent->GetComponentLocation().ToString(), *UpdatedComponent->GetComponentRotation().ToCompactString(), NewMove->DeltaTime, *GetMovementName(), | |
*GetNameSafe(NewMove->EndBase.Get()), *NewMove->EndBoneName.ToString(), MovementBaseUtility::IsDynamicBase(NewMove->EndBase.Get()) ? 1 : 0, ClientData->PendingMove.IsValid() ? 1 : 0); | |
bool bSendServerMove = true; | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
// Testing options: Simulated packet loss to server | |
const float TimeSinceLossStart = (MyWorld->RealTimeSeconds - ClientData->DebugForcedPacketLossTimerStart); | |
if (ClientData->DebugForcedPacketLossTimerStart > 0.f && (TimeSinceLossStart < MMOCharacterMovementCVars::NetForceClientServerMoveLossDuration)) | |
{ | |
bSendServerMove = false; | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("Drop ServerMove, %.2f time remains"), MMOCharacterMovementCVars::NetForceClientServerMoveLossDuration - TimeSinceLossStart); | |
} | |
else if (MMOCharacterMovementCVars::NetForceClientServerMoveLossPercent != 0.f && (RandomStream.FRand() < MMOCharacterMovementCVars::NetForceClientServerMoveLossPercent)) | |
{ | |
bSendServerMove = false; | |
ClientData->DebugForcedPacketLossTimerStart = (MMOCharacterMovementCVars::NetForceClientServerMoveLossDuration > 0) ? MyWorld->RealTimeSeconds : 0.0f; | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("Drop ServerMove, %.2f time remains"), MMOCharacterMovementCVars::NetForceClientServerMoveLossDuration); | |
} | |
else | |
{ | |
ClientData->DebugForcedPacketLossTimerStart = 0.f; | |
} | |
#endif | |
// Send move to server if this character is replicating movement | |
if (bSendServerMove) | |
{ | |
CallServerMove(NewMove, OldMove.Get()); | |
} | |
} | |
ClientData->PendingMove = NULL; | |
} | |
void UMMOPlayerMovement::CallServerMove | |
( | |
const class FMMOSavedMove_Character* NewMove, | |
const class FMMOSavedMove_Character* OldMove | |
) | |
{ | |
check(NewMove != nullptr); | |
// Compress rotation down to 5 bytes | |
uint32 ClientYawPitchINT = 0; | |
uint8 ClientRollBYTE = 0; | |
NewMove->GetPackedAngles(ClientYawPitchINT, ClientRollBYTE); | |
// Determine if we send absolute or relative location | |
UPrimitiveComponent* ClientMovementBase = NewMove->EndBase.Get(); | |
const FName ClientBaseBone = NewMove->EndBoneName; | |
const FVector SendLocation = MovementBaseUtility::UseRelativeLocation(ClientMovementBase) ? NewMove->SavedRelativeLocation : NewMove->SavedLocation; | |
// send old move if it exists | |
if (OldMove) | |
{ | |
ServerMoveOld(OldMove->TimeStamp, OldMove->Acceleration, OldMove->GetCompressedFlags()); | |
} | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (const FMMOSavedMove_Character* const PendingMove = ClientData->PendingMove.Get()) | |
{ | |
uint32 OldClientYawPitchINT = 0; | |
uint8 OldClientRollBYTE = 0; | |
ClientData->PendingMove->GetPackedAngles(OldClientYawPitchINT, OldClientRollBYTE); | |
// If we delayed a move without root motion, and our new move has root motion, send these through a special function, so the server knows how to process them. | |
if ((PendingMove->RootMotionMontage == NULL) && (NewMove->RootMotionMontage != NULL)) | |
{ | |
// send two moves simultaneously | |
ServerMoveDualHybridRootMotion( | |
PendingMove->TimeStamp, | |
PendingMove->Acceleration, | |
PendingMove->GetCompressedFlags(), | |
OldClientYawPitchINT, | |
NewMove->TimeStamp, | |
NewMove->Acceleration, | |
SendLocation, | |
NewMove->GetCompressedFlags(), | |
ClientRollBYTE, | |
ClientYawPitchINT, | |
ClientMovementBase, | |
ClientBaseBone, | |
NewMove->EndPackedMovementMode | |
); | |
} | |
else | |
{ | |
// send two moves simultaneously | |
ServerMoveDual( | |
PendingMove->TimeStamp, | |
PendingMove->Acceleration, | |
PendingMove->GetCompressedFlags(), | |
OldClientYawPitchINT, | |
NewMove->TimeStamp, | |
NewMove->Acceleration, | |
SendLocation, | |
NewMove->GetCompressedFlags(), | |
ClientRollBYTE, | |
ClientYawPitchINT, | |
ClientMovementBase, | |
ClientBaseBone, | |
NewMove->EndPackedMovementMode | |
); | |
} | |
} | |
else | |
{ | |
ServerMove( | |
NewMove->TimeStamp, | |
NewMove->Acceleration, | |
SendLocation, | |
NewMove->GetCompressedFlags(), | |
ClientRollBYTE, | |
ClientYawPitchINT, | |
ClientMovementBase, | |
ClientBaseBone, | |
NewMove->EndPackedMovementMode | |
); | |
} | |
MarkForClientCameraUpdate(); | |
} | |
void UMMOPlayerMovement::ServerMoveOld_Implementation | |
( | |
float OldTimeStamp, | |
FVector_NetQuantize10 OldAccel, | |
uint8 OldMoveFlags | |
) | |
{ | |
if (!HasValidData() || !IsActive()) | |
{ | |
return; | |
} | |
FMMONetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character(); | |
check(ServerData); | |
if (!VerifyClientTimeStamp(OldTimeStamp, *ServerData)) | |
{ | |
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("ServerMoveOld: TimeStamp expired. %f, CurrentTimeStamp: %f, Character: %s"), OldTimeStamp, ServerData->CurrentClientTimeStamp, *GetNameSafe(CharacterOwner)); | |
return; | |
} | |
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("Recovered move from OldTimeStamp %f, DeltaTime: %f"), OldTimeStamp, OldTimeStamp - ServerData->CurrentClientTimeStamp); | |
const UWorld* MyWorld = GetWorld(); | |
const float DeltaTime = ServerData->GetServerMoveDeltaTime(OldTimeStamp, CharacterOwner->GetActorTimeDilation(*MyWorld)); | |
if (DeltaTime > 0.f) | |
{ | |
ServerData->CurrentClientTimeStamp = OldTimeStamp; | |
ServerData->ServerAccumulatedClientTimeStamp += DeltaTime; | |
ServerData->ServerTimeStamp = MyWorld->GetTimeSeconds(); | |
ServerData->ServerTimeStampLastServerMove = ServerData->ServerTimeStamp; | |
MoveAutonomous(OldTimeStamp, DeltaTime, OldMoveFlags, OldAccel); | |
} | |
else | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("OldTimeStamp(%f) results in zero or negative actual DeltaTime(%f). Theoretical DeltaTime(%f)"), | |
OldTimeStamp, DeltaTime, OldTimeStamp - ServerData->CurrentClientTimeStamp); | |
} | |
} | |
void UMMOPlayerMovement::ServerMoveDual_Implementation( | |
float TimeStamp0, | |
FVector_NetQuantize10 InAccel0, | |
uint8 PendingFlags, | |
uint32 View0, | |
float TimeStamp, | |
FVector_NetQuantize10 InAccel, | |
FVector_NetQuantize100 ClientLoc, | |
uint8 NewFlags, | |
uint8 ClientRoll, | |
uint32 View, | |
UPrimitiveComponent* ClientMovementBase, | |
FName ClientBaseBone, | |
uint8 ClientMovementMode) | |
{ | |
// Optional scoped movement update to combine moves for cheaper performance on the server. | |
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableServerDualMoveScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates); | |
ServerMove_Implementation(TimeStamp0, InAccel0, FVector(1.f, 2.f, 3.f), PendingFlags, ClientRoll, View0, ClientMovementBase, ClientBaseBone, ClientMovementMode); | |
ServerMove_Implementation(TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBone, ClientMovementMode); | |
} | |
void UMMOPlayerMovement::ServerMoveDualHybridRootMotion_Implementation( | |
float TimeStamp0, | |
FVector_NetQuantize10 InAccel0, | |
uint8 PendingFlags, | |
uint32 View0, | |
float TimeStamp, | |
FVector_NetQuantize10 InAccel, | |
FVector_NetQuantize100 ClientLoc, | |
uint8 NewFlags, | |
uint8 ClientRoll, | |
uint32 View, | |
UPrimitiveComponent* ClientMovementBase, | |
FName ClientBaseBone, | |
uint8 ClientMovementMode) | |
{ | |
// First move received didn't use root motion, process it as such. | |
CharacterOwner->bServerMoveIgnoreRootMotion = CharacterOwner->IsPlayingNetworkedRootMotionMontage(); | |
ServerMove_Implementation(TimeStamp0, InAccel0, FVector(1.f, 2.f, 3.f), PendingFlags, ClientRoll, View0, ClientMovementBase, ClientBaseBone, ClientMovementMode); | |
CharacterOwner->bServerMoveIgnoreRootMotion = false; | |
ServerMove_Implementation(TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBone, ClientMovementMode); | |
} | |
bool UMMOPlayerMovement::VerifyClientTimeStamp(float TimeStamp, FMMONetworkPredictionData_Server_Character& ServerData) | |
{ | |
bool bTimeStampResetDetected = false; | |
const bool bIsValid = IsClientTimeStampValid(TimeStamp, ServerData, bTimeStampResetDetected); | |
if (bIsValid) | |
{ | |
if (bTimeStampResetDetected) | |
{ | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("TimeStamp reset detected. CurrentTimeStamp: %f, new TimeStamp: %f"), ServerData.CurrentClientTimeStamp, TimeStamp); | |
LastTimeStampResetServerTime = GetWorld()->GetTimeSeconds(); | |
OnClientTimeStampResetDetected(); | |
ServerData.CurrentClientTimeStamp -= MinTimeBetweenTimeStampResets; | |
// Also apply the reset to any active root motions. | |
CurrentRootMotion.ApplyTimeStampReset(MinTimeBetweenTimeStampResets); | |
} | |
else | |
{ | |
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("TimeStamp %f Accepted! CurrentTimeStamp: %f"), TimeStamp, ServerData.CurrentClientTimeStamp); | |
ProcessClientTimeStampForTimeDiscrepancy(TimeStamp, ServerData); | |
} | |
return true; | |
} | |
else | |
{ | |
if (bTimeStampResetDetected) | |
{ | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("TimeStamp expired. Before TimeStamp Reset. CurrentTimeStamp: %f, TimeStamp: %f"), ServerData.CurrentClientTimeStamp, TimeStamp); | |
} | |
return false; | |
} | |
} | |
void UMMOPlayerMovement::ProcessClientTimeStampForTimeDiscrepancy(float ClientTimeStamp, FMMONetworkPredictionData_Server_Character& ServerData) | |
{ | |
// Should only be called on server in network games | |
check(CharacterOwner != NULL); | |
check(CharacterOwner->GetLocalRole() == ROLE_Authority); | |
checkSlow(GetNetMode() < NM_Client); | |
// Movement time discrepancy detection and resolution (potentially caused by client speed hacks, time manipulation) | |
// Track client reported time deltas through ServerMove RPCs vs actual server time, when error accumulates enough | |
// trigger prevention measures where client must "pay back" the time difference | |
const bool bServerMoveHasOccurred = ServerData.ServerTimeStampLastServerMove != 0.f; | |
const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject()); | |
if (GameNetworkManager != nullptr && GameNetworkManager->bMovementTimeDiscrepancyDetection && bServerMoveHasOccurred) | |
{ | |
const float WorldTimeSeconds = GetWorld()->GetTimeSeconds(); | |
const float ServerDelta = (WorldTimeSeconds - ServerData.ServerTimeStamp) * CharacterOwner->CustomTimeDilation; | |
const float ClientDelta = ClientTimeStamp - ServerData.CurrentClientTimeStamp; | |
const float ClientError = ClientDelta - ServerDelta; // Difference between how much time client has ticked since last move vs server | |
// Accumulate raw total discrepancy, unfiltered/unbound (for tracking more long-term trends over the lifetime of the CharacterMovementComponent) | |
ServerData.LifetimeRawTimeDiscrepancy += ClientError; | |
// | |
// 1. Determine total effective discrepancy | |
// | |
// NewTimeDiscrepancy is bounded and has a DriftAllowance to limit momentary burst packet loss or | |
// low framerate from having significant impacts, which could cause needing multiple seconds worth of | |
// slow-down/speed-up even though it wasn't intentional time manipulation | |
float NewTimeDiscrepancy = ServerData.TimeDiscrepancy + ClientError; | |
{ | |
// Apply drift allowance - forgiving percent difference per time for error | |
const float DriftAllowance = GameNetworkManager->MovementTimeDiscrepancyDriftAllowance; | |
if (DriftAllowance > 0.f) | |
{ | |
if (NewTimeDiscrepancy > 0.f) | |
{ | |
NewTimeDiscrepancy = FMath::Max(NewTimeDiscrepancy - ServerDelta * DriftAllowance, 0.f); | |
} | |
else | |
{ | |
NewTimeDiscrepancy = FMath::Min(NewTimeDiscrepancy + ServerDelta * DriftAllowance, 0.f); | |
} | |
} | |
// Enforce bounds | |
// Never go below MinTimeMargin - ClientError being negative means the client is BEHIND | |
// the server (they are going slower). | |
NewTimeDiscrepancy = FMath::Max(NewTimeDiscrepancy, GameNetworkManager->MovementTimeDiscrepancyMinTimeMargin); | |
} | |
// Determine EffectiveClientError, which is error for the currently-being-processed move after | |
// drift allowances/clamping/resolution mode modifications. | |
// We need to know how much the current move contributed towards actionable error so that we don't | |
// count error that the server never allowed to impact movement to matter | |
float EffectiveClientError = ClientError; | |
{ | |
const float NewTimeDiscrepancyRaw = ServerData.TimeDiscrepancy + ClientError; | |
if (NewTimeDiscrepancyRaw != 0.f) | |
{ | |
EffectiveClientError = ClientError * (NewTimeDiscrepancy / NewTimeDiscrepancyRaw); | |
} | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
// Per-frame spew of time discrepancy-related values - useful for investigating state of time discrepancy tracking | |
if (MMOCharacterMovementCVars::DebugTimeDiscrepancy > 0) | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("TimeDiscrepancyDetection: ClientError: %f, TimeDiscrepancy: %f, LifetimeRawTimeDiscrepancy: %f (Lifetime %f), Resolving: %d, ClientDelta: %f, ServerDelta: %f, ClientTimeStamp: %f"), | |
ClientError, ServerData.TimeDiscrepancy, ServerData.LifetimeRawTimeDiscrepancy, WorldTimeSeconds - ServerData.WorldCreationTime, ServerData.bResolvingTimeDiscrepancy, ClientDelta, ServerDelta, ClientTimeStamp); | |
} | |
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
// | |
// 2. If we were in resolution mode, determine if we still need to be | |
// | |
ServerData.bResolvingTimeDiscrepancy = ServerData.bResolvingTimeDiscrepancy && (ServerData.TimeDiscrepancy > 0.f); | |
// | |
// 3. Determine if NewTimeDiscrepancy is significant enough to trigger detection, and if so, trigger resolution if enabled | |
// | |
if (!ServerData.bResolvingTimeDiscrepancy) | |
{ | |
if (NewTimeDiscrepancy > GameNetworkManager->MovementTimeDiscrepancyMaxTimeMargin) | |
{ | |
// Time discrepancy detected - client timestamp ahead of where the server thinks it should be! | |
// Trigger logic for resolving time discrepancies | |
if (GameNetworkManager->bMovementTimeDiscrepancyResolution) | |
{ | |
// Trigger Resolution | |
ServerData.bResolvingTimeDiscrepancy = true; | |
// Transfer calculated error to official TimeDiscrepancy value, which is the time that will be resolved down | |
// in this and subsequent moves until it reaches 0 (meaning we equalize the error) | |
// Don't include contribution to error for this move, since we are now going to be in resolution mode | |
// and the expected client error (though it did help trigger resolution) won't be allowed | |
// to increase error this frame | |
ServerData.TimeDiscrepancy = (NewTimeDiscrepancy - EffectiveClientError); | |
} | |
else | |
{ | |
// We're detecting discrepancy but not handling resolving that through movement component. | |
// Clear time stamp error accumulated that triggered detection so we start fresh (maybe it was triggered | |
// during severe hitches/packet loss/other non-goodness) | |
ServerData.TimeDiscrepancy = 0.f; | |
} | |
// Project-specific resolution (reporting/recording/analytics) | |
OnTimeDiscrepancyDetected(NewTimeDiscrepancy, ServerData.LifetimeRawTimeDiscrepancy, WorldTimeSeconds - ServerData.WorldCreationTime, ClientError); | |
} | |
else | |
{ | |
// When not in resolution mode and still within error tolerances, accrue total discrepancy | |
ServerData.TimeDiscrepancy = NewTimeDiscrepancy; | |
} | |
} | |
// | |
// 4. If we are actively resolving time discrepancy, we do so by altering the DeltaTime for the current ServerMove | |
// | |
if (ServerData.bResolvingTimeDiscrepancy) | |
{ | |
// Optionally force client corrections during time discrepancy resolution | |
// This is useful when default project movement error checking is lenient or ClientAuthorativePosition is enabled | |
// to ensure time discrepancy resolution is enforced | |
if (GameNetworkManager->bMovementTimeDiscrepancyForceCorrectionsDuringResolution) | |
{ | |
ServerData.bForceClientUpdate = true; | |
} | |
// Movement time discrepancy resolution | |
// When the server has detected a significant time difference between what the client ServerMove RPCs are reporting | |
// and the actual time that has passed on the server (pointing to potential speed hacks/time manipulation by client), | |
// we enter a resolution mode where the usual "base delta's off of client's reported timestamps" is clamped down | |
// to the server delta since last movement update, so that during resolution we're not allowing further advantage. | |
// Out of that ServerDelta-based move delta, we also need the client to "pay back" the time stolen from initial | |
// time discrepancy detection (held in TimeDiscrepancy) at a specified rate (AGameNetworkManager::TimeDiscrepancyResolutionRate) | |
// to equalize movement time passed on client and server before we can consider the discrepancy "resolved" | |
const float ServerCurrentTimeStamp = WorldTimeSeconds; | |
const float ServerDeltaSinceLastMovementUpdate = (ServerCurrentTimeStamp - ServerData.ServerTimeStamp) * CharacterOwner->CustomTimeDilation; | |
const bool bIsFirstServerMoveThisServerTick = ServerDeltaSinceLastMovementUpdate > 0.f; | |
// Restrict ServerMoves to server deltas during time discrepancy resolution | |
// (basing moves off of trusted server time, not client timestamp deltas) | |
const float BaseDeltaTime = ServerData.GetBaseServerMoveDeltaTime(ClientTimeStamp, CharacterOwner->GetActorTimeDilation()); | |
if (!bIsFirstServerMoveThisServerTick) | |
{ | |
// Accumulate client deltas for multiple ServerMoves per server tick so that the next server tick | |
// can pay back the full amount of that tick and not be bounded by a single small Move delta | |
ServerData.TimeDiscrepancyAccumulatedClientDeltasSinceLastServerTick += BaseDeltaTime; | |
} | |
float ServerBoundDeltaTime = FMath::Min(BaseDeltaTime + ServerData.TimeDiscrepancyAccumulatedClientDeltasSinceLastServerTick, ServerDeltaSinceLastMovementUpdate); | |
ServerBoundDeltaTime = FMath::Max(ServerBoundDeltaTime, 0.f); // No negative deltas allowed | |
if (bIsFirstServerMoveThisServerTick) | |
{ | |
// The first ServerMove for a server tick has used the accumulated client delta in the ServerBoundDeltaTime | |
// calculation above, clear it out for next frame where we have multiple ServerMoves | |
ServerData.TimeDiscrepancyAccumulatedClientDeltasSinceLastServerTick = 0.f; | |
} | |
// Calculate current move DeltaTime and PayBack time based on resolution rate | |
const float ResolutionRate = FMath::Clamp(GameNetworkManager->MovementTimeDiscrepancyResolutionRate, 0.f, 1.f); | |
float TimeToPayBack = FMath::Min(ServerBoundDeltaTime * ResolutionRate, ServerData.TimeDiscrepancy); // Make sure we only pay back the time we need to | |
float DeltaTimeAfterPayback = ServerBoundDeltaTime - TimeToPayBack; | |
// Adjust deltas so current move DeltaTime adheres to minimum tick time | |
DeltaTimeAfterPayback = FMath::Max(DeltaTimeAfterPayback, UMMOPlayerMovement::MIN_TICK_TIME); | |
TimeToPayBack = ServerBoundDeltaTime - DeltaTimeAfterPayback; | |
// Output of resolution: an overridden delta time that will be picked up for this ServerMove, and removing the time | |
// we paid back by overriding the DeltaTime to TimeDiscrepancy (time needing resolved) | |
ServerData.TimeDiscrepancyResolutionMoveDeltaOverride = DeltaTimeAfterPayback; | |
ServerData.TimeDiscrepancy -= TimeToPayBack; | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
// Per-frame spew of time discrepancy resolution related values - useful for investigating state of time discrepancy tracking | |
if (MMOCharacterMovementCVars::DebugTimeDiscrepancy > 1) | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("TimeDiscrepancyResolution: DeltaOverride: %f, TimeToPayBack: %f, BaseDelta: %f, ServerDeltaSinceLastMovementUpdate: %f, TimeDiscrepancyAccumulatedClientDeltasSinceLastServerTick: %f"), | |
ServerData.TimeDiscrepancyResolutionMoveDeltaOverride, TimeToPayBack, BaseDeltaTime, ServerDeltaSinceLastMovementUpdate, ServerData.TimeDiscrepancyAccumulatedClientDeltasSinceLastServerTick); | |
} | |
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
} | |
} | |
} | |
bool UMMOPlayerMovement::IsClientTimeStampValid(float TimeStamp, const FMMONetworkPredictionData_Server_Character& ServerData, bool& bTimeStampResetDetected) const | |
{ | |
if (TimeStamp <= 0.f || !FMath::IsFinite(TimeStamp)) | |
{ | |
return false; | |
} | |
// Very large deltas happen around a TimeStamp reset. | |
const float DeltaTimeStamp = (TimeStamp - ServerData.CurrentClientTimeStamp); | |
if (FMath::Abs(DeltaTimeStamp) > (MinTimeBetweenTimeStampResets * 0.5f)) | |
{ | |
// Client is resetting TimeStamp to increase accuracy. | |
bTimeStampResetDetected = true; | |
if (DeltaTimeStamp < 0.f) | |
{ | |
// Validate that elapsed time since last reset is reasonable, otherwise client could be manipulating resets. | |
if (GetWorld()->TimeSince(LastTimeStampResetServerTime) < (MinTimeBetweenTimeStampResets * 0.5f)) | |
{ | |
// Reset too recently | |
return false; | |
} | |
else | |
{ | |
// TimeStamp accepted with reset | |
return true; | |
} | |
} | |
else | |
{ | |
// We already reset the TimeStamp, but we just got an old outdated move before the switch, not valid. | |
return false; | |
} | |
} | |
// If TimeStamp is in the past, move is outdated, not valid. | |
if (TimeStamp <= ServerData.CurrentClientTimeStamp) | |
{ | |
return false; | |
} | |
// Precision issues (or reordered timestamps from old moves) can cause very small or zero deltas which cause problems. | |
if (DeltaTimeStamp < UMMOPlayerMovement::MIN_TICK_TIME) | |
{ | |
return false; | |
} | |
// TimeStamp valid. | |
return true; | |
} | |
void UMMOPlayerMovement::OnClientTimeStampResetDetected() | |
{ | |
} | |
void UMMOPlayerMovement::OnTimeDiscrepancyDetected(float CurrentTimeDiscrepancy, float LifetimeRawTimeDiscrepancy, float Lifetime, float CurrentMoveError) | |
{ | |
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("Movement Time Discrepancy detected between client-reported time and server on character %s. CurrentTimeDiscrepancy: %f, LifetimeRawTimeDiscrepancy: %f, Lifetime: %f, CurrentMoveError %f"), | |
CharacterOwner ? *CharacterOwner->GetHumanReadableName() : TEXT("<UNKNOWN>"), | |
CurrentTimeDiscrepancy, | |
LifetimeRawTimeDiscrepancy, | |
Lifetime, | |
CurrentMoveError); | |
} | |
void UMMOPlayerMovement::ServerMove(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
if (MovementBaseUtility::IsDynamicBase(ClientMovementBase)) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("ServerMove: base %s"), *ClientMovementBase->GetName()); | |
CharacterOwner->ServerMove(TimeStamp, InAccel, ClientLoc, CompressedMoveFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
else | |
{ | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("ServerMoveNoBase")); | |
CharacterOwner->ServerMoveNoBase(TimeStamp, InAccel, ClientLoc, CompressedMoveFlags, ClientRoll, View, ClientMovementMode); | |
} | |
} | |
void UMMOPlayerMovement::ServerMove_Implementation( | |
float TimeStamp, | |
FVector_NetQuantize10 InAccel, | |
FVector_NetQuantize100 ClientLoc, | |
uint8 MoveFlags, | |
uint8 ClientRoll, | |
uint32 View, | |
UPrimitiveComponent* ClientMovementBase, | |
FName ClientBaseBoneName, | |
uint8 ClientMovementMode) | |
{ | |
if (!HasValidData() || !IsActive()) | |
{ | |
return; | |
} | |
FMMONetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character(); | |
check(ServerData); | |
if (!VerifyClientTimeStamp(TimeStamp, *ServerData)) | |
{ | |
const float ServerTimeStamp = ServerData->CurrentClientTimeStamp; | |
// This is more severe if the timestamp has a large discrepancy and hasn't been recently reset. | |
if (ServerTimeStamp > 1.0f && FMath::Abs(ServerTimeStamp - TimeStamp) > MMOCharacterMovementCVars::NetServerMoveTimestampExpiredWarningThreshold) | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("ServerMove: TimeStamp expired: %f, CurrentTimeStamp: %f, Character: %s"), TimeStamp, ServerTimeStamp, *GetNameSafe(CharacterOwner)); | |
} | |
else | |
{ | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("ServerMove: TimeStamp expired: %f, CurrentTimeStamp: %f, Character: %s"), TimeStamp, ServerTimeStamp, *GetNameSafe(CharacterOwner)); | |
} | |
return; | |
} | |
bool bServerReadyForClient = true; | |
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController()); | |
if (PC) | |
{ | |
bServerReadyForClient = PC->NotifyServerReceivedClientData(CharacterOwner, TimeStamp); | |
if (!bServerReadyForClient) | |
{ | |
InAccel = FVector::ZeroVector; | |
} | |
} | |
// View components | |
const uint16 ViewPitch = (View & 65535); | |
const uint16 ViewYaw = (View >> 16); | |
const FVector Accel = InAccel; | |
const UWorld* MyWorld = GetWorld(); | |
const float DeltaTime = ServerData->GetServerMoveDeltaTime(TimeStamp, CharacterOwner->GetActorTimeDilation(*MyWorld)); | |
ServerData->CurrentClientTimeStamp = TimeStamp; | |
ServerData->ServerAccumulatedClientTimeStamp += DeltaTime; | |
ServerData->ServerTimeStamp = MyWorld->GetTimeSeconds(); | |
ServerData->ServerTimeStampLastServerMove = ServerData->ServerTimeStamp; | |
FRotator ViewRot; | |
ViewRot.Pitch = FRotator::DecompressAxisFromShort(ViewPitch); | |
ViewRot.Yaw = FRotator::DecompressAxisFromShort(ViewYaw); | |
ViewRot.Roll = FRotator::DecompressAxisFromByte(ClientRoll); | |
if (PC) | |
{ | |
PC->SetControlRotation(ViewRot); | |
} | |
if (!bServerReadyForClient) | |
{ | |
return; | |
} | |
// Perform actual movement | |
if ((MyWorld->GetWorldSettings()->GetPauserPlayerState() == NULL) && (DeltaTime > 0.f)) | |
{ | |
if (PC) | |
{ | |
PC->UpdateRotation(DeltaTime); | |
} | |
MoveAutonomous(TimeStamp, DeltaTime, MoveFlags, Accel); | |
} | |
UE_CLOG(CharacterOwner && UpdatedComponent, LogNetPlayerMovement, VeryVerbose, TEXT("ServerMove Time %f Acceleration %s Velocity %s Position %s Rotation %s DeltaTime %f Mode %s MovementBase %s.%s (Dynamic:%d)"), | |
TimeStamp, *Accel.ToString(), *Velocity.ToString(), *UpdatedComponent->GetComponentLocation().ToString(), *UpdatedComponent->GetComponentRotation().ToCompactString(), DeltaTime, *GetMovementName(), | |
*GetNameSafe(GetMovementBase()), *CharacterOwner->GetBasedMovement().BoneName.ToString(), MovementBaseUtility::IsDynamicBase(GetMovementBase()) ? 1 : 0); | |
ServerMoveHandleClientError(TimeStamp, DeltaTime, Accel, ClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
void UMMOPlayerMovement::ServerMoveHandleClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& RelativeClientLoc, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
if (RelativeClientLoc == FVector(1.f, 2.f, 3.f)) // first part of double servermove | |
{ | |
return; | |
} | |
FMMONetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character(); | |
check(ServerData); | |
// Don't prevent more recent updates from being sent if received this frame. | |
// We're going to send out an update anyway, might as well be the most recent one. | |
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController()); | |
if ((ServerData->LastUpdateTime != GetWorld()->TimeSeconds)) | |
{ | |
const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject()); | |
if (GameNetworkManager->WithinUpdateDelayBounds(PC, ServerData->LastUpdateTime)) | |
{ | |
return; | |
} | |
} | |
// Offset may be relative to base component | |
FVector ClientLoc = RelativeClientLoc; | |
if (MovementBaseUtility::UseRelativeLocation(ClientMovementBase)) | |
{ | |
FVector BaseLocation; | |
FQuat BaseRotation; | |
MovementBaseUtility::GetMovementBaseTransform(ClientMovementBase, ClientBaseBoneName, BaseLocation, BaseRotation); | |
ClientLoc += BaseLocation; | |
} | |
// Client may send a null movement base when walking on bases with no relative location (to save bandwidth). | |
// In this case don't check movement base in error conditions, use the server one (which avoids an error based on differing bases). Position will still be validated. | |
if (ClientMovementBase == nullptr && ClientMovementMode == MOVE_Walking) | |
{ | |
ClientMovementBase = CharacterOwner->GetBasedMovement().MovementBase; | |
ClientBaseBoneName = CharacterOwner->GetBasedMovement().BoneName; | |
} | |
// Compute the client error from the server's position | |
// If client has accumulated a noticeable positional error, correct them. | |
bNetworkLargeClientCorrection = ServerData->bForceClientUpdate; | |
if (ServerData->bForceClientUpdate || ServerCheckClientError(ClientTimeStamp, DeltaTime, Accel, ClientLoc, RelativeClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode)) | |
{ | |
UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase(); | |
ServerData->PendingAdjustment.NewVel = Velocity; | |
ServerData->PendingAdjustment.NewBase = MovementBase; | |
ServerData->PendingAdjustment.NewBaseBoneName = CharacterOwner->GetBasedMovement().BoneName; | |
ServerData->PendingAdjustment.NewLoc = FRepMovement::RebaseOntoZeroOrigin(UpdatedComponent->GetComponentLocation(), this); | |
ServerData->PendingAdjustment.NewRot = UpdatedComponent->GetComponentRotation(); | |
ServerData->PendingAdjustment.bBaseRelativePosition = MovementBaseUtility::UseRelativeLocation(MovementBase); | |
if (ServerData->PendingAdjustment.bBaseRelativePosition) | |
{ | |
// Relative location | |
ServerData->PendingAdjustment.NewLoc = CharacterOwner->GetBasedMovement().Location; | |
// TODO: this could be a relative rotation, but all client corrections ignore rotation right now except the root motion one, which would need to be updated. | |
//ServerData->PendingAdjustment.NewRot = CharacterOwner->GetBasedMovement().Rotation; | |
} | |
#if !UE_BUILD_SHIPPING | |
if (MMOCharacterMovementCVars::MMONetShowCorrections != 0) | |
{ | |
const FVector LocDiff = UpdatedComponent->GetComponentLocation() - ClientLoc; | |
const FString BaseString = MovementBase ? MovementBase->GetPathName(MovementBase->GetOutermost()) : TEXT("None"); | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("*** Server: Error for %s at Time=%.3f is %3.3f LocDiff(%s) ClientLoc(%s) ServerLoc(%s) Base: %s Bone: %s Accel(%s) Velocity(%s)"), | |
*GetNameSafe(CharacterOwner), ClientTimeStamp, LocDiff.Size(), *LocDiff.ToString(), *ClientLoc.ToString(), *UpdatedComponent->GetComponentLocation().ToString(), *BaseString, *ServerData->PendingAdjustment.NewBaseBoneName.ToString(), *Accel.ToString(), *Velocity.ToString()); | |
const float DebugLifetime = MMOCharacterMovementCVars::MMONetCorrectionLifetime; | |
DrawDebugCapsule(GetWorld(), UpdatedComponent->GetComponentLocation(), CharacterOwner->GetSimpleCollisionHalfHeight(), CharacterOwner->GetSimpleCollisionRadius(), FQuat::Identity, FColor(100, 255, 100), false, DebugLifetime); | |
DrawDebugCapsule(GetWorld(), ClientLoc, CharacterOwner->GetSimpleCollisionHalfHeight(), CharacterOwner->GetSimpleCollisionRadius(), FQuat::Identity, FColor(255, 100, 100), false, DebugLifetime); | |
} | |
#endif | |
ServerData->LastUpdateTime = GetWorld()->TimeSeconds; | |
ServerData->PendingAdjustment.DeltaTime = DeltaTime; | |
ServerData->PendingAdjustment.TimeStamp = ClientTimeStamp; | |
ServerData->PendingAdjustment.bAckGoodMove = false; | |
ServerData->PendingAdjustment.MovementMode = PackNetworkMovementMode(); | |
// #if USE_SERVER_PERF_COUNTERS | |
// PerfCountersIncrement(PerfCounter_NumServerMoveCorrections); | |
// #endif | |
} | |
else | |
{ | |
if (ServerShouldUseAuthoritativePosition(ClientTimeStamp, DeltaTime, Accel, ClientLoc, RelativeClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode)) | |
{ | |
const FVector LocDiff = UpdatedComponent->GetComponentLocation() - ClientLoc; //-V595 | |
if (!LocDiff.IsZero() || ClientMovementMode != PackNetworkMovementMode() || GetMovementBase() != ClientMovementBase || (CharacterOwner && CharacterOwner->GetBasedMovement().BoneName != ClientBaseBoneName)) | |
{ | |
// Just set the position. On subsequent moves we will resolve initially overlapping conditions. | |
UpdatedComponent->SetWorldLocation(ClientLoc, false); //-V595 | |
// Trust the client's movement mode. | |
ApplyNetworkMovementMode(ClientMovementMode); | |
// Update base and floor at new location. | |
SetBase(ClientMovementBase, ClientBaseBoneName); | |
UpdateFloorFromAdjustment(); | |
// Even if base has not changed, we need to recompute the relative offsets (since we've moved). | |
SaveBaseLocation(); | |
LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector; | |
LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity; | |
LastUpdateVelocity = Velocity; | |
} | |
} | |
// acknowledge receipt of this successful servermove() | |
ServerData->PendingAdjustment.TimeStamp = ClientTimeStamp; | |
ServerData->PendingAdjustment.bAckGoodMove = true; | |
} | |
// #if USE_SERVER_PERF_COUNTERS | |
// PerfCountersIncrement(PerfCounter_NumServerMoves); | |
// #endif | |
ServerData->bForceClientUpdate = false; | |
} | |
bool UMMOPlayerMovement::ServerCheckClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
// Check location difference against global setting | |
if (!bIgnoreClientMovementErrorChecksAndCorrection) | |
{ | |
if (ServerExceedsAllowablePositionError(ClientTimeStamp, DeltaTime, Accel, ClientWorldLocation, RelativeClientLocation, ClientMovementBase, ClientBaseBoneName, ClientMovementMode)) | |
{ | |
return true; | |
} | |
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
if (MMOCharacterMovementCVars::NetForceClientAdjustmentPercent > SMALL_NUMBER) | |
{ | |
if (RandomStream.FRand() < MMOCharacterMovementCVars::NetForceClientAdjustmentPercent) | |
{ | |
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("** ServerCheckClientError forced by p.NetForceClientAdjustmentPercent")); | |
return true; | |
} | |
} | |
#endif | |
} | |
else | |
{ | |
#if !UE_BUILD_SHIPPING | |
if (MMOCharacterMovementCVars::MMONetShowCorrections != 0) | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("*** Server: %s is set to ignore error checks and corrections."), *GetNameSafe(CharacterOwner)); | |
} | |
#endif // !UE_BUILD_SHIPPING | |
} | |
return false; | |
} | |
bool UMMOPlayerMovement::ServerExceedsAllowablePositionError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
// Check for disagreement in movement mode | |
const uint8 CurrentPackedMovementMode = PackNetworkMovementMode(); | |
if (CurrentPackedMovementMode != ClientMovementMode) | |
{ | |
// Consider this a major correction, see SendClientAdjustment() | |
bNetworkLargeClientCorrection = true; | |
return true; | |
} | |
const FVector LocDiff = UpdatedComponent->GetComponentLocation() - ClientWorldLocation; | |
const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject()); | |
if (GameNetworkManager->ExceedsAllowablePositionError(LocDiff)) | |
{ | |
bNetworkLargeClientCorrection |= (LocDiff.SizeSquared() > FMath::Square(NetworkLargeClientCorrectionDistance)); | |
return true; | |
} | |
return false; | |
} | |
bool UMMOPlayerMovement::ServerShouldUseAuthoritativePosition(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
if (bServerAcceptClientAuthoritativePosition) | |
{ | |
return true; | |
} | |
const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject()); | |
if (GameNetworkManager->ClientAuthorativePosition) | |
{ | |
return true; | |
} | |
return false; | |
} | |
bool UMMOPlayerMovement::ServerMove_Validate(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 MoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
return true; | |
} | |
void UMMOPlayerMovement::ServerMoveDual(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
if (MovementBaseUtility::IsDynamicBase(ClientMovementBase)) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("ServerMoveDual: base %s"), *ClientMovementBase->GetName()); | |
CharacterOwner->ServerMoveDual(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
else | |
{ | |
//UE_LOG(LogMMOCharacterMovement, Log, TEXT("ServerMoveDualNoBase")); | |
CharacterOwner->ServerMoveDualNoBase(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementMode); | |
} | |
} | |
bool UMMOPlayerMovement::ServerMoveDual_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
return true; | |
} | |
void UMMOPlayerMovement::ServerMoveDualHybridRootMotion(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
CharacterOwner->ServerMoveDualHybridRootMotion(TimeStamp0, InAccel0, PendingFlags, View0, TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBoneName, ClientMovementMode); | |
} | |
bool UMMOPlayerMovement::ServerMoveDualHybridRootMotion_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) | |
{ | |
return true; | |
} | |
void UMMOPlayerMovement::ServerMoveOld(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags) | |
{ | |
CharacterOwner->ServerMoveOld(OldTimeStamp, OldAccel, OldMoveFlags); | |
} | |
bool UMMOPlayerMovement::ServerMoveOld_Validate(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags) | |
{ | |
return true; | |
} | |
void UMMOPlayerMovement::MoveAutonomous | |
( | |
float ClientTimeStamp, | |
float DeltaTime, | |
uint8 CompressedFlags, | |
const FVector& NewAccel | |
) | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
UpdateFromCompressedFlags(CompressedFlags); | |
CharacterOwner->CheckJumpInput(DeltaTime); | |
Acceleration = ConstrainInputAcceleration(NewAccel); | |
Acceleration = Acceleration.GetClampedToMaxSize(GetMaxAcceleration()); | |
AnalogInputModifier = ComputeAnalogInputModifier(); | |
const FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
const FQuat OldRotation = UpdatedComponent->GetComponentQuat(); | |
const bool bWasPlayingRootMotion = CharacterOwner->IsPlayingRootMotion(); | |
PerformMovement(DeltaTime); | |
// Check if data is valid as PerformMovement can mark character for pending kill | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// If not playing root motion, tick animations after physics. We do this here to keep events, notifies, states and transitions in sync with client updates. | |
if (CharacterOwner && !CharacterOwner->bClientUpdating && !CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh()) | |
{ | |
if (!bWasPlayingRootMotion) // If we were playing root motion before PerformMovement but aren't anymore, we're on the last frame of anim root motion and have already ticked character | |
{ | |
TickCharacterPose(DeltaTime); | |
} | |
// TODO: SaveBaseLocation() in case tick moves us? | |
// Trigger Events right away, as we could be receiving multiple ServerMoves per frame. | |
CharacterOwner->GetMesh()->ConditionallyDispatchQueuedAnimEvents(); | |
} | |
if (CharacterOwner && UpdatedComponent) | |
{ | |
// Smooth local view of remote clients on listen servers | |
if (MMOCharacterMovementCVars::NetEnableListenServerSmoothing && | |
CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy && | |
IsNetMode(NM_ListenServer)) | |
{ | |
SmoothCorrection(OldLocation, OldRotation, UpdatedComponent->GetComponentLocation(), UpdatedComponent->GetComponentQuat()); | |
} | |
} | |
} | |
void UMMOPlayerMovement::UpdateFloorFromAdjustment() | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
// If walking, try to update the cached floor so it is current. This is necessary for UpdateBasedMovement() and MoveAlongFloor() to work properly. | |
// If base is now NULL, presumably we are no longer walking. If we had a valid floor but don't find one now, we'll likely start falling. | |
if (CharacterOwner->GetMovementBase()) | |
{ | |
FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, false); | |
} | |
else | |
{ | |
CurrentFloor.Clear(); | |
} | |
} | |
void UMMOPlayerMovement::SendClientAdjustment() | |
{ | |
if (!HasValidData()) | |
{ | |
return; | |
} | |
FMMONetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character(); | |
check(ServerData); | |
if (ServerData->PendingAdjustment.TimeStamp <= 0.f) | |
{ | |
return; | |
} | |
const float CurrentTime = GetWorld()->GetTimeSeconds(); | |
if (ServerData->PendingAdjustment.bAckGoodMove) | |
{ | |
// just notify client this move was received | |
if (CurrentTime - ServerLastClientGoodMoveAckTime > NetworkMinTimeBetweenClientAckGoodMoves) | |
{ | |
ServerLastClientGoodMoveAckTime = CurrentTime; | |
ClientAckGoodMove(ServerData->PendingAdjustment.TimeStamp); | |
} | |
} | |
else | |
{ | |
// We won't be back in here until the next client move and potential correction is received, so use the correct time now. | |
// Protect against bad data by taking appropriate min/max of editable values. | |
const float AdjustmentTimeThreshold = bNetworkLargeClientCorrection ? | |
FMath::Min(NetworkMinTimeBetweenClientAdjustmentsLargeCorrection, NetworkMinTimeBetweenClientAdjustments) : | |
FMath::Max(NetworkMinTimeBetweenClientAdjustmentsLargeCorrection, NetworkMinTimeBetweenClientAdjustments); | |
// Check if correction is throttled based on time limit between updates. | |
if (CurrentTime - ServerLastClientAdjustmentTime > AdjustmentTimeThreshold) | |
{ | |
ServerLastClientAdjustmentTime = CurrentTime; | |
const bool bIsPlayingNetworkedRootMotionMontage = CharacterOwner->IsPlayingNetworkedRootMotionMontage(); | |
if (CurrentRootMotion.HasActiveRootMotionSources()) | |
{ | |
FRotator Rotation = ServerData->PendingAdjustment.NewRot.GetNormalized(); | |
FVector_NetQuantizeNormal CompressedRotation(Rotation.Pitch / 180.f, Rotation.Yaw / 180.f, Rotation.Roll / 180.f); | |
ClientAdjustRootMotionSourcePosition | |
( | |
ServerData->PendingAdjustment.TimeStamp, | |
CurrentRootMotion, | |
bIsPlayingNetworkedRootMotionMontage, | |
bIsPlayingNetworkedRootMotionMontage ? CharacterOwner->GetRootMotionAnimMontageInstance()->GetPosition() : -1.f, | |
ServerData->PendingAdjustment.NewLoc, | |
CompressedRotation, | |
ServerData->PendingAdjustment.NewVel.Z, | |
ServerData->PendingAdjustment.NewBase, | |
ServerData->PendingAdjustment.NewBaseBoneName, | |
ServerData->PendingAdjustment.NewBase != NULL, | |
ServerData->PendingAdjustment.bBaseRelativePosition, | |
PackNetworkMovementMode() | |
); | |
} | |
else if (bIsPlayingNetworkedRootMotionMontage) | |
{ | |
FRotator Rotation = ServerData->PendingAdjustment.NewRot.GetNormalized(); | |
FVector_NetQuantizeNormal CompressedRotation(Rotation.Pitch / 180.f, Rotation.Yaw / 180.f, Rotation.Roll / 180.f); | |
ClientAdjustRootMotionPosition | |
( | |
ServerData->PendingAdjustment.TimeStamp, | |
CharacterOwner->GetRootMotionAnimMontageInstance()->GetPosition(), | |
ServerData->PendingAdjustment.NewLoc, | |
CompressedRotation, | |
ServerData->PendingAdjustment.NewVel.Z, | |
ServerData->PendingAdjustment.NewBase, | |
ServerData->PendingAdjustment.NewBaseBoneName, | |
ServerData->PendingAdjustment.NewBase != NULL, | |
ServerData->PendingAdjustment.bBaseRelativePosition, | |
PackNetworkMovementMode() | |
); | |
} | |
else if (ServerData->PendingAdjustment.NewVel.IsZero()) | |
{ | |
ClientVeryShortAdjustPosition | |
( | |
ServerData->PendingAdjustment.TimeStamp, | |
ServerData->PendingAdjustment.NewLoc, | |
ServerData->PendingAdjustment.NewBase, | |
ServerData->PendingAdjustment.NewBaseBoneName, | |
ServerData->PendingAdjustment.NewBase != NULL, | |
ServerData->PendingAdjustment.bBaseRelativePosition, | |
PackNetworkMovementMode() | |
); | |
} | |
else | |
{ | |
ClientAdjustPosition | |
( | |
ServerData->PendingAdjustment.TimeStamp, | |
ServerData->PendingAdjustment.NewLoc, | |
ServerData->PendingAdjustment.NewVel, | |
ServerData->PendingAdjustment.NewBase, | |
ServerData->PendingAdjustment.NewBaseBoneName, | |
ServerData->PendingAdjustment.NewBase != NULL, | |
ServerData->PendingAdjustment.bBaseRelativePosition, | |
PackNetworkMovementMode() | |
); | |
} | |
} | |
} | |
ServerData->PendingAdjustment.TimeStamp = 0; | |
ServerData->PendingAdjustment.bAckGoodMove = false; | |
ServerData->bForceClientUpdate = false; | |
} | |
void UMMOPlayerMovement::ClientVeryShortAdjustPosition(float TimeStamp, FVector NewLoc, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
CharacterOwner->ClientVeryShortAdjustPosition(TimeStamp, NewLoc, NewBase, NewBaseBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
void UMMOPlayerMovement::ClientVeryShortAdjustPosition_Implementation | |
( | |
float TimeStamp, | |
FVector NewLoc, | |
UPrimitiveComponent* NewBase, | |
FName NewBaseBoneName, | |
bool bHasBase, | |
bool bBaseRelativePosition, | |
uint8 ServerMovementMode | |
) | |
{ | |
if (HasValidData()) | |
{ | |
ClientAdjustPosition(TimeStamp, NewLoc, FVector::ZeroVector, NewBase, NewBaseBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
} | |
void UMMOPlayerMovement::ClientAdjustPosition(float TimeStamp, FVector NewLoc, FVector NewVel, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
CharacterOwner->ClientAdjustPosition(TimeStamp, NewLoc, NewVel, NewBase, NewBaseBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
void UMMOPlayerMovement::ClientAdjustPosition_Implementation | |
( | |
float TimeStamp, | |
FVector NewLocation, | |
FVector NewVelocity, | |
UPrimitiveComponent* NewBase, | |
FName NewBaseBoneName, | |
bool bHasBase, | |
bool bBaseRelativePosition, | |
uint8 ServerMovementMode | |
) | |
{ | |
if (!HasValidData() || !IsActive()) | |
{ | |
return; | |
} | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
check(ClientData); | |
// Make sure the base actor exists on this client. | |
const bool bUnresolvedBase = bHasBase && (NewBase == NULL); | |
if (bUnresolvedBase) | |
{ | |
if (bBaseRelativePosition) | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("ClientAdjustPosition_Implementation could not resolve the new relative movement base actor, ignoring server correction! Client currently at world location %s on base %s"), | |
*UpdatedComponent->GetComponentLocation().ToString(), *GetNameSafe(GetMovementBase())); | |
return; | |
} | |
else | |
{ | |
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ClientAdjustPosition_Implementation could not resolve the new absolute movement base actor, but WILL use the position!")); | |
} | |
} | |
// Ack move if it has not expired. | |
int32 MoveIndex = ClientData->GetSavedMoveIndex(TimeStamp); | |
if (MoveIndex == INDEX_NONE) | |
{ | |
if (ClientData->LastAckedMove.IsValid()) | |
{ | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("ClientAdjustPosition_Implementation could not find Move for TimeStamp: %f, LastAckedTimeStamp: %f, CurrentTimeStamp: %f"), TimeStamp, ClientData->LastAckedMove->TimeStamp, ClientData->CurrentTimeStamp); | |
} | |
return; | |
} | |
ClientData->AckMove(MoveIndex, *this); | |
FVector WorldShiftedNewLocation; | |
// Received Location is relative to dynamic base | |
if (bBaseRelativePosition) | |
{ | |
FVector BaseLocation; | |
FQuat BaseRotation; | |
MovementBaseUtility::GetMovementBaseTransform(NewBase, NewBaseBoneName, BaseLocation, BaseRotation); // TODO: error handling if returns false | |
WorldShiftedNewLocation = NewLocation + BaseLocation; | |
} | |
else | |
{ | |
WorldShiftedNewLocation = FRepMovement::RebaseOntoLocalOrigin(NewLocation, this); | |
} | |
// Trigger event | |
OnClientCorrectionReceived(*ClientData, TimeStamp, WorldShiftedNewLocation, NewVelocity, NewBase, NewBaseBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
// Trust the server's positioning. | |
if (UpdatedComponent) | |
{ | |
UpdatedComponent->SetWorldLocation(WorldShiftedNewLocation, false, nullptr, ETeleportType::TeleportPhysics); | |
} | |
Velocity = NewVelocity; | |
// Trust the server's movement mode | |
UPrimitiveComponent* PreviousBase = CharacterOwner->GetMovementBase(); | |
ApplyNetworkMovementMode(ServerMovementMode); | |
// Set base component | |
UPrimitiveComponent* FinalBase = NewBase; | |
FName FinalBaseBoneName = NewBaseBoneName; | |
if (bUnresolvedBase) | |
{ | |
check(NewBase == NULL); | |
check(!bBaseRelativePosition); | |
// We had an unresolved base from the server | |
// If walking, we'd like to continue walking if possible, to avoid falling for a frame, so try to find a base where we moved to. | |
if (PreviousBase && UpdatedComponent) | |
{ | |
FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, false); | |
if (CurrentFloor.IsWalkableFloor()) | |
{ | |
FinalBase = CurrentFloor.HitResult.Component.Get(); | |
FinalBaseBoneName = CurrentFloor.HitResult.BoneName; | |
} | |
else | |
{ | |
FinalBase = nullptr; | |
FinalBaseBoneName = NAME_None; | |
} | |
} | |
} | |
SetBase(FinalBase, FinalBaseBoneName); | |
// Update floor at new location | |
UpdateFloorFromAdjustment(); | |
bJustTeleported = true; | |
// Even if base has not changed, we need to recompute the relative offsets (since we've moved). | |
SaveBaseLocation(); | |
LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector; | |
LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity; | |
LastUpdateVelocity = Velocity; | |
UpdateComponentVelocity(); | |
ClientData->bUpdatePosition = true; | |
} | |
void UMMOPlayerMovement::ClientAdjustRootMotionPosition(float TimeStamp, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
CharacterOwner->ClientAdjustRootMotionPosition(TimeStamp, ServerMontageTrackPosition, ServerLoc, ServerRotation, ServerVelZ, ServerBase, ServerBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
void UMMOPlayerMovement::OnClientCorrectionReceived(FMMONetworkPredictionData_Client_Character& ClientData, float TimeStamp, FVector NewLocation, FVector NewVelocity, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
#if !UE_BUILD_SHIPPING | |
if (MMOCharacterMovementCVars::MMONetShowCorrections != 0) | |
{ | |
const FVector ClientLocAtCorrectedMove = ClientData.LastAckedMove.IsValid() ? ClientData.LastAckedMove->SavedLocation : UpdatedComponent->GetComponentLocation(); | |
const FVector LocDiff = ClientLocAtCorrectedMove - NewLocation; | |
const FString NewBaseString = NewBase ? NewBase->GetPathName(NewBase->GetOutermost()) : TEXT("None"); | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("*** Client: Error for %s at Time=%.3f is %3.3f LocDiff(%s) ClientLoc(%s) ServerLoc(%s) NewBase: %s NewBone: %s ClientVel(%s) ServerVel(%s) SavedMoves %d"), | |
*GetNameSafe(CharacterOwner), TimeStamp, LocDiff.Size(), *LocDiff.ToString(), *ClientLocAtCorrectedMove.ToString(), *NewLocation.ToString(), *NewBaseString, *NewBaseBoneName.ToString(), *Velocity.ToString(), *NewVelocity.ToString(), ClientData.SavedMoves.Num()); | |
const float DebugLifetime = MMOCharacterMovementCVars::MMONetCorrectionLifetime; | |
if (!LocDiff.IsNearlyZero()) | |
{ | |
// When server corrects us to a new location, draw red at location where client thought they were, green where the server corrected us to | |
DrawDebugCapsule(GetWorld(), ClientLocAtCorrectedMove, CharacterOwner->GetSimpleCollisionHalfHeight(), CharacterOwner->GetSimpleCollisionRadius(), FQuat::Identity, FColor(255, 100, 100), false, DebugLifetime); | |
DrawDebugCapsule(GetWorld(), NewLocation, CharacterOwner->GetSimpleCollisionHalfHeight(), CharacterOwner->GetSimpleCollisionRadius(), FQuat::Identity, FColor(100, 255, 100), false, DebugLifetime); | |
} | |
else | |
{ | |
// When we receive a server correction that doesn't change our position from where our client move had us, draw yellow (otherwise would be overlapping) | |
// This occurs when we receive an initial correction, replay moves to get us into the right location, and then receive subsequent corrections by the server (who doesn't know if we corrected already | |
// so continues to send corrections). This is a "no-op" server correction with regards to location since we already corrected (occurs with latency) | |
DrawDebugCapsule(GetWorld(), NewLocation, CharacterOwner->GetSimpleCollisionHalfHeight(), CharacterOwner->GetSimpleCollisionRadius(), FQuat::Identity, FColor(255, 255, 100), false, DebugLifetime); | |
} | |
} | |
#endif //!UE_BUILD_SHIPPING | |
} | |
void UMMOPlayerMovement::ClientAdjustRootMotionPosition_Implementation( | |
float TimeStamp, | |
float ServerMontageTrackPosition, | |
FVector ServerLoc, | |
FVector_NetQuantizeNormal ServerRotation, | |
float ServerVelZ, | |
UPrimitiveComponent* ServerBase, | |
FName ServerBaseBoneName, | |
bool bHasBase, | |
bool bBaseRelativePosition, | |
uint8 ServerMovementMode) | |
{ | |
if (!HasValidData() || !IsActive()) | |
{ | |
return; | |
} | |
} | |
void UMMOPlayerMovement::ClientAdjustRootMotionSourcePosition(float TimeStamp, FRootMotionSourceGroup ServerRootMotion, bool bHasAnimRootMotion, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode) | |
{ | |
CharacterOwner->ClientAdjustRootMotionSourcePosition(TimeStamp, ServerRootMotion, bHasAnimRootMotion, ServerMontageTrackPosition, ServerLoc, ServerRotation, ServerVelZ, ServerBase, ServerBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode); | |
} | |
void UMMOPlayerMovement::ClientAdjustRootMotionSourcePosition_Implementation( | |
float TimeStamp, | |
FRootMotionSourceGroup ServerRootMotion, | |
bool bHasAnimRootMotion, | |
float ServerMontageTrackPosition, | |
FVector ServerLoc, | |
FVector_NetQuantizeNormal ServerRotation, | |
float ServerVelZ, | |
UPrimitiveComponent* ServerBase, | |
FName ServerBaseBoneName, | |
bool bHasBase, | |
bool bBaseRelativePosition, | |
uint8 ServerMovementMode) | |
{ | |
if (!HasValidData() || !IsActive()) | |
{ | |
return; | |
} | |
} | |
void UMMOPlayerMovement::ClientAckGoodMove(float TimeStamp) | |
{ | |
CharacterOwner->ClientAckGoodMove(TimeStamp); | |
} | |
void UMMOPlayerMovement::ClientAckGoodMove_Implementation(float TimeStamp) | |
{ | |
if (!HasValidData() || !IsActive()) | |
{ | |
return; | |
} | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
check(ClientData); | |
// Ack move if it has not expired. | |
int32 MoveIndex = ClientData->GetSavedMoveIndex(TimeStamp); | |
if (MoveIndex == INDEX_NONE) | |
{ | |
if (ClientData->LastAckedMove.IsValid()) | |
{ | |
UE_LOG(LogNetPlayerMovement, Log, TEXT("ClientAckGoodMove_Implementation could not find Move for TimeStamp: %f, LastAckedTimeStamp: %f, CurrentTimeStamp: %f"), TimeStamp, ClientData->LastAckedMove->TimeStamp, ClientData->CurrentTimeStamp); | |
} | |
return; | |
} | |
ClientData->AckMove(MoveIndex, *this); | |
} | |
void UMMOPlayerMovement::CapsuleTouched(UPrimitiveComponent* OverlappedComp, AActor* Other, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) | |
{ | |
if (!bEnablePhysicsInteraction) | |
{ | |
return; | |
} | |
if (OtherComp != NULL && OtherComp->IsAnySimulatingPhysics()) | |
{ | |
const FVector OtherLoc = OtherComp->GetComponentLocation(); | |
const FVector Loc = UpdatedComponent->GetComponentLocation(); | |
FVector ImpulseDir = FVector(OtherLoc.X - Loc.X, OtherLoc.Y - Loc.Y, 0.25f).GetSafeNormal(); | |
ImpulseDir = (ImpulseDir + Velocity.GetSafeNormal2D()) * 0.5f; | |
ImpulseDir.Normalize(); | |
FName BoneName = NAME_None; | |
if (OtherBodyIndex != INDEX_NONE) | |
{ | |
BoneName = ((USkinnedMeshComponent*)OtherComp)->GetBoneName(OtherBodyIndex); | |
} | |
float TouchForceFactorModified = TouchForceFactor; | |
if (bTouchForceScaledToMass) | |
{ | |
FBodyInstance* BI = OtherComp->GetBodyInstance(BoneName); | |
TouchForceFactorModified *= BI ? BI->GetBodyMass() : 1.0f; | |
} | |
float ImpulseStrength = FMath::Clamp(Velocity.Size2D() * TouchForceFactorModified, | |
MinTouchForce > 0.0f ? MinTouchForce : -FLT_MAX, | |
MaxTouchForce > 0.0f ? MaxTouchForce : FLT_MAX); | |
FVector Impulse = ImpulseDir * ImpulseStrength; | |
OtherComp->AddImpulse(Impulse, BoneName); | |
} | |
} | |
void UMMOPlayerMovement::SetAvoidanceGroup(int32 GroupFlags) | |
{ | |
AvoidanceGroup.SetFlagsDirectly(GroupFlags); | |
} | |
void UMMOPlayerMovement::SetAvoidanceGroupMask(const FNavAvoidanceMask& GroupMask) | |
{ | |
AvoidanceGroup.SetFlagsDirectly(GroupMask.Packed); | |
} | |
void UMMOPlayerMovement::SetGroupsToAvoid(int32 GroupFlags) | |
{ | |
GroupsToAvoid.SetFlagsDirectly(GroupFlags); | |
} | |
void UMMOPlayerMovement::SetGroupsToAvoidMask(const FNavAvoidanceMask& GroupMask) | |
{ | |
GroupsToAvoid.SetFlagsDirectly(GroupMask.Packed); | |
} | |
void UMMOPlayerMovement::SetGroupsToIgnore(int32 GroupFlags) | |
{ | |
GroupsToIgnore.SetFlagsDirectly(GroupFlags); | |
} | |
void UMMOPlayerMovement::SetGroupsToIgnoreMask(const FNavAvoidanceMask& GroupMask) | |
{ | |
GroupsToIgnore.SetFlagsDirectly(GroupMask.Packed); | |
} | |
void UMMOPlayerMovement::SetAvoidanceEnabled(bool bEnable) | |
{ | |
if (bUseRVOAvoidance != bEnable) | |
{ | |
bUseRVOAvoidance = bEnable; | |
// reset id, RegisterMovementComponent call is required to initialize update timers in avoidance manager | |
AvoidanceUID = 0; | |
// this is a safety check - it's possible to not have CharacterOwner at this point if this function gets | |
// called too early | |
ensure(GetCharacterOwner()); | |
if (GetCharacterOwner() != nullptr) | |
{ | |
UAvoidanceManager* AvoidanceManager = GetWorld()->GetAvoidanceManager(); | |
if (AvoidanceManager && bEnable) | |
{ | |
AvoidanceManager->RegisterMovementComponent(this, AvoidanceWeight); | |
} | |
} | |
} | |
} | |
void UMMOPlayerMovement::ApplyDownwardForce(float DeltaSeconds) | |
{ | |
if (StandingDownwardForceScale != 0.0f && CurrentFloor.HitResult.IsValidBlockingHit()) | |
{ | |
UPrimitiveComponent* BaseComp = CurrentFloor.HitResult.GetComponent(); | |
const FVector Gravity = FVector(0.0f, 0.0f, GetGravityZ()); | |
if (BaseComp && BaseComp->IsAnySimulatingPhysics() && !Gravity.IsZero()) | |
{ | |
BaseComp->AddForceAtLocation(Gravity * Mass * StandingDownwardForceScale, CurrentFloor.HitResult.ImpactPoint, CurrentFloor.HitResult.BoneName); | |
} | |
} | |
} | |
void UMMOPlayerMovement::ApplyRepulsionForce(float DeltaSeconds) | |
{ | |
if (UpdatedPrimitive && RepulsionForce > 0.0f && CharacterOwner != nullptr) | |
{ | |
const TArray<FOverlapInfo>& Overlaps = UpdatedPrimitive->GetOverlapInfos(); | |
if (Overlaps.Num() > 0) | |
{ | |
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(CMC_ApplyRepulsionForce)); | |
QueryParams.bReturnFaceIndex = false; | |
QueryParams.bReturnPhysicalMaterial = false; | |
float CapsuleRadius = 0.f; | |
float CapsuleHalfHeight = 0.f; | |
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(CapsuleRadius, CapsuleHalfHeight); | |
const float RepulsionForceRadius = CapsuleRadius * 1.2f; | |
const float StopBodyDistance = 2.5f; | |
const FVector MyLocation = UpdatedPrimitive->GetComponentLocation(); | |
for (int32 i = 0; i < Overlaps.Num(); i++) | |
{ | |
const FOverlapInfo& Overlap = Overlaps[i]; | |
UPrimitiveComponent* OverlapComp = Overlap.OverlapInfo.Component.Get(); | |
if (!OverlapComp || OverlapComp->Mobility < EComponentMobility::Movable) | |
{ | |
continue; | |
} | |
// Use the body instead of the component for cases where we have multi-body overlaps enabled | |
FBodyInstance* OverlapBody = nullptr; | |
const int32 OverlapBodyIndex = Overlap.GetBodyIndex(); | |
const USkeletalMeshComponent* SkelMeshForBody = (OverlapBodyIndex != INDEX_NONE) ? Cast<USkeletalMeshComponent>(OverlapComp) : nullptr; | |
if (SkelMeshForBody != nullptr) | |
{ | |
OverlapBody = SkelMeshForBody->Bodies.IsValidIndex(OverlapBodyIndex) ? SkelMeshForBody->Bodies[OverlapBodyIndex] : nullptr; | |
} | |
else | |
{ | |
OverlapBody = OverlapComp->GetBodyInstance(); | |
} | |
if (!OverlapBody) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("%s could not find overlap body for body index %d"), *GetName(), OverlapBodyIndex); | |
continue; | |
} | |
if (!OverlapBody->IsInstanceSimulatingPhysics()) | |
{ | |
continue; | |
} | |
FTransform BodyTransform = OverlapBody->GetUnrealWorldTransform(); | |
FVector BodyVelocity = OverlapBody->GetUnrealWorldVelocity(); | |
FVector BodyLocation = BodyTransform.GetLocation(); | |
// Trace to get the hit location on the capsule | |
FHitResult Hit; | |
bool bHasHit = UpdatedPrimitive->LineTraceComponent(Hit, BodyLocation, | |
FVector(MyLocation.X, MyLocation.Y, BodyLocation.Z), | |
QueryParams); | |
FVector HitLoc = Hit.ImpactPoint; | |
bool bIsPenetrating = Hit.bStartPenetrating || Hit.PenetrationDepth > StopBodyDistance; | |
// If we didn't hit the capsule, we're inside the capsule | |
if (!bHasHit) | |
{ | |
HitLoc = BodyLocation; | |
bIsPenetrating = true; | |
} | |
const float DistanceNow = (HitLoc - BodyLocation).SizeSquared2D(); | |
const float DistanceLater = (HitLoc - (BodyLocation + BodyVelocity * DeltaSeconds)).SizeSquared2D(); | |
if (bHasHit && DistanceNow < StopBodyDistance && !bIsPenetrating) | |
{ | |
OverlapBody->SetLinearVelocity(FVector(0.0f, 0.0f, 0.0f), false); | |
} | |
else if (DistanceLater <= DistanceNow || bIsPenetrating) | |
{ | |
FVector ForceCenter = MyLocation; | |
if (bHasHit) | |
{ | |
ForceCenter.Z = HitLoc.Z; | |
} | |
else | |
{ | |
ForceCenter.Z = FMath::Clamp(BodyLocation.Z, MyLocation.Z - CapsuleHalfHeight, MyLocation.Z + CapsuleHalfHeight); | |
} | |
OverlapBody->AddRadialForceToBody(ForceCenter, RepulsionForceRadius, RepulsionForce * Mass, ERadialImpulseFalloff::RIF_Constant); | |
} | |
} | |
} | |
} | |
} | |
void UMMOPlayerMovement::ApplyAccumulatedForces(float DeltaSeconds) | |
{ | |
if (PendingImpulseToApply.Z != 0.f || PendingForceToApply.Z != 0.f) | |
{ | |
// check to see if applied momentum is enough to overcome gravity | |
if (IsMovingOnGround() && (PendingImpulseToApply.Z + (PendingForceToApply.Z * DeltaSeconds) + (GetGravityZ() * DeltaSeconds) > SMALL_NUMBER)) | |
{ | |
SetMovementMode(MOVE_Falling); | |
} | |
} | |
Velocity += PendingImpulseToApply + (PendingForceToApply * DeltaSeconds); | |
// Don't call ClearAccumulatedForces() because it could affect launch velocity | |
PendingImpulseToApply = FVector::ZeroVector; | |
PendingForceToApply = FVector::ZeroVector; | |
} | |
void UMMOPlayerMovement::ClearAccumulatedForces() | |
{ | |
PendingImpulseToApply = FVector::ZeroVector; | |
PendingForceToApply = FVector::ZeroVector; | |
PendingLaunchVelocity = FVector::ZeroVector; | |
} | |
void UMMOPlayerMovement::AddRadialForce(const FVector& Origin, float Radius, float Strength, enum ERadialImpulseFalloff Falloff) | |
{ | |
FVector Delta = UpdatedComponent->GetComponentLocation() - Origin; | |
const float DeltaMagnitude = Delta.Size(); | |
// Do nothing if outside radius | |
if (DeltaMagnitude > Radius) | |
{ | |
return; | |
} | |
Delta = Delta.GetSafeNormal(); | |
float ForceMagnitude = Strength; | |
if (Falloff == RIF_Linear && Radius > 0.0f) | |
{ | |
ForceMagnitude *= (1.0f - (DeltaMagnitude / Radius)); | |
} | |
AddForce(Delta * ForceMagnitude); | |
} | |
void UMMOPlayerMovement::AddRadialImpulse(const FVector& Origin, float Radius, float Strength, enum ERadialImpulseFalloff Falloff, bool bVelChange) | |
{ | |
FVector Delta = UpdatedComponent->GetComponentLocation() - Origin; | |
const float DeltaMagnitude = Delta.Size(); | |
// Do nothing if outside radius | |
if (DeltaMagnitude > Radius) | |
{ | |
return; | |
} | |
Delta = Delta.GetSafeNormal(); | |
float ImpulseMagnitude = Strength; | |
if (Falloff == RIF_Linear && Radius > 0.0f) | |
{ | |
ImpulseMagnitude *= (1.0f - (DeltaMagnitude / Radius)); | |
} | |
AddImpulse(Delta * ImpulseMagnitude, bVelChange); | |
} | |
void UMMOPlayerMovement::RegisterComponentTickFunctions(bool bRegister) | |
{ | |
Super::RegisterComponentTickFunctions(bRegister); | |
if (bRegister) | |
{ | |
if (SetupActorComponentTickFunction(&PostPhysicsTickFunction)) | |
{ | |
PostPhysicsTickFunction.Target = this; | |
PostPhysicsTickFunction.AddPrerequisite(this, this->PrimaryComponentTick); | |
} | |
} | |
else | |
{ | |
if (PostPhysicsTickFunction.IsTickFunctionRegistered()) | |
{ | |
PostPhysicsTickFunction.UnRegisterTickFunction(); | |
} | |
} | |
} | |
void UMMOPlayerMovement::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) | |
{ | |
OldBaseLocation += InOffset; | |
LastUpdateLocation += InOffset; | |
if (CharacterOwner != nullptr && CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy) | |
{ | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (ClientData != nullptr) | |
{ | |
const int32 NumSavedMoves = ClientData->SavedMoves.Num(); | |
for (int32 i = 0; i < NumSavedMoves - 1; i++) | |
{ | |
FMMOSavedMove_Character* const CurrentMove = ClientData->SavedMoves[i].Get(); | |
CurrentMove->StartLocation += InOffset; | |
CurrentMove->SavedLocation += InOffset; | |
} | |
if (FMMOSavedMove_Character* const PendingMove = ClientData->PendingMove.Get()) | |
{ | |
PendingMove->StartLocation += InOffset; | |
PendingMove->SavedLocation += InOffset; | |
} | |
for (int32 i = 0; i < ClientData->ReplaySamples.Num(); i++) | |
{ | |
ClientData->ReplaySamples[i].Location += InOffset; | |
} | |
} | |
} | |
} | |
void UMMOPlayerMovement::TickCharacterPose(float DeltaTime) | |
{ | |
if (DeltaTime < UMMOPlayerMovement::MIN_TICK_TIME) | |
{ | |
return; | |
} | |
check(CharacterOwner && CharacterOwner->GetMesh()); | |
USkeletalMeshComponent* CharacterMesh = CharacterOwner->GetMesh(); | |
// bAutonomousTickPose is set, we control TickPose from the Character's Movement and Networking updates, and bypass the Component's update. | |
// (Or Simulating Root Motion for remote clients) | |
CharacterMesh->bIsAutonomousTickPose = true; | |
if (CharacterMesh->ShouldTickPose()) | |
{ | |
// Keep track of if we're playing root motion, just in case the root motion montage ends this frame. | |
const bool bWasPlayingRootMotion = CharacterOwner->IsPlayingRootMotion(); | |
CharacterMesh->TickPose(DeltaTime, true); | |
// Grab root motion now that we have ticked the pose | |
if (CharacterOwner->IsPlayingRootMotion() || bWasPlayingRootMotion) | |
{ | |
FRootMotionMovementParams RootMotion = CharacterMesh->ConsumeRootMotion(); | |
if (RootMotion.bHasRootMotion) | |
{ | |
RootMotion.ScaleRootMotionTranslation(CharacterOwner->GetAnimRootMotionTranslationScale()); | |
RootMotionParams.Accumulate(RootMotion); | |
} | |
#if !(UE_BUILD_SHIPPING) | |
// Debugging | |
{ | |
FAnimMontageInstance* RootMotionMontageInstance = CharacterOwner->GetRootMotionAnimMontageInstance(); | |
UE_LOG(LogRootMotion, Log, TEXT("UMMOPlayerMovement::TickCharacterPose Role: %s, RootMotionMontage: %s, MontagePos: %f, DeltaTime: %f, ExtractedRootMotion: %s, AccumulatedRootMotion: %s") | |
, *UEnum::GetValueAsString(TEXT("Engine.ENetRole"), CharacterOwner->GetLocalRole()) | |
, *GetNameSafe(RootMotionMontageInstance ? RootMotionMontageInstance->Montage : NULL) | |
, RootMotionMontageInstance ? RootMotionMontageInstance->GetPosition() : -1.f | |
, DeltaTime | |
, *RootMotion.GetRootMotionTransform().GetTranslation().ToCompactString() | |
, *RootMotionParams.GetRootMotionTransform().GetTranslation().ToCompactString() | |
); | |
} | |
#endif // !(UE_BUILD_SHIPPING) | |
} | |
} | |
CharacterMesh->bIsAutonomousTickPose = false; | |
} | |
/** | |
* Root Motion | |
*/ | |
bool UMMOPlayerMovement::HasRootMotionSources() const | |
{ | |
return CurrentRootMotion.HasActiveRootMotionSources() || (CharacterOwner && CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh()); | |
} | |
uint16 UMMOPlayerMovement::ApplyRootMotionSource(FRootMotionSource* SourcePtr) | |
{ | |
if (SourcePtr != nullptr) | |
{ | |
// Set default StartTime if it hasn't been set manually | |
if (!SourcePtr->IsStartTimeValid()) | |
{ | |
if (CharacterOwner) | |
{ | |
if (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy) | |
{ | |
// Autonomous defaults to local timestamp | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (ClientData) | |
{ | |
SourcePtr->StartTime = ClientData->CurrentTimeStamp; | |
} | |
} | |
else if (CharacterOwner->GetLocalRole() == ROLE_Authority && !IsNetMode(NM_Client)) | |
{ | |
// Authority defaults to current client time stamp, meaning it'll start next tick if not corrected | |
FMMONetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character(); | |
if (ServerData) | |
{ | |
SourcePtr->StartTime = ServerData->CurrentClientTimeStamp; | |
} | |
} | |
} | |
} | |
OnRootMotionSourceBeingApplied(SourcePtr); | |
return CurrentRootMotion.ApplyRootMotionSource(SourcePtr); | |
} | |
else | |
{ | |
checkf(false, TEXT("Passing nullptr into UMMOPlayerMovement::ApplyRootMotionSource")); | |
} | |
return (uint16)ERootMotionSourceID::Invalid; | |
} | |
void UMMOPlayerMovement::OnRootMotionSourceBeingApplied(const FRootMotionSource* Source) | |
{ | |
} | |
TSharedPtr<FRootMotionSource> UMMOPlayerMovement::GetRootMotionSource(FName InstanceName) | |
{ | |
return CurrentRootMotion.GetRootMotionSource(InstanceName); | |
} | |
TSharedPtr<FRootMotionSource> UMMOPlayerMovement::GetRootMotionSourceByID(uint16 RootMotionSourceID) | |
{ | |
return CurrentRootMotion.GetRootMotionSourceByID(RootMotionSourceID); | |
} | |
void UMMOPlayerMovement::RemoveRootMotionSource(FName InstanceName) | |
{ | |
CurrentRootMotion.RemoveRootMotionSource(InstanceName); | |
} | |
void UMMOPlayerMovement::RemoveRootMotionSourceByID(uint16 RootMotionSourceID) | |
{ | |
CurrentRootMotion.RemoveRootMotionSourceByID(RootMotionSourceID); | |
} | |
void UMMOPlayerMovement::ConvertRootMotionServerIDsToLocalIDs(const FRootMotionSourceGroup& LocalRootMotionToMatchWith, FRootMotionSourceGroup& InOutServerRootMotion, float TimeStamp) | |
{ | |
// Remove out of date mappings, they can never be used again. | |
for (int32 MappingIndex = 0; MappingIndex < RootMotionIDMappings.Num(); MappingIndex++) | |
{ | |
if (RootMotionIDMappings[MappingIndex].IsStillValid(TimeStamp)) | |
{ | |
// MappingIndex is valid, remove anything before it. | |
const int32 CutOffIndex = MappingIndex - 1; | |
if (CutOffIndex >= 0) | |
{ | |
// Most recent entries added last, so we can cull the top of the list. | |
RootMotionIDMappings.RemoveAt(0, CutOffIndex + 1, false); | |
break; | |
} | |
} | |
} | |
// Remove mappings that don't map to an active local root motion source. | |
for (int32 MappingIndex = RootMotionIDMappings.Num() - 1; MappingIndex >= 0; MappingIndex--) | |
{ | |
bool bFoundLocalSource = false; | |
for (const TSharedPtr<FRootMotionSource>& LocalRootMotionSource : LocalRootMotionToMatchWith.RootMotionSources) | |
{ | |
if (LocalRootMotionSource.IsValid() && (LocalRootMotionSource->LocalID == RootMotionIDMappings[MappingIndex].LocalID)) | |
{ | |
bFoundLocalSource = true; | |
break; | |
} | |
} | |
if (!bFoundLocalSource) | |
{ | |
RootMotionIDMappings.RemoveAt(MappingIndex, 1, false); | |
} | |
} | |
bool bDumpDebugInfo = false; | |
// Root Motion Sources are applied independently on servers and clients. | |
// FRootMotionSource::LocalID is an ID added when that Source is applied. | |
// When we receive RootMotionSource data from the server, LocalIDs on that | |
// RootMotion data are the server LocalIDs. When processing an FRootMotionSourceGroup | |
// for use on clients, we want to map server LocalIDs to our LocalIDs. | |
// We save off these mappings for quicker access and to save having to | |
// "find best match" every time we receive server data. | |
for (TSharedPtr<FRootMotionSource>& ServerRootMotionSource : InOutServerRootMotion.RootMotionSources) | |
{ | |
if (ServerRootMotionSource.IsValid()) | |
{ | |
const uint16 ServerID = ServerRootMotionSource->LocalID; | |
// Reset LocalID of replicated ServerRootMotionSource, and find a local match. | |
ServerRootMotionSource->LocalID = (uint16)ERootMotionSourceID::Invalid; | |
// See if we have any recent mappings that match this server ID | |
// If we do, change it to that mapping and update the timestamp | |
{ | |
bool bMappingFound = false; | |
for (FRootMotionServerToLocalIDMapping& Mapping : RootMotionIDMappings) | |
{ | |
if (ServerID == Mapping.ServerID) | |
{ | |
ServerRootMotionSource->LocalID = Mapping.LocalID; | |
Mapping.TimeStamp = TimeStamp; | |
bMappingFound = true; | |
break; // Found it, don't need to search any more mappings | |
} | |
} | |
if (bMappingFound) | |
{ | |
// We rely on this rule (Matches) being always true, so in non-shipping builds make sure it never breaks. | |
for (const TSharedPtr<FRootMotionSource>& LocalRootMotionSource : LocalRootMotionToMatchWith.RootMotionSources) | |
{ | |
if (LocalRootMotionSource.IsValid() && (LocalRootMotionSource->LocalID == ServerRootMotionSource->LocalID)) | |
{ | |
if (!LocalRootMotionSource->Matches(ServerRootMotionSource.Get())) | |
{ | |
ensureMsgf(false, | |
TEXT("Character(%s) Local RootMotionSource(%s) has the same LocalID(%d) as a non-matching ServerRootMotionSource(%s)!"), | |
*GetNameSafe(CharacterOwner), *LocalRootMotionSource->ToSimpleString(), LocalRootMotionSource->LocalID, *ServerRootMotionSource->ToSimpleString()); | |
bDumpDebugInfo = true; | |
} | |
break; | |
} | |
} | |
// We've found the correct LocalID, done with this one, process next ServerRootMotionSource | |
continue; | |
} | |
} | |
// If no mapping found, find match out of Local RootMotionSources that are not already mapped | |
bool bMatchFound = false; | |
TArray<TSharedPtr<FRootMotionSource>> LocalRootMotionSources; | |
LocalRootMotionSources.Reserve(LocalRootMotionToMatchWith.RootMotionSources.Num() + LocalRootMotionToMatchWith.PendingAddRootMotionSources.Num()); | |
LocalRootMotionSources.Append(LocalRootMotionToMatchWith.RootMotionSources); | |
LocalRootMotionSources.Append(LocalRootMotionToMatchWith.PendingAddRootMotionSources); | |
for (const TSharedPtr<FRootMotionSource>& LocalRootMotionSource : LocalRootMotionSources) | |
{ | |
if (LocalRootMotionSource.IsValid()) | |
{ | |
const uint16 LocalID = LocalRootMotionSource->LocalID; | |
// Check if the LocalID is already mapped to a ServerID; if it's already "claimed", | |
// it's not valid for being a match to our unmatched server source | |
{ | |
bool bMappingFound = false; | |
for (FRootMotionServerToLocalIDMapping& Mapping : RootMotionIDMappings) | |
{ | |
if (LocalID == Mapping.LocalID) | |
{ | |
bMappingFound = true; | |
break; // Found it, don't need to search any more mappings | |
} | |
} | |
if (bMappingFound) | |
{ | |
continue; // We found a ServerID matching this LocalID, so we don't try to match this | |
} | |
} | |
// This LocalRootMotionSource is a valid possible match to the ServerRootMotionSource | |
if (LocalRootMotionSource->Matches(ServerRootMotionSource.Get())) | |
{ | |
// We have a match! | |
// Assign LocalID | |
ServerRootMotionSource->LocalID = LocalID; | |
// Add to Mapping | |
{ | |
FRootMotionServerToLocalIDMapping NewMapping; | |
NewMapping.LocalID = LocalID; | |
NewMapping.ServerID = ServerID; | |
NewMapping.TimeStamp = TimeStamp; | |
RootMotionIDMappings.Add(NewMapping); | |
bMatchFound = true; | |
break; // Stop searching LocalRootMotionSources, we've found a match | |
} | |
} | |
} | |
} // loop through LocalRootMotionSources | |
// if we don't find a match, set an invalid LocalID so that we know it's an invalid ID from the server | |
// This doesn't mean it's a "bad" RootMotionSource; just that the Server sent a RootMotionSource | |
// that we don't have in the current LocalRootMotion group we're searching. It's possible that next | |
// frame the LocalRootMotionSource was added/will be added and from then on we'll match & correct from | |
// the Server | |
if (!bMatchFound) | |
{ | |
ServerRootMotionSource->LocalID = (uint16)ERootMotionSourceID::Invalid; | |
} | |
} | |
} // loop through ServerRootMotionSources | |
if (bDumpDebugInfo) | |
{ | |
UE_LOG(LogRootMotion, Warning, TEXT("Dumping current mappings:")); | |
for (FRootMotionServerToLocalIDMapping& Mapping : RootMotionIDMappings) | |
{ | |
UE_LOG(LogRootMotion, Warning, TEXT("- LocalID(%d) ServerID(%d)"), Mapping.LocalID, Mapping.ServerID); | |
} | |
UE_LOG(LogRootMotion, Warning, TEXT("Dumping local RootMotionSources:")); | |
for (const TSharedPtr<FRootMotionSource>& LocalRootMotionSource : LocalRootMotionToMatchWith.RootMotionSources) | |
{ | |
if (LocalRootMotionSource.IsValid()) | |
{ | |
UE_LOG(LogRootMotion, Warning, TEXT("- LocalRootMotionSource(%d)"), *LocalRootMotionSource->ToSimpleString()); | |
} | |
} | |
UE_LOG(LogRootMotion, Warning, TEXT("Dumping server RootMotionSources:")); | |
for (TSharedPtr<FRootMotionSource>& ServerRootMotionSource : InOutServerRootMotion.RootMotionSources) | |
{ | |
if (ServerRootMotionSource.IsValid()) | |
{ | |
UE_LOG(LogRootMotion, Warning, TEXT("- ServerRootMotionSource(%d)"), *ServerRootMotionSource->ToSimpleString()); | |
} | |
} | |
} | |
} | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS // For deprecated members of FMMONetworkPredictionData_Client_Character | |
FMMONetworkPredictionData_Client_Character::FMMONetworkPredictionData_Client_Character(const UMMOPlayerMovement& ClientMovement) | |
: ClientUpdateTime(0.f) | |
, CurrentTimeStamp(0.f) | |
, LastReceivedAckRealTime(0.f) | |
, PendingMove(NULL) | |
, LastAckedMove(NULL) | |
, MaxFreeMoveCount(96) | |
, MaxSavedMoveCount(96) | |
, bUpdatePosition(false) | |
, bSmoothNetUpdates(false) // Deprecated | |
, OriginalMeshTranslationOffset(ForceInitToZero) | |
, MeshTranslationOffset(ForceInitToZero) | |
, OriginalMeshRotationOffset(FQuat::Identity) | |
, MeshRotationOffset(FQuat::Identity) | |
, MeshRotationTarget(FQuat::Identity) | |
, LastCorrectionDelta(0.f) | |
, LastCorrectionTime(0.f) | |
, MaxClientSmoothingDeltaTime(0.5f) | |
, SmoothingServerTimeStamp(0.f) | |
, SmoothingClientTimeStamp(0.f) | |
, CurrentSmoothTime(0.f) // Deprecated | |
, bUseLinearSmoothing(false) // Deprecated | |
, MaxSmoothNetUpdateDist(0.f) | |
, NoSmoothNetUpdateDist(0.f) | |
, SmoothNetUpdateTime(0.f) | |
, SmoothNetUpdateRotationTime(0.f) | |
, MaxResponseTime(0.125f) // Deprecated, use MaxMoveDeltaTime instead | |
, MaxMoveDeltaTime(0.125f) | |
, LastSmoothLocation(FVector::ZeroVector) | |
, LastServerLocation(FVector::ZeroVector) | |
, SimulatedDebugDrawTime(0.0f) | |
, DebugForcedPacketLossTimerStart(0.0f) | |
{ | |
MaxSmoothNetUpdateDist = ClientMovement.NetworkMaxSmoothUpdateDistance; | |
NoSmoothNetUpdateDist = ClientMovement.NetworkNoSmoothUpdateDistance; | |
const bool bIsListenServer = (ClientMovement.GetNetMode() == NM_ListenServer); | |
SmoothNetUpdateTime = (bIsListenServer ? ClientMovement.ListenServerNetworkSimulatedSmoothLocationTime : ClientMovement.NetworkSimulatedSmoothLocationTime); | |
SmoothNetUpdateRotationTime = (bIsListenServer ? ClientMovement.ListenServerNetworkSimulatedSmoothRotationTime : ClientMovement.NetworkSimulatedSmoothRotationTime); | |
const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject()); | |
if (GameNetworkManager) | |
{ | |
MaxMoveDeltaTime = GameNetworkManager->MaxMoveDeltaTime; | |
MaxClientSmoothingDeltaTime = FMath::Max(GameNetworkManager->MaxClientSmoothingDeltaTime, MaxMoveDeltaTime * 2.0f); | |
} | |
MaxResponseTime = MaxMoveDeltaTime; // MaxResponseTime is deprecated, use MaxMoveDeltaTime instead | |
if (ClientMovement.GetOwnerRole() == ROLE_AutonomousProxy) | |
{ | |
SavedMoves.Reserve(MaxSavedMoveCount); | |
FreeMoves.Reserve(MaxFreeMoveCount); | |
} | |
} | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS // For deprecated members of FMMONetworkPredictionData_Client_Character | |
FMMONetworkPredictionData_Client_Character::~FMMONetworkPredictionData_Client_Character() | |
{ | |
SavedMoves.Empty(); | |
FreeMoves.Empty(); | |
PendingMove = NULL; | |
LastAckedMove = NULL; | |
} | |
FMMOSavedMovePtr FMMONetworkPredictionData_Client_Character::CreateSavedMove() | |
{ | |
if (SavedMoves.Num() >= MaxSavedMoveCount) | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("CreateSavedMove: Hit limit of %d saved moves (timing out or very bad ping?)"), SavedMoves.Num()); | |
// Free all saved moves | |
for (int32 i = 0; i < SavedMoves.Num(); i++) | |
{ | |
FreeMove(SavedMoves[i]); | |
} | |
SavedMoves.Reset(); | |
} | |
if (FreeMoves.Num() == 0) | |
{ | |
// No free moves, allocate a new one. | |
FMMOSavedMovePtr NewMove = AllocateNewMove(); | |
checkSlow(NewMove.IsValid()); | |
NewMove->Clear(); | |
return NewMove; | |
} | |
else | |
{ | |
// Pull from the free pool | |
const bool bAllowShrinking = false; | |
FMMOSavedMovePtr FirstFree = FreeMoves.Pop(bAllowShrinking); | |
FirstFree->Clear(); | |
return FirstFree; | |
} | |
} | |
FMMOSavedMovePtr FMMONetworkPredictionData_Client_Character::AllocateNewMove() | |
{ | |
return FMMOSavedMovePtr(new FMMOSavedMove_Character()); | |
} | |
void FMMONetworkPredictionData_Client_Character::FreeMove(const FMMOSavedMovePtr& Move) | |
{ | |
if (Move.IsValid()) | |
{ | |
// Only keep a pool of a limited number of moves. | |
if (FreeMoves.Num() < MaxFreeMoveCount) | |
{ | |
FreeMoves.Push(Move); | |
} | |
// Shouldn't keep a reference to the move on the free list. | |
if (PendingMove == Move) | |
{ | |
PendingMove = NULL; | |
} | |
if (LastAckedMove == Move) | |
{ | |
LastAckedMove = NULL; | |
} | |
} | |
} | |
int32 FMMONetworkPredictionData_Client_Character::GetSavedMoveIndex(float TimeStamp) const | |
{ | |
if (SavedMoves.Num() > 0) | |
{ | |
// If LastAckedMove isn't using an old TimeStamp (before reset), we can prevent the iteration if incoming TimeStamp is outdated | |
if (LastAckedMove.IsValid() && !LastAckedMove->bOldTimeStampBeforeReset && (TimeStamp <= LastAckedMove->TimeStamp)) | |
{ | |
return INDEX_NONE; | |
} | |
// Otherwise see if we can find this move. | |
for (int32 Index = 0; Index < SavedMoves.Num(); Index++) | |
{ | |
const FMMOSavedMove_Character* CurrentMove = SavedMoves[Index].Get(); | |
checkSlow(CurrentMove != nullptr); | |
if (CurrentMove->TimeStamp == TimeStamp) | |
{ | |
return Index; | |
} | |
} | |
} | |
return INDEX_NONE; | |
} | |
void FMMONetworkPredictionData_Client_Character::AckMove(int32 AckedMoveIndex, UMMOPlayerMovement& CharacterMovementComponent) | |
{ | |
// It is important that we know the move exists before we go deleting outdated moves. | |
// Timestamps are not guaranteed to be increasing order all the time, since they can be reset! | |
if (AckedMoveIndex != INDEX_NONE) | |
{ | |
// Keep reference to LastAckedMove | |
const FMMOSavedMovePtr& AckedMove = SavedMoves[AckedMoveIndex]; | |
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("AckedMove Index: %2d (%2d moves). TimeStamp: %f, CurrentTimeStamp: %f"), AckedMoveIndex, SavedMoves.Num(), AckedMove->TimeStamp, CurrentTimeStamp); | |
if (LastAckedMove.IsValid()) | |
{ | |
FreeMove(LastAckedMove); | |
} | |
LastAckedMove = AckedMove; | |
// Free expired moves. | |
for (int32 MoveIndex = 0; MoveIndex < AckedMoveIndex; MoveIndex++) | |
{ | |
const FMMOSavedMovePtr& Move = SavedMoves[MoveIndex]; | |
FreeMove(Move); | |
} | |
// And finally cull all of those, so only the unacknowledged moves remain in SavedMoves. | |
const bool bAllowShrinking = false; | |
SavedMoves.RemoveAt(0, AckedMoveIndex + 1, bAllowShrinking); | |
} | |
if (const UWorld* const World = CharacterMovementComponent.GetWorld()) | |
{ | |
LastReceivedAckRealTime = World->GetRealTimeSeconds(); | |
} | |
} | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS // For deprecated members of FMMONetworkPredictionData_Server_Character | |
FMMONetworkPredictionData_Server_Character::FMMONetworkPredictionData_Server_Character(const UMMOPlayerMovement& ServerMovement) | |
: PendingAdjustment() | |
, CurrentClientTimeStamp(0.f) | |
, ServerAccumulatedClientTimeStamp(0.0) | |
, LastUpdateTime(0.f) | |
, ServerTimeStampLastServerMove(0.f) | |
, MaxResponseTime(0.125f) // Deprecated, use MaxMoveDeltaTime instead | |
, MaxMoveDeltaTime(0.125f) | |
, bForceClientUpdate(false) | |
, LifetimeRawTimeDiscrepancy(0.f) | |
, TimeDiscrepancy(0.f) | |
, bResolvingTimeDiscrepancy(false) | |
, TimeDiscrepancyResolutionMoveDeltaOverride(0.f) | |
, TimeDiscrepancyAccumulatedClientDeltasSinceLastServerTick(0.f) | |
, WorldCreationTime(0.f) | |
{ | |
const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject()); | |
if (GameNetworkManager) | |
{ | |
MaxMoveDeltaTime = GameNetworkManager->MaxMoveDeltaTime; | |
if (GameNetworkManager->MaxMoveDeltaTime > GameNetworkManager->MAXCLIENTUPDATEINTERVAL) | |
{ | |
UE_LOG(LogNetPlayerMovement, Warning, TEXT("GameNetworkManager::MaxMoveDeltaTime (%f) is greater than GameNetworkManager::MAXCLIENTUPDATEINTERVAL (%f)! Server will interfere with move deltas that large!"), GameNetworkManager->MaxMoveDeltaTime, GameNetworkManager->MAXCLIENTUPDATEINTERVAL); | |
} | |
} | |
const UWorld* World = ServerMovement.GetWorld(); | |
if (World) | |
{ | |
WorldCreationTime = World->GetTimeSeconds(); | |
ServerTimeStamp = World->GetTimeSeconds(); | |
} | |
MaxResponseTime = MaxMoveDeltaTime; // Deprecated, use MaxMoveDeltaTime instead | |
} | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS // For deprecated members of FMMONetworkPredictionData_Server_Character | |
FMMONetworkPredictionData_Server_Character::~FMMONetworkPredictionData_Server_Character() | |
{ | |
} | |
float FMMONetworkPredictionData_Server_Character::GetServerMoveDeltaTime(float ClientTimeStamp, float ActorTimeDilation) const | |
{ | |
if (bResolvingTimeDiscrepancy) | |
{ | |
return TimeDiscrepancyResolutionMoveDeltaOverride; | |
} | |
else | |
{ | |
return GetBaseServerMoveDeltaTime(ClientTimeStamp, ActorTimeDilation); | |
} | |
} | |
float FMMONetworkPredictionData_Server_Character::GetBaseServerMoveDeltaTime(float ClientTimeStamp, float ActorTimeDilation) const | |
{ | |
const float DeltaTime = FMath::Min(MaxMoveDeltaTime * ActorTimeDilation, ClientTimeStamp - CurrentClientTimeStamp); | |
return DeltaTime; | |
} | |
FMMOSavedMove_Character::FMMOSavedMove_Character() | |
{ | |
AccelMagThreshold = 1.f; | |
AccelDotThreshold = 0.9f; | |
AccelDotThresholdCombine = 0.996f; // approx 5 degrees. | |
MaxSpeedThresholdCombine = 10.0f; | |
} | |
FMMOSavedMove_Character::~FMMOSavedMove_Character() | |
{ | |
} | |
void FMMOSavedMove_Character::Clear() | |
{ | |
bPressedJump = false; | |
bWantsToCrouch = false; | |
bForceMaxAccel = false; | |
bForceNoCombine = false; | |
bOldTimeStampBeforeReset = false; | |
bWasJumping = false; | |
TimeStamp = 0.f; | |
DeltaTime = 0.f; | |
CustomTimeDilation = 1.0f; | |
JumpKeyHoldTime = 0.0f; | |
JumpForceTimeRemaining = 0.0f; | |
JumpCurrentCount = 0; | |
JumpMaxCount = 1; | |
MovementMode = 0; // Deprecated, keep backwards compat until removed | |
StartPackedMovementMode = 0; | |
StartLocation = FVector::ZeroVector; | |
StartRelativeLocation = FVector::ZeroVector; | |
StartVelocity = FVector::ZeroVector; | |
StartFloor = FMMOFindFloorResult(); | |
StartRotation = FRotator::ZeroRotator; | |
StartControlRotation = FRotator::ZeroRotator; | |
StartBaseRotation = FQuat::Identity; | |
StartCapsuleRadius = 0.f; | |
StartCapsuleHalfHeight = 0.f; | |
StartBase = nullptr; | |
StartBoneName = NAME_None; | |
StartActorOverlapCounter = 0; | |
StartComponentOverlapCounter = 0; | |
StartAttachParent = nullptr; | |
StartAttachSocketName = NAME_None; | |
StartAttachRelativeLocation = FVector::ZeroVector; | |
StartAttachRelativeRotation = FRotator::ZeroRotator; | |
SavedLocation = FVector::ZeroVector; | |
SavedRotation = FRotator::ZeroRotator; | |
SavedRelativeLocation = FVector::ZeroVector; | |
SavedControlRotation = FRotator::ZeroRotator; | |
Acceleration = FVector::ZeroVector; | |
MaxSpeed = 0.0f; | |
AccelMag = 0.0f; | |
AccelNormal = FVector::ZeroVector; | |
EndBase = nullptr; | |
EndBoneName = NAME_None; | |
EndActorOverlapCounter = 0; | |
EndComponentOverlapCounter = 0; | |
EndPackedMovementMode = 0; | |
EndAttachParent = nullptr; | |
EndAttachSocketName = NAME_None; | |
EndAttachRelativeLocation = FVector::ZeroVector; | |
EndAttachRelativeRotation = FRotator::ZeroRotator; | |
RootMotionMontage = NULL; | |
RootMotionTrackPosition = 0.f; | |
RootMotionMovement.Clear(); | |
SavedRootMotion.Clear(); | |
} | |
void FMMOSavedMove_Character::SetMoveFor(AMMOCharacter* Character, float InDeltaTime, FVector const& NewAccel, class FMMONetworkPredictionData_Client_Character& ClientData) | |
{ | |
CharacterOwner = Character; | |
DeltaTime = InDeltaTime; | |
SetInitialPosition(Character); | |
AccelMag = NewAccel.Size(); | |
AccelNormal = (AccelMag > SMALL_NUMBER ? NewAccel / AccelMag : FVector::ZeroVector); | |
// Round value, so that client and server match exactly (and so we can send with less bandwidth). This rounded value is copied back to the client in ReplicateMoveToServer. | |
// This is done after the AccelMag and AccelNormal are computed above, because those are only used client-side for combining move logic and need to remain accurate. | |
Acceleration = Character->GetCharacterMovement()->RoundAcceleration(NewAccel); | |
MaxSpeed = Character->GetCharacterMovement()->GetMaxSpeed(); | |
// CheckJumpInput will increment JumpCurrentCount. | |
// Therefore, for replicated moves we want it to set it at 1 less to properly | |
// handle the change. | |
JumpCurrentCount = Character->JumpCurrentCount > 0 ? Character->JumpCurrentCount - 1 : 0; | |
bWantsToCrouch = Character->GetCharacterMovement()->bWantsToCrouch; | |
bForceMaxAccel = Character->GetCharacterMovement()->bForceMaxAccel; | |
StartPackedMovementMode = Character->GetCharacterMovement()->PackNetworkMovementMode(); | |
MovementMode = StartPackedMovementMode; // Deprecated, keep backwards compat until removed | |
// Root motion source-containing moves should never be combined | |
// Main discovered issue being a move without root motion combining with | |
// a move with it will cause the DeltaTime for that next move to be larger than | |
// intended (effectively root motion applies to movement that happened prior to its activation) | |
if (Character->GetCharacterMovement()->CurrentRootMotion.HasActiveRootMotionSources()) | |
{ | |
bForceNoCombine = true; | |
} | |
// Moves with anim root motion should not be combined | |
const FAnimMontageInstance* RootMotionMontageInstance = Character->GetRootMotionAnimMontageInstance(); | |
if (RootMotionMontageInstance) | |
{ | |
bForceNoCombine = true; | |
} | |
// Launch velocity gives instant and potentially huge change of velocity | |
// Avoid combining move to prevent from reverting locations until server catches up | |
const bool bHasLaunchVelocity = !Character->GetCharacterMovement()->PendingLaunchVelocity.IsZero(); | |
if (bHasLaunchVelocity) | |
{ | |
bForceNoCombine = true; | |
} | |
TimeStamp = ClientData.CurrentTimeStamp; | |
} | |
void FMMOSavedMove_Character::SetInitialPosition(AMMOCharacter* Character) | |
{ | |
StartLocation = Character->GetActorLocation(); | |
StartRotation = Character->GetActorRotation(); | |
StartVelocity = Character->GetCharacterMovement()->Velocity; | |
UPrimitiveComponent* const MovementBase = Character->GetMovementBase(); | |
StartBase = MovementBase; | |
StartBaseRotation = FQuat::Identity; | |
StartFloor = Character->GetCharacterMovement()->CurrentFloor; | |
CustomTimeDilation = Character->CustomTimeDilation; | |
StartBoneName = Character->GetBasedMovement().BoneName; | |
StartActorOverlapCounter = Character->NumActorOverlapEventsCounter; | |
StartComponentOverlapCounter = UPrimitiveComponent::GlobalOverlapEventsCounter; | |
if (MovementBaseUtility::UseRelativeLocation(MovementBase)) | |
{ | |
StartRelativeLocation = Character->GetBasedMovement().Location; | |
FVector StartBaseLocation_Unused; | |
MovementBaseUtility::GetMovementBaseTransform(MovementBase, StartBoneName, StartBaseLocation_Unused, StartBaseRotation); | |
} | |
// Attachment state | |
if (const USceneComponent* UpdatedComponent = Character->GetCharacterMovement()->UpdatedComponent) | |
{ | |
StartAttachParent = UpdatedComponent->GetAttachParent(); | |
StartAttachSocketName = UpdatedComponent->GetAttachSocketName(); | |
StartAttachRelativeLocation = UpdatedComponent->GetRelativeLocation(); | |
StartAttachRelativeRotation = UpdatedComponent->GetRelativeRotation(); | |
} | |
StartControlRotation = Character->GetControlRotation().Clamp(); | |
Character->GetCapsuleComponent()->GetScaledCapsuleSize(StartCapsuleRadius, StartCapsuleHalfHeight); | |
// Jump state | |
bPressedJump = Character->bPressedJump; | |
bWasJumping = Character->bWasJumping; | |
JumpKeyHoldTime = Character->JumpKeyHoldTime; | |
JumpForceTimeRemaining = Character->JumpForceTimeRemaining; | |
JumpMaxCount = Character->JumpMaxCount; | |
} | |
void FMMOSavedMove_Character::PostUpdate(AMMOCharacter* Character, FMMOSavedMove_Character::EPostUpdateMode PostUpdateMode) | |
{ | |
// Common code for both recording and after a replay. | |
{ | |
EndPackedMovementMode = Character->GetCharacterMovement()->PackNetworkMovementMode(); | |
MovementMode = EndPackedMovementMode; // Deprecated, keep backwards compat until removed | |
SavedLocation = Character->GetActorLocation(); | |
SavedRotation = Character->GetActorRotation(); | |
SavedVelocity = Character->GetVelocity(); | |
#if ENABLE_NAN_DIAGNOSTIC | |
const float WarnVelocitySqr = 20000.f * 20000.f; | |
if (SavedVelocity.SizeSquared() > WarnVelocitySqr) | |
{ | |
if (Character->SavedRootMotion.HasActiveRootMotionSources()) | |
{ | |
UE_LOG(LogMMOCharacterMovement, Log, TEXT("FMMOSavedMove_Character::PostUpdate detected very high Velocity! (%s), but with active root motion sources (could be intentional)"), *SavedVelocity.ToString()); | |
} | |
else | |
{ | |
UE_LOG(LogMMOCharacterMovement, Warning, TEXT("FMMOSavedMove_Character::PostUpdate detected very high Velocity! (%s)"), *SavedVelocity.ToString()); | |
} | |
} | |
#endif | |
UPrimitiveComponent* const MovementBase = Character->GetMovementBase(); | |
EndBase = MovementBase; | |
EndBoneName = Character->GetBasedMovement().BoneName; | |
if (MovementBaseUtility::UseRelativeLocation(MovementBase)) | |
{ | |
SavedRelativeLocation = Character->GetBasedMovement().Location; | |
} | |
// Attachment state | |
if (const USceneComponent* UpdatedComponent = Character->GetCharacterMovement()->UpdatedComponent) | |
{ | |
EndAttachParent = UpdatedComponent->GetAttachParent(); | |
EndAttachSocketName = UpdatedComponent->GetAttachSocketName(); | |
EndAttachRelativeLocation = UpdatedComponent->GetRelativeLocation(); | |
EndAttachRelativeRotation = UpdatedComponent->GetRelativeRotation(); | |
} | |
SavedControlRotation = Character->GetControlRotation().Clamp(); | |
} | |
// Only save RootMotion params when initially recording | |
if (PostUpdateMode == PostUpdate_Record) | |
{ | |
const FAnimMontageInstance* RootMotionMontageInstance = Character->GetRootMotionAnimMontageInstance(); | |
if (RootMotionMontageInstance) | |
{ | |
if (!RootMotionMontageInstance->IsRootMotionDisabled()) | |
{ | |
RootMotionMontage = RootMotionMontageInstance->Montage; | |
RootMotionTrackPosition = RootMotionMontageInstance->GetPosition(); | |
RootMotionMovement = Character->ClientRootMotionParams; | |
} | |
// Moves where anim root motion is being played should not be combined | |
bForceNoCombine = true; | |
} | |
// Save off Root Motion Sources | |
if (Character->SavedRootMotion.HasActiveRootMotionSources()) | |
{ | |
SavedRootMotion = Character->SavedRootMotion; | |
bForceNoCombine = true; | |
} | |
// Don't want to combine moves that trigger overlaps, because by moving back and replaying the move we could retrigger overlaps. | |
EndActorOverlapCounter = Character->NumActorOverlapEventsCounter; | |
EndComponentOverlapCounter = UPrimitiveComponent::GlobalOverlapEventsCounter; | |
if ((StartActorOverlapCounter != EndActorOverlapCounter) || (StartComponentOverlapCounter != EndComponentOverlapCounter)) | |
{ | |
bForceNoCombine = true; | |
} | |
// Don't combine or delay moves where velocity changes to/from zero. | |
if (StartVelocity.IsZero() != SavedVelocity.IsZero()) | |
{ | |
bForceNoCombine = true; | |
} | |
// Don't combine if this move caused us to change movement modes during the move. | |
if (StartPackedMovementMode != EndPackedMovementMode) | |
{ | |
bForceNoCombine = true; | |
} | |
// Don't combine when jump input just began or ended during the move. | |
if (bPressedJump != CharacterOwner->bPressedJump) | |
{ | |
bForceNoCombine = true; | |
} | |
} | |
else if (PostUpdateMode == PostUpdate_Replay) | |
{ | |
if (Character->bClientResimulateRootMotionSources) | |
{ | |
// When replaying moves, the next move should use the results of this move | |
// so that future replayed moves account for the server correction | |
Character->SavedRootMotion = Character->GetCharacterMovement()->CurrentRootMotion; | |
} | |
} | |
} | |
bool FMMOSavedMove_Character::IsImportantMove(const FMMOSavedMovePtr& LastAckedMovePtr) const | |
{ | |
const FMMOSavedMove_Character* LastAckedMove = LastAckedMovePtr.Get(); | |
// Check if any important movement flags have changed status. | |
if (GetCompressedFlags() != LastAckedMove->GetCompressedFlags()) | |
{ | |
return true; | |
} | |
if (StartPackedMovementMode != LastAckedMove->EndPackedMovementMode) | |
{ | |
return true; | |
} | |
if (EndPackedMovementMode != LastAckedMove->EndPackedMovementMode) | |
{ | |
return true; | |
} | |
// check if acceleration has changed significantly | |
if (Acceleration != LastAckedMove->Acceleration) | |
{ | |
// Compare magnitude and orientation | |
if ((FMath::Abs(AccelMag - LastAckedMove->AccelMag) > AccelMagThreshold) || ((AccelNormal | LastAckedMove->AccelNormal) < AccelDotThreshold)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
FVector FMMOSavedMove_Character::GetRevertedLocation() const | |
{ | |
if (const USceneComponent* AttachParent = StartAttachParent.Get()) | |
{ | |
return AttachParent->GetSocketTransform(StartAttachSocketName).TransformPosition(StartAttachRelativeLocation); | |
} | |
const UPrimitiveComponent* MovementBase = StartBase.Get(); | |
if (MovementBaseUtility::UseRelativeLocation(MovementBase)) | |
{ | |
FVector BaseLocation; FQuat BaseRotation; | |
MovementBaseUtility::GetMovementBaseTransform(MovementBase, StartBoneName, BaseLocation, BaseRotation); | |
return BaseLocation + StartRelativeLocation; | |
} | |
return StartLocation; | |
} | |
bool UMMOPlayerMovement::CanDelaySendingMove(const FMMOSavedMovePtr& NewMovePtr) | |
{ | |
const FMMOSavedMove_Character* NewMove = NewMovePtr.Get(); | |
// Don't delay moves that change movement mode over the course of the move. | |
if (NewMove->StartPackedMovementMode != NewMove->EndPackedMovementMode) | |
{ | |
return false; | |
} | |
// If we know we don't want to combine this move, reduce latency and avoid misprediction by flushing immediately. | |
if (NewMove->bForceNoCombine) | |
{ | |
return false; | |
} | |
return true; | |
} | |
float UMMOPlayerMovement::GetClientNetSendDeltaTime(const APlayerController* PC, const FMMONetworkPredictionData_Client_Character* ClientData, const FMMOSavedMovePtr& NewMove) const | |
{ | |
const UPlayer* Player = (PC ? PC->Player : nullptr); | |
const UWorld* MyWorld = GetWorld(); | |
const AGameStateBase* const GameState = MyWorld->GetGameState(); | |
const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject()); | |
float NetMoveDelta = GameNetworkManager->ClientNetSendMoveDeltaTime; | |
if (PC && Player) | |
{ | |
// send moves more frequently in small games where server isn't likely to be saturated | |
if ((Player->CurrentNetSpeed > GameNetworkManager->ClientNetSendMoveThrottleAtNetSpeed) && (GameState != nullptr) && (GameState->PlayerArray.Num() <= GameNetworkManager->ClientNetSendMoveThrottleOverPlayerCount)) | |
{ | |
NetMoveDelta = GameNetworkManager->ClientNetSendMoveDeltaTime; | |
} | |
else | |
{ | |
NetMoveDelta = FMath::Max(GameNetworkManager->ClientNetSendMoveDeltaTimeThrottled, 2 * GameNetworkManager->MoveRepSize / Player->CurrentNetSpeed); | |
} | |
// Lower frequency for standing still and not rotating camera | |
if (Acceleration.IsZero() && Velocity.IsZero() && ClientData->LastAckedMove.IsValid() && ClientData->LastAckedMove->IsMatchingStartControlRotation(PC)) | |
{ | |
NetMoveDelta = FMath::Max(GameNetworkManager->ClientNetSendMoveDeltaTimeStationary, NetMoveDelta); | |
} | |
} | |
return NetMoveDelta; | |
} | |
bool FMMOSavedMove_Character::IsMatchingStartControlRotation(const APlayerController* PC) const | |
{ | |
return PC ? StartControlRotation.Equals(PC->GetControlRotation(), MMOCharacterMovementCVars::NetStationaryRotationTolerance) : false; | |
} | |
void FMMOSavedMove_Character::GetPackedAngles(uint32& YawAndPitchPack, uint8& RollPack) const | |
{ | |
// Compress rotation down to 5 bytes | |
YawAndPitchPack = UMMOPlayerMovement::PackYawAndPitchTo32(SavedControlRotation.Yaw, SavedControlRotation.Pitch); | |
RollPack = FRotator::CompressAxisToByte(SavedControlRotation.Roll); | |
} | |
bool FMMOSavedMove_Character::CanCombineWith(const FMMOSavedMovePtr& NewMovePtr, AMMOCharacter* Character, float MaxDelta) const | |
{ | |
const FMMOSavedMove_Character* NewMove = NewMovePtr.Get(); | |
if (bForceNoCombine || NewMove->bForceNoCombine) | |
{ | |
return false; | |
} | |
if (bOldTimeStampBeforeReset) | |
{ | |
return false; | |
} | |
// Cannot combine moves which contain root motion for now. | |
// @fixme laurent - we should be able to combine most of them though, but current scheme of resetting pawn location and resimulating forward doesn't work. | |
// as we don't want to tick montage twice (so we don't fire events twice). So we need to rearchitecture this so we tick only the second part of the move, and reuse the first part. | |
if ((RootMotionMontage != NULL) || (NewMove->RootMotionMontage != NULL)) | |
{ | |
return false; | |
} | |
if (NewMove->Acceleration.IsZero()) | |
{ | |
if (!Acceleration.IsZero()) | |
{ | |
return false; | |
} | |
} | |
else | |
{ | |
if (NewMove->DeltaTime + DeltaTime >= MaxDelta) | |
{ | |
return false; | |
} | |
if (!FVector::Coincident(AccelNormal, NewMove->AccelNormal, AccelDotThresholdCombine)) | |
{ | |
return false; | |
} | |
} | |
// Don't combine moves where velocity changes to zero or from zero. | |
if (StartVelocity.IsZero() != NewMove->StartVelocity.IsZero()) | |
{ | |
return false; | |
} | |
if (!FMath::IsNearlyEqual(MaxSpeed, NewMove->MaxSpeed, MaxSpeedThresholdCombine)) | |
{ | |
return false; | |
} | |
if ((MaxSpeed == 0.0f) != (NewMove->MaxSpeed == 0.0f)) | |
{ | |
return false; | |
} | |
// Don't combine on changes to/from zero JumpKeyHoldTime. | |
if ((JumpKeyHoldTime == 0.f) != (NewMove->JumpKeyHoldTime == 0.f)) | |
{ | |
return false; | |
} | |
if ((bWasJumping != NewMove->bWasJumping) || (JumpCurrentCount != NewMove->JumpCurrentCount) || (JumpMaxCount != NewMove->JumpMaxCount)) | |
{ | |
return false; | |
} | |
// Don't combine on changes to/from zero. | |
if ((JumpForceTimeRemaining == 0.f) != (NewMove->JumpForceTimeRemaining == 0.f)) | |
{ | |
return false; | |
} | |
// Compressed flags not equal, can't combine. This covers jump and crouch as well as any custom movement flags from overrides. | |
if (GetCompressedFlags() != NewMove->GetCompressedFlags()) | |
{ | |
return false; | |
} | |
const UPrimitiveComponent* OldBasePtr = StartBase.Get(); | |
const UPrimitiveComponent* NewBasePtr = NewMove->StartBase.Get(); | |
const bool bDynamicBaseOld = MovementBaseUtility::IsDynamicBase(OldBasePtr); | |
const bool bDynamicBaseNew = MovementBaseUtility::IsDynamicBase(NewBasePtr); | |
// Change between static/dynamic requires separate moves (position sent as world vs relative) | |
if (bDynamicBaseOld != bDynamicBaseNew) | |
{ | |
return false; | |
} | |
// Only need to prevent combining when on a dynamic base that changes (unless forced off via CVar). Again, because relative location can change. | |
const bool bPreventOnStaticBaseChange = (MMOCharacterMovementCVars::NetEnableMoveCombiningOnStaticBaseChange == 0); | |
if (bPreventOnStaticBaseChange || (bDynamicBaseOld || bDynamicBaseNew)) | |
{ | |
if (OldBasePtr != NewBasePtr) | |
{ | |
return false; | |
} | |
if (StartBoneName != NewMove->StartBoneName) | |
{ | |
return false; | |
} | |
} | |
if (StartPackedMovementMode != NewMove->StartPackedMovementMode) | |
{ | |
return false; | |
} | |
if (EndPackedMovementMode != NewMove->StartPackedMovementMode) | |
{ | |
return false; | |
} | |
if (StartCapsuleRadius != NewMove->StartCapsuleRadius) | |
{ | |
return false; | |
} | |
if (StartCapsuleHalfHeight != NewMove->StartCapsuleHalfHeight) | |
{ | |
return false; | |
} | |
// No combining if attach parent changed. | |
const USceneComponent* OldStartAttachParent = StartAttachParent.Get(); | |
const USceneComponent* OldEndAttachParent = EndAttachParent.Get(); | |
const USceneComponent* NewStartAttachParent = NewMove->StartAttachParent.Get(); | |
if (OldStartAttachParent != NewStartAttachParent || OldEndAttachParent != NewStartAttachParent) | |
{ | |
return false; | |
} | |
// No combining if attach socket changed. | |
if (StartAttachSocketName != NewMove->StartAttachSocketName || EndAttachSocketName != NewMove->StartAttachSocketName) | |
{ | |
return false; | |
} | |
if (NewStartAttachParent != nullptr) | |
{ | |
// If attached, no combining if relative location changed. | |
const FVector RelativeLocationDelta = (StartAttachRelativeLocation - NewMove->StartAttachRelativeLocation); | |
if (!RelativeLocationDelta.IsNearlyZero(MMOCharacterMovementCVars::NetMoveCombiningAttachedLocationTolerance)) | |
{ | |
//UE_LOG(LogMMOCharacterMovement, Warning, TEXT("NoCombine: DeltaLocation(%s)"), *RelativeLocationDelta.ToString()); | |
return false; | |
} | |
// For rotation, Yaw doesn't matter for capsules | |
FRotator RelativeRotationDelta = StartAttachRelativeRotation - NewMove->StartAttachRelativeRotation; | |
RelativeRotationDelta.Yaw = 0.0f; | |
if (!RelativeRotationDelta.IsNearlyZero(MMOCharacterMovementCVars::NetMoveCombiningAttachedRotationTolerance)) | |
{ | |
return false; | |
} | |
} | |
else | |
{ | |
// Not attached to anything. Only combine if base hasn't rotated. | |
if (!StartBaseRotation.Equals(NewMove->StartBaseRotation)) | |
{ | |
return false; | |
} | |
} | |
if (CustomTimeDilation != NewMove->CustomTimeDilation) | |
{ | |
return false; | |
} | |
// Don't combine moves with overlap event changes, since reverting back and then moving forward again can cause event spam. | |
// This catches events between movement updates; moves that trigger events already set bForceNoCombine to false. | |
if (EndActorOverlapCounter != NewMove->StartActorOverlapCounter) | |
{ | |
return false; | |
} | |
return true; | |
} | |
void FMMOSavedMove_Character::CombineWith(const FMMOSavedMove_Character* OldMove, AMMOCharacter* InCharacter, APlayerController* PC, const FVector& OldStartLocation) | |
{ | |
UMMOPlayerMovement* CharMovement = InCharacter->GetCharacterMovement(); | |
// to combine move, first revert pawn position to PendingMove start position, before playing combined move on client | |
if (const USceneComponent* AttachParent = StartAttachParent.Get()) | |
{ | |
CharMovement->UpdatedComponent->SetRelativeLocationAndRotation(StartAttachRelativeLocation, StartAttachRelativeRotation, false, nullptr, CharMovement->GetTeleportType()); | |
} | |
else | |
{ | |
CharMovement->UpdatedComponent->SetWorldLocationAndRotation(OldStartLocation, OldMove->StartRotation, false, nullptr, CharMovement->GetTeleportType()); | |
} | |
CharMovement->Velocity = OldMove->StartVelocity; | |
CharMovement->SetBase(OldMove->StartBase.Get(), OldMove->StartBoneName); | |
CharMovement->CurrentFloor = OldMove->StartFloor; | |
// Now that we have reverted to the old position, prepare a new move from that position, | |
// using our current velocity, acceleration, and rotation, but applied over the combined time from the old and new move. | |
// Combine times for both moves | |
DeltaTime += OldMove->DeltaTime; | |
// Roll back jump force counters. SetInitialPosition() below will copy them to the saved move. | |
// Changes in certain counters like JumpCurrentCount don't allow move combining, so no need to roll those back (they are the same). | |
InCharacter->JumpForceTimeRemaining = OldMove->JumpForceTimeRemaining; | |
InCharacter->JumpKeyHoldTime = OldMove->JumpKeyHoldTime; | |
} | |
void FMMOSavedMove_Character::PrepMoveFor(AMMOCharacter* Character) | |
{ | |
Character->GetCharacterMovement()->bForceMaxAccel = bForceMaxAccel; | |
Character->bWasJumping = bWasJumping; | |
Character->JumpKeyHoldTime = JumpKeyHoldTime; | |
Character->JumpForceTimeRemaining = JumpForceTimeRemaining; | |
Character->JumpMaxCount = JumpMaxCount; | |
Character->JumpCurrentCount = JumpCurrentCount; | |
StartPackedMovementMode = Character->GetCharacterMovement()->PackNetworkMovementMode(); | |
} | |
uint8 FMMOSavedMove_Character::GetCompressedFlags() const | |
{ | |
uint8 Result = 0; | |
if (bPressedJump) | |
{ | |
Result |= FLAG_JumpPressed; | |
} | |
if (bWantsToCrouch) | |
{ | |
Result |= FLAG_WantsToCrouch; | |
} | |
return Result; | |
} | |
void UMMOPlayerMovement::UpdateFromCompressedFlags(uint8 Flags) | |
{ | |
if (!CharacterOwner) | |
{ | |
return; | |
} | |
const bool bWasPressingJump = CharacterOwner->bPressedJump; | |
CharacterOwner->bPressedJump = ((Flags & FMMOSavedMove_Character::FLAG_JumpPressed) != 0); | |
bWantsToCrouch = ((Flags & FMMOSavedMove_Character::FLAG_WantsToCrouch) != 0); | |
// Detect change in jump press on the server | |
if (CharacterOwner->GetLocalRole() == ROLE_Authority) | |
{ | |
const bool bIsPressingJump = CharacterOwner->bPressedJump; | |
if (bIsPressingJump && !bWasPressingJump) | |
{ | |
CharacterOwner->Jump(); | |
} | |
else if (!bIsPressingJump) | |
{ | |
CharacterOwner->StopJumping(); | |
} | |
} | |
} | |
void UMMOPlayerMovement::FlushServerMoves() | |
{ | |
// Send pendingMove to server if this character is replicating movement | |
if (CharacterOwner && CharacterOwner->IsReplicatingMovement() && (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy)) | |
{ | |
FMMONetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character(); | |
if (!ClientData) | |
{ | |
return; | |
} | |
if (ClientData->PendingMove.IsValid()) | |
{ | |
const UWorld* MyWorld = GetWorld(); | |
ClientData->ClientUpdateTime = MyWorld->TimeSeconds; | |
FMMOSavedMovePtr NewMove = ClientData->PendingMove; | |
ClientData->PendingMove = nullptr; | |
UE_CLOG(CharacterOwner && UpdatedComponent, LogNetPlayerMovement, Verbose, TEXT("ClientMove (Flush) Time %f Acceleration %s Velocity %s Position %s DeltaTime %f Mode %s MovementBase %s.%s (Dynamic:%d) DualMove? %d"), | |
NewMove->TimeStamp, *NewMove->Acceleration.ToString(), *Velocity.ToString(), *UpdatedComponent->GetComponentLocation().ToString(), NewMove->DeltaTime, *GetMovementName(), | |
*GetNameSafe(NewMove->EndBase.Get()), *NewMove->EndBoneName.ToString(), MovementBaseUtility::IsDynamicBase(NewMove->EndBase.Get()) ? 1 : 0, ClientData->PendingMove.IsValid() ? 1 : 0); | |
CallServerMove(NewMove.Get(), nullptr); | |
} | |
} | |
} | |
ETeleportType UMMOPlayerMovement::GetTeleportType() const | |
{ | |
return bJustTeleported || bNetworkLargeClientCorrection ? ETeleportType::TeleportPhysics : ETeleportType::None; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Epic Games, Inc. All Rights Reserved. | |
#pragma once | |
#include "CoreMinimal.h" | |
#include "Math/RandomStream.h" | |
#include "UObject/ObjectMacros.h" | |
#include "UObject/UObjectGlobals.h" | |
#include "Engine/NetSerialization.h" | |
#include "Engine/EngineTypes.h" | |
#include "Engine/EngineBaseTypes.h" | |
#include "WorldCollision.h" | |
#include "AI/Navigation/NavigationTypes.h" | |
#include "Animation/AnimationAsset.h" | |
#include "Animation/AnimMontage.h" | |
#include "GameFramework/RootMotionSource.h" | |
#include "AI/Navigation/NavigationAvoidanceTypes.h" | |
#include "AI/RVOAvoidanceInterface.h" | |
#include "GameFramework/PawnMovementComponent.h" | |
#include "Interfaces/NetworkPredictionInterface.h" | |
#include "MMOPlayerMovement.generated.h" | |
class AMMOCharacter; | |
class FDebugDisplayInfo; | |
class FMMONetworkPredictionData_Server_Character; | |
class FMMOSavedMove_Character; | |
class UPrimitiveComponent; | |
class INavigationData; | |
class UMMOPlayerMovement; | |
DECLARE_DELEGATE_RetVal_TwoParams(FTransform, FMMOOnProcessRootMotion, const FTransform&, UMMOPlayerMovement*) | |
/** Data about the floor for walking movement, used by CharacterMovementComponent. */ | |
USTRUCT(BlueprintType) | |
struct MMOEY_API FMMOFindFloorResult | |
{ | |
GENERATED_USTRUCT_BODY() | |
/** | |
* True if there was a blocking hit in the floor test that was NOT in initial penetration. | |
* The HitResult can give more info about other circumstances. | |
*/ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = CharacterFloor) | |
uint32 bBlockingHit : 1; | |
/** True if the hit found a valid walkable floor. */ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = CharacterFloor) | |
uint32 bWalkableFloor : 1; | |
/** True if the hit found a valid walkable floor using a line trace (rather than a sweep test, which happens when the sweep test fails to yield a walkable surface). */ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = CharacterFloor) | |
uint32 bLineTrace : 1; | |
/** The distance to the floor, computed from the swept capsule trace. */ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = CharacterFloor) | |
float FloorDist; | |
/** The distance to the floor, computed from the trace. Only valid if bLineTrace is true. */ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = CharacterFloor) | |
float LineDist; | |
/** Hit result of the test that found a floor. Includes more specific data about the point of impact and surface normal at that point. */ | |
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = CharacterFloor) | |
FHitResult HitResult; | |
public: | |
FMMOFindFloorResult() | |
: bBlockingHit(false) | |
, bWalkableFloor(false) | |
, bLineTrace(false) | |
, FloorDist(0.f) | |
, LineDist(0.f) | |
, HitResult(1.f) | |
{ | |
} | |
/** Returns true if the floor result hit a walkable surface. */ | |
bool IsWalkableFloor() const | |
{ | |
return bBlockingHit && bWalkableFloor; | |
} | |
void Clear() | |
{ | |
bBlockingHit = false; | |
bWalkableFloor = false; | |
bLineTrace = false; | |
FloorDist = 0.f; | |
LineDist = 0.f; | |
HitResult.Reset(1.f, false); | |
} | |
/** Gets the distance to floor, either LineDist or FloorDist. */ | |
float GetDistanceToFloor() const | |
{ | |
// When the floor distance is set using SetFromSweep, the LineDist value will be reset. | |
// However, when SetLineFromTrace is used, there's no guarantee that FloorDist is set. | |
return bLineTrace ? LineDist : FloorDist; | |
} | |
void SetFromSweep(const FHitResult& InHit, const float InSweepFloorDist, const bool bIsWalkableFloor); | |
void SetFromLineTrace(const FHitResult& InHit, const float InSweepFloorDist, const float InLineDist, const bool bIsWalkableFloor); | |
}; | |
/** | |
* Tick function that calls UMMOPlayerMovement::PostPhysicsTickComponent | |
**/ | |
USTRUCT() | |
struct FMMOCharacterMovementComponentPostPhysicsTickFunction : public FTickFunction | |
{ | |
GENERATED_USTRUCT_BODY() | |
/** CharacterMovementComponent that is the target of this tick **/ | |
class UMMOPlayerMovement* Target; | |
/** | |
* Abstract function actually execute the tick. | |
* @param DeltaTime - frame time to advance, in seconds | |
* @param TickType - kind of tick for this frame | |
* @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created | |
* @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completion of this task until certain child tasks are complete. | |
**/ | |
virtual void ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; | |
/** Abstract function to describe this tick. Used to print messages about illegal cycles in the dependency graph **/ | |
virtual FString DiagnosticMessage() override; | |
/** Function used to describe this tick for active tick reporting. **/ | |
virtual FName DiagnosticContext(bool bDetailed) override; | |
}; | |
template<> | |
struct TStructOpsTypeTraits<FMMOCharacterMovementComponentPostPhysicsTickFunction> : public TStructOpsTypeTraitsBase2<FMMOCharacterMovementComponentPostPhysicsTickFunction> | |
{ | |
enum | |
{ | |
WithCopy = false | |
}; | |
}; | |
/** Shared pointer for easy memory management of FMMOSavedMove_Character, for accumulating and replaying network moves. */ | |
typedef TSharedPtr<class FMMOSavedMove_Character> FMMOSavedMovePtr; | |
//============================================================================= | |
/** | |
* CharacterMovementComponent handles movement logic for the associated Character owner. | |
* It supports various movement modes including: walking, falling, swimming, flying, custom. | |
* | |
* Movement is affected primarily by current Velocity and Acceleration. Acceleration is updated each frame | |
* based on the input vector accumulated thus far (see UPawnMovementComponent::GetPendingInputVector()). | |
* | |
* Networking is fully implemented, with server-client correction and prediction included. | |
* | |
* @see AMMOCharacter, UPawnMovementComponent | |
* @see https://docs.unrealengine.com/latest/INT/Gameplay/Framework/Pawn/Character/ | |
*/ | |
UCLASS(ClassGroup = Movement, meta = (BlueprintSpawnableComponent)) | |
class MMOEY_API UMMOPlayerMovement : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface | |
{ | |
GENERATED_BODY() | |
public: | |
/** | |
* Default UObject constructor. | |
*/ | |
UMMOPlayerMovement(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); | |
protected: | |
/** Character movement component belongs to */ | |
UPROPERTY(Transient, DuplicateTransient) | |
AMMOCharacter* CharacterOwner; | |
public: | |
/** Custom gravity scale. Gravity is multiplied by this amount for the character. */ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite) | |
float GravityScale; | |
/** Maximum height character can step up */ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxStepHeight; | |
/** Initial velocity (instantaneous vertical acceleration) when jumping. */ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, meta = (DisplayName = "Jump Z Velocity", ClampMin = "0", UIMin = "0")) | |
float JumpZVelocity; | |
/** Fraction of JumpZVelocity to use when automatically "jumping off" of a base actor that's not allowed to be a base for a character. (For example, if you're not allowed to stand on other players.) */ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float JumpOffJumpZFactor; | |
private: | |
/** | |
* Max angle in degrees of a walkable surface. Any greater than this and it is too steep to be walkable. | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, meta = (ClampMin = "0.0", ClampMax = "90.0", UIMin = "0.0", UIMax = "90.0")) | |
float WalkableFloorAngle; | |
/** | |
* Minimum Z value for floor normal. If less, not a walkable surface. Computed from WalkableFloorAngle. | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", VisibleAnywhere) | |
float WalkableFloorZ; | |
public: | |
/** | |
* Actor's current movement mode (walking, falling, etc). | |
* - walking: Walking on a surface, under the effects of friction, and able to "step up" barriers. Vertical velocity is zero. | |
* - falling: Falling under the effects of gravity, after jumping or walking off the edge of a surface. | |
* - flying: Flying, ignoring the effects of gravity. | |
* - swimming: Swimming through a fluid volume, under the effects of gravity and buoyancy. | |
* - custom: User-defined custom movement mode, including many possible sub-modes. | |
* This is automatically replicated through the Character owner and for client-server movement functions. | |
* @see SetMovementMode(), CustomMovementMode | |
*/ | |
UPROPERTY(Category = "Character Movement: MovementMode", BlueprintReadOnly) | |
TEnumAsByte<enum EMovementMode> MovementMode; | |
/** | |
* Current custom sub-mode if MovementMode is set to Custom. | |
* This is automatically replicated through the Character owner and for client-server movement functions. | |
* @see SetMovementMode() | |
*/ | |
UPROPERTY(Category = "Character Movement: MovementMode", BlueprintReadOnly) | |
uint8 CustomMovementMode; | |
/** Smoothing mode for simulated proxies in network game. */ | |
UPROPERTY(Category = "Character Movement (Networking)", EditAnywhere, BlueprintReadOnly) | |
ENetworkSmoothingMode NetworkSmoothingMode; | |
/** | |
* Setting that affects movement control. Higher values allow faster changes in direction. | |
* If bUseSeparateBrakingFriction is false, also affects the ability to stop more quickly when braking (whenever Acceleration is zero), where it is multiplied by BrakingFrictionFactor. | |
* When braking, this property allows you to control how much friction is applied when moving across the ground, applying an opposing force that scales with current velocity. | |
* This can be used to simulate slippery surfaces such as ice or oil by changing the value (possibly based on the material pawn is standing on). | |
* @see BrakingDecelerationWalking, BrakingFriction, bUseSeparateBrakingFriction, BrakingFrictionFactor | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float GroundFriction; | |
/** Saved location of object we are standing on, for UpdateBasedMovement() to determine if base moved in the last frame, and therefore pawn needs an update. */ | |
FQuat OldBaseQuat; | |
/** Saved location of object we are standing on, for UpdateBasedMovement() to determine if base moved in the last frame, and therefore pawn needs an update. */ | |
FVector OldBaseLocation; | |
/** The maximum ground speed when walking. Also determines maximum lateral speed when falling. */ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxWalkSpeed; | |
/** The maximum ground speed when walking and crouched. */ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxWalkSpeedCrouched; | |
/** The maximum swimming speed. */ | |
UPROPERTY(Category = "Character Movement: Swimming", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxSwimSpeed; | |
/** The maximum flying speed. */ | |
UPROPERTY(Category = "Character Movement: Flying", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxFlySpeed; | |
/** The maximum speed when using Custom movement mode. */ | |
UPROPERTY(Category = "Character Movement: Custom Movement", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxCustomMovementSpeed; | |
/** Max Acceleration (rate of change of velocity) */ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxAcceleration; | |
/** The ground speed that we should accelerate up to when walking at minimum analog stick tilt */ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float MinAnalogWalkSpeed; | |
/** | |
* Factor used to multiply actual value of friction used when braking. | |
* This applies to any friction value that is currently used, which may depend on bUseSeparateBrakingFriction. | |
* @note This is 2 by default for historical reasons, a value of 1 gives the true drag equation. | |
* @see bUseSeparateBrakingFriction, GroundFriction, BrakingFriction | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float BrakingFrictionFactor; | |
/** | |
* Friction (drag) coefficient applied when braking (whenever Acceleration = 0, or if character is exceeding max speed); actual value used is this multiplied by BrakingFrictionFactor. | |
* When braking, this property allows you to control how much friction is applied when moving across the ground, applying an opposing force that scales with current velocity. | |
* Braking is composed of friction (velocity-dependent drag) and constant deceleration. | |
* This is the current value, used in all movement modes; if this is not desired, override it or bUseSeparateBrakingFriction when movement mode changes. | |
* @note Only used if bUseSeparateBrakingFriction setting is true, otherwise current friction such as GroundFriction is used. | |
* @see bUseSeparateBrakingFriction, BrakingFrictionFactor, GroundFriction, BrakingDecelerationWalking | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0", EditCondition = "bUseSeparateBrakingFriction")) | |
float BrakingFriction; | |
/** | |
* Time substepping when applying braking friction. Smaller time steps increase accuracy at the slight cost of performance, especially if there are large frame times. | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0.0166", ClampMax = "0.05", UIMin = "0.0166", UIMax = "0.05")) | |
float BrakingSubStepTime; | |
/** | |
* Deceleration when walking and not applying acceleration. This is a constant opposing force that directly lowers velocity by a constant value. | |
* @see GroundFriction, MaxAcceleration | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float BrakingDecelerationWalking; | |
/** | |
* Lateral deceleration when falling and not applying acceleration. | |
* @see MaxAcceleration | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float BrakingDecelerationFalling; | |
/** | |
* Deceleration when swimming and not applying acceleration. | |
* @see MaxAcceleration | |
*/ | |
UPROPERTY(Category = "Character Movement: Swimming", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float BrakingDecelerationSwimming; | |
/** | |
* Deceleration when flying and not applying acceleration. | |
* @see MaxAcceleration | |
*/ | |
UPROPERTY(Category = "Character Movement: Flying", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float BrakingDecelerationFlying; | |
/** | |
* When falling, amount of lateral movement control available to the character. | |
* 0 = no control, 1 = full control at max speed of MaxWalkSpeed. | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float AirControl; | |
/** | |
* When falling, multiplier applied to AirControl when lateral velocity is less than AirControlBoostVelocityThreshold. | |
* Setting this to zero will disable air control boosting. Final result is clamped at 1. | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float AirControlBoostMultiplier; | |
/** | |
* When falling, if lateral velocity magnitude is less than this value, AirControl is multiplied by AirControlBoostMultiplier. | |
* Setting this to zero will disable air control boosting. | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float AirControlBoostVelocityThreshold; | |
/** | |
* Friction to apply to lateral air movement when falling. | |
* If bUseSeparateBrakingFriction is false, also affects the ability to stop more quickly when braking (whenever Acceleration is zero). | |
* @see BrakingFriction, bUseSeparateBrakingFriction | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float FallingLateralFriction; | |
/** Collision half-height when crouching (component scale is applied separately) */ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadOnly, meta = (ClampMin = "0", UIMin = "0")) | |
float CrouchedHalfHeight; | |
/** Water buoyancy. A ratio (1.0 = neutral buoyancy, 0.0 = no buoyancy) */ | |
UPROPERTY(Category = "Character Movement: Swimming", EditAnywhere, BlueprintReadWrite) | |
float Buoyancy; | |
/** | |
* Don't allow the character to perch on the edge of a surface if the contact is this close to the edge of the capsule. | |
* Note that characters will not fall off if they are within MaxStepHeight of a walkable surface below. | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float PerchRadiusThreshold; | |
/** | |
* When perching on a ledge, add this additional distance to MaxStepHeight when determining how high above a walkable floor we can perch. | |
* Note that we still enforce MaxStepHeight to start the step up; this just allows the character to hang off the edge or step slightly higher off the floor. | |
* (@see PerchRadiusThreshold) | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float PerchAdditionalHeight; | |
/** Change in rotation per second, used when UseControllerDesiredRotation or OrientRotationToMovement are true. Set a negative value for infinite rotation rate and instant turns. */ | |
UPROPERTY(Category = "Character Movement (Rotation Settings)", EditAnywhere, BlueprintReadWrite) | |
FRotator RotationRate; | |
/** | |
* If true, BrakingFriction will be used to slow the character to a stop (when there is no Acceleration). | |
* If false, braking uses the same friction passed to CalcVelocity() (ie GroundFriction when walking), multiplied by BrakingFrictionFactor. | |
* This setting applies to all movement modes; if only desired in certain modes, consider toggling it when movement modes change. | |
* @see BrakingFriction | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditDefaultsOnly, BlueprintReadWrite) | |
uint8 bUseSeparateBrakingFriction : 1; | |
/** | |
* Apply gravity while the character is actively jumping (e.g. holding the jump key). | |
* Helps remove frame-rate dependent jump height, but may alter base jump height. | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
uint8 bApplyGravityWhileJumping : 1; | |
/** | |
* If true, smoothly rotate the Character toward the Controller's desired rotation (typically Controller->ControlRotation), using RotationRate as the rate of rotation change. Overridden by OrientRotationToMovement. | |
* Normally you will want to make sure that other settings are cleared, such as bUseControllerRotationYaw on the Character. | |
*/ | |
UPROPERTY(Category = "Character Movement (Rotation Settings)", EditAnywhere, BlueprintReadWrite) | |
uint8 bUseControllerDesiredRotation : 1; | |
/** | |
* If true, rotate the Character toward the direction of acceleration, using RotationRate as the rate of rotation change. Overrides UseControllerDesiredRotation. | |
* Normally you will want to make sure that other settings are cleared, such as bUseControllerRotationYaw on the Character. | |
*/ | |
UPROPERTY(Category = "Character Movement (Rotation Settings)", EditAnywhere, BlueprintReadWrite) | |
uint8 bOrientRotationToMovement : 1; | |
/** | |
* Whether or not the character should sweep for collision geometry while walking. | |
* @see USceneComponent::MoveComponent. | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite) | |
uint8 bSweepWhileNavWalking : 1; | |
private: | |
// Tracks whether or not we need to update the bSweepWhileNavWalking flag do to an upgrade. | |
uint8 bNeedsSweepWhileWalkingUpdate : 1; | |
protected: | |
/** | |
* True during movement update. | |
* Used internally so that attempts to change CharacterOwner and UpdatedComponent are deferred until after an update. | |
* @see IsMovementInProgress() | |
*/ | |
UPROPERTY() | |
uint8 bMovementInProgress : 1; | |
public: | |
/** | |
* If true, high-level movement updates will be wrapped in a movement scope that accumulates updates and defers a bulk of the work until the end. | |
* When enabled, touch and hit events will not be triggered until the end of multiple moves within an update, which can improve performance. | |
* | |
* @see FScopedMovementUpdate | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, AdvancedDisplay) | |
uint8 bEnableScopedMovementUpdates : 1; | |
/** | |
* Optional scoped movement update to combine moves for cheaper performance on the server when the client sends two moves in one packet. | |
* Be warned that since this wraps a larger scope than is normally done with bEnableScopedMovementUpdates, this can result in subtle changes in behavior | |
* in regards to when overlap events are handled, when attached components are moved, etc. | |
* | |
* @see bEnableScopedMovementUpdates | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, AdvancedDisplay) | |
uint8 bEnableServerDualMoveScopedMovementUpdates : 1; | |
/** Ignores size of acceleration component, and forces max acceleration to drive character at full velocity. */ | |
UPROPERTY() | |
uint8 bForceMaxAccel : 1; | |
/** | |
* If true, movement will be performed even if there is no Controller for the Character owner. | |
* Normally without a Controller, movement will be aborted and velocity and acceleration are zeroed if the character is walking. | |
* Characters that are spawned without a Controller but with this flag enabled will initialize the movement mode to DefaultLandMovementMode or DefaultWaterMovementMode appropriately. | |
* @see DefaultLandMovementMode, DefaultWaterMovementMode | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
uint8 bRunPhysicsWithNoController : 1; | |
/** | |
* Force the Character in MOVE_Walking to do a check for a valid floor even if he hasn't moved. Cleared after next floor check. | |
* Normally if bAlwaysCheckFloor is false we try to avoid the floor check unless some conditions are met, but this can be used to force the next check to always run. | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", VisibleInstanceOnly, BlueprintReadWrite, AdvancedDisplay) | |
uint8 bForceNextFloorCheck : 1; | |
/** If true, the capsule needs to be shrunk on this simulated proxy, to avoid replication rounding putting us in geometry. | |
* Whenever this is set to true, this will cause the capsule to be shrunk again on the next update, and then set to false. */ | |
UPROPERTY() | |
uint8 bShrinkProxyCapsule : 1; | |
/** If true, Character can walk off a ledge. */ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite) | |
uint8 bCanWalkOffLedges : 1; | |
/** If true, Character can walk off a ledge when crouching. */ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite) | |
uint8 bCanWalkOffLedgesWhenCrouching : 1; | |
/** | |
* Signals that smoothed position/rotation has reached target, and no more smoothing is necessary until a future update. | |
* This is used as an optimization to skip calls to SmoothClientPosition() when true. SmoothCorrection() sets it false when a new network update is received. | |
* SmoothClientPosition_Interpolate() sets this to true when the interpolation reaches the target, before one last call to SmoothClientPosition_UpdateVisuals(). | |
* If this is not desired, override SmoothClientPosition() to always set this to false to avoid this feature. | |
*/ | |
uint8 bNetworkSmoothingComplete : 1; | |
/** Flag indicating the client correction was larger than NetworkLargeClientCorrectionThreshold. */ | |
uint8 bNetworkLargeClientCorrection : 1; | |
/** | |
* Whether we skip prediction on frames where a proxy receives a network update. This can avoid expensive prediction on those frames, | |
* with the side-effect of predicting with a frame of additional latency. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly) | |
uint8 bNetworkSkipProxyPredictionOnNetUpdate : 1; | |
/** | |
* Flag used on the server to determine whether to always replicate ReplicatedServerLastTransformUpdateTimeStamp to clients. | |
* Normally this is only sent when the network smoothing mode on character movement is set to Linear smoothing (on the server), to save bandwidth. | |
* Setting this to true will force the timestamp to replicate regardless, in case the server doesn't know about the smoothing mode, or if the timestamp is used for another purpose. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, AdvancedDisplay) | |
uint8 bNetworkAlwaysReplicateTransformUpdateTimestamp : 1; | |
public: | |
/** true to update CharacterOwner and UpdatedComponent after movement ends */ | |
UPROPERTY() | |
uint8 bDeferUpdateMoveComponent : 1; | |
/** If enabled, the player will interact with physics objects when walking into them. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite) | |
uint8 bEnablePhysicsInteraction : 1; | |
/** If enabled, the TouchForceFactor is applied per kg mass of the affected object. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
uint8 bTouchForceScaledToMass : 1; | |
/** If enabled, the PushForceFactor is applied per kg mass of the affected object. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
uint8 bPushForceScaledToMass : 1; | |
/** If enabled, the PushForce location is moved using PushForcePointZOffsetFactor. Otherwise simply use the impact point. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
uint8 bPushForceUsingZOffset : 1; | |
/** If enabled, the applied push force will try to get the physics object to the same velocity than the player, not faster. This will only | |
scale the force down, it will never apply more force than defined by PushForceFactor. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
uint8 bScalePushForceToVelocity : 1; | |
/** What to update CharacterOwner and UpdatedComponent after movement ends */ | |
UPROPERTY() | |
USceneComponent* DeferredUpdatedMoveComponent; | |
/** Maximum step height for getting out of water */ | |
UPROPERTY(Category = "Character Movement: Swimming", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxOutOfWaterStepHeight; | |
/** Z velocity applied when pawn tries to get out of water */ | |
UPROPERTY(Category = "Character Movement: Swimming", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
float OutofWaterZ; | |
/** Mass of pawn (for when momentum is imparted to it). */ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", UIMin = "0")) | |
float Mass; | |
/** Force applied to objects we stand on (due to Mass and Gravity) is scaled by this amount. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
float StandingDownwardForceScale; | |
/** Initial impulse force to apply when the player bounces into a blocking physics object. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
float InitialPushForceFactor; | |
/** Force to apply when the player collides with a blocking physics object. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
float PushForceFactor; | |
/** Z-Offset for the position the force is applied to. 0.0f is the center of the physics object, 1.0f is the top and -1.0f is the bottom of the object. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (UIMin = "-1.0", UIMax = "1.0"), meta = (editcondition = "bEnablePhysicsInteraction")) | |
float PushForcePointZOffsetFactor; | |
/** Force to apply to physics objects that are touched by the player. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
float TouchForceFactor; | |
/** Minimum Force applied to touched physics objects. If < 0.0f, there is no minimum. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
float MinTouchForce; | |
/** Maximum force applied to touched physics objects. If < 0.0f, there is no maximum. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
float MaxTouchForce; | |
/** Force per kg applied constantly to all overlapping components. */ | |
UPROPERTY(Category = "Character Movement: Physics Interaction", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bEnablePhysicsInteraction")) | |
float RepulsionForce; | |
public: | |
#if WITH_EDITORONLY_DATA | |
// Deprecated properties | |
UPROPERTY() | |
uint32 bForceBraking_DEPRECATED : 1; | |
/** Multiplier to max ground speed to use when crouched */ | |
UPROPERTY() | |
float CrouchedSpeedMultiplier_DEPRECATED; | |
UPROPERTY() | |
float UpperImpactNormalScale_DEPRECATED; | |
#endif | |
protected: | |
/** | |
* Current acceleration vector (with magnitude). | |
* This is calculated each update based on the input vector and the constraints of MaxAcceleration and the current movement mode. | |
*/ | |
UPROPERTY() | |
FVector Acceleration; | |
/** | |
* Rotation after last PerformMovement or SimulateMovement update. | |
*/ | |
UPROPERTY() | |
FQuat LastUpdateRotation; | |
/** | |
* Location after last PerformMovement or SimulateMovement update. Used internally to detect changes in position from outside character movement to try to validate the current floor. | |
*/ | |
UPROPERTY() | |
FVector LastUpdateLocation; | |
/** | |
* Velocity after last PerformMovement or SimulateMovement update. Used internally to detect changes in velocity from external sources. | |
*/ | |
UPROPERTY() | |
FVector LastUpdateVelocity; | |
/** Timestamp when location or rotation last changed during an update. Only valid on the server. */ | |
UPROPERTY(Transient) | |
float ServerLastTransformUpdateTimeStamp; | |
/** Timestamp of last client adjustment sent. See NetworkMinTimeBetweenClientAdjustments. */ | |
UPROPERTY(Transient) | |
float ServerLastClientGoodMoveAckTime; | |
/** Timestamp of last client adjustment sent. See NetworkMinTimeBetweenClientAdjustments. */ | |
UPROPERTY(Transient) | |
float ServerLastClientAdjustmentTime; | |
/** Accumulated impulse to be added next tick. */ | |
UPROPERTY() | |
FVector PendingImpulseToApply; | |
/** Accumulated force to be added next tick. */ | |
UPROPERTY() | |
FVector PendingForceToApply; | |
/** | |
* Modifier to applied to values such as acceleration and max speed due to analog input. | |
*/ | |
UPROPERTY() | |
float AnalogInputModifier; | |
/** Computes the analog input modifier based on current input vector and/or acceleration. */ | |
virtual float ComputeAnalogInputModifier() const; | |
/** Used for throttling "stuck in geometry" logging. */ | |
float LastStuckWarningTime; | |
/** Used when throttling "stuck in geometry" logging, to output the number of events we skipped if throttling. */ | |
uint32 StuckWarningCountSinceNotify; | |
/** | |
* Used to limit number of jump apex attempts per tick. | |
* @see MaxJumpApexAttemptsPerSimulation | |
*/ | |
int32 NumJumpApexAttempts; | |
public: | |
/** Returns the location at the end of the last tick. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
FVector GetLastUpdateLocation() const { return LastUpdateLocation; } | |
/** Returns the rotation at the end of the last tick. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
FRotator GetLastUpdateRotation() const { return LastUpdateRotation.Rotator(); } | |
/** Returns the rotation Quat at the end of the last tick. */ | |
FQuat GetLastUpdateQuat() const { return LastUpdateRotation; } | |
/** Returns the velocity at the end of the last tick. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
FVector GetLastUpdateVelocity() const { return LastUpdateVelocity; } | |
/** Get the value of ServerLastTransformUpdateTimeStamp. */ | |
FORCEINLINE float GetServerLastTransformUpdateTimeStamp() const { return ServerLastTransformUpdateTimeStamp; } | |
/** | |
* Compute remaining time step given remaining time and current iterations. | |
* The last iteration (limited by MaxSimulationIterations) always returns the remaining time, which may violate MaxSimulationTimeStep. | |
* | |
* @param RemainingTime Remaining time in the tick. | |
* @param Iterations Current iteration of the tick (starting at 1). | |
* @return The remaining time step to use for the next sub-step of iteration. | |
* @see MaxSimulationTimeStep, MaxSimulationIterations | |
*/ | |
float GetSimulationTimeStep(float RemainingTime, int32 Iterations) const; | |
/** | |
* Max time delta for each discrete simulation step. | |
* Used primarily in the the more advanced movement modes that break up larger time steps (usually those applying gravity such as falling and walking). | |
* Lowering this value can address issues with fast-moving objects or complex collision scenarios, at the cost of performance. | |
* | |
* WARNING: if (MaxSimulationTimeStep * MaxSimulationIterations) is too low for the min framerate, the last simulation step may exceed MaxSimulationTimeStep to complete the simulation. | |
* @see MaxSimulationIterations | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0.0166", ClampMax = "0.50", UIMin = "0.0166", UIMax = "0.50")) | |
float MaxSimulationTimeStep; | |
/** | |
* Max number of iterations used for each discrete simulation step. | |
* Used primarily in the the more advanced movement modes that break up larger time steps (usually those applying gravity such as falling and walking). | |
* Increasing this value can address issues with fast-moving objects or complex collision scenarios, at the cost of performance. | |
* | |
* WARNING: if (MaxSimulationTimeStep * MaxSimulationIterations) is too low for the min framerate, the last simulation step may exceed MaxSimulationTimeStep to complete the simulation. | |
* @see MaxSimulationTimeStep | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "1", ClampMax = "25", UIMin = "1", UIMax = "25")) | |
int32 MaxSimulationIterations; | |
/** | |
* Max number of attempts per simulation to attempt to exactly reach the jump apex when falling movement reaches the top of the arc. | |
* Limiting this prevents deep recursion when special cases cause collision or other conditions which reactivate the apex condition. | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "1", ClampMax = "4", UIMin = "1", UIMax = "4")) | |
int32 MaxJumpApexAttemptsPerSimulation; | |
/** | |
* Max distance we allow simulated proxies to depenetrate when moving out of anything but Pawns. | |
* This is generally more tolerant than with Pawns, because other geometry is either not moving, or is moving predictably with a bit of delay compared to on the server. | |
* @see MaxDepenetrationWithGeometryAsProxy, MaxDepenetrationWithPawn, MaxDepenetrationWithPawnAsProxy | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxDepenetrationWithGeometry; | |
/** | |
* Max distance we allow simulated proxies to depenetrate when moving out of anything but Pawns. | |
* This is generally more tolerant than with Pawns, because other geometry is either not moving, or is moving predictably with a bit of delay compared to on the server. | |
* @see MaxDepenetrationWithGeometry, MaxDepenetrationWithPawn, MaxDepenetrationWithPawnAsProxy | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxDepenetrationWithGeometryAsProxy; | |
/** | |
* Max distance we are allowed to depenetrate when moving out of other Pawns. | |
* @see MaxDepenetrationWithGeometry, MaxDepenetrationWithGeometryAsProxy, MaxDepenetrationWithPawnAsProxy | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxDepenetrationWithPawn; | |
/** | |
* Max distance we allow simulated proxies to depenetrate when moving out of other Pawns. | |
* Typically we don't want a large value, because we receive a server authoritative position that we should not then ignore by pushing them out of the local player. | |
* @see MaxDepenetrationWithGeometry, MaxDepenetrationWithGeometryAsProxy, MaxDepenetrationWithPawn | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta = (ClampMin = "0", UIMin = "0")) | |
float MaxDepenetrationWithPawnAsProxy; | |
/** | |
* How long to take to smoothly interpolate from the old pawn position on the client to the corrected one sent by the server. Not used by Linear smoothing. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, AdvancedDisplay, meta = (ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0")) | |
float NetworkSimulatedSmoothLocationTime; | |
/** | |
* How long to take to smoothly interpolate from the old pawn rotation on the client to the corrected one sent by the server. Not used by Linear smoothing. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, AdvancedDisplay, meta = (ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0")) | |
float NetworkSimulatedSmoothRotationTime; | |
/** | |
* Similar setting as NetworkSimulatedSmoothLocationTime but only used on Listen servers. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, AdvancedDisplay, meta = (ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0")) | |
float ListenServerNetworkSimulatedSmoothLocationTime; | |
/** | |
* Similar setting as NetworkSimulatedSmoothRotationTime but only used on Listen servers. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, AdvancedDisplay, meta = (ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0")) | |
float ListenServerNetworkSimulatedSmoothRotationTime; | |
/** | |
* Shrink simulated proxy capsule radius by this amount, to account for network rounding that may cause encroachment. Changing during gameplay is not supported. | |
* @see AdjustProxyCapsuleSize() | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, AdvancedDisplay, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetProxyShrinkRadius; | |
/** | |
* Shrink simulated proxy capsule half height by this amount, to account for network rounding that may cause encroachment. Changing during gameplay is not supported. | |
* @see AdjustProxyCapsuleSize() | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, AdvancedDisplay, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetProxyShrinkHalfHeight; | |
/** Maximum distance character is allowed to lag behind server location when interpolating between updates. */ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetworkMaxSmoothUpdateDistance; | |
/** | |
* Maximum distance beyond which character is teleported to the new server location without any smoothing. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetworkNoSmoothUpdateDistance; | |
/** | |
* Minimum time on the server between acknowledging good client moves. This can save on bandwidth. Set to 0 to disable throttling. | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetworkMinTimeBetweenClientAckGoodMoves; | |
/** | |
* Minimum time on the server between sending client adjustments when client has exceeded allowable position error. | |
* Should be >= NetworkMinTimeBetweenClientAdjustmentsLargeCorrection (the larger value is used regardless). | |
* This can save on bandwidth. Set to 0 to disable throttling. | |
* @see ServerLastClientAdjustmentTime | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetworkMinTimeBetweenClientAdjustments; | |
/** | |
* Minimum time on the server between sending client adjustments when client has exceeded allowable position error by a large amount (NetworkLargeClientCorrectionDistance). | |
* Should be <= NetworkMinTimeBetweenClientAdjustments (the smaller value is used regardless). | |
* @see NetworkMinTimeBetweenClientAdjustments | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetworkMinTimeBetweenClientAdjustmentsLargeCorrection; | |
/** | |
* If client error is larger than this, sets bNetworkLargeClientCorrection to reduce delay between client adjustments. | |
* @see NetworkMinTimeBetweenClientAdjustments, NetworkMinTimeBetweenClientAdjustmentsLargeCorrection | |
*/ | |
UPROPERTY(Category = "Character Movement (Networking)", EditDefaultsOnly, meta = (ClampMin = "0.0", UIMin = "0.0")) | |
float NetworkLargeClientCorrectionDistance; | |
/** Used in determining if pawn is going off ledge. If the ledge is "shorter" than this value then the pawn will be able to walk off it. **/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
float LedgeCheckThreshold; | |
/** When exiting water, jump if control pitch angle is this high or above. */ | |
UPROPERTY(Category = "Character Movement: Swimming", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
float JumpOutOfWaterPitch; | |
/** Information about the floor the Character is standing on (updated only during walking movement). */ | |
UPROPERTY(Category = "Character Movement: Walking", VisibleInstanceOnly, BlueprintReadOnly) | |
FMMOFindFloorResult CurrentFloor; | |
/** | |
* Default movement mode when not in water. Used at player startup or when teleported. | |
* @see DefaultWaterMovementMode | |
* @see bRunPhysicsWithNoController | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite) | |
TEnumAsByte<enum EMovementMode> DefaultLandMovementMode; | |
/** | |
* Default movement mode when in water. Used at player startup or when teleported. | |
* @see DefaultLandMovementMode | |
* @see bRunPhysicsWithNoController | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite) | |
TEnumAsByte<enum EMovementMode> DefaultWaterMovementMode; | |
private: | |
/** | |
* Ground movement mode to switch to after falling and resuming ground movement. | |
* Only allowed values are: MOVE_Walking, MOVE_NavWalking. | |
* @see SetGroundMovementMode(), GetGroundMovementMode() | |
*/ | |
UPROPERTY(Transient) | |
TEnumAsByte<enum EMovementMode> GroundMovementMode; | |
public: | |
/** | |
* If true, walking movement always maintains horizontal velocity when moving up ramps, which causes movement up ramps to be faster parallel to the ramp surface. | |
* If false, then walking movement maintains velocity magnitude parallel to the ramp surface. | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite) | |
uint8 bMaintainHorizontalGroundVelocity : 1; | |
/** If true, impart the base actor's X velocity when falling off it (which includes jumping) */ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite) | |
uint8 bImpartBaseVelocityX : 1; | |
/** If true, impart the base actor's Y velocity when falling off it (which includes jumping) */ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite) | |
uint8 bImpartBaseVelocityY : 1; | |
/** If true, impart the base actor's Z velocity when falling off it (which includes jumping) */ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite) | |
uint8 bImpartBaseVelocityZ : 1; | |
/** | |
* If true, impart the base component's tangential components of angular velocity when jumping or falling off it. | |
* Only those components of the velocity allowed by the separate component settings (bImpartBaseVelocityX etc) will be applied. | |
* @see bImpartBaseVelocityX, bImpartBaseVelocityY, bImpartBaseVelocityZ | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite) | |
uint8 bImpartBaseAngularVelocity : 1; | |
/** Used by movement code to determine if a change in position is based on normal movement or a teleport. If not a teleport, velocity can be recomputed based on the change in position. */ | |
UPROPERTY(Category = "Character Movement (General Settings)", Transient, VisibleInstanceOnly, BlueprintReadWrite) | |
uint8 bJustTeleported : 1; | |
/** True when a network replication update is received for simulated proxies. */ | |
UPROPERTY(Transient) | |
uint8 bNetworkUpdateReceived : 1; | |
/** True when the networked movement mode has been replicated. */ | |
UPROPERTY(Transient) | |
uint8 bNetworkMovementModeChanged : 1; | |
/** | |
* If true, we should ignore server location difference checks for client error on this movement component. | |
* This can be useful when character is moving at extreme speeds for a duration and you need it to look | |
* smooth on clients without the server correcting the client. Make sure to disable when done, as this would | |
* break this character's server-client movement correction. | |
* @see bServerAcceptClientAuthoritativePosition, ServerCheckClientError() | |
*/ | |
UPROPERTY(Transient, Category = "Character Movement", EditAnywhere, BlueprintReadWrite) | |
uint8 bIgnoreClientMovementErrorChecksAndCorrection : 1; | |
/** | |
* If true, and server does not detect client position error, server will copy the client movement location/velocity/etc after simulating the move. | |
* This can be useful for short bursts of movement that are difficult to sync over the network. | |
* Note that if bIgnoreClientMovementErrorChecksAndCorrection is used, this means the server will not detect an error. | |
* Also see GameNetworkManager->ClientAuthorativePosition which permanently enables this behavior. | |
* @see bIgnoreClientMovementErrorChecksAndCorrection, ServerShouldUseAuthoritativePosition() | |
*/ | |
UPROPERTY(Transient, Category = "Character Movement", EditAnywhere, BlueprintReadWrite) | |
uint8 bServerAcceptClientAuthoritativePosition : 1; | |
/** | |
* If true, event NotifyJumpApex() to CharacterOwner's controller when at apex of jump. Is cleared when event is triggered. | |
* By default this is off, and if you want the event to fire you typically set it to true when movement mode changes to "Falling" from another mode (see OnMovementModeChanged). | |
*/ | |
UPROPERTY(Category = "Character Movement: Jumping / Falling", VisibleAnywhere, BlueprintReadWrite) | |
uint8 bNotifyApex : 1; | |
/** Instantly stop when in flying mode and no acceleration is being applied. */ | |
UPROPERTY() | |
uint8 bCheatFlying : 1; | |
/** If true, try to crouch (or keep crouching) on next update. If false, try to stop crouching on next update. */ | |
UPROPERTY(Category = "Character Movement (General Settings)", VisibleInstanceOnly, BlueprintReadOnly) | |
uint8 bWantsToCrouch : 1; | |
/** | |
* If true, crouching should keep the base of the capsule in place by lowering the center of the shrunken capsule. If false, the base of the capsule moves up and the center stays in place. | |
* The same behavior applies when the character uncrouches: if true, the base is kept in the same location and the center moves up. If false, the capsule grows and only moves up if the base impacts something. | |
* By default this variable is set when the movement mode changes: set to true when walking and false otherwise. Feel free to override the behavior when the movement mode changes. | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", VisibleInstanceOnly, BlueprintReadWrite, AdvancedDisplay) | |
uint8 bCrouchMaintainsBaseLocation : 1; | |
/** | |
* Whether the character ignores changes in rotation of the base it is standing on. | |
* If true, the character maintains current world rotation. | |
* If false, the character rotates with the moving base. | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite) | |
uint8 bIgnoreBaseRotation : 1; | |
/** | |
* Set this to true if riding on a moving base that you know is clear from non-moving world obstructions. | |
* Optimization to avoid sweeps during based movement, use with care. | |
*/ | |
UPROPERTY() | |
uint8 bFastAttachedMove : 1; | |
/** | |
* Whether we always force floor checks for stationary Characters while walking. | |
* Normally floor checks are avoided if possible when not moving, but this can be used to force them if there are use-cases where they are being skipped erroneously | |
* (such as objects moving up into the character from below). | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
uint8 bAlwaysCheckFloor : 1; | |
/** | |
* Performs floor checks as if the character is using a shape with a flat base. | |
* This avoids the situation where characters slowly lower off the side of a ledge (as their capsule 'balances' on the edge). | |
*/ | |
UPROPERTY(Category = "Character Movement: Walking", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
uint8 bUseFlatBaseForFloorChecks : 1; | |
/** Used to prevent reentry of JumpOff() */ | |
UPROPERTY() | |
uint8 bPerformingJumpOff : 1; | |
/** Used to safely leave NavWalking movement mode */ | |
UPROPERTY() | |
uint8 bWantsToLeaveNavWalking : 1; | |
/** If set, component will use RVO avoidance. This only runs on the server. */ | |
UPROPERTY(Category = "Character Movement: Avoidance", EditAnywhere, BlueprintReadOnly) | |
uint8 bUseRVOAvoidance : 1; | |
/** | |
* Should use acceleration for path following? | |
* If true, acceleration is applied when path following to reach the target velocity. | |
* If false, path following velocity is set directly, disregarding acceleration. | |
*/ | |
UPROPERTY(Category = "Character Movement (General Settings)", EditAnywhere, BlueprintReadWrite, AdvancedDisplay) | |
uint8 bRequestedMoveUseAcceleration : 1; | |
/** Set on clients when server's movement mode is NavWalking */ | |
uint8 bIsNavWalkingOnServer : 1; | |
/** True when SimulatedProxies are simulating RootMotion */ | |
UPROPERTY(Transient) | |
uint8 bWasSimulatingRootMotion : 1; | |
UPROPERTY(Category = "RootMotion", EditAnywhere, BlueprintReadWrite) | |
uint8 bAllowPhysicsRotationDuringAnimRootMotion : 1; | |
protected: | |
// AI PATH FOLLOWING | |
/** Was velocity requested by path following? */ | |
UPROPERTY(Transient) | |
uint8 bHasRequestedVelocity : 1; | |
/** Was acceleration requested to be always max speed? */ | |
UPROPERTY(Transient) | |
uint8 bRequestedMoveWithMaxSpeed : 1; | |
/** Was avoidance updated in this frame? */ | |
UPROPERTY(Transient) | |
uint8 bWasAvoidanceUpdated : 1; | |
/** if set, PostProcessAvoidanceVelocity will be called */ | |
uint8 bUseRVOPostProcess : 1; | |
/** Flag set in pre-physics update to indicate that based movement should be updated post-physics */ | |
uint8 bDeferUpdateBasedMovement : 1; | |
/** Whether to raycast to underlying geometry to better conform navmesh-walking characters */ | |
UPROPERTY(Category = "Character Movement: NavMesh Movement", EditAnywhere, BlueprintReadOnly) | |
uint8 bProjectNavMeshWalking : 1; | |
/** Use both WorldStatic and WorldDynamic channels for NavWalking geometry conforming */ | |
UPROPERTY(Category = "Character Movement: NavMesh Movement", EditAnywhere, BlueprintReadOnly, AdvancedDisplay) | |
uint8 bProjectNavMeshOnBothWorldChannels : 1; | |
/** forced avoidance velocity, used when AvoidanceLockTimer is > 0 */ | |
FVector AvoidanceLockVelocity; | |
/** remaining time of avoidance velocity lock */ | |
float AvoidanceLockTimer; | |
public: | |
UPROPERTY(Category = "Character Movement: Avoidance", EditAnywhere, BlueprintReadOnly) | |
float AvoidanceConsiderationRadius; | |
/** | |
* Velocity requested by path following. | |
* @see RequestDirectMove() | |
*/ | |
UPROPERTY(Transient) | |
FVector RequestedVelocity; | |
/** No default value, for now it's assumed to be valid if GetAvoidanceManager() returns non-NULL. */ | |
UPROPERTY(Category = "Character Movement: Avoidance", VisibleAnywhere, BlueprintReadOnly, AdvancedDisplay) | |
int32 AvoidanceUID; | |
/** Moving actor's group mask */ | |
UPROPERTY(Category = "Character Movement: Avoidance", EditAnywhere, BlueprintReadOnly, AdvancedDisplay) | |
FNavAvoidanceMask AvoidanceGroup; | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DeprecatedFunction, DeprecationMessage = "Please use SetAvoidanceGroupMask function instead.")) | |
void SetAvoidanceGroup(int32 GroupFlags); | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
void SetAvoidanceGroupMask(const FNavAvoidanceMask& GroupMask); | |
/** Will avoid other agents if they are in one of specified groups */ | |
UPROPERTY(Category = "Character Movement: Avoidance", EditAnywhere, BlueprintReadOnly, AdvancedDisplay) | |
FNavAvoidanceMask GroupsToAvoid; | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DeprecatedFunction, DeprecationMessage = "Please use SetGroupsToAvoidMask function instead.")) | |
void SetGroupsToAvoid(int32 GroupFlags); | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
void SetGroupsToAvoidMask(const FNavAvoidanceMask& GroupMask); | |
/** Will NOT avoid other agents if they are in one of specified groups, higher priority than GroupsToAvoid */ | |
UPROPERTY(Category = "Character Movement: Avoidance", EditAnywhere, BlueprintReadOnly, AdvancedDisplay) | |
FNavAvoidanceMask GroupsToIgnore; | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DeprecatedFunction, DeprecationMessage = "Please use SetGroupsToIgnoreMask function instead.")) | |
void SetGroupsToIgnore(int32 GroupFlags); | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
void SetGroupsToIgnoreMask(const FNavAvoidanceMask& GroupMask); | |
/** De facto default value 0.5 (due to that being the default in the avoidance registration function), indicates RVO behavior. */ | |
UPROPERTY(Category = "Character Movement: Avoidance", EditAnywhere, BlueprintReadOnly) | |
float AvoidanceWeight; | |
/** Temporarily holds launch velocity when pawn is to be launched so it happens at end of movement. */ | |
UPROPERTY() | |
FVector PendingLaunchVelocity; | |
/** last known location projected on navmesh, used by NavWalking mode */ | |
FNavLocation CachedNavLocation; | |
/** Last valid projected hit result from raycast to geometry from navmesh */ | |
FHitResult CachedProjectedNavMeshHitResult; | |
/** How often we should raycast to project from navmesh to underlying geometry */ | |
UPROPERTY(Category = "Character Movement: NavMesh Movement", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bProjectNavMeshWalking")) | |
float NavMeshProjectionInterval; | |
UPROPERTY(Transient) | |
float NavMeshProjectionTimer; | |
/** Speed at which to interpolate agent navmesh offset between traces. 0: Instant (no interp) > 0: Interp speed") */ | |
UPROPERTY(Category = "Character Movement: NavMesh Movement", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bProjectNavMeshWalking", ClampMin = "0", UIMin = "0")) | |
float NavMeshProjectionInterpSpeed; | |
/** | |
* Scale of the total capsule height to use for projection from navmesh to underlying geometry in the upward direction. | |
* In other words, start the trace at [CapsuleHeight * NavMeshProjectionHeightScaleUp] above nav mesh. | |
*/ | |
UPROPERTY(Category = "Character Movement: NavMesh Movement", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bProjectNavMeshWalking", ClampMin = "0", UIMin = "0")) | |
float NavMeshProjectionHeightScaleUp; | |
/** | |
* Scale of the total capsule height to use for projection from navmesh to underlying geometry in the downward direction. | |
* In other words, trace down to [CapsuleHeight * NavMeshProjectionHeightScaleDown] below nav mesh. | |
*/ | |
UPROPERTY(Category = "Character Movement: NavMesh Movement", EditAnywhere, BlueprintReadWrite, meta = (editcondition = "bProjectNavMeshWalking", ClampMin = "0", UIMin = "0")) | |
float NavMeshProjectionHeightScaleDown; | |
/** Ignore small differences in ground height between server and client data during NavWalking mode */ | |
UPROPERTY(Category = "Character Movement: NavMesh Movement", EditAnywhere, BlueprintReadWrite) | |
float NavWalkingFloorDistTolerance; | |
/** Change avoidance state and registers in RVO manager if needed */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (UnsafeDuringActorConstruction = "true")) | |
void SetAvoidanceEnabled(bool bEnable); | |
/** Get the Character that owns UpdatedComponent. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
AMMOCharacter* GetCharacterOwner() const; | |
/** | |
* Change movement mode. | |
* | |
* @param NewMovementMode The new movement mode | |
* @param NewCustomMode The new custom sub-mode, only applicable if NewMovementMode is Custom. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual void SetMovementMode(EMovementMode NewMovementMode, uint8 NewCustomMode = 0); | |
/** | |
* Set movement mode to use when returning to walking movement (either MOVE_Walking or MOVE_NavWalking). | |
* If movement mode is currently one of Walking or NavWalking, this will also change the current movement mode (via SetMovementMode()) | |
* if the new mode is not the current ground mode. | |
* | |
* @param NewGroundMovementMode New ground movement mode. Must be either MOVE_Walking or MOVE_NavWalking, other values are ignored. | |
* @see GroundMovementMode | |
*/ | |
void SetGroundMovementMode(EMovementMode NewGroundMovementMode); | |
/** | |
* Get current GroundMovementMode value. | |
* @return current GroundMovementMode | |
* @see GroundMovementMode, SetGroundMovementMode() | |
*/ | |
EMovementMode GetGroundMovementMode() const { return GroundMovementMode; } | |
protected: | |
/** Called after MovementMode has changed. Base implementation does special handling for starting certain modes, then notifies the CharacterOwner. */ | |
virtual void OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode); | |
public: | |
virtual uint8 PackNetworkMovementMode() const; | |
virtual void UnpackNetworkMovementMode(const uint8 ReceivedMode, TEnumAsByte<EMovementMode>& OutMode, uint8& OutCustomMode, TEnumAsByte<EMovementMode>& OutGroundMode) const; | |
virtual void ApplyNetworkMovementMode(const uint8 ReceivedMode); | |
// Begin UObject Interface | |
virtual void Serialize(FArchive& Archive) override; | |
// End UObject Interface | |
//Begin UActorComponent Interface | |
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; | |
virtual void OnRegister() override; | |
virtual void BeginDestroy() override; | |
virtual void PostLoad() override; | |
virtual void Deactivate() override; | |
virtual void RegisterComponentTickFunctions(bool bRegister) override; | |
virtual void ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) override; | |
//End UActorComponent Interface | |
//BEGIN UMovementComponent Interface | |
virtual float GetMaxSpeed() const override; | |
virtual void StopActiveMovement() override; | |
virtual bool IsCrouching() const override; | |
virtual bool IsFalling() const override; | |
virtual bool IsMovingOnGround() const override; | |
virtual bool IsSwimming() const override; | |
virtual bool IsFlying() const override; | |
virtual float GetGravityZ() const override; | |
virtual void AddRadialForce(const FVector& Origin, float Radius, float Strength, enum ERadialImpulseFalloff Falloff) override; | |
virtual void AddRadialImpulse(const FVector& Origin, float Radius, float Strength, enum ERadialImpulseFalloff Falloff, bool bVelChange) override; | |
//END UMovementComponent Interface | |
/** Returns true if the character is in the 'Walking' movement mode. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
bool IsWalking() const; | |
/** | |
* Returns true if currently performing a movement update. | |
* @see bMovementInProgress | |
*/ | |
bool IsMovementInProgress() const { return bMovementInProgress; } | |
//BEGIN UNavMovementComponent Interface | |
virtual void RequestDirectMove(const FVector& MoveVelocity, bool bForceMaxSpeed) override; | |
virtual void RequestPathMove(const FVector& MoveInput) override; | |
virtual bool CanStartPathFollowing() const override; | |
virtual bool CanStopPathFollowing() const override; | |
virtual float GetPathFollowingBrakingDistance(float MaxSpeed) const override; | |
//END UNaVMovementComponent Interface | |
//Begin UPawnMovementComponent Interface | |
virtual void NotifyBumpedPawn(APawn* BumpedPawn) override; | |
//End UPawnMovementComponent Interface | |
#if WITH_EDITOR | |
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; | |
#endif // WITH_EDITOR | |
/** Make movement impossible (sets movement mode to MOVE_None). */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual void DisableMovement(); | |
/** Return true if we have a valid CharacterOwner and UpdatedComponent. */ | |
virtual bool HasValidData() const; | |
/** | |
* If ShouldPerformAirControlForPathFollowing() returns true, it will update Velocity and Acceleration to air control in the desired Direction for character using path following. | |
* @param Direction is the desired direction of movement | |
* @param ZDiff is the height difference between the destination and the Pawn's current position | |
* @see RequestDirectMove() | |
*/ | |
virtual void PerformAirControlForPathFollowing(FVector Direction, float ZDiff); | |
/** | |
* Whether Character should perform air control via PerformAirControlForPathFollowing when falling and following a path at the same time | |
* Default implementation always returns true during MOVE_Falling. | |
*/ | |
virtual bool ShouldPerformAirControlForPathFollowing() const; | |
/** Transition from walking to falling */ | |
virtual void StartFalling(int32 Iterations, float remainingTime, float timeTick, const FVector& Delta, const FVector& subLoc); | |
/** | |
* Whether Character should go into falling mode when walking and changing position, based on an old and new floor result (both of which are considered walkable). | |
* Default implementation always returns false. | |
* @return true if Character should start falling | |
*/ | |
virtual bool ShouldCatchAir(const FMMOFindFloorResult& OldFloor, const FMMOFindFloorResult& NewFloor); | |
/** | |
* Trigger OnWalkingOffLedge event on CharacterOwner. | |
*/ | |
virtual void HandleWalkingOffLedge(const FVector& PreviousFloorImpactNormal, const FVector& PreviousFloorContactNormal, const FVector& PreviousLocation, float TimeDelta); | |
/** Adjust distance from floor, trying to maintain a slight offset from the floor when walking (based on CurrentFloor). */ | |
virtual void AdjustFloorHeight(); | |
/** Return PrimitiveComponent we are based on (standing and walking on). */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
UPrimitiveComponent* GetMovementBase() const; | |
/** Update or defer updating of position based on Base movement */ | |
virtual void MaybeUpdateBasedMovement(float DeltaSeconds); | |
/** Update position based on Base movement */ | |
virtual void UpdateBasedMovement(float DeltaSeconds); | |
/** Update controller's view rotation as pawn's base rotates */ | |
virtual void UpdateBasedRotation(FRotator& FinalRotation, const FRotator& ReducedRotation); | |
/** Call SaveBaseLocation() if not deferring updates (bDeferUpdateBasedMovement is false). */ | |
virtual void MaybeSaveBaseLocation(); | |
/** Update OldBaseLocation and OldBaseQuat if there is a valid movement base, and store the relative location/rotation if necessary. Ignores bDeferUpdateBasedMovement and forces the update. */ | |
virtual void SaveBaseLocation(); | |
/** changes physics based on MovementMode */ | |
virtual void StartNewPhysics(float deltaTime, int32 Iterations); | |
/** | |
* Perform jump. Called by Character when a jump has been detected because Character->bPressedJump was true. Checks Character->CanJump(). | |
* Note that you should usually trigger a jump through Character::Jump() instead. | |
* @param bReplayingMoves: true if this is being done as part of replaying moves on a locally controlled client after a server correction. | |
* @return True if the jump was triggered successfully. | |
*/ | |
virtual bool DoJump(bool bReplayingMoves); | |
/** | |
* Returns true if current movement state allows an attempt at jumping. Used by Character::CanJump(). | |
*/ | |
virtual bool CanAttemptJump() const; | |
/** Queue a pending launch with velocity LaunchVel. */ | |
virtual void Launch(FVector const& LaunchVel); | |
/** Handle a pending launch during an update. Returns true if the launch was triggered. */ | |
virtual bool HandlePendingLaunch(); | |
/** | |
* If we have a movement base, get the velocity that should be imparted by that base, usually when jumping off of it. | |
* Only applies the components of the velocity enabled by bImpartBaseVelocityX, bImpartBaseVelocityY, bImpartBaseVelocityZ. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual FVector GetImpartedMovementBaseVelocity() const; | |
/** Force this pawn to bounce off its current base, which isn't an acceptable base for it. */ | |
virtual void JumpOff(AActor* MovementBaseActor); | |
/** Can be overridden to choose to jump based on character velocity, base actor dimensions, etc. */ | |
virtual FVector GetBestDirectionOffActor(AActor* BaseActor) const; // Calculates the best direction to go to "jump off" an actor. | |
/** | |
* Determine whether the Character should jump when exiting water. | |
* @param JumpDir is the desired direction to jump out of water | |
* @return true if Pawn should jump out of water | |
*/ | |
virtual bool ShouldJumpOutOfWater(FVector& JumpDir); | |
/** Jump onto shore from water */ | |
virtual void JumpOutOfWater(FVector WallNormal); | |
/** Returns how far to rotate character during the time interval DeltaTime. */ | |
virtual FRotator GetDeltaRotation(float DeltaTime) const; | |
/** | |
* Compute a target rotation based on current movement. Used by PhysicsRotation() when bOrientRotationToMovement is true. | |
* Default implementation targets a rotation based on Acceleration. | |
* | |
* @param CurrentRotation - Current rotation of the Character | |
* @param DeltaTime - Time slice for this movement | |
* @param DeltaRotation - Proposed rotation change based simply on DeltaTime * RotationRate | |
* | |
* @return The target rotation given current movement. | |
*/ | |
virtual FRotator ComputeOrientToMovementRotation(const FRotator& CurrentRotation, float DeltaTime, FRotator& DeltaRotation) const; | |
/** | |
* Use velocity requested by path following to compute a requested acceleration and speed. | |
* This does not affect the Acceleration member variable, as that is used to indicate input acceleration. | |
* This may directly affect current Velocity. | |
* | |
* @param DeltaTime Time slice for this operation | |
* @param MaxAccel Max acceleration allowed in OutAcceleration result. | |
* @param MaxSpeed Max speed allowed when computing OutRequestedSpeed. | |
* @param Friction Current friction. | |
* @param BrakingDeceleration Current braking deceleration. | |
* @param OutAcceleration Acceleration computed based on requested velocity. | |
* @param OutRequestedSpeed Speed of resulting velocity request, which can affect the max speed allowed by movement. | |
* @return Whether there is a requested velocity and acceleration, resulting in valid OutAcceleration and OutRequestedSpeed values. | |
*/ | |
virtual bool ApplyRequestedMove(float DeltaTime, float MaxAccel, float MaxSpeed, float Friction, float BrakingDeceleration, FVector& OutAcceleration, float& OutRequestedSpeed); | |
/** Called if bNotifyApex is true and character has just passed the apex of its jump. */ | |
virtual void NotifyJumpApex(); | |
/** | |
* Compute new falling velocity from given velocity and gravity. Applies the limits of the current Physics Volume's TerminalVelocity. | |
*/ | |
virtual FVector NewFallVelocity(const FVector& InitialVelocity, const FVector& Gravity, float DeltaTime) const; | |
/* Determine how deep in water the character is immersed. | |
* @return float in range 0.0 = not in water, 1.0 = fully immersed | |
*/ | |
virtual float ImmersionDepth() const; | |
/** | |
* Updates Velocity and Acceleration based on the current state, applying the effects of friction and acceleration or deceleration. Does not apply gravity. | |
* This is used internally during movement updates. Normally you don't need to call this from outside code, but you might want to use it for custom movement modes. | |
* | |
* @param DeltaTime time elapsed since last frame. | |
* @param Friction coefficient of friction when not accelerating, or in the direction opposite acceleration. | |
* @param bFluid true if moving through a fluid, causing Friction to always be applied regardless of acceleration. | |
* @param BrakingDeceleration deceleration applied when not accelerating, or when exceeding max velocity. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual void CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration); | |
/** | |
* Compute the max jump height based on the JumpZVelocity velocity and gravity. | |
* This does not take into account the CharacterOwner's MaxJumpHoldTime. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual float GetMaxJumpHeight() const; | |
/** | |
* Compute the max jump height based on the JumpZVelocity velocity and gravity. | |
* This does take into account the CharacterOwner's MaxJumpHoldTime. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual float GetMaxJumpHeightWithJumpTime() const; | |
/** Returns maximum acceleration for the current state. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual float GetMinAnalogSpeed() const; | |
UE_DEPRECATED(4.3, "GetModifiedMaxAcceleration() is deprecated, apply your own modifiers to GetMaxAcceleration() if desired.") | |
virtual float GetModifiedMaxAcceleration() const; | |
/** Returns maximum acceleration for the current state, based on MaxAcceleration and any additional modifiers. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DeprecatedFunction, DisplayName = "GetModifiedMaxAcceleration", ScriptName = "GetModifiedMaxAcceleration", DeprecationMessage = "GetModifiedMaxAcceleration() is deprecated, apply your own modifiers to GetMaxAcceleration() if desired.")) | |
virtual float K2_GetModifiedMaxAcceleration() const; | |
/** Returns maximum acceleration for the current state. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual float GetMaxAcceleration() const; | |
/** Returns maximum deceleration for the current state when braking (ie when there is no acceleration). */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual float GetMaxBrakingDeceleration() const; | |
/** Returns current acceleration, computed from input vector each update. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (Keywords = "Acceleration GetAcceleration")) | |
FVector GetCurrentAcceleration() const; | |
/** Returns modifier [0..1] based on the magnitude of the last input vector, which is used to modify the acceleration and max speed during movement. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
float GetAnalogInputModifier() const; | |
/** Returns true if we can step up on the actor in the given FHitResult. */ | |
virtual bool CanStepUp(const FHitResult& Hit) const; | |
/** Struct updated by StepUp() to return result of final step down, if applicable. */ | |
struct FStepDownResult | |
{ | |
uint32 bComputedFloor : 1; // True if the floor was computed as a result of the step down. | |
FMMOFindFloorResult FloorResult; // The result of the floor test if the floor was updated. | |
FStepDownResult() | |
: bComputedFloor(false) | |
{ | |
} | |
}; | |
/** | |
* Move up steps or slope. Does nothing and returns false if CanStepUp(Hit) returns false. | |
* | |
* @param GravDir Gravity vector direction (assumed normalized or zero) | |
* @param Delta Requested move | |
* @param Hit [In] The hit before the step up. | |
* @param OutStepDownResult [Out] If non-null, a floor check will be performed if possible as part of the final step down, and it will be updated to reflect this result. | |
* @return true if the step up was successful. | |
*/ | |
virtual bool StepUp(const FVector& GravDir, const FVector& Delta, const FHitResult& Hit, struct UMMOPlayerMovement::FStepDownResult* OutStepDownResult = NULL); | |
/** Update the base of the character, which is the PrimitiveComponent we are standing on. */ | |
virtual void SetBase(UPrimitiveComponent* NewBase, const FName BoneName = NAME_None, bool bNotifyActor = true); | |
/** | |
* Update the base of the character, using the given floor result if it is walkable, or null if not. Calls SetBase(). | |
*/ | |
void SetBaseFromFloor(const FMMOFindFloorResult& FloorResult); | |
/** | |
* Applies downward force when walking on top of physics objects. | |
* @param DeltaSeconds Time elapsed since last frame. | |
*/ | |
virtual void ApplyDownwardForce(float DeltaSeconds); | |
/** Applies repulsion force to all touched components. */ | |
virtual void ApplyRepulsionForce(float DeltaSeconds); | |
/** Applies momentum accumulated through AddImpulse() and AddForce(), then clears those forces. Does *not* use ClearAccumulatedForces() since that would clear pending launch velocity as well. */ | |
virtual void ApplyAccumulatedForces(float DeltaSeconds); | |
/** Clears forces accumulated through AddImpulse() and AddForce(), and also pending launch velocity. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual void ClearAccumulatedForces(); | |
/** Update the character state in PerformMovement right before doing the actual position change */ | |
virtual void UpdateCharacterStateBeforeMovement(float DeltaSeconds); | |
/** Update the character state in PerformMovement after the position change. Some rotation updates happen after this. */ | |
virtual void UpdateCharacterStateAfterMovement(float DeltaSeconds); | |
/** | |
* Handle start swimming functionality | |
* @param OldLocation - Location on last tick | |
* @param OldVelocity - velocity at last tick | |
* @param timeTick - time since at OldLocation | |
* @param remainingTime - DeltaTime to complete transition to swimming | |
* @param Iterations - physics iteration count | |
*/ | |
void StartSwimming(FVector OldLocation, FVector OldVelocity, float timeTick, float remainingTime, int32 Iterations); | |
/* Swimming uses gravity - but scaled by (1.f - buoyancy) */ | |
float Swim(FVector Delta, FHitResult& Hit); | |
/** Get as close to waterline as possible, staying on same side as currently. */ | |
FVector FindWaterLine(FVector Start, FVector End); | |
/** Handle falling movement. */ | |
virtual void PhysFalling(float deltaTime, int32 Iterations); | |
// Helpers for PhysFalling | |
/** | |
* Get the lateral acceleration to use during falling movement. The Z component of the result is ignored. | |
* Default implementation returns current Acceleration value modified by GetAirControl(), with Z component removed, | |
* with magnitude clamped to GetMaxAcceleration(). | |
* This function is used internally by PhysFalling(). | |
* | |
* @param DeltaTime Time step for the current update. | |
* @return Acceleration to use during falling movement. | |
*/ | |
virtual FVector GetFallingLateralAcceleration(float DeltaTime); | |
/** | |
* Returns true if falling movement should limit air control. Limiting air control prevents input acceleration during falling movement | |
* from allowing velocity to redirect forces upwards while falling, which could result in slower falling or even upward boosting. | |
* | |
* @see GetFallingLateralAcceleration(), BoostAirControl(), GetAirControl(), LimitAirControl() | |
*/ | |
virtual bool ShouldLimitAirControl(float DeltaTime, const FVector& FallAcceleration) const; | |
/** | |
* Get the air control to use during falling movement. | |
* Given an initial air control (TickAirControl), applies the result of BoostAirControl(). | |
* This function is used internally by GetFallingLateralAcceleration(). | |
* | |
* @param DeltaTime Time step for the current update. | |
* @param TickAirControl Current air control value. | |
* @param FallAcceleration Acceleration used during movement. | |
* @return Air control to use during falling movement. | |
* @see AirControl, BoostAirControl(), LimitAirControl(), GetFallingLateralAcceleration() | |
*/ | |
virtual FVector GetAirControl(float DeltaTime, float TickAirControl, const FVector& FallAcceleration); | |
protected: | |
/** | |
* Increase air control if conditions of AirControlBoostMultiplier and AirControlBoostVelocityThreshold are met. | |
* This function is used internally by GetAirControl(). | |
* | |
* @param DeltaTime Time step for the current update. | |
* @param TickAirControl Current air control value. | |
* @param FallAcceleration Acceleration used during movement. | |
* @return Modified air control to use during falling movement | |
* @see GetAirControl() | |
*/ | |
virtual float BoostAirControl(float DeltaTime, float TickAirControl, const FVector& FallAcceleration); | |
/** | |
* Limits the air control to use during falling movement, given an impact while falling. | |
* This function is used internally by PhysFalling(). | |
* | |
* @param DeltaTime Time step for the current update. | |
* @param FallAcceleration Acceleration used during movement. | |
* @param HitResult Result of impact. | |
* @param bCheckForValidLandingSpot If true, will use IsValidLandingSpot() to determine if HitResult is a walkable surface. If false, this check is skipped. | |
* @return Modified air control acceleration to use during falling movement. | |
* @see PhysFalling() | |
*/ | |
virtual FVector LimitAirControl(float DeltaTime, const FVector& FallAcceleration, const FHitResult& HitResult, bool bCheckForValidLandingSpot); | |
/** Handle landing against Hit surface over remaingTime and iterations, calling SetPostLandedPhysics() and starting the new movement mode. */ | |
virtual void ProcessLanded(const FHitResult& Hit, float remainingTime, int32 Iterations); | |
/** Use new physics after landing. Defaults to swimming if in water, walking otherwise. */ | |
virtual void SetPostLandedPhysics(const FHitResult& Hit); | |
/** Switch collision settings for NavWalking mode (ignore world collisions) */ | |
virtual void SetNavWalkingPhysics(bool bEnable); | |
/** Get Navigation data for the Character. Returns null if there is no associated nav data. */ | |
const class INavigationDataInterface* GetNavData() const; | |
/** | |
* Checks to see if the current location is not encroaching blocking geometry so the character can leave NavWalking. | |
* Restores collision settings and adjusts character location to avoid getting stuck in geometry. | |
* If it's not possible, MovementMode change will be delayed until character reach collision free spot. | |
* @return True if movement mode was successfully changed | |
*/ | |
virtual bool TryToLeaveNavWalking(); | |
/** | |
* Attempts to better align navmesh walking characters with underlying geometry (sometimes | |
* navmesh can differ quite significantly from geometry). | |
* Updates CachedProjectedNavMeshHitResult, access this for more info about hits. | |
*/ | |
virtual FVector ProjectLocationFromNavMesh(float DeltaSeconds, const FVector& CurrentFeetLocation, const FVector& TargetNavLocation, float UpOffset, float DownOffset); | |
/** Performs trace for ProjectLocationFromNavMesh */ | |
virtual void FindBestNavMeshLocation(const FVector& TraceStart, const FVector& TraceEnd, const FVector& CurrentFeetLocation, const FVector& TargetNavLocation, FHitResult& OutHitResult) const; | |
/** | |
* When a character requests a velocity (like when following a path), this method returns true if when we should compute the | |
* acceleration toward requested velocity (including friction). If it returns false, it will snap instantly to requested velocity. | |
*/ | |
virtual bool ShouldComputeAccelerationToReachRequestedVelocity(const float RequestedSpeed) const; | |
public: | |
/** Called by owning Character upon successful teleport from AActor::TeleportTo(). */ | |
virtual void OnTeleported() override; | |
/** | |
* Checks if new capsule size fits (no encroachment), and call CharacterOwner->OnStartCrouch() if successful. | |
* In general you should set bWantsToCrouch instead to have the crouch persist during movement, or just use the crouch functions on the owning Character. | |
* @param bClientSimulation true when called when bIsCrouched is replicated to non owned clients, to update collision cylinder and offset. | |
*/ | |
virtual void Crouch(bool bClientSimulation = false); | |
/** | |
* Checks if default capsule size fits (no encroachment), and trigger OnEndCrouch() on the owner if successful. | |
* @param bClientSimulation true when called when bIsCrouched is replicated to non owned clients, to update collision cylinder and offset. | |
*/ | |
virtual void UnCrouch(bool bClientSimulation = false); | |
/** Returns true if the character is allowed to crouch in the current state. By default it is allowed when walking or falling, if CanEverCrouch() is true. */ | |
virtual bool CanCrouchInCurrentState() const; | |
/** Returns true if there is a suitable floor SideStep from current position. */ | |
virtual bool CheckLedgeDirection(const FVector& OldLocation, const FVector& SideStep, const FVector& GravDir) const; | |
/** | |
* @param Delta is the current move delta (which ended up going over a ledge). | |
* @return new delta which moves along the ledge | |
*/ | |
virtual FVector GetLedgeMove(const FVector& OldLocation, const FVector& Delta, const FVector& GravDir) const; | |
/** Check if pawn is falling */ | |
virtual bool CheckFall(const FMMOFindFloorResult& OldFloor, const FHitResult& Hit, const FVector& Delta, const FVector& OldLocation, float remainingTime, float timeTick, int32 Iterations, bool bMustJump); | |
/** | |
* Revert to previous position OldLocation, return to being based on OldBase. | |
* if bFailMove, stop movement and notify controller | |
*/ | |
void RevertMove(const FVector& OldLocation, UPrimitiveComponent* OldBase, const FVector& InOldBaseLocation, const FMMOFindFloorResult& OldFloor, bool bFailMove); | |
/** Perform rotation over deltaTime */ | |
virtual void PhysicsRotation(float DeltaTime); | |
/** if true, DesiredRotation will be restricted to only Yaw component in PhysicsRotation() */ | |
virtual bool ShouldRemainVertical() const; | |
/** Delegate when PhysicsVolume of UpdatedComponent has been changed **/ | |
virtual void PhysicsVolumeChanged(class APhysicsVolume* NewVolume) override; | |
/** Set movement mode to the default based on the current physics volume. */ | |
virtual void SetDefaultMovementMode(); | |
/** | |
* Moves along the given movement direction using simple movement rules based on the current movement mode (usually used by simulated proxies). | |
* | |
* @param InVelocity: Velocity of movement | |
* @param DeltaSeconds: Time over which movement occurs | |
* @param OutStepDownResult: [Out] If non-null, and a floor check is performed, this will be updated to reflect that result. | |
*/ | |
virtual void MoveSmooth(const FVector& InVelocity, const float DeltaSeconds, FStepDownResult* OutStepDownResult = NULL); | |
/** | |
* Used during SimulateMovement for proxies, this computes a new value for Acceleration before running proxy simulation. | |
* The base implementation simply derives a value from the normalized Velocity value, which may help animations that want some indication of the direction of movement. | |
* Proxies don't implement predictive acceleration by default so this value is not used for the actual simulation. | |
*/ | |
virtual void UpdateProxyAcceleration(); | |
virtual void SetUpdatedComponent(USceneComponent* NewUpdatedComponent) override; | |
/** Returns MovementMode string */ | |
virtual FString GetMovementName() const; | |
/** | |
* Add impulse to character. Impulses are accumulated each tick and applied together | |
* so multiple calls to this function will accumulate. | |
* An impulse is an instantaneous force, usually applied once. If you want to continually apply | |
* forces each frame, use AddForce(). | |
* Note that changing the momentum of characters like this can change the movement mode. | |
* | |
* @param Impulse Impulse to apply. | |
* @param bVelocityChange Whether or not the impulse is relative to mass. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual void AddImpulse(FVector Impulse, bool bVelocityChange = false); | |
/** | |
* Add force to character. Forces are accumulated each tick and applied together | |
* so multiple calls to this function will accumulate. | |
* Forces are scaled depending on timestep, so they can be applied each frame. If you want an | |
* instantaneous force, use AddImpulse. | |
* Adding a force always takes the actor's mass into account. | |
* Note that changing the momentum of characters like this can change the movement mode. | |
* | |
* @param Force Force to apply. | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual void AddForce(FVector Force); | |
/** | |
* Draw important variables on canvas. Character will call DisplayDebug() on the current ViewTarget when the ShowDebug exec is used | |
* | |
* @param Canvas - Canvas to draw on | |
* @param DebugDisplay - Contains information about what debug data to display | |
* @param YL - Height of the current font | |
* @param YPos - Y position on Canvas. YPos += YL, gives position to draw text for next debug line. | |
*/ | |
virtual void DisplayDebug(class UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos); | |
/** | |
* Draw in-world debug information for character movement (called with p.VisualizeMovement > 0). | |
*/ | |
virtual float VisualizeMovement() const; | |
/** Check if swimming pawn just ran into edge of the pool and should jump out. */ | |
virtual bool CheckWaterJump(FVector CheckPoint, FVector& WallNormal); | |
/** Returns whether this pawn is currently allowed to walk off ledges */ | |
virtual bool CanWalkOffLedges() const; | |
/** Returns The distance from the edge of the capsule within which we don't allow the character to perch on the edge of a surface. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
float GetPerchRadiusThreshold() const; | |
/** | |
* Returns the radius within which we can stand on the edge of a surface without falling (if this is a walkable surface). | |
* Simply computed as the capsule radius minus the result of GetPerchRadiusThreshold(). | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
float GetValidPerchRadius() const; | |
/** Return true if the hit result should be considered a walkable surface for the character. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
virtual bool IsWalkable(const FHitResult& Hit) const; | |
/** Get the max angle in degrees of a walkable surface for the character. */ | |
FORCEINLINE float GetWalkableFloorAngle() const { return WalkableFloorAngle; } | |
/** Get the max angle in degrees of a walkable surface for the character. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DisplayName = "GetWalkableFloorAngle", ScriptName = "GetWalkableFloorAngle")) | |
float K2_GetWalkableFloorAngle() const; | |
/** Set the max angle in degrees of a walkable surface for the character. Also computes WalkableFloorZ. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
void SetWalkableFloorAngle(float InWalkableFloorAngle); | |
/** Get the Z component of the normal of the steepest walkable surface for the character. Any lower than this and it is not walkable. */ | |
FORCEINLINE float GetWalkableFloorZ() const { return WalkableFloorZ; } | |
/** Get the Z component of the normal of the steepest walkable surface for the character. Any lower than this and it is not walkable. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DisplayName = "GetWalkableFloorZ", ScriptName = "GetWalkableFloorZ")) | |
float K2_GetWalkableFloorZ() const; | |
/** Set the Z component of the normal of the steepest walkable surface for the character. Also computes WalkableFloorAngle. */ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement") | |
void SetWalkableFloorZ(float InWalkableFloorZ); | |
/** Post-physics tick function for this character */ | |
UPROPERTY() | |
struct FMMOCharacterMovementComponentPostPhysicsTickFunction PostPhysicsTickFunction; | |
/** Tick function called after physics (sync scene) has finished simulation, before cloth */ | |
virtual void PostPhysicsTickComponent(float DeltaTime, FMMOCharacterMovementComponentPostPhysicsTickFunction& ThisTickFunction); | |
protected: | |
/** @note Movement update functions should only be called through StartNewPhysics()*/ | |
virtual void PhysWalking(float deltaTime, int32 Iterations); | |
/** @note Movement update functions should only be called through StartNewPhysics()*/ | |
virtual void PhysNavWalking(float deltaTime, int32 Iterations); | |
/** @note Movement update functions should only be called through StartNewPhysics()*/ | |
virtual void PhysFlying(float deltaTime, int32 Iterations); | |
/** @note Movement update functions should only be called through StartNewPhysics()*/ | |
virtual void PhysSwimming(float deltaTime, int32 Iterations); | |
/** @note Movement update functions should only be called through StartNewPhysics()*/ | |
virtual void PhysCustom(float deltaTime, int32 Iterations); | |
/* Allow custom handling when character hits a wall while swimming. */ | |
virtual void HandleSwimmingWallHit(const FHitResult& Hit, float DeltaTime); | |
/** | |
* Compute a vector of movement, given a delta and a hit result of the surface we are on. | |
* | |
* @param Delta: Attempted movement direction | |
* @param RampHit: Hit result of sweep that found the ramp below the capsule | |
* @param bHitFromLineTrace: Whether the floor trace came from a line trace | |
* | |
* @return If on a walkable surface, this returns a vector that moves parallel to the surface. The magnitude may be scaled if bMaintainHorizontalGroundVelocity is true. | |
* If a ramp vector can't be computed, this will just return Delta. | |
*/ | |
virtual FVector ComputeGroundMovementDelta(const FVector& Delta, const FHitResult& RampHit, const bool bHitFromLineTrace) const; | |
/** | |
* Move along the floor, using CurrentFloor and ComputeGroundMovementDelta() to get a movement direction. | |
* If a second walkable surface is hit, it will also be moved along using the same approach. | |
* | |
* @param InVelocity: Velocity of movement | |
* @param DeltaSeconds: Time over which movement occurs | |
* @param OutStepDownResult: [Out] If non-null, and a floor check is performed, this will be updated to reflect that result. | |
*/ | |
virtual void MoveAlongFloor(const FVector& InVelocity, float DeltaSeconds, FStepDownResult* OutStepDownResult = NULL); | |
/** Notification that the character is stuck in geometry. Only called during walking movement. */ | |
virtual void OnCharacterStuckInGeometry(const FHitResult* Hit); | |
/** | |
* Adjusts velocity when walking so that Z velocity is zero. | |
* When bMaintainHorizontalGroundVelocity is false, also rescales the velocity vector to maintain the original magnitude, but in the horizontal direction. | |
*/ | |
virtual void MaintainHorizontalGroundVelocity(); | |
/** Overridden to enforce max distances based on hit geometry. */ | |
virtual FVector GetPenetrationAdjustment(const FHitResult& Hit) const override; | |
/** Overridden to set bJustTeleported to true, so we don't make incorrect velocity calculations based on adjusted movement. */ | |
virtual bool ResolvePenetrationImpl(const FVector& Adjustment, const FHitResult& Hit, const FQuat& NewRotation) override; | |
/** Handle a blocking impact. Calls ApplyImpactPhysicsForces for the hit, if bEnablePhysicsInteraction is true. */ | |
virtual void HandleImpact(const FHitResult& Hit, float TimeSlice = 0.f, const FVector& MoveDelta = FVector::ZeroVector) override; | |
/** | |
* Apply physics forces to the impacted component, if bEnablePhysicsInteraction is true. | |
* @param Impact HitResult that resulted in the impact | |
* @param ImpactAcceleration Acceleration of the character at the time of impact | |
* @param ImpactVelocity Velocity of the character at the time of impact | |
*/ | |
virtual void ApplyImpactPhysicsForces(const FHitResult& Impact, const FVector& ImpactAcceleration, const FVector& ImpactVelocity); | |
/** Custom version of SlideAlongSurface that handles different movement modes separately; namely during walking physics we might not want to slide up slopes. */ | |
virtual float SlideAlongSurface(const FVector& Delta, float Time, const FVector& Normal, FHitResult& Hit, bool bHandleImpact) override; | |
/** Custom version that allows upwards slides when walking if the surface is walkable. */ | |
virtual void TwoWallAdjust(FVector& Delta, const FHitResult& Hit, const FVector& OldHitNormal) const override; | |
/** | |
* Calculate slide vector along a surface. | |
* Has special treatment when falling, to avoid boosting up slopes (calling HandleSlopeBoosting() in this case). | |
* | |
* @param Delta: Attempted move. | |
* @param Time: Amount of move to apply (between 0 and 1). | |
* @param Normal: Normal opposed to movement. Not necessarily equal to Hit.Normal (but usually is). | |
* @param Hit: HitResult of the move that resulted in the slide. | |
* @return New deflected vector of movement. | |
*/ | |
virtual FVector ComputeSlideVector(const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const override; | |
/** | |
* Limit the slide vector when falling if the resulting slide might boost the character faster upwards. | |
* @param SlideResult: Vector of movement for the slide (usually the result of ComputeSlideVector) | |
* @param Delta: Original attempted move | |
* @param Time: Amount of move to apply (between 0 and 1). | |
* @param Normal: Normal opposed to movement. Not necessarily equal to Hit.Normal (but usually is). | |
* @param Hit: HitResult of the move that resulted in the slide. | |
* @return: New slide result. | |
*/ | |
virtual FVector HandleSlopeBoosting(const FVector& SlideResult, const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const; | |
/** Slows towards stop. */ | |
virtual void ApplyVelocityBraking(float DeltaTime, float Friction, float BrakingDeceleration); | |
public: | |
/** | |
* Return true if the 2D distance to the impact point is inside the edge tolerance (CapsuleRadius minus a small rejection threshold). | |
* Useful for rejecting adjacent hits when finding a floor or landing spot. | |
*/ | |
virtual bool IsWithinEdgeTolerance(const FVector& CapsuleLocation, const FVector& TestImpactPoint, const float CapsuleRadius) const; | |
/** | |
* Sweeps a vertical trace to find the floor for the capsule at the given location. Will attempt to perch if ShouldComputePerchResult() returns true for the downward sweep result. | |
* No floor will be found if collision is disabled on the capsule! | |
* | |
* @param CapsuleLocation Location where the capsule sweep should originate | |
* @param OutFloorResult [Out] Contains the result of the floor check. The HitResult will contain the valid sweep or line test upon success, or the result of the sweep upon failure. | |
* @param bCanUseCachedLocation If true, may use a cached value (can be used to avoid unnecessary floor tests, if for example the capsule was not moving since the last test). | |
* @param DownwardSweepResult If non-null and it contains valid blocking hit info, this will be used as the result of a downward sweep test instead of doing it as part of the update. | |
*/ | |
virtual void FindFloor(const FVector& CapsuleLocation, FMMOFindFloorResult& OutFloorResult, bool bCanUseCachedLocation, const FHitResult* DownwardSweepResult = NULL) const; | |
/** | |
* Sweeps a vertical trace to find the floor for the capsule at the given location. Will attempt to perch if ShouldComputePerchResult() returns true for the downward sweep result. | |
* No floor will be found if collision is disabled on the capsule! | |
* | |
* @param CapsuleLocation Location where the capsule sweep should originate | |
* @param FloorResult Result of the floor check | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DisplayName = "FindFloor", ScriptName = "FindFloor")) | |
virtual void K2_FindFloor(FVector CapsuleLocation, FMMOFindFloorResult& FloorResult) const; | |
/** | |
* Compute distance to the floor from bottom sphere of capsule and store the result in OutFloorResult. | |
* This distance is the swept distance of the capsule to the first point impacted by the lower hemisphere, or distance from the bottom of the capsule in the case of a line trace. | |
* This function does not care if collision is disabled on the capsule (unlike FindFloor). | |
* @see FindFloor | |
* | |
* @param CapsuleLocation: Location of the capsule used for the query | |
* @param LineDistance: If non-zero, max distance to test for a simple line check from the capsule base. Used only if the sweep test fails to find a walkable floor, and only returns a valid result if the impact normal is a walkable normal. | |
* @param SweepDistance: If non-zero, max distance to use when sweeping a capsule downwards for the test. MUST be greater than or equal to the line distance. | |
* @param OutFloorResult: Result of the floor check. The HitResult will contain the valid sweep or line test upon success, or the result of the sweep upon failure. | |
* @param SweepRadius: The radius to use for sweep tests. Should be <= capsule radius. | |
* @param DownwardSweepResult: If non-null and it contains valid blocking hit info, this will be used as the result of a downward sweep test instead of doing it as part of the update. | |
*/ | |
virtual void ComputeFloorDist(const FVector& CapsuleLocation, float LineDistance, float SweepDistance, FMMOFindFloorResult& OutFloorResult, float SweepRadius, const FHitResult* DownwardSweepResult = NULL) const; | |
/** | |
* Compute distance to the floor from bottom sphere of capsule and store the result in FloorResult. | |
* This distance is the swept distance of the capsule to the first point impacted by the lower hemisphere, or distance from the bottom of the capsule in the case of a line trace. | |
* This function does not care if collision is disabled on the capsule (unlike FindFloor). | |
* | |
* @param CapsuleLocation Location where the capsule sweep should originate | |
* @param LineDistance If non-zero, max distance to test for a simple line check from the capsule base. Used only if the sweep test fails to find a walkable floor, and only returns a valid result if the impact normal is a walkable normal. | |
* @param SweepDistance If non-zero, max distance to use when sweeping a capsule downwards for the test. MUST be greater than or equal to the line distance. | |
* @param SweepRadius The radius to use for sweep tests. Should be <= capsule radius. | |
* @param FloorResult Result of the floor check | |
*/ | |
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|CharacterMovement", meta = (DisplayName = "ComputeFloorDistance", ScriptName = "ComputeFloorDistance")) | |
virtual void K2_ComputeFloorDist(FVector CapsuleLocation, float LineDistance, float SweepDistance, float SweepRadius, FMMOFindFloorResult& FloorResult) const; | |
/** | |
* Sweep against the world and return the first blocking hit. | |
* Intended for tests against the floor, because it may change the result of impacts on the lower area of the test (especially if bUseFlatBaseForFloorChecks is true). | |
* | |
* @param OutHit First blocking hit found. | |
* @param Start Start location of the capsule. | |
* @param End End location of the capsule. | |
* @param TraceChannel The 'channel' that this trace is in, used to determine which components to hit. | |
* @param CollisionShape Capsule collision shape. | |
* @param Params Additional parameters used for the trace. | |
* @param ResponseParam ResponseContainer to be used for this trace. | |
* @return True if OutHit contains a blocking hit entry. | |
*/ | |
virtual bool FloorSweepTest( | |
struct FHitResult& OutHit, | |
const FVector& Start, | |
const FVector& End, | |
ECollisionChannel TraceChannel, | |
const struct FCollisionShape& CollisionShape, | |
const struct FCollisionQueryParams& Params, | |
const struct FCollisionResponseParams& ResponseParam | |
) const; | |
/** Verify that the supplied hit result is a valid landing spot when falling. */ | |
virtual bool IsValidLandingSpot(const FVector& CapsuleLocation, const FHitResult& Hit) const; | |
/** | |
* Determine whether we should try to find a valid landing spot after an impact with an invalid one (based on the Hit result). | |
* For example, landing on the lower portion of the capsule on the edge of geometry may be a walkable surface, but could have reported an unwalkable impact normal. | |
*/ | |
virtual bool ShouldCheckForValidLandingSpot(float DeltaTime, const FVector& Delta, const FHitResult& Hit) const; | |
/** | |
* Check if the result of a sweep test (passed in InHit) might be a valid location to perch, in which case we should use ComputePerchResult to validate the location. | |
* @see ComputePerchResult | |
* @param InHit: Result of the last sweep test before this query. | |
* @param bCheckRadius: If true, only allow the perch test if the impact point is outside the radius returned by GetValidPerchRadius(). | |
* @return Whether perching may be possible, such that ComputePerchResult can return a valid result. | |
*/ | |
virtual bool ShouldComputePerchResult(const FHitResult& InHit, bool bCheckRadius = true) const; | |
/** | |
* Compute the sweep result of the smaller capsule with radius specified by GetValidPerchRadius(), | |
* and return true if the sweep contacts a valid walkable normal within InMaxFloorDist of InHit.ImpactPoint. | |
* This may be used to determine if the capsule can or cannot stay at the current location if perched on the edge of a small ledge or unwalkable surface. | |
* Note: Only returns a valid result if ShouldComputePerchResult returned true for the supplied hit value. | |
* | |
* @param TestRadius: Radius to use for the sweep, usually GetValidPerchRadius(). | |
* @param InHit: Result of the last sweep test before the query. | |
* @param InMaxFloorDist: Max distance to floor allowed by perching, from the supplied contact point (InHit.ImpactPoint). | |
* @param OutPerchFloorResult: Contains the result of the perch floor test. | |
* @return True if the current location is a valid spot at which to perch. | |
*/ | |
virtual bool ComputePerchResult(const float TestRadius, const FHitResult& InHit, const float InMaxFloorDist, FMMOFindFloorResult& OutPerchFloorResult) const; | |
protected: | |
/** Called when the collision capsule touches another primitive component */ | |
UFUNCTION() | |
virtual void CapsuleTouched(UPrimitiveComponent* OverlappedComp, AActor* Other, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); | |
// Enum used to control GetPawnCapsuleExtent behavior | |
enum EShrinkCapsuleExtent | |
{ | |
SHRINK_None, // Don't change the size of the capsule | |
SHRINK_RadiusCustom, // Change only the radius, based on a supplied param | |
SHRINK_HeightCustom, // Change only the height, based on a supplied param | |
SHRINK_AllCustom, // Change both radius and height, based on a supplied param | |
}; | |
/** Get the capsule extent for the Pawn owner, possibly reduced in size depending on ShrinkMode. | |
* @param ShrinkMode Controls the way the capsule is resized. | |
* @param CustomShrinkAmount The amount to shrink the capsule, used only for ShrinkModes that specify custom. | |
* @return The capsule extent of the Pawn owner, possibly reduced in size depending on ShrinkMode. | |
*/ | |
FVector GetPawnCapsuleExtent(const EShrinkCapsuleExtent ShrinkMode, const float CustomShrinkAmount = 0.f) const; | |
/** Get the collision shape for the Pawn owner, possibly reduced in size depending on ShrinkMode. | |
* @param ShrinkMode Controls the way the capsule is resized. | |
* @param CustomShrinkAmount The amount to shrink the capsule, used only for ShrinkModes that specify custom. | |
* @return The capsule extent of the Pawn owner, possibly reduced in size depending on ShrinkMode. | |
*/ | |
FCollisionShape GetPawnCapsuleCollisionShape(const EShrinkCapsuleExtent ShrinkMode, const float CustomShrinkAmount = 0.f) const; | |
/** Adjust the size of the capsule on simulated proxies, to avoid overlaps due to replication rounding. | |
* Changes to the capsule size on the proxy should set bShrinkProxyCapsule=true and possibly call AdjustProxyCapsuleSize() immediately if applicable. | |
*/ | |
virtual void AdjustProxyCapsuleSize(); | |
/** Enforce constraints on input given current state. For instance, don't move upwards if walking and looking up. */ | |
virtual FVector ConstrainInputAcceleration(const FVector& InputAcceleration) const; | |
/** Scale input acceleration, based on movement acceleration rate. */ | |
virtual FVector ScaleInputAcceleration(const FVector& InputAcceleration) const; | |
/** | |
* Event triggered at the end of a movement update. If scoped movement updates are enabled (bEnableScopedMovementUpdates), this is within such a scope. | |
* If that is not desired, bind to the CharacterOwner's OnMovementUpdated event instead, as that is triggered after the scoped movement update. | |
*/ | |
virtual void OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity); | |
/** Internal function to call OnMovementUpdated delegate on CharacterOwner. */ | |
virtual void CallMovementUpdateDelegate(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity); | |
/** | |
* Event triggered when we are moving on a base but we are not able to move the full DeltaPosition because something has blocked us. | |
* Note: MoveComponentFlags includes the flag to ignore the movement base while this event is fired. | |
* @param DeltaPosition How far we tried to move with the base. | |
* @param OldLocation Location before we tried to move with the base. | |
* @param MoveOnBaseHit Hit result for the object we hit when trying to move with the base. | |
*/ | |
virtual void OnUnableToFollowBaseMove(const FVector& DeltaPosition, const FVector& OldLocation, const FHitResult& MoveOnBaseHit); | |
public: | |
/** | |
* Project a location to navmesh to find adjusted height. | |
* @param TestLocation Location to project | |
* @param NavFloorLocation Location on navmesh | |
* @return True if projection was performed (successfully or not) | |
*/ | |
virtual bool FindNavFloor(const FVector& TestLocation, FNavLocation& NavFloorLocation) const; | |
protected: | |
// Movement functions broken out based on owner's network Role. | |
// TickComponent calls the correct version based on the Role. | |
// These may be called during move playback and correction during network updates. | |
// | |
/** Perform movement on an autonomous client */ | |
virtual void PerformMovement(float DeltaTime); | |
/** Special Tick for Simulated Proxies */ | |
virtual void SimulatedTick(float DeltaSeconds); | |
/** Simulate movement on a non-owning client. Called by SimulatedTick(). */ | |
virtual void SimulateMovement(float DeltaTime); | |
public: | |
/** Force a client update by making it appear on the server that the client hasn't updated in a long time. */ | |
virtual void ForceReplicationUpdate(); | |
/** Force a client adjustment. Resets ServerLastClientAdjustmentTime. */ | |
void ForceClientAdjustment(); | |
/** | |
* Generate a random angle in degrees that is approximately equal between client and server. | |
* Note that in networked games this result changes with low frequency and has a low period, | |
* so should not be used for frequent randomization. | |
*/ | |
virtual float GetNetworkSafeRandomAngleDegrees() const; | |
/** Round acceleration, for better consistency and lower bandwidth in networked games. */ | |
virtual FVector RoundAcceleration(FVector InAccel) const; | |
//-------------------------------- | |
// INetworkPredictionInterface implementation | |
//-------------------------------- | |
// Server hook | |
//-------------------------------- | |
virtual void SendClientAdjustment() override; | |
virtual bool ForcePositionUpdate(float DeltaTime) override; | |
//-------------------------------- | |
// Client hook | |
//-------------------------------- | |
/** | |
* React to new transform from network update. Sets bNetworkSmoothingComplete to false to ensure future smoothing updates. | |
* IMPORTANT: It is expected that this function triggers any movement/transform updates to match the network update if desired. | |
*/ | |
virtual void SmoothCorrection(const FVector& OldLocation, const FQuat& OldRotation, const FVector& NewLocation, const FQuat& NewRotation) override; | |
/** Get prediction data for a client game. Should not be used if not running as a client. Allocates the data on demand and can be overridden to allocate a custom override if desired. Result must be a FMMONetworkPredictionData_Client_Character. */ | |
virtual class FNetworkPredictionData_Client* GetPredictionData_Client() const override; | |
/** Get prediction data for a server game. Should not be used if not running as a server. Allocates the data on demand and can be overridden to allocate a custom override if desired. Result must be a FMMONetworkPredictionData_Server_Character. */ | |
virtual class FNetworkPredictionData_Server* GetPredictionData_Server() const override; | |
class FMMONetworkPredictionData_Client_Character* GetPredictionData_Client_Character() const; | |
class FMMONetworkPredictionData_Server_Character* GetPredictionData_Server_Character() const; | |
virtual bool HasPredictionData_Client() const override; | |
virtual bool HasPredictionData_Server() const override; | |
virtual void ResetPredictionData_Client() override; | |
virtual void ResetPredictionData_Server() override; | |
static uint32 PackYawAndPitchTo32(const float Yaw, const float Pitch); | |
protected: | |
class FMMONetworkPredictionData_Client_Character* ClientPredictionData; | |
class FMMONetworkPredictionData_Server_Character* ServerPredictionData; | |
FRandomStream RandomStream; | |
/** | |
* Smooth mesh location for network interpolation, based on values set up by SmoothCorrection. | |
* Internally this simply calls SmoothClientPosition_Interpolate() then SmoothClientPosition_UpdateVisuals(). | |
* This function is not called when bNetworkSmoothingComplete is true. | |
* @param DeltaSeconds Time since last update. | |
*/ | |
virtual void SmoothClientPosition(float DeltaSeconds); | |
/** | |
* Update interpolation values for client smoothing. Does not change actual mesh location. | |
* Sets bNetworkSmoothingComplete to true when the interpolation reaches the target. | |
*/ | |
void SmoothClientPosition_Interpolate(float DeltaSeconds); | |
/** Update mesh location based on interpolated values. */ | |
void SmoothClientPosition_UpdateVisuals(); | |
/* | |
======================================================================== | |
Here's how player movement prediction, replication and correction works in network games: | |
Every tick, the TickComponent() function is called. It figures out the acceleration and rotation change for the frame, | |
and then calls PerformMovement() (for locally controlled Characters), or ReplicateMoveToServer() (if it's a network client). | |
ReplicateMoveToServer() saves the move (in the PendingMove list), calls PerformMovement(), and then replicates the move | |
to the server by calling the replicated function ServerMove() - passing the movement parameters, the client's | |
resultant position, and a timestamp. | |
ServerMove() is executed on the server. It decodes the movement parameters and causes the appropriate movement | |
to occur. It then looks at the resulting position and if enough time has passed since the last response, or the | |
position error is significant enough, the server calls ClientAdjustPosition(), a replicated function. | |
ClientAdjustPosition() is executed on the client. The client sets its position to the servers version of position, | |
and sets the bUpdatePosition flag to true. | |
When TickComponent() is called on the client again, if bUpdatePosition is true, the client will call | |
ClientUpdatePosition() before calling PerformMovement(). ClientUpdatePosition() replays all the moves in the pending | |
move list which occurred after the timestamp of the move the server was adjusting. | |
*/ | |
/** Perform local movement and send the move to the server. */ | |
virtual void ReplicateMoveToServer(float DeltaTime, const FVector& NewAcceleration); | |
/** If bUpdatePosition is true, then replay any unacked moves. Returns whether any moves were actually replayed. */ | |
virtual bool ClientUpdatePositionAfterServerUpdate(); | |
/** Call the appropriate replicated servermove() function to send a client player move to the server. */ | |
virtual void CallServerMove(const class FMMOSavedMove_Character* NewMove, const class FMMOSavedMove_Character* OldMove); | |
/** | |
* Have the server check if the client is outside an error tolerance, and queue a client adjustment if so. | |
* If either GetPredictionData_Server_Character()->bForceClientUpdate or ServerCheckClientError() are true, the client adjustment will be sent. | |
* RelativeClientLocation will be a relative location if MovementBaseUtility::UseRelativePosition(ClientMovementBase) is true, or a world location if false. | |
* @see ServerCheckClientError() | |
*/ | |
virtual void ServerMoveHandleClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/** | |
* Check for Server-Client disagreement in position or other movement state important enough to trigger a client correction. | |
* @see ServerMoveHandleClientError() | |
*/ | |
virtual bool ServerCheckClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/** | |
* Check position error within ServerCheckClientError(). Set bNetworkLargeClientCorrection to true if the correction should be prioritized (delayed less in SendClientAdjustment). | |
*/ | |
virtual bool ServerExceedsAllowablePositionError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/** | |
* If ServerCheckClientError() does not find an error, this determines if the server should also copy the client's movement params rather than keep the server sim result. | |
*/ | |
virtual bool ServerShouldUseAuthoritativePosition(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/* Process a move at the given time stamp, given the compressed flags representing various events that occurred (ie jump). */ | |
virtual void MoveAutonomous(float ClientTimeStamp, float DeltaTime, uint8 CompressedFlags, const FVector& NewAccel); | |
/** Unpack compressed flags from a saved move and set state accordingly. See FMMOSavedMove_Character. */ | |
virtual void UpdateFromCompressedFlags(uint8 Flags); | |
/** Return true if it is OK to delay sending this player movement to the server, in order to conserve bandwidth. */ | |
virtual bool CanDelaySendingMove(const FMMOSavedMovePtr& NewMove); | |
/** Determine minimum delay between sending client updates to the server. If updates occur more frequently this than this time, moves may be combined delayed. */ | |
virtual float GetClientNetSendDeltaTime(const APlayerController* PC, const FMMONetworkPredictionData_Client_Character* ClientData, const FMMOSavedMovePtr& NewMove) const; | |
/** Ticks the characters pose and accumulates root motion */ | |
void TickCharacterPose(float DeltaTime); | |
/** On the server if we know we are having our replication rate throttled, this method checks if important replicated properties have changed that should cause us to return to the normal replication rate. */ | |
virtual bool ShouldCancelAdaptiveReplication() const; | |
public: | |
/** React to instantaneous change in position. Invalidates cached floor recomputes it if possible if there is a current movement base. */ | |
virtual void UpdateFloorFromAdjustment(); | |
/** Minimum time between client TimeStamp resets. | |
!! This has to be large enough so that we don't confuse the server if the client can stall or timeout. | |
We do this as we use floats for TimeStamps, and server derives DeltaTime from two TimeStamps. | |
As time goes on, accuracy decreases from those floating point numbers. | |
So we trigger a TimeStamp reset at regular intervals to maintain a high level of accuracy. */ | |
UPROPERTY() | |
float MinTimeBetweenTimeStampResets; | |
/** On the Server, verify that an incoming client TimeStamp is valid and has not yet expired. | |
It will also handle TimeStamp resets if it detects a gap larger than MinTimeBetweenTimeStampResets / 2.f | |
!! ServerData.CurrentClientTimeStamp can be reset !! | |
@returns true if TimeStamp is valid, or false if it has expired. */ | |
virtual bool VerifyClientTimeStamp(float TimeStamp, FMMONetworkPredictionData_Server_Character& ServerData); | |
protected: | |
/** Clock time on the server of the last timestamp reset. */ | |
float LastTimeStampResetServerTime; | |
/** Internal const check for client timestamp validity without side-effects. | |
* @see VerifyClientTimeStamp */ | |
bool IsClientTimeStampValid(float TimeStamp, const FMMONetworkPredictionData_Server_Character& ServerData, bool& bTimeStampResetDetected) const; | |
/** Called by UMMOPlayerMovement::VerifyClientTimeStamp() when a client timestamp reset has been detected and is valid. */ | |
virtual void OnClientTimeStampResetDetected(); | |
/** | |
* Processes client timestamps from ServerMoves, detects and protects against time discrepancy between client-reported times and server time | |
* Called by UMMOPlayerMovement::VerifyClientTimeStamp() for valid timestamps. | |
*/ | |
virtual void ProcessClientTimeStampForTimeDiscrepancy(float ClientTimeStamp, FMMONetworkPredictionData_Server_Character& ServerData); | |
/** | |
* Called by UMMOPlayerMovement::ProcessClientTimeStampForTimeDiscrepancy() (on server) when the time from client moves | |
* significantly differs from the server time, indicating potential time manipulation by clients (speed hacks, significant network | |
* issues, client performance problems) | |
* @param CurrentTimeDiscrepancy Accumulated time difference between client ServerMove and server time - this is bounded | |
* by MovementTimeDiscrepancy config variables in AGameNetworkManager, and is the value with which | |
* we test against to trigger this function. This is reset when MovementTimeDiscrepancy resolution | |
* is enabled | |
* @param LifetimeRawTimeDiscrepancy Accumulated time difference between client ServerMove and server time - this is unbounded | |
* and does NOT get affected by MovementTimeDiscrepancy resolution, and is useful as a longer-term | |
* view of how the given client is performing. High magnitude unbounded error points to | |
* intentional tampering by a client vs. occasional "naturally caused" spikes in error due to | |
* burst packet loss/performance hitches | |
* @param Lifetime Game time over which LifetimeRawTimeDiscrepancy has accrued (useful for determining severity | |
* of LifetimeUnboundedError) | |
* @param CurrentMoveError Time difference between client ServerMove and how much time has passed on the server for the | |
* current move that has caused TimeDiscrepancy to accumulate enough to trigger detection. | |
*/ | |
virtual void OnTimeDiscrepancyDetected(float CurrentTimeDiscrepancy, float LifetimeRawTimeDiscrepancy, float Lifetime, float CurrentMoveError); | |
public: | |
//////////////////////////////////// | |
// Network RPCs for movement | |
//////////////////////////////////// | |
/** | |
* The actual RPCs are passed to AMMOCharacter, which wrap to the _Implementation and _Validate call here, to avoid Component RPC overhead. | |
* For example: | |
* Client: UMMOPlayerMovement::ServerMove(...) => Calls CharacterOwner->ServerMove(...) triggering RPC on server | |
* Server: AMMOCharacter::ServerMove_Implementation(...) => Calls CharacterMovement->ServerMove_Implementation | |
* To override the client call to the server RPC (on CharacterOwner), override ServerMove(). | |
* To override the server implementation, override ServerMove_Implementation(). | |
*/ | |
/** | |
* Replicated function sent by client to server - contains client movement and view info. | |
* Calls either CharacterOwner->ServerMove() or CharacterOwner->ServerMoveNoBase() depending on whehter ClientMovementBase is null. | |
*/ | |
virtual void ServerMove(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
virtual void ServerMove_Implementation(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
virtual bool ServerMove_Validate(float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 CompressedMoveFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/** | |
* Replicated function sent by client to server - contains client movement and view info for two moves. | |
* Calls either CharacterOwner->ServerMoveDual() or CharacterOwner->ServerMoveDualNoBase() depending on whehter ClientMovementBase is null. | |
*/ | |
virtual void ServerMoveDual(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
virtual void ServerMoveDual_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
virtual bool ServerMoveDual_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/** Replicated function sent by client to server - contains client movement and view info for two moves. First move is non root motion, second is root motion. */ | |
virtual void ServerMoveDualHybridRootMotion(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
virtual void ServerMoveDualHybridRootMotion_Implementation(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
virtual bool ServerMoveDualHybridRootMotion_Validate(float TimeStamp0, FVector_NetQuantize10 InAccel0, uint8 PendingFlags, uint32 View0, float TimeStamp, FVector_NetQuantize10 InAccel, FVector_NetQuantize100 ClientLoc, uint8 NewFlags, uint8 ClientRoll, uint32 View, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode); | |
/* Resending an (important) old move. Process it if not already processed. */ | |
virtual void ServerMoveOld(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags); | |
virtual void ServerMoveOld_Implementation(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags); | |
virtual bool ServerMoveOld_Validate(float OldTimeStamp, FVector_NetQuantize10 OldAccel, uint8 OldMoveFlags); | |
/** If no client adjustment is needed after processing received ServerMove(), ack the good move so client can remove it from SavedMoves */ | |
virtual void ClientAckGoodMove(float TimeStamp); | |
virtual void ClientAckGoodMove_Implementation(float TimeStamp); | |
/** Replicate position correction to client, associated with a timestamped servermove. Client will replay subsequent moves after applying adjustment. */ | |
virtual void ClientAdjustPosition(float TimeStamp, FVector NewLoc, FVector NewVel, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
virtual void ClientAdjustPosition_Implementation(float TimeStamp, FVector NewLoc, FVector NewVel, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
/* Bandwidth saving version, when velocity is zeroed */ | |
virtual void ClientVeryShortAdjustPosition(float TimeStamp, FVector NewLoc, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
virtual void ClientVeryShortAdjustPosition_Implementation(float TimeStamp, FVector NewLoc, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
/** Replicate position correction to client when using root motion for movement. (animation root motion specific) */ | |
virtual void ClientAdjustRootMotionPosition(float TimeStamp, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
virtual void ClientAdjustRootMotionPosition_Implementation(float TimeStamp, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
/** Replicate root motion source correction to client when using root motion for movement. */ | |
virtual void ClientAdjustRootMotionSourcePosition(float TimeStamp, FRootMotionSourceGroup ServerRootMotion, bool bHasAnimRootMotion, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
virtual void ClientAdjustRootMotionSourcePosition_Implementation(float TimeStamp, FRootMotionSourceGroup ServerRootMotion, bool bHasAnimRootMotion, float ServerMontageTrackPosition, FVector ServerLoc, FVector_NetQuantizeNormal ServerRotation, float ServerVelZ, UPrimitiveComponent* ServerBase, FName ServerBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
protected: | |
/** Event notification when client receives correction data from the server, before applying the data. Base implementation logs relevant data and draws debug info if "p.NetShowCorrections" is not equal to 0. */ | |
virtual void OnClientCorrectionReceived(class FMMONetworkPredictionData_Client_Character& ClientData, float TimeStamp, FVector NewLocation, FVector NewVelocity, UPrimitiveComponent* NewBase, FName NewBaseBoneName, bool bHasBase, bool bBaseRelativePosition, uint8 ServerMovementMode); | |
// Root Motion | |
public: | |
/** Root Motion Group containing active root motion sources being applied to movement */ | |
UPROPERTY(Transient) | |
FRootMotionSourceGroup CurrentRootMotion; | |
/** Returns true if we have Root Motion from any source to use in PerformMovement() physics. */ | |
bool HasRootMotionSources() const; | |
/** Apply a RootMotionSource to current root motion | |
* @return LocalID for this Root Motion Source */ | |
uint16 ApplyRootMotionSource(FRootMotionSource* SourcePtr); | |
/** Called during ApplyRootMotionSource call, useful for project-specific alerts for "something is about to be altering our movement" */ | |
virtual void OnRootMotionSourceBeingApplied(const FRootMotionSource* Source); | |
/** Get a RootMotionSource from current root motion by name */ | |
TSharedPtr<FRootMotionSource> GetRootMotionSource(FName InstanceName); | |
/** Get a RootMotionSource from current root motion by ID */ | |
TSharedPtr<FRootMotionSource> GetRootMotionSourceByID(uint16 RootMotionSourceID); | |
/** Remove a RootMotionSource from current root motion by name */ | |
void RemoveRootMotionSource(FName InstanceName); | |
/** Remove a RootMotionSource from current root motion by ID */ | |
void RemoveRootMotionSourceByID(uint16 RootMotionSourceID); | |
/** Converts received server IDs in a root motion group to local IDs */ | |
void ConvertRootMotionServerIDsToLocalIDs(const FRootMotionSourceGroup& LocalRootMotionToMatchWith, FRootMotionSourceGroup& InOutServerRootMotion, float TimeStamp); | |
/** Collection of the most recent ID mappings */ | |
enum class ERootMotionMapping : uint32 { MapSize = 16 }; | |
TArray<FRootMotionServerToLocalIDMapping, TInlineAllocator<(uint32)ERootMotionMapping::MapSize> > RootMotionIDMappings; | |
protected: | |
/** Restores Velocity to LastPreAdditiveVelocity during Root Motion Phys*() function calls */ | |
void RestorePreAdditiveRootMotionVelocity(); | |
/** Applies root motion from root motion sources to velocity (override and additive) */ | |
void ApplyRootMotionToVelocity(float deltaTime); | |
public: | |
/** | |
* Animation root motion (special case for now) | |
*/ | |
/** Root Motion movement params. Holds result of anim montage root motion during PerformMovement(), and is overridden | |
* during autonomous move playback to force historical root motion for MoveAutonomous() calls */ | |
UPROPERTY(Transient) | |
FRootMotionMovementParams RootMotionParams; | |
/** Velocity extracted from RootMotionParams when there is anim root motion active. Invalid to use when HasAnimRootMotion() returns false. */ | |
UPROPERTY(Transient) | |
FVector AnimRootMotionVelocity; | |
/** Returns true if we have Root Motion from animation to use in PerformMovement() physics. | |
Not valid outside of the scope of that function. Since RootMotion is extracted and used in it. */ | |
bool HasAnimRootMotion() const | |
{ | |
return RootMotionParams.bHasRootMotion; | |
} | |
// Takes component space root motion and converts it to world space | |
FTransform ConvertLocalRootMotionToWorld(const FTransform& InLocalRootMotion); | |
// Delegate for modifying root motion pre conversion from component space to world space. | |
FMMOOnProcessRootMotion ProcessRootMotionPreConvertToWorld; | |
// Delegate for modifying root motion post conversion from component space to world space. | |
FMMOOnProcessRootMotion ProcessRootMotionPostConvertToWorld; | |
/** Simulate Root Motion physics on Simulated Proxies */ | |
void SimulateRootMotion(float DeltaSeconds, const FTransform& LocalRootMotionTransform); | |
/** | |
* Calculate velocity from anim root motion. | |
* @param RootMotionDeltaMove Change in location from root motion. | |
* @param DeltaSeconds Elapsed time | |
* @param CurrentVelocity Non-root motion velocity at current time, used for components of result that may ignore root motion. | |
* @see ConstrainAnimRootMotionVelocity | |
*/ | |
virtual FVector CalcAnimRootMotionVelocity(const FVector& RootMotionDeltaMove, float DeltaSeconds, const FVector& CurrentVelocity) const; | |
UE_DEPRECATED(4.13, "CalcRootMotionVelocity() has been replaced by CalcAnimRootMotionVelocity() instead, and ConstrainAnimRootMotionVelocity() now handles restricting root motion velocity under different conditions.") | |
virtual FVector CalcRootMotionVelocity(const FVector& RootMotionDeltaMove, float DeltaSeconds, const FVector& CurrentVelocity) const; | |
/** | |
* Constrain components of root motion velocity that may not be appropriate given the current movement mode (e.g. when falling Z may be ignored). | |
*/ | |
virtual FVector ConstrainAnimRootMotionVelocity(const FVector& RootMotionVelocity, const FVector& CurrentVelocity) const; | |
// RVO Avoidance | |
/** calculate RVO avoidance and apply it to current velocity */ | |
virtual void CalcAvoidanceVelocity(float DeltaTime); | |
/** allows modifing avoidance velocity, called when bUseRVOPostProcess is set */ | |
virtual void PostProcessAvoidanceVelocity(FVector& NewVelocity); | |
virtual void FlushServerMoves(); | |
/** | |
* When moving the character, we should inform physics as to whether we are teleporting. | |
* This allows physics to avoid injecting forces into simulations from client corrections (etc.) | |
*/ | |
ETeleportType GetTeleportType() const; | |
protected: | |
/** called in Tick to update data in RVO avoidance manager */ | |
void UpdateDefaultAvoidance(); | |
public: | |
/** lock avoidance velocity */ | |
void SetAvoidanceVelocityLock(class UAvoidanceManager* Avoidance, float Duration); | |
/** BEGIN IRVOAvoidanceInterface */ | |
virtual void SetRVOAvoidanceUID(int32 UID) override; | |
virtual int32 GetRVOAvoidanceUID() override; | |
virtual void SetRVOAvoidanceWeight(float Weight) override; | |
virtual float GetRVOAvoidanceWeight() override; | |
virtual FVector GetRVOAvoidanceOrigin() override; | |
virtual float GetRVOAvoidanceRadius() override; | |
virtual float GetRVOAvoidanceHeight() override; | |
virtual float GetRVOAvoidanceConsiderationRadius() override; | |
virtual FVector GetVelocityForRVOConsideration() override; | |
virtual int32 GetAvoidanceGroupMask() override; | |
virtual int32 GetGroupsToAvoidMask() override; | |
virtual int32 GetGroupsToIgnoreMask() override; | |
/** END IRVOAvoidanceInterface */ | |
/** a shortcut function to be called instead of GetRVOAvoidanceUID when | |
* callee knows it's dealing with a char movement comp */ | |
int32 GetRVOAvoidanceUIDFast() const { return AvoidanceUID; } | |
public: | |
/** Minimum delta time considered when ticking. Delta times below this are not considered. This is a very small non-zero value to avoid potential divide-by-zero in simulation code. */ | |
static const float MIN_TICK_TIME; | |
/** Minimum acceptable distance for Character capsule to float above floor when walking. */ | |
static const float MIN_FLOOR_DIST; | |
/** Maximum acceptable distance for Character capsule to float above floor when walking. */ | |
static const float MAX_FLOOR_DIST; | |
/** Reject sweep impacts that are this close to the edge of the vertical portion of the capsule when performing vertical sweeps, and try again with a smaller capsule. */ | |
static const float SWEEP_EDGE_REJECT_DISTANCE; | |
/** Stop completely when braking and velocity magnitude is lower than this. */ | |
static const float BRAKE_TO_STOP_VELOCITY; | |
}; | |
FORCEINLINE AMMOCharacter* UMMOPlayerMovement::GetCharacterOwner() const | |
{ | |
return CharacterOwner; | |
} | |
FORCEINLINE_DEBUGGABLE bool UMMOPlayerMovement::IsWalking() const | |
{ | |
return IsMovingOnGround(); | |
} | |
FORCEINLINE uint32 UMMOPlayerMovement::PackYawAndPitchTo32(const float Yaw, const float Pitch) | |
{ | |
const uint32 YawShort = FRotator::CompressAxisToShort(Yaw); | |
const uint32 PitchShort = FRotator::CompressAxisToShort(Pitch); | |
const uint32 Rotation32 = (YawShort << 16) | PitchShort; | |
return Rotation32; | |
} | |
/** FMMOSavedMove_Character represents a saved move on the client that has been sent to the server and might need to be played back. */ | |
class MMOEY_API FMMOSavedMove_Character | |
{ | |
public: | |
FMMOSavedMove_Character(); | |
virtual ~FMMOSavedMove_Character(); | |
// UE_DEPRECATED_FORGAME(4.20) | |
FMMOSavedMove_Character(const FMMOSavedMove_Character&); | |
FMMOSavedMove_Character(FMMOSavedMove_Character&&); | |
FMMOSavedMove_Character& operator=(const FMMOSavedMove_Character&); | |
FMMOSavedMove_Character& operator=(FMMOSavedMove_Character&&); | |
AMMOCharacter* CharacterOwner; | |
uint32 bPressedJump : 1; | |
uint32 bWantsToCrouch : 1; | |
uint32 bForceMaxAccel : 1; | |
/** If true, can't combine this move with another move. */ | |
uint32 bForceNoCombine : 1; | |
/** If true this move is using an old TimeStamp, before a reset occurred. */ | |
uint32 bOldTimeStampBeforeReset : 1; | |
uint32 bWasJumping : 1; | |
float TimeStamp; // Time of this move. | |
float DeltaTime; // amount of time for this move | |
float CustomTimeDilation; | |
float JumpKeyHoldTime; | |
float JumpForceTimeRemaining; | |
int32 JumpMaxCount; | |
int32 JumpCurrentCount; | |
UE_DEPRECATED_FORGAME(4.20, "This property is deprecated, use StartPackedMovementMode or EndPackedMovementMode instead.") | |
uint8 MovementMode; | |
// Information at the start of the move | |
uint8 StartPackedMovementMode; | |
FVector StartLocation; | |
FVector StartRelativeLocation; | |
FVector StartVelocity; | |
FMMOFindFloorResult StartFloor; | |
FRotator StartRotation; | |
FRotator StartControlRotation; | |
FQuat StartBaseRotation; // rotation of the base component (or bone), only saved if it can move. | |
float StartCapsuleRadius; | |
float StartCapsuleHalfHeight; | |
TWeakObjectPtr<UPrimitiveComponent> StartBase; | |
FName StartBoneName; | |
uint32 StartActorOverlapCounter; | |
uint32 StartComponentOverlapCounter; | |
TWeakObjectPtr<USceneComponent> StartAttachParent; | |
FName StartAttachSocketName; | |
FVector StartAttachRelativeLocation; | |
FRotator StartAttachRelativeRotation; | |
// Information after the move has been performed | |
uint8 EndPackedMovementMode; | |
FVector SavedLocation; | |
FRotator SavedRotation; | |
FVector SavedVelocity; | |
FVector SavedRelativeLocation; | |
FRotator SavedControlRotation; | |
TWeakObjectPtr<UPrimitiveComponent> EndBase; | |
FName EndBoneName; | |
uint32 EndActorOverlapCounter; | |
uint32 EndComponentOverlapCounter; | |
TWeakObjectPtr<USceneComponent> EndAttachParent; | |
FName EndAttachSocketName; | |
FVector EndAttachRelativeLocation; | |
FRotator EndAttachRelativeRotation; | |
FVector Acceleration; | |
float MaxSpeed; | |
// Cached to speed up iteration over IsImportantMove(). | |
FVector AccelNormal; | |
float AccelMag; | |
TWeakObjectPtr<class UAnimMontage> RootMotionMontage; | |
float RootMotionTrackPosition; | |
FRootMotionMovementParams RootMotionMovement; | |
FRootMotionSourceGroup SavedRootMotion; | |
/** Threshold for deciding this is an "important" move based on DP with last acked acceleration. */ | |
float AccelDotThreshold; | |
/** Threshold for deciding is this is an important move because acceleration magnitude has changed too much */ | |
float AccelMagThreshold; | |
/** Threshold for deciding if we can combine two moves, true if cosine of angle between them is <= this. */ | |
float AccelDotThresholdCombine; | |
/** Client saved moves will not combine if the result of GetMaxSpeed() differs by this much between moves. */ | |
float MaxSpeedThresholdCombine; | |
/** Clear saved move properties, so it can be re-used. */ | |
virtual void Clear(); | |
/** Called to set up this saved move (when initially created) to make a predictive correction. */ | |
virtual void SetMoveFor(AMMOCharacter* C, float InDeltaTime, FVector const& NewAccel, class FMMONetworkPredictionData_Client_Character& ClientData); | |
/** Set the properties describing the position, etc. of the moved pawn at the start of the move. */ | |
virtual void SetInitialPosition(AMMOCharacter* C); | |
/** Returns true if this move is an "important" move that should be sent again if not acked by the server */ | |
virtual bool IsImportantMove(const FMMOSavedMovePtr& LastAckedMove) const; | |
/** Returns starting position if we were to revert the move, either absolute StartLocation, or StartRelativeLocation offset from MovementBase's current location (since we want to try to move forward at this time). */ | |
virtual FVector GetRevertedLocation() const; | |
enum EPostUpdateMode | |
{ | |
PostUpdate_Record, // Record a move after having run the simulation | |
PostUpdate_Replay, // Update after replaying a move for a client correction | |
}; | |
/** Set the properties describing the final position, etc. of the moved pawn. */ | |
virtual void PostUpdate(AMMOCharacter* C, EPostUpdateMode PostUpdateMode); | |
/** Returns true if this move can be combined with NewMove for replication without changing any behavior */ | |
virtual bool CanCombineWith(const FMMOSavedMovePtr& NewMove, AMMOCharacter* InCharacter, float MaxDelta) const; | |
/** Combine this move with an older move and update relevant state. */ | |
virtual void CombineWith(const FMMOSavedMove_Character* OldMove, AMMOCharacter* InCharacter, APlayerController* PC, const FVector& OldStartLocation); | |
/** Called before ClientUpdatePosition uses this SavedMove to make a predictive correction */ | |
virtual void PrepMoveFor(AMMOCharacter* C); | |
/** Returns a byte containing encoded special movement information (jumping, crouching, etc.) */ | |
virtual uint8 GetCompressedFlags() const; | |
/** Compare current control rotation with stored starting data */ | |
virtual bool IsMatchingStartControlRotation(const APlayerController* PC) const; | |
/** Packs control rotation for network transport */ | |
virtual void GetPackedAngles(uint32& YawAndPitchPack, uint8& RollPack) const; | |
// Bit masks used by GetCompressedFlags() to encode movement information. | |
enum CompressedFlags | |
{ | |
FLAG_JumpPressed = 0x01, // Jump pressed | |
FLAG_WantsToCrouch = 0x02, // Wants to crouch | |
FLAG_Reserved_1 = 0x04, // Reserved for future use | |
FLAG_Reserved_2 = 0x08, // Reserved for future use | |
// Remaining bit masks are available for custom flags. | |
FLAG_Custom_0 = 0x10, | |
FLAG_Custom_1 = 0x20, | |
FLAG_Custom_2 = 0x40, | |
FLAG_Custom_3 = 0x80, | |
}; | |
}; | |
//UE_DEPRECATED_FORGAME(4.20) | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS | |
inline FMMOSavedMove_Character::FMMOSavedMove_Character(const FMMOSavedMove_Character&) = default; | |
inline FMMOSavedMove_Character::FMMOSavedMove_Character(FMMOSavedMove_Character&&) = default; | |
inline FMMOSavedMove_Character& FMMOSavedMove_Character::operator=(const FMMOSavedMove_Character&) = default; | |
inline FMMOSavedMove_Character& FMMOSavedMove_Character::operator=(FMMOSavedMove_Character&&) = default; | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS | |
// ClientAdjustPosition replication (event called at end of frame by server) | |
struct MMOEY_API FMMOClientAdjustment | |
{ | |
public: | |
FMMOClientAdjustment() | |
: TimeStamp(0.f) | |
, DeltaTime(0.f) | |
, NewLoc(ForceInitToZero) | |
, NewVel(ForceInitToZero) | |
, NewRot(ForceInitToZero) | |
, NewBase(NULL) | |
, NewBaseBoneName(NAME_None) | |
, bAckGoodMove(false) | |
, bBaseRelativePosition(false) | |
, MovementMode(0) | |
{ | |
} | |
float TimeStamp; | |
float DeltaTime; | |
FVector NewLoc; | |
FVector NewVel; | |
FRotator NewRot; | |
UPrimitiveComponent* NewBase; | |
FName NewBaseBoneName; | |
bool bAckGoodMove; | |
bool bBaseRelativePosition; | |
uint8 MovementMode; | |
}; | |
class FMMOCharacterReplaySample | |
{ | |
public: | |
FMMOCharacterReplaySample() : RemoteViewPitch(0), Time(0.0f) | |
{ | |
} | |
friend MMOEY_API FArchive& operator<<(FArchive& Ar, FMMOCharacterReplaySample& V); | |
FVector Location; | |
FRotator Rotation; | |
FVector Velocity; | |
FVector Acceleration; | |
uint8 RemoteViewPitch; | |
float Time; // This represents time since replay started | |
}; | |
class MMOEY_API FMMONetworkPredictionData_Client_Character : public FNetworkPredictionData_Client, protected FNoncopyable | |
{ | |
public: | |
FMMONetworkPredictionData_Client_Character(const UMMOPlayerMovement& ClientMovement); | |
virtual ~FMMONetworkPredictionData_Client_Character(); | |
/** Client timestamp of last time it sent a servermove() to the server. This is an increasing timestamp from the owning UWorld. Used for holding off on sending movement updates to save bandwidth. */ | |
float ClientUpdateTime; | |
/** Current TimeStamp for sending new Moves to the Server. This time resets to zero at a frequency of MinTimeBetweenTimeStampResets. */ | |
float CurrentTimeStamp; | |
/** Last World timestamp (undilated, real time) at which we received a server ack for a move. This could be either a good move or a correction from the server. */ | |
float LastReceivedAckRealTime; | |
TArray<FMMOSavedMovePtr> SavedMoves; // Buffered moves pending position updates, orderd oldest to newest. Moves that have been acked by the server are removed. | |
TArray<FMMOSavedMovePtr> FreeMoves; // freed moves, available for buffering | |
FMMOSavedMovePtr PendingMove; // PendingMove already processed on client - waiting to combine with next movement to reduce client to server bandwidth | |
FMMOSavedMovePtr LastAckedMove; // Last acknowledged sent move. | |
int32 MaxFreeMoveCount; // Limit on size of free list | |
int32 MaxSavedMoveCount; // Limit on the size of the saved move buffer | |
/** RootMotion saved while animation is updated, so we can store it and replay if needed in case of a position correction. */ | |
FRootMotionMovementParams RootMotionMovement; | |
uint32 bUpdatePosition : 1; // when true, update the position (via ClientUpdatePosition) | |
// Mesh smoothing variables (for network smoothing) | |
// | |
/** Whether to smoothly interpolate pawn position corrections on clients based on received location updates */ | |
UE_DEPRECATED(4.11, "bSmoothNetUpdates will be removed, use UMMOPlayerMovement::NetworkSmoothingMode instead.") | |
uint32 bSmoothNetUpdates : 1; | |
/** Used for position smoothing in net games */ | |
FVector OriginalMeshTranslationOffset; | |
/** World space offset of the mesh. Target value is zero offset. Used for position smoothing in net games. */ | |
FVector MeshTranslationOffset; | |
/** Used for rotation smoothing in net games (only used by linear smoothing). */ | |
FQuat OriginalMeshRotationOffset; | |
/** Component space offset of the mesh. Used for rotation smoothing in net games. */ | |
FQuat MeshRotationOffset; | |
/** Target for mesh rotation interpolation. */ | |
FQuat MeshRotationTarget; | |
/** Used for remembering how much time has passed between server corrections */ | |
float LastCorrectionDelta; | |
/** Used to track time of last correction */ | |
float LastCorrectionTime; | |
/** Max time delta between server updates over which client smoothing is allowed to interpolate. */ | |
float MaxClientSmoothingDeltaTime; | |
/** Used to track the timestamp of the last server move. */ | |
double SmoothingServerTimeStamp; | |
/** Used to track the client time as we try to match the server.*/ | |
double SmoothingClientTimeStamp; | |
/** Used to track how much time has elapsed since last correction. It can be computed as World->TimeSince(LastCorrectionTime). */ | |
UE_DEPRECATED(4.11, "CurrentSmoothTime will be removed, use LastCorrectionTime instead.") | |
float CurrentSmoothTime; | |
/** Used to signify that linear smoothing is desired */ | |
UE_DEPRECATED(4.11, "bUseLinearSmoothing will be removed, use UMMOPlayerMovement::NetworkSmoothingMode instead.") | |
bool bUseLinearSmoothing; | |
/** | |
* Copied value from UMMOPlayerMovement::NetworkMaxSmoothUpdateDistance. | |
* @see UMMOPlayerMovement::NetworkMaxSmoothUpdateDistance | |
*/ | |
float MaxSmoothNetUpdateDist; | |
/** | |
* Copied value from UMMOPlayerMovement::NetworkNoSmoothUpdateDistance. | |
* @see UMMOPlayerMovement::NetworkNoSmoothUpdateDistance | |
*/ | |
float NoSmoothNetUpdateDist; | |
/** How long to take to smoothly interpolate from the old pawn position on the client to the corrected one sent by the server. Must be >= 0. Not used for linear smoothing. */ | |
float SmoothNetUpdateTime; | |
/** How long to take to smoothly interpolate from the old pawn rotation on the client to the corrected one sent by the server. Must be >= 0. Not used for linear smoothing. */ | |
float SmoothNetUpdateRotationTime; | |
/** (DEPRECATED) How long server will wait for client move update before setting position */ | |
UE_DEPRECATED(4.12, "MaxResponseTime has been renamed to MaxMoveDeltaTime for clarity in what it does and will be removed, use MaxMoveDeltaTime instead.") | |
float MaxResponseTime; | |
/** | |
* Max delta time for a given move, in real seconds | |
* Based off of AGameNetworkManager::MaxMoveDeltaTime config setting, but can be modified per actor | |
* if needed. | |
* This value is mirrored in FNetworkPredictionData_Server, which is what server logic runs off of. | |
* Client needs to know this in order to not send move deltas that are going to get clamped anyway (meaning | |
* they'll be rejected/corrected). | |
* Note: This was previously named MaxResponseTime, but has been renamed to reflect what it does more accurately | |
*/ | |
float MaxMoveDeltaTime; | |
/** Values used for visualization and debugging of simulated net corrections */ | |
FVector LastSmoothLocation; | |
FVector LastServerLocation; | |
float SimulatedDebugDrawTime; | |
/** Array of replay samples that we use to interpolate between to get smooth location/rotation/velocity/ect */ | |
TArray< FMMOCharacterReplaySample > ReplaySamples; | |
/** Finds SavedMove index for given TimeStamp. Returns INDEX_NONE if not found (move has been already Acked or cleared). */ | |
int32 GetSavedMoveIndex(float TimeStamp) const; | |
/** Ack a given move. This move will become LastAckedMove, SavedMoves will be adjusted to only contain unAcked moves. */ | |
void AckMove(int32 AckedMoveIndex, UMMOPlayerMovement& CharacterMovementComponent); | |
/** Allocate a new saved move. Subclasses should override this if they want to use a custom move class. */ | |
virtual FMMOSavedMovePtr AllocateNewMove(); | |
/** Return a move to the free move pool. Assumes that 'Move' will no longer be referenced by anything but possibly the FreeMoves list. Clears PendingMove if 'Move' is PendingMove. */ | |
virtual void FreeMove(const FMMOSavedMovePtr& Move); | |
/** Tries to pull a pooled move off the free move list, otherwise allocates a new move. Returns NULL if the limit on saves moves is hit. */ | |
virtual FMMOSavedMovePtr CreateSavedMove(); | |
/** Update CurentTimeStamp from passed in DeltaTime. | |
It will measure the accuracy between passed in DeltaTime and how Server will calculate its DeltaTime. | |
If inaccuracy is too high, it will reset CurrentTimeStamp to maintain a high level of accuracy. | |
@return DeltaTime to use for Client's physics simulation prior to replicate move to server. */ | |
float UpdateTimeStampAndDeltaTime(float DeltaTime, AMMOCharacter& CharacterOwner, class UMMOPlayerMovement& CharacterMovementComponent); | |
/** Used for simulated packet loss in development builds. */ | |
float DebugForcedPacketLossTimerStart; | |
}; | |
class MMOEY_API FMMONetworkPredictionData_Server_Character : public FNetworkPredictionData_Server, protected FNoncopyable | |
{ | |
public: | |
FMMONetworkPredictionData_Server_Character(const UMMOPlayerMovement& ServerMovement); | |
virtual ~FMMONetworkPredictionData_Server_Character(); | |
FMMOClientAdjustment PendingAdjustment; | |
/** Timestamp from the client of most recent ServerMove() processed for this player. Reset occasionally for timestamp resets (to maintain accuracy). */ | |
float CurrentClientTimeStamp; | |
/** Timestamp of total elapsed client time. Similar to CurrentClientTimestamp but this is accumulated with the calculated DeltaTime for each move on the server. */ | |
double ServerAccumulatedClientTimeStamp; | |
/** Last time server updated client with a move correction */ | |
float LastUpdateTime; | |
/** Server clock time when last server move was received from client (does NOT include forced moves on server) */ | |
float ServerTimeStampLastServerMove; | |
/** (DEPRECATED) How long server will wait for client move update before setting position */ | |
UE_DEPRECATED(4.12, "MaxResponseTime has been renamed to MaxMoveDeltaTime for clarity in what it does and will be removed, use MaxMoveDeltaTime instead.") | |
float MaxResponseTime; | |
/** | |
* Max delta time for a given move, in real seconds | |
* Based off of AGameNetworkManager::MaxMoveDeltaTime config setting, but can be modified per actor | |
* if needed. | |
* Note: This was previously named MaxResponseTime, but has been renamed to reflect what it does more accurately | |
*/ | |
float MaxMoveDeltaTime; | |
/** Force client update on the next ServerMoveHandleClientError() call. */ | |
uint32 bForceClientUpdate : 1; | |
/** Accumulated timestamp difference between autonomous client and server for tracking long-term trends */ | |
float LifetimeRawTimeDiscrepancy; | |
/** | |
* Current time discrepancy between client-reported moves and time passed | |
* on the server. Time discrepancy resolution's goal is to keep this near zero. | |
*/ | |
float TimeDiscrepancy; | |
/** True if currently in the process of resolving time discrepancy */ | |
bool bResolvingTimeDiscrepancy; | |
/** | |
* When bResolvingTimeDiscrepancy is true, we are in time discrepancy resolution mode whose output is | |
* this value (to be used as the DeltaTime for current ServerMove) | |
*/ | |
float TimeDiscrepancyResolutionMoveDeltaOverride; | |
/** | |
* When bResolvingTimeDiscrepancy is true, we are in time discrepancy resolution mode where we bound | |
* move deltas by Server Deltas. In cases where there are multiple ServerMove RPCs handled within one | |
* server frame tick, we need to accumulate the client deltas of the "no tick" Moves so that the next | |
* Move processed that the server server has ticked for takes into account those previous deltas. | |
* If we did not use this, the higher the framerate of a client vs the server results in more | |
* punishment/payback time. | |
*/ | |
float TimeDiscrepancyAccumulatedClientDeltasSinceLastServerTick; | |
/** Creation time of this prediction data, used to contextualize LifetimeRawTimeDiscrepancy */ | |
float WorldCreationTime; | |
/** Returns time delta to use for the current ServerMove(). Takes into account time discrepancy resolution if active. */ | |
float GetServerMoveDeltaTime(float ClientTimeStamp, float ActorTimeDilation) const; | |
/** Returns base time delta to use for a ServerMove, default calculation (no time discrepancy resolution) */ | |
float GetBaseServerMoveDeltaTime(float ClientTimeStamp, float ActorTimeDilation) const; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment