// Copyright Epic Games, Inc. All Rights Reserved. #include "LyraCameraMode_ThirdPerson.h" #include "Curves/CurveVector.h" #include "Engine/Canvas.h" #include "GameFramework/CameraBlockingVolume.h" #include "LyraCameraAssistInterface.h" #include "GameFramework/Controller.h" #include "GameFramework/Character.h" namespace LyraCameraMode_ThirdPerson_Statics { static const FName NAME_IgnoreCameraCollision = TEXT("IgnoreCameraCollision"); } ULyraCameraMode_ThirdPerson::ULyraCameraMode_ThirdPerson() { TargetOffsetCurve = nullptr; PenetrationAvoidanceFeelers.Add(FLyraPenetrationAvoidanceFeeler(FRotator(+00.0f, +00.0f, 0.0f), 1.00f, 1.00f, 14.f, 0)); PenetrationAvoidanceFeelers.Add(FLyraPenetrationAvoidanceFeeler(FRotator(+00.0f, +16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3)); PenetrationAvoidanceFeelers.Add(FLyraPenetrationAvoidanceFeeler(FRotator(+00.0f, -16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3)); PenetrationAvoidanceFeelers.Add(FLyraPenetrationAvoidanceFeeler(FRotator(+00.0f, +32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5)); PenetrationAvoidanceFeelers.Add(FLyraPenetrationAvoidanceFeeler(FRotator(+00.0f, -32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5)); PenetrationAvoidanceFeelers.Add(FLyraPenetrationAvoidanceFeeler(FRotator(+20.0f, +00.0f, 0.0f), 1.00f, 1.00f, 00.f, 4)); PenetrationAvoidanceFeelers.Add(FLyraPenetrationAvoidanceFeeler(FRotator(-20.0f, +00.0f, 0.0f), 0.50f, 0.50f, 00.f, 4)); } void ULyraCameraMode_ThirdPerson::UpdateView(float DeltaTime) { UpdateForTarget(DeltaTime); UpdateCrouchOffset(DeltaTime); FVector PivotLocation = GetPivotLocation() + CurrentCrouchOffset; FRotator PivotRotation = GetPivotRotation(); PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax); View.Location = PivotLocation; View.Rotation = PivotRotation; View.ControlRotation = View.Rotation; View.FieldOfView = FieldOfView; // Apply third person offset using pitch. if (!bUseRuntimeFloatCurves) { if (TargetOffsetCurve) { const FVector TargetOffset = TargetOffsetCurve->GetVectorValue(PivotRotation.Pitch); View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset); } } else { FVector TargetOffset(0.0f); TargetOffset.X = TargetOffsetX.GetRichCurveConst()->Eval(PivotRotation.Pitch); TargetOffset.Y = TargetOffsetY.GetRichCurveConst()->Eval(PivotRotation.Pitch); TargetOffset.Z = TargetOffsetZ.GetRichCurveConst()->Eval(PivotRotation.Pitch); View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset); } // Adjust final desired camera location to prevent any penetration UpdatePreventPenetration(DeltaTime); } void ULyraCameraMode_ThirdPerson::UpdateForTarget(float DeltaTime) { if (const ACharacter* TargetCharacter = Cast(GetTargetActor())) { if (TargetCharacter->bIsCrouched) { const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject(); const float CrouchedHeightAdjustment = TargetCharacterCDO->CrouchedEyeHeight - TargetCharacterCDO->BaseEyeHeight; SetTargetCrouchOffset(FVector(0.f, 0.f, CrouchedHeightAdjustment)); return; } } SetTargetCrouchOffset(FVector::ZeroVector); } void ULyraCameraMode_ThirdPerson::DrawDebug(UCanvas* Canvas) const { Super::DrawDebug(Canvas); #if ENABLE_DRAW_DEBUG FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager; for (int i = 0; i < DebugActorsHitDuringCameraPenetration.Num(); i++) { DisplayDebugManager.DrawString( FString::Printf(TEXT("HitActorDuringPenetration[%d]: %s") , i , *DebugActorsHitDuringCameraPenetration[i]->GetName())); } LastDrawDebugTime = GetWorld()->GetTimeSeconds(); #endif } void ULyraCameraMode_ThirdPerson::UpdatePreventPenetration(float DeltaTime) { if (!bPreventPenetration) { return; } AActor* TargetActor = GetTargetActor(); APawn* TargetPawn = Cast(TargetActor); AController* TargetController = TargetPawn ? TargetPawn->GetController() : nullptr; ILyraCameraAssistInterface* TargetControllerAssist = Cast(TargetController); ILyraCameraAssistInterface* TargetActorAssist = Cast(TargetActor); TOptional OptionalPPTarget = TargetActorAssist ? TargetActorAssist->GetCameraPreventPenetrationTarget() : TOptional(); AActor* PPActor = OptionalPPTarget.IsSet() ? OptionalPPTarget.GetValue() : TargetActor; ILyraCameraAssistInterface* PPActorAssist = OptionalPPTarget.IsSet() ? Cast(PPActor) : nullptr; const UPrimitiveComponent* PPActorRootComponent = Cast(PPActor->GetRootComponent()); if (PPActorRootComponent) { // Attempt at picking SafeLocation automatically, so we reduce camera translation when aiming. // Our camera is our reticle, so we want to preserve our aim and keep that as steady and smooth as possible. // Pick closest point on capsule to our aim line. FVector ClosestPointOnLineToCapsuleCenter; FVector SafeLocation = PPActor->GetActorLocation(); FMath::PointDistToLine(SafeLocation, View.Rotation.Vector(), View.Location, ClosestPointOnLineToCapsuleCenter); // Adjust Safe distance height to be same as aim line, but within capsule. float const PushInDistance = PenetrationAvoidanceFeelers[0].Extent + CollisionPushOutDistance; float const MaxHalfHeight = PPActor->GetSimpleCollisionHalfHeight() - PushInDistance; SafeLocation.Z = FMath::Clamp(ClosestPointOnLineToCapsuleCenter.Z, SafeLocation.Z - MaxHalfHeight, SafeLocation.Z + MaxHalfHeight); float DistanceSqr; PPActorRootComponent->GetSquaredDistanceToCollision(ClosestPointOnLineToCapsuleCenter, DistanceSqr, SafeLocation); // Push back inside capsule to avoid initial penetration when doing line checks. if (PenetrationAvoidanceFeelers.Num() > 0) { SafeLocation += (SafeLocation - ClosestPointOnLineToCapsuleCenter).GetSafeNormal() * PushInDistance; } // Then aim line to desired camera position bool const bSingleRayPenetrationCheck = !bDoPredictiveAvoidance; PreventCameraPenetration(*PPActor, SafeLocation, View.Location, DeltaTime, AimLineToDesiredPosBlockedPct, bSingleRayPenetrationCheck); ILyraCameraAssistInterface* AssistArray[] = { TargetControllerAssist, TargetActorAssist, PPActorAssist }; if (AimLineToDesiredPosBlockedPct < ReportPenetrationPercent) { for (ILyraCameraAssistInterface* Assist : AssistArray) { if (Assist) { // camera is too close, tell the assists Assist->OnCameraPenetratingTarget(); } } } } } void ULyraCameraMode_ThirdPerson::PreventCameraPenetration(class AActor const& ViewTarget, FVector const& SafeLoc, FVector& CameraLoc, float const& DeltaTime, float& DistBlockedPct, bool bSingleRayOnly) { #if ENABLE_DRAW_DEBUG DebugActorsHitDuringCameraPenetration.Reset(); #endif float HardBlockedPct = DistBlockedPct; float SoftBlockedPct = DistBlockedPct; FVector BaseRay = CameraLoc - SafeLoc; FRotationMatrix BaseRayMatrix(BaseRay.Rotation()); FVector BaseRayLocalUp, BaseRayLocalFwd, BaseRayLocalRight; BaseRayMatrix.GetScaledAxes(BaseRayLocalFwd, BaseRayLocalRight, BaseRayLocalUp); float DistBlockedPctThisFrame = 1.f; int32 const NumRaysToShoot = bSingleRayOnly ? FMath::Min(1, PenetrationAvoidanceFeelers.Num()) : PenetrationAvoidanceFeelers.Num(); FCollisionQueryParams SphereParams(SCENE_QUERY_STAT(CameraPen), false, nullptr/*PlayerCamera*/); SphereParams.AddIgnoredActor(&ViewTarget); //TODO ILyraCameraTarget.GetIgnoredActorsForCameraPentration(); //if (IgnoreActorForCameraPenetration) //{ // SphereParams.AddIgnoredActor(IgnoreActorForCameraPenetration); //} FCollisionShape SphereShape = FCollisionShape::MakeSphere(0.f); UWorld* World = GetWorld(); for (int32 RayIdx = 0; RayIdx < NumRaysToShoot; ++RayIdx) { FLyraPenetrationAvoidanceFeeler& Feeler = PenetrationAvoidanceFeelers[RayIdx]; if (Feeler.FramesUntilNextTrace <= 0) { // calc ray target FVector RayTarget; { FVector RotatedRay = BaseRay.RotateAngleAxis(Feeler.AdjustmentRot.Yaw, BaseRayLocalUp); RotatedRay = RotatedRay.RotateAngleAxis(Feeler.AdjustmentRot.Pitch, BaseRayLocalRight); RayTarget = SafeLoc + RotatedRay; } // cast for world and pawn hits separately. this is so we can safely ignore the // camera's target pawn SphereShape.Sphere.Radius = Feeler.Extent; ECollisionChannel TraceChannel = ECC_Camera; //(Feeler.PawnWeight > 0.f) ? ECC_Pawn : ECC_Camera; // do multi-line check to make sure the hits we throw out aren't // masking real hits behind (these are important rays). // MT-> passing camera as actor so that camerablockingvolumes know when it's the camera doing traces FHitResult Hit; const bool bHit = World->SweepSingleByChannel(Hit, SafeLoc, RayTarget, FQuat::Identity, TraceChannel, SphereShape, SphereParams); #if ENABLE_DRAW_DEBUG if (World->TimeSince(LastDrawDebugTime) < 1.f) { DrawDebugSphere(World, SafeLoc, SphereShape.Sphere.Radius, 8, FColor::Red); DrawDebugSphere(World, bHit ? Hit.Location : RayTarget, SphereShape.Sphere.Radius, 8, FColor::Red); DrawDebugLine(World, SafeLoc, bHit ? Hit.Location : RayTarget, FColor::Red); } #endif // ENABLE_DRAW_DEBUG Feeler.FramesUntilNextTrace = Feeler.TraceInterval; const AActor* HitActor = Hit.GetActor(); if (bHit && HitActor) { bool bIgnoreHit = false; if (HitActor->ActorHasTag(LyraCameraMode_ThirdPerson_Statics::NAME_IgnoreCameraCollision)) { bIgnoreHit = true; SphereParams.AddIgnoredActor(HitActor); } // Ignore CameraBlockingVolume hits that occur in front of the ViewTarget. if (!bIgnoreHit && HitActor->IsA()) { const FVector ViewTargetForwardXY = ViewTarget.GetActorForwardVector().GetSafeNormal2D(); const FVector ViewTargetLocation = ViewTarget.GetActorLocation(); const FVector HitOffset = Hit.Location - ViewTargetLocation; const FVector HitDirectionXY = HitOffset.GetSafeNormal2D(); const float DotHitDirection = FVector::DotProduct(ViewTargetForwardXY, HitDirectionXY); if (DotHitDirection > 0.0f) { bIgnoreHit = true; // Ignore this CameraBlockingVolume on the remaining sweeps. SphereParams.AddIgnoredActor(HitActor); } else { #if ENABLE_DRAW_DEBUG DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr(HitActor)); #endif } } if (!bIgnoreHit) { float const Weight = Cast(Hit.GetActor()) ? Feeler.PawnWeight : Feeler.WorldWeight; float NewBlockPct = Hit.Time; NewBlockPct += (1.f - NewBlockPct) * (1.f - Weight); // Recompute blocked pct taking into account pushout distance. NewBlockPct = ((Hit.Location - SafeLoc).Size() - CollisionPushOutDistance) / (RayTarget - SafeLoc).Size(); DistBlockedPctThisFrame = FMath::Min(NewBlockPct, DistBlockedPctThisFrame); // This feeler got a hit, so do another trace next frame Feeler.FramesUntilNextTrace = 0; #if ENABLE_DRAW_DEBUG DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr(HitActor)); #endif } } if (RayIdx == 0) { // don't interpolate toward this one, snap to it // assumes ray 0 is the center/main ray HardBlockedPct = DistBlockedPctThisFrame; } else { SoftBlockedPct = DistBlockedPctThisFrame; } } else { --Feeler.FramesUntilNextTrace; } } if (bResetInterpolation) { DistBlockedPct = DistBlockedPctThisFrame; } else if (DistBlockedPct < DistBlockedPctThisFrame) { // interpolate smoothly out if (PenetrationBlendOutTime > DeltaTime) { DistBlockedPct = DistBlockedPct + DeltaTime / PenetrationBlendOutTime * (DistBlockedPctThisFrame - DistBlockedPct); } else { DistBlockedPct = DistBlockedPctThisFrame; } } else { if (DistBlockedPct > HardBlockedPct) { DistBlockedPct = HardBlockedPct; } else if (DistBlockedPct > SoftBlockedPct) { // interpolate smoothly in if (PenetrationBlendInTime > DeltaTime) { DistBlockedPct = DistBlockedPct - DeltaTime / PenetrationBlendInTime * (DistBlockedPct - SoftBlockedPct); } else { DistBlockedPct = SoftBlockedPct; } } } DistBlockedPct = FMath::Clamp(DistBlockedPct, 0.f, 1.f); if (DistBlockedPct < (1.f - ZERO_ANIMWEIGHT_THRESH)) { CameraLoc = SafeLoc + (CameraLoc - SafeLoc) * DistBlockedPct; } } void ULyraCameraMode_ThirdPerson::SetTargetCrouchOffset(FVector NewTargetOffset) { CrouchOffsetBlendPct = 0.0f; InitialCrouchOffset = CurrentCrouchOffset; TargetCrouchOffset = NewTargetOffset; } void ULyraCameraMode_ThirdPerson::UpdateCrouchOffset(float DeltaTime) { if (CrouchOffsetBlendPct < 1.0f) { CrouchOffsetBlendPct = FMath::Min(CrouchOffsetBlendPct + DeltaTime * CrouchOffsetBlendMultiplier, 1.0f); CurrentCrouchOffset = FMath::InterpEaseInOut(InitialCrouchOffset, TargetCrouchOffset, CrouchOffsetBlendPct, 1.0f); } else { CurrentCrouchOffset = TargetCrouchOffset; CrouchOffsetBlendPct = 1.0f; } }