diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp index 98e7d6c..b1e5e6f 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp @@ -15,6 +15,8 @@ #include "Engine/World.h" #include "TimerManager.h" #include "Containers/Ticker.h" +#include "Camera/PlayerCameraManager.h" +#include "UnrealClient.h" #include "HAL/IConsoleManager.h" #include "Kismet/GameplayStatics.h" #include "Misc/CommandLine.h" @@ -333,6 +335,7 @@ void AGigLineCharacter::BeginPlay() } bWalkForever = FParse::Param(FCommandLine::Get(), TEXT("walk")); + bCameraTest = FParse::Param(FCommandLine::Get(), TEXT("cameratest")); // `-perf` starts the overlay on: the performance capture and any manual frame // check should not depend on someone pressing F3 first. The built demo has @@ -346,6 +349,120 @@ void AGigLineCharacter::BeginPlay() } } +float AGigLineCharacter::GetThirdPersonArmLength() const +{ + return ThirdPersonArm ? ThirdPersonArm->TargetArmLength : 0.f; +} + +FVector AGigLineCharacter::GetThirdPersonCameraLocation() const +{ + return ThirdPersonCamera ? ThirdPersonCamera->GetComponentLocation() : GetActorLocation(); +} + +void AGigLineCharacter::UpdateCameraCollisionTest(float DeltaSeconds) +{ + // A dedicated harness rather than a step inside -qaselftest: the QA timeline is + // tightly timed and an added teleport perturbed the reset check by 105 cm. + CameraTestElapsed += DeltaSeconds; + if (CameraTestPlaced && CameraTestElapsed < 2.0f) + { + return; + } + if (!CameraTestPlaced) + { + CameraTestPlaced = true; + UWorld* World = GetWorld(); + bool bFound = false; + if (World) + { + // Search outward for the nearest blocking wall and take the closest hit. + float BestDist = TNumericLimits::Max(); + FHitResult Best; + for (int32 i = 0; i < 16; ++i) + { + const FVector Dir = FRotator(0.f, i * 22.5f, 0.f).Vector(); + FHitResult Hit; + FCollisionQueryParams Q(TEXT("GigLineCameraTest"), false, this); + if (World->LineTraceSingleByChannel(Hit, GetActorLocation(), + GetActorLocation() + Dir * 6000.f, ECC_Visibility, Q)) + { + // A wall, not the ground: the first run picked a floor hit and + // rotated the pawn to face straight down, so the boom pointed at + // open sky and the arm never had anything to collide with. + if (FMath::Abs(Hit.ImpactNormal.Z) > 0.5f) + { + continue; + } + if (Hit.Distance < BestDist) + { + BestDist = Hit.Distance; + Best = Hit; + bFound = true; + } + } + } + if (bFound) + { + // The impact normal points OUT of the wall into open space, so standing + // along it and facing along it puts the wall behind us. The first two + // attempts used its inverse and faced the pawn into the wall, which left + // the boom pointing at open sky and the arm with nothing to collide with. + SetActorLocation(Best.ImpactPoint + Best.ImpactNormal * 70.f); + LastWallNormal = Best.ImpactNormal; + const FRotator Facing = Best.ImpactNormal.Rotation(); + SetActorRotation(Facing); + if (APlayerController* PC = Cast(GetController())) + { + PC->SetControlRotation(FRotator(0.f, Facing.Yaw, 0.f)); + } + } + } + if (!bThirdPerson) + { + ToggleCamera(); + } + CameraTestPlaced = true; + UE_LOG(LogTemp, Warning, + TEXT("GigLine CAMTEST placed wall=%d fwd=(%.2f,%.2f) normal=(%.2f,%.2f) collisionTest=%d"), + bFound ? 1 : 0, GetActorForwardVector().X, GetActorForwardVector().Y, + LastWallNormal.X, LastWallNormal.Y, + ThirdPersonArm && ThirdPersonArm->bDoCollisionTest ? 1 : 0); + return; + } + + UWorld* World = GetWorld(); + // Measure the spring arm itself, not the camera manager: the arm is what carries + // the collision test, and its arm length is the value that must shorten. + const float Rest = ThirdPersonBoomLength; + const float Arm = GetThirdPersonArmLength(); + const FVector CameraLoc = GetThirdPersonCameraLocation(); + const float Shortened = Rest - Arm; + const FVector Head = GetActorLocation() + FVector(0.f, 0.f, 60.f); + const float Dist = FVector::Dist(Head, CameraLoc); + FHitResult Hit; + FCollisionQueryParams Q(TEXT("GigLineCameraClear"), false, this); + const bool bBlocked = World && CameraLoc.SizeSquared() > 1.0f + && World->LineTraceSingleByChannel(Hit, Head, CameraLoc, ECC_Visibility, Q); + // Second trace, from the camera back toward the pawn: if this one IS blocked while + // the arm length is still at rest, the spring arm's collision test is not running. + FHitResult BackHit; + FCollisionQueryParams QBack(TEXT("GigLineCameraBack"), false, this); + const bool bBackBlocked = World && CameraLoc.SizeSquared() > 1.0f + && World->LineTraceSingleByChannel(BackHit, CameraLoc, Head, ECC_Visibility, QBack); + const bool bClear = !bBlocked || Hit.Distance > Dist - 25.f; + const bool bShortened = Shortened > 30.f; + UE_LOG(LogTemp, Warning, + TEXT("GigLine CAMTEST arm %.0f of %.0f cm (shortened %.0f), cam %.0f cm from head, " + "blocked=%d -> %s"), + Arm, Rest, Shortened, Dist, bBlocked ? 1 : 0, + (bClear || bShortened) ? TEXT("PASS") : TEXT("FAIL")); + UE_LOG(LogTemp, Warning, + TEXT("GigLine CAMTEST back-trace blocked=%d dist=%.0f (arm rest %.0f)"), + bBackBlocked ? 1 : 0, bBackBlocked ? BackHit.Distance : -1.f, Rest); + FScreenshotRequest::RequestScreenshot(TEXT("review-demo-camera-collision"), true, false); + bCameraTest = false; +} + void AGigLineCharacter::ToggleDebugOverlay() { static IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(TEXT("gigline.Debug")); @@ -852,6 +969,11 @@ void AGigLineCharacter::Tick(float DeltaSeconds) Super::Tick(DeltaSeconds); // Speed-driven animation state: correct whether the player walked, was pushed, // or fell, and independent of which input produced the movement. + if (bCameraTest) + { + UpdateCameraCollisionTest(GetWorld() ? GetWorld()->GetDeltaSeconds() : 0.f); + } + WalkForever(); UpdateBodyAnimation(); diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.h b/Game/GigLine/Source/GigLine/GigLineCharacter.h index 4759592..eb1a570 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.h +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.h @@ -200,6 +200,18 @@ protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") TObjectPtr ToggleDebugAction; + /** Current boom length and camera position of the third-person rig. */ + float GetThirdPersonArmLength() const; + FVector GetThirdPersonCameraLocation() const; + + /** `-cameratest`: back the pawn against a wall and check the boom does not clip. */ + void UpdateCameraCollisionTest(float DeltaSeconds); + + FVector LastWallNormal = FVector::ZeroVector; + bool bCameraTest = false; + bool CameraTestPlaced = false; + float CameraTestElapsed = 0.0f; + /** Flips the gigline.Debug console variable that the HUD reads. */ void ToggleDebugOverlay(); diff --git a/TODO.md b/TODO.md index e3cc9a1..646b71a 100644 --- a/TODO.md +++ b/TODO.md @@ -11,9 +11,13 @@ Actionable task list for this project. This file is the single, central place fo ## 0.2.3 — Camera clipping fixes (patch) -### Patch tasks +### Patch tasks - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `camera-clip-0.2.3`) -- [ ] Fix remaining camera clipping cases in `0.2.x`. +- [A] Fix remaining camera clipping cases in `0.2.x`. The spring arm has collision testing on `ECC_Camera` with a 12 cm probe, but **no test had ever exercised it**. + - **Harness built:** `-cameratest` finds the nearest wall from the spawn in 16 directions, stands the pawn 70 cm off it facing away (through the controller, because a possessed pawn's actor rotation is overwritten every tick), waits 2 s for the boom, then reports the arm length, whether the boom ends inside geometry, and a back-trace from the camera to the head. It also requests a screenshot so the placement can be checked by eye. + - **Inconclusive so far, and the setup is the suspect, not the engine.** Every run reports arm 340 of 340 (no shortening), but **both** traces - head to camera and camera to head - come back clear, so nothing was between the pawn and the boom. That points at the placement: the wall the probe found is not ending up behind the boom. + - **Three wrong attempts cost runs and are recorded:** the probe first accepted the floor as a "wall" (a hit whose normal points up), then faced the pawn into the wall by negating the impact normal (the normal points _away_ from the wall), then set actor rotation only, which a possessed pawn discards. + - **Next step:** capture and look at the placement screenshot before trusting the arm measurement, then re-run. If, with a wall confirmed behind the boom, the arm still reads 340, the spring arm's collision genuinely is not engaging and that is the `0.2.3` fix. - [ ] Tag `v0.2.3` and write `CHANGELOG.md` entry; then remove this section. ## 0.3.0 — Chaos vehicle, enter/exit, closed driving route (weeks 10–13)