// Copyright 2025 https://yuewu.dev/en All Rights Reserved. #include "GCMS_CameraMode_WithPenetrationAvoidance.h" #include "Engine/Canvas.h" #include "GameFramework/Pawn.h" #include "Components/PrimitiveComponent.h" #include "GameFramework/Controller.h" #include "Engine/HitResult.h" #include "GCMS_CameraAssistInterface.h" #include "GameFramework/CameraBlockingVolume.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode_WithPenetrationAvoidance) namespace GMS_CameraMode_WithPenetrationAvoidance_Statics { static const FName NAME_IgnoreCameraCollision = TEXT("IgnoreCameraCollision"); } UGCMS_CameraMode_WithPenetrationAvoidance::UGCMS_CameraMode_WithPenetrationAvoidance() { PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +00.0f, 0.0f), 1.00f, 1.00f, 14.f, 0)); PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3)); PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3)); PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5)); PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5)); PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+20.0f, +00.0f, 0.0f), 1.00f, 1.00f, 00.f, 4)); PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(-20.0f, +00.0f, 0.0f), 0.50f, 0.50f, 00.f, 4)); } void UGCMS_CameraMode_WithPenetrationAvoidance::UpdatePreventPenetration(float DeltaTime) { if (!bPreventPenetration) { return; } AActor* TargetActor = GetTargetActor(); APawn* TargetPawn = Cast(TargetActor); AController* TargetController = TargetPawn ? TargetPawn->GetController() : nullptr; IGCMS_CameraAssistInterface* TargetControllerAssist = Cast(TargetController); IGCMS_CameraAssistInterface* TargetActorAssist = Cast(TargetActor); TOptional OptionalPPTarget = TargetActorAssist ? TargetActorAssist->GetCameraPreventPenetrationTarget() : TOptional(); AActor* PPActor = OptionalPPTarget.IsSet() ? OptionalPPTarget.GetValue() : TargetActor; IGCMS_CameraAssistInterface* 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(bSingleRayPenetrationCheck, DeltaTime, PPActor, SafeLocation, View.Location, AimLineToDesiredPosBlockedPct); IGCMS_CameraAssistInterface* AssistArray[] = {TargetControllerAssist, TargetActorAssist, PPActorAssist}; if (AimLineToDesiredPosBlockedPct < ReportPenetrationPercent) { for (IGCMS_CameraAssistInterface* Assist : AssistArray) { if (Assist) { // camera is too close, tell the assists Assist->OnCameraPenetratingTarget(); } } } } } void UGCMS_CameraMode_WithPenetrationAvoidance::PreventCameraPenetration(bool bSingleRayOnly, const float& DeltaTime, const AActor* ViewTarget, FVector const& SafeLoc, FVector& CameraLoc, float& DistBlockedPct) { #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) { FGCMS_CameraPenetrationAvoidanceFeeler& 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(GMS_CameraMode_WithPenetrationAvoidance_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 UGCMS_CameraMode_WithPenetrationAvoidance::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 }