#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 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 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 RestoreAcceleration(Acceleration, FVector::ZeroVector); TGuardValue 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(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(); }