Fix sliding issue when standing on "vertical" walls

This commit is contained in:
Kevin Poretti 2023-01-07 17:03:42 -05:00
parent 2bda0ce351
commit cf85b7f916
4 changed files with 639 additions and 3 deletions

Binary file not shown.

BIN
GravityStomp/Content/Maps/Debug/Test.umap (Stored with Git LFS)

Binary file not shown.

View File

@ -22,6 +22,13 @@ namespace CharacterMovementCVars
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 bool bAddFormerBaseVelocityToRootMotionOverrideWhenFalling = true;
FAutoConsoleVariableRef CVarAddFormerBaseVelocityToRootMotionOverrideWhenFalling(
TEXT("gs.AddFormerBaseVelocityToRootMotionOverrideWhenFalling"),
bAddFormerBaseVelocityToRootMotionOverrideWhenFalling,
TEXT("To avoid sudden velocity changes when a root motion source moves the pawn from a moving base to free fall, this CVar will enable the FormerBaseVelocityDecayHalfLife property on CharacterMovementComponent."),
ECVF_Default);
}
@ -631,9 +638,619 @@ bool UGSCharacterMovementComponent::FloorSweepTest(FHitResult& OutHit, const FVe
return bBlockingHit;
}
bool UGSCharacterMovementComponent::IsCharacterUpAlignedToWorldUp() const
{
return FMath::Abs(CharacterUpDirection.Dot(FVector::UpVector)) > UE_KINDA_SMALL_NUMBER;
}
void UGSCharacterMovementComponent::SetCharacterUpDirection(FVector NewUpDirection)
{
NewUpDirection.Normalize();
CharacterUpDirection = NewUpDirection;
}
void UGSCharacterMovementComponent::OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode)
{
if (!HasValidData())
{
return;
}
// Update collision settings if needed
if (MovementMode == MOVE_NavWalking)
{
// Reset cached nav location used by NavWalking
CachedNavLocation = FNavLocation();
SetGroundMovementMode(MovementMode);
// Walking is either only X or Z depending on the character's up direction
if(IsCharacterUpAlignedToWorldUp())
{
Velocity.Z = 0.f;
}
else
{
Velocity.Y = 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 is either only X or Z depending on the character's up direction
if(IsCharacterUpAlignedToWorldUp())
{
Velocity.Z = 0.f;
}
else
{
Velocity.Y = 0.f;
}
bCrouchMaintainsBaseLocation = true;
SetGroundMovementMode(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)
{
DecayingFormerBaseVelocity = GetImpartedMovementBaseVelocity();
Velocity += DecayingFormerBaseVelocity;
if (bMovementInProgress && CurrentRootMotion.HasAdditiveVelocity())
{
// If we leave a base during movement and we have additive root motion, we need to add the imparted velocity so that it retains it next tick
CurrentRootMotion.LastPreAdditiveVelocity += DecayingFormerBaseVelocity;
}
if (!CharacterMovementCVars::bAddFormerBaseVelocityToRootMotionOverrideWhenFalling || FormerBaseVelocityDecayHalfLife == 0.f)
{
DecayingFormerBaseVelocity = FVector::ZeroVector;
}
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(GetGroundMovementMode() == MOVE_Walking || GetGroundMovementMode() == MOVE_NavWalking, TEXT("Invalid GroundMovementMode %d. MovementMode: %d, PreviousMovementMode: %d"), GetGroundMovementMode(), MovementMode.GetValue(), PreviousMovementMode);
}
void UGSCharacterMovementComponent::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;
}
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 FFindFloorResult OldFloor = CurrentFloor;
RestorePreAdditiveRootMotionVelocity();
// Ensure velocity is horizontal.
MaintainHorizontalGroundVelocity();
const FVector OldVelocity = Velocity;
if(IsCharacterUpAlignedToWorldUp())
{
Acceleration.Z = 0.f;
}
else
{
Acceleration.Y = 0.f;
}
// Apply acceleration
if( !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() )
{
CalcVelocity(timeTick, GroundFriction, false, GetMaxBrakingDeceleration());
}
ApplyRootMotionToVelocity(timeTick);
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;
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 + CharacterUpDirection * 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();
}
}
void UGSCharacterMovementComponent::MoveAlongFloor(const FVector& InVelocity, float DeltaSeconds,
FStepDownResult* OutStepDownResult)
{
if (!CurrentFloor.IsWalkableFloor())
{
return;
}
// Move along the current floor
FVector MovementAxis = CharacterUpDirection.Cross(FVector::ForwardVector).GetAbs();
FString DebugAxisMsg = FString::Printf(TEXT("Movement Axis: %s"), *MovementAxis.ToString());
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Cyan, DebugAxisMsg);
const FVector Delta = (MovementAxis * InVelocity) * DeltaSeconds;
FString DebugVelMsg = FString::Printf(TEXT("Move Along Floor Velocity: %s"), *(MovementAxis * InVelocity).ToString());
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Cyan, DebugVelMsg);
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) && (IsCharacterUpAlignedToWorldUp() ? Hit.Normal.Z: Hit.Normal.Y > UE_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() != nullptr && Hit.HitObjectHandle == CharacterOwner->GetMovementBase()->GetOwner()))
{
// hit a barrier, try to step up
const FVector PreStepUpLocation = UpdatedComponent->GetComponentLocation();
const FVector GravDir = -CharacterUpDirection;
if (!StepUp(GravDir, Delta * (1.f - PercentTimeApplied), Hit, OutStepDownResult))
{
//UE_LOG(LogCharacterMovement, 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
{
//UE_LOG(LogCharacterMovement, Verbose, TEXT("+ StepUp (ImpactNormal %s, Normal %s"), *Hit.ImpactNormal.ToString(), *Hit.Normal.ToString());
if (!bMaintainHorizontalGroundVelocity)
{
// Don't recalculate velocity based on this height adjustment, if considering vertical adjustments. Only consider horizontal movement.
bJustTeleported = true;
const float StepUpTimeSlice = (1.f - PercentTimeApplied) * DeltaSeconds;
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && StepUpTimeSlice >= UE_KINDA_SMALL_NUMBER)
{
Velocity = (UpdatedComponent->GetComponentLocation() - PreStepUpLocation) / StepUpTimeSlice;
if(IsCharacterUpAlignedToWorldUp())
{
Velocity.Z = 0;
}
else
{
Velocity.Y = 0;
}
}
}
}
}
else if ( Hit.Component.IsValid() && !Hit.Component.Get()->CanCharacterStepUp(CharacterOwner) )
{
HandleImpact(Hit, LastMoveTimeSlice, RampVector);
SlideAlongSurface(Delta, 1.f - PercentTimeApplied, Hit.Normal, Hit, true);
}
}
}
}
FVector UGSCharacterMovementComponent::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 - UE_KINDA_SMALL_NUMBER) && FloorNormal.Z > UE_KINDA_SMALL_NUMBER && ContactNormal.Z > UE_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 UGSCharacterMovementComponent::MaintainHorizontalGroundVelocity()
{
if (IsCharacterUpAlignedToWorldUp())
{
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();
}
}
}
else
{
if(Velocity.Y != 0.f)
{
if (bMaintainHorizontalGroundVelocity)
{
// Ramp movement already maintained the velocity, so we just want to remove the vertical component.
Velocity.Y = 0.f;
}
else
{
FVector NewVelocity = Velocity.GetSafeNormal();
Velocity = FVector(0.0f, NewVelocity.Y, NewVelocity.Z) * Velocity.Size();
}
}
}
}
float UGSCharacterMovementComponent::SlideAlongSurface(const FVector& Delta, float Time, const FVector& InNormal,
FHitResult& Hit, bool bHandleImpact)
{
if (!Hit.bBlockingHit)
{
return 0.f;
}
bool IsUp = IsCharacterUpAlignedToWorldUp();
FVector Normal(InNormal);
float ZComp = Normal.Dot(CharacterUpDirection);
if (IsMovingOnGround())
{
// We don't want to be pushed up an unwalkable surface.
if (ZComp > 0.f)
{
if (!IsWalkable(Hit))
{
FVector NewNormal = Normal.GetSafeNormal();
Normal = IsUp ? FVector(NewNormal.X, NewNormal.Y, 0.0f) : FVector(0.0f, NewNormal.Y, NewNormal.Z);
}
}
else if (ZComp < -UE_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;
float FloorZComp = FloorNormal.Dot(CharacterUpDirection);
const bool bFloorOpposedToMovement = (Delta | FloorNormal) < 0.f && (FloorZComp < 1.f - UE_DELTA);
if (bFloorOpposedToMovement)
{
Normal = FloorNormal;
}
FVector NewNormal = Normal.GetSafeNormal();
Normal = IsUp ? FVector(NewNormal.X, NewNormal.Y, 0.0f) : FVector(0.0f, NewNormal.Y, NewNormal.Z);
}
}
}
return Super::SlideAlongSurface(Delta, Time, Normal, Hit, bHandleImpact);
}
void UGSCharacterMovementComponent::AdjustFloorHeight()
{
bool IsUp = IsCharacterUpAlignedToWorldUp();
// 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)
{
// This would cause us to scale unwalkable walls
if (OldFloorDist < MIN_FLOOR_DIST && CurrentFloor.LineDist >= MIN_FLOOR_DIST)
{
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 = IsUp ? UpdatedComponent->GetComponentLocation().Z : UpdatedComponent->GetComponentLocation().Y;
const float AvgFloorDist = (MIN_FLOOR_DIST + MAX_FLOOR_DIST) * 0.5f;
const float MoveDist = AvgFloorDist - OldFloorDist;
SafeMoveUpdatedComponent(CharacterUpDirection * MoveDist, UpdatedComponent->GetComponentQuat(), true, AdjustHit );
if (!AdjustHit.IsValidBlockingHit())
{
CurrentFloor.FloorDist += MoveDist;
}
else if (MoveDist > 0.f)
{
const float CurrentZ = IsUp ? UpdatedComponent->GetComponentLocation().Z : UpdatedComponent->GetComponentLocation().Y;
CurrentFloor.FloorDist += CurrentZ - InitialZ;
}
else
{
checkSlow(MoveDist < 0.f);
const float CurrentZ = IsUp ? UpdatedComponent->GetComponentLocation().Z : UpdatedComponent->GetComponentLocation().Y;
CurrentFloor.FloorDist = CurrentZ - IsUp ? AdjustHit.Location.Z : AdjustHit.Location.Y;
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.
if (CharacterOwner && CharacterOwner->GetLocalRole() != ROLE_SimulatedProxy)
{
bForceNextFloorCheck = true;
}
}
}

View File

@ -23,6 +23,23 @@ public:
void SetCharacterUpDirection(FVector NewUpDirection);
// UCharacterMovementComponent interface
virtual void OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode) override;
// walking
virtual void PhysWalking(float deltaTime, int32 Iterations) override;
virtual void MoveAlongFloor(const FVector& InVelocity, float DeltaSeconds, FStepDownResult* OutStepDownResult) override;
virtual FVector ComputeGroundMovementDelta(const FVector& Delta, const FHitResult& RampHit, const bool bHitFromLineTrace) const override;
virtual void MaintainHorizontalGroundVelocity() override;
virtual float SlideAlongSurface(const FVector& Delta, float Time, const FVector& InNormal, FHitResult& Hit, bool bHandleImpact) override;
virtual void AdjustFloorHeight() override;
// falling
virtual void PhysFalling(float deltaTime, int32 Iterations) override;
virtual void ComputeFloorDist(const FVector& CapsuleLocation, float LineDistance, float SweepDistance, FFindFloorResult& OutFloorResult, float SweepRadius, const FHitResult* DownwardSweepResult) const override;
@ -36,4 +53,6 @@ public:
private:
FVector CharacterUpDirection = FVector::UpVector;
FORCEINLINE bool IsCharacterUpAlignedToWorldUp() const;
};