Pawn/Pawn_Unreal/Source/Pawn/Private/Characters/PwnCharacterMovementComponent.cpp

690 lines
28 KiB
C++

#include "Characters/PwnCharacterMovementComponent.h"
#include "Characters/PwnCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "Components/SplineComponent.h"
#include "GameplayModes/PwnGameplayModeSubsystem.h"
#include "GameplayModes/Combat/PwnCombatPlatformerPath.h"
#include "Utils/EngineUtils.h"
constexpr float LineTraceDistance = 10000.0f;
UPwnCharacterMovementComponent::UPwnCharacterMovementComponent() {
PrimaryComponentTick.bCanEverTick = true;
}
void UPwnCharacterMovementComponent::BeginPlay() {
Super::BeginPlay();
}
float UPwnCharacterMovementComponent::GetMaxBrakingDeceleration() const {
if (MovementMode == MOVE_Custom) {
switch (CustomMovementMode) {
case SplineWalking: return BrakingDecelerationWalking;
case SplineFalling: return BrakingDecelerationFalling;
default: break;
}
}
return Super::GetMaxBrakingDeceleration();
}
float UPwnCharacterMovementComponent::GetMaxSpeed() const {
if (MovementMode == MOVE_Custom) {
switch (CustomMovementMode) {
case SplineWalking: return IsCrouching() ? MaxWalkSpeedCrouched : MaxWalkSpeed;;
case SplineFalling: return MaxWalkSpeed;
default: break;
}
}
return Super::GetMaxSpeed();
}
bool UPwnCharacterMovementComponent::IsMovingOnGround() const {
return Super::IsMovingOnGround() || IsCustomMovementMode(SplineWalking);
}
bool UPwnCharacterMovementComponent::IsFalling() const {
return Super::IsFalling() || IsCustomMovementMode(SplineFalling);
}
void UPwnCharacterMovementComponent::OnMovementModeChanged(const EMovementMode PreviousMovementMode, const uint8 PreviousCustomMode) {
Super::OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode);
// React to changes in the movement mode
if (MovementMode == MOVE_Custom && CustomMovementMode == SplineWalking) {
// Make sure we update our new floor/base on initial entry of the walking physics
FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, false);
AdjustFloorHeight();
SetBaseFromFloor(CurrentFloor);
}
}
void UPwnCharacterMovementComponent::UpdateCharacterStateBeforeMovement(const float DeltaSeconds) {
if (MovementMode == MOVE_Falling && IsFollowingSpline) {
SetMovementMode(MOVE_Custom, SplineFalling);
} else if (MovementMode == MOVE_Walking && IsFollowingSpline) {
SetMovementMode(MOVE_Custom, SplineWalking);
}
Super::UpdateCharacterStateBeforeMovement(DeltaSeconds);
}
void UPwnCharacterMovementComponent::PhysCustom(const float DeltaTime, const int32 Iterations) {
Super::PhysCustom(DeltaTime, Iterations);
switch (CustomMovementMode) {
case SplineWalking: PhysSplineWalking(DeltaTime, Iterations);
break;
case SplineFalling: PhysSplineFalling(DeltaTime, Iterations);
break;
default:
UE_LOG(LogTemp, Error, TEXT("Invalid custom movement mode for PhysCustom call."));
}
}
void UPwnCharacterMovementComponent::EnterSplineFollowMode() {
IsFollowingSpline = true;
SetMovementMode(MOVE_Custom, SplineWalking);
UpdateCurrentCombatPath(true);
}
void UPwnCharacterMovementComponent::ExitSplineFollowMode() {
IsFollowingSpline = false;
CombatPath = nullptr;
SetMovementMode(MOVE_Walking);
}
bool UPwnCharacterMovementComponent::IsCustomMovementMode(const ECustomMovementMode Mode) const {
return MovementMode == MOVE_Custom && CustomMovementMode == Mode;
}
bool UPwnCharacterMovementComponent::LineTraceToGround(FHitResult& OutHit, const FVector& StartLocation) const {
const FVector EndLocation = StartLocation + FVector(0.0f, 0.0f, -LineTraceDistance);
return GetWorld()->LineTraceSingleByChannel(OutHit, StartLocation, EndLocation, ECC_Visibility, IGNORE_OWNER_PARAMS);
}
void UPwnCharacterMovementComponent::UpdateCurrentCombatPath(const bool UpdateLocation) {
APwnCombatPlatformerPath* OutCombatPath;
FHitResult Hit;
if (LineTraceToGround(Hit, UpdatedComponent->GetComponentLocation())
&& UPwnGameplayModeSubsystem::Get(this).FindClosestCombatPathLocation(Hit.ImpactPoint, OutCombatPath)
&& OutCombatPath != CombatPath) {
CombatPath = OutCombatPath;
DotDirection = CombatPath->Reversed ? -1.0f : 1.0f;
const float ClosestInputKey = CombatPath->Spline->FindInputKeyClosestToWorldLocation(UpdatedComponent->GetComponentLocation());
const FVector ClosestLocation = CombatPath->Spline->GetLocationAtSplineInputKey(ClosestInputKey, ESplineCoordinateSpace::World);
if (LineTraceToGround(Hit, ClosestLocation)) {
const float SplineKey = CombatPath->FlattenedSpline->FindInputKeyClosestToWorldLocation(Hit.ImpactPoint);
DistanceAlongSpline = CombatPath->FlattenedSpline->GetDistanceAlongSplineAtSplineInputKey(SplineKey);
if (UpdateLocation) {
FVector NewLocation = Hit.ImpactPoint;
NewLocation.Z += CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
UpdatedComponent->SetWorldLocation(NewLocation);
}
}
}
}
void UPwnCharacterMovementComponent::UpdateTangentAndAcceleration() {
Tangent2D = CombatPath->FlattenedSpline->GetTangentAtDistanceAlongSpline(DistanceAlongSpline, ESplineCoordinateSpace::World).GetSafeNormal2D();
// Recalculate acceleration so the input is relative to the spline
Acceleration = Tangent2D * Acceleration.Size2D() * FMath::Sign(Acceleration.X) * DotDirection;
}
void UPwnCharacterMovementComponent::UpdatePawnVelocity(const float TimeTick) {
const FVector OldVelocity = Velocity;
const FVector OldLocation = UpdatedComponent->GetComponentLocation();
const FVector VelocityNormal2D = Velocity.GetSafeNormal2D();
const float VelocitySize2D = Velocity.Size2D();
float VelocityDirection = Tangent2D.Dot(VelocityNormal2D);
if (VelocityNormal2D.IsNearlyZero()) {
VelocityDirection = 0;
}
VelocityDirection = FMath::Sign(VelocityDirection); // -1, 0 or 1
DistanceAlongSpline = DistanceAlongSpline + VelocityDirection * VelocitySize2D * TimeTick;
DistanceAlongSpline = FMath::Clamp(DistanceAlongSpline, 0.0f, CombatPath->FlattenedSpline->GetSplineLength());
FVector TargetLocation = CombatPath->FlattenedSpline->GetLocationAtDistanceAlongSpline(DistanceAlongSpline, ESplineCoordinateSpace::World);
TargetLocation.Z = OldLocation.Z;
const FVector Direction = (TargetLocation - OldLocation).GetSafeNormal2D();
Velocity = VelocitySize2D * Direction;
Velocity.Z = OldVelocity.Z;
}
void UPwnCharacterMovementComponent::UpdateDistanceAlongSpline() {
FHitResult Hit;
if (LineTraceToGround(Hit, UpdatedComponent->GetComponentLocation())) {
const FVector ImpactPoint = Hit.ImpactPoint;
const float InputKey = CombatPath->FlattenedSpline->FindInputKeyClosestToWorldLocation(ImpactPoint);
DistanceAlongSpline = CombatPath->FlattenedSpline->GetDistanceAlongSplineAtSplineInputKey(InputKey);
}
}
void UPwnCharacterMovementComponent::UpdateLocationOnFlattenedSpline() const {
// Maintain coherent distance along spline with location
FVector TargetLocation = CombatPath->FlattenedSpline->GetLocationAtDistanceAlongSpline(DistanceAlongSpline, ESplineCoordinateSpace::World);
TargetLocation.Z = UpdatedComponent->GetComponentLocation().Z;
UpdatedComponent->SetWorldLocation(TargetLocation);
}
void UPwnCharacterMovementComponent::PhysSplineWalking(const float DeltaTime, int32 Iterations) {
if (DeltaTime < MIN_TICK_TIME) {
return;
}
UpdateCurrentCombatPath(); /* -- PAWN MODIFICATIONS -- */
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;
}
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 != nullptr) ? OldBase->GetComponentLocation() : FVector::ZeroVector;
const FVector OldLocation = UpdatedComponent->GetComponentLocation();
const FFindFloorResult OldFloor = CurrentFloor;
UpdateTangentAndAcceleration(); /* -- PAWN MODIFICATIONS -- */
Acceleration.Z = 0.f;
RestorePreAdditiveRootMotionVelocity();
// Ensure velocity is horizontal.
MaintainHorizontalGroundVelocity();
const FVector OldVelocity = Velocity;
// Apply acceleration
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity()) {
CalcVelocity(TimeTick, GroundFriction, false, GetMaxBrakingDeceleration());
}
ApplyRootMotionToVelocity(TimeTick);
UpdatePawnVelocity(TimeTick); /* -- PAWN MODIFICATIONS -- */
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 > UE_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;
UpdatePawnVelocity(TimeTick); /* -- PAWN MODIFICATIONS -- */
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;
MaintainHorizontalGroundVelocity();
}
}
// 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();
}
/* -- PAWN MODIFICATIONS -- */
UpdateDistanceAlongSpline();
UpdateLocationOnFlattenedSpline();
}
void UPwnCharacterMovementComponent::PhysSplineFalling(const float DeltaTime, int32 Iterations) {
if (DeltaTime < MIN_TICK_TIME) {
return;
}
UpdateCurrentCombatPath(); /* -- PAWN MODIFICATIONS -- */
UpdateTangentAndAcceleration(); /* -- PAWN MODIFICATIONS -- */
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;
const FVector OldVelocityWithRootMotion = Velocity;
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);
//UE_LOG(LogCharacterMovement, Log, TEXT("dt=(%.6f) OldLocation=(%s) OldVelocity=(%s) OldVelocityWithRootMotion=(%s) NewVelocity=(%s)"), timeTick, *(UpdatedComponent->GetComponentLocation()).ToString(), *OldVelocity.ToString(), *OldVelocityWithRootMotion.ToString(), *Velocity.ToString());
ApplyRootMotionToVelocity(TimeTick);
DecayFormerBaseVelocity(TimeTick);
int32 ForceJumpPeakSubstep = 1; /* -- PAWN MODIFICATIONS -- */
// 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 (ForceJumpPeakSubstep && OldVelocityWithRootMotion.Z > 0.f && Velocity.Z <= 0.f && NumJumpApexAttempts <
MaxJumpApexAttemptsPerSimulation) {
const FVector DerivedAccel = (Velocity - OldVelocityWithRootMotion) / TimeTick;
if (!FMath::IsNearlyZero(DerivedAccel.Z)) {
const float TimeToApex = -OldVelocityWithRootMotion.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 = OldVelocityWithRootMotion + (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.
const float TimeToRefund = (TimeTick - TimeToApex);
RemainingTime += TimeToRefund;
TimeTick = TimeToApex;
Iterations--;
NumJumpApexAttempts++;
// Refund time to any active Root Motion Sources as well
for (TSharedPtr<FRootMotionSource> RootMotionSource : CurrentRootMotion.RootMotionSources) {
const float RewoundRMSTime = FMath::Max(0.0f, RootMotionSource->GetTime() - TimeToRefund);
RootMotionSource->SetTime(RewoundRMSTime);
}
}
}
}
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 * (OldVelocityWithRootMotion + 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 = (OldVelocityWithRootMotion * NonGravityTime) + (0.5f * (OldVelocityWithRootMotion + Velocity) * GravityTime);
}
UpdatePawnVelocity(TimeTick); /* -- PAWN MODIFICATIONS -- */
// 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();
FFindFloorResult 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);
UpdatePawnVelocity(TimeTick); /* -- PAWN MODIFICATIONS -- */
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)
const UPrimitiveComponent* HitComponent = Hit.GetComponent();
int32 UseTargetVelocityOnImpact = 1; /* -- PAWN MODIFICATIONS -- */
if (UseTargetVelocityOnImpact && !Velocity.IsNearlyZero() && MovementBaseUtility::IsSimulatedBase(HitComponent)) {
const FVector ContactVelocity = MovementBaseUtility::GetMovementBaseVelocity(HitComponent, NAME_None) +
MovementBaseUtility::GetMovementBaseTangentialVelocity(HitComponent, NAME_None, Hit.ImpactPoint);
const FVector NewVelocity = Velocity - Hit.ImpactNormal * FVector::DotProduct(Velocity - ContactVelocity, Hit.ImpactNormal);
Velocity = HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocityWithIgnoreZAccumulate()
? FVector(Velocity.X, Velocity.Y, NewVelocity.Z)
: NewVelocity;
} else if (SubTimeTickRemaining > UE_KINDA_SMALL_NUMBER && !bJustTeleported) {
const FVector NewVelocity = (Delta / SubTimeTickRemaining);
Velocity = HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocityWithIgnoreZAccumulate()
? FVector(Velocity.X, Velocity.Y, NewVelocity.Z)
: NewVelocity;
}
if (SubTimeTickRemaining > UE_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.
const float VERTICAL_SLOPE_NORMAL_Z = 0.001f; /* -- PAWN MODIFICATIONS -- */
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 > UE_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 it can stand on
bool bDitch = ((OldHitImpactNormal.Z > 0.f) && (Hit.ImpactNormal.Z > 0.f) && (FMath::Abs(Delta.Z) <= UE_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 >= GetWalkableFloorZ()) {
// 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() <= UE_KINDA_SMALL_NUMBER * 10.f) {
Velocity.X = 0.f;
Velocity.Y = 0.f;
}
}
/* -- PAWN MODIFICATIONS -- */
UpdateDistanceAlongSpline();
UpdateLocationOnFlattenedSpline();
}