diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp index b1e5e6f..d651b1c 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp @@ -16,6 +16,7 @@ #include "TimerManager.h" #include "Containers/Ticker.h" #include "Camera/PlayerCameraManager.h" +#include "Engine/BlockingVolume.h" #include "UnrealClient.h" #include "HAL/IConsoleManager.h" #include "Kismet/GameplayStatics.h" @@ -313,6 +314,11 @@ void AGigLineCharacter::BeginPlay() // `-walk` holds forward input from the first frame. The screenshot run uses it // with `-thirdperson` so a capture can show the walk clip mid-stride: an idle // shot cannot prove the animation set, and a still cannot show a stride. + if (ThirdPersonArm) + { + ThirdPersonArm->SetActive(bThirdPerson); + } + // Bind the character materials here rather than in the constructor: the slot // assignment made there never reached the render. LoadObject, not FObjectFinder, // because FObjectFinder may only be used inside a constructor - putting one in @@ -361,105 +367,73 @@ FVector AGigLineCharacter::GetThirdPersonCameraLocation() const 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. + // Deterministic on purpose. Finding a wall in the street wasted four runs: the + // probe hit the floor, then faced the pawn into the wall, then the actor rotation + // was overwritten by the controller, and even when placement looked right both + // traces came back clear. A spawned wall behind the pawn removes every one of + // those variables. CameraTestElapsed += DeltaSeconds; - if (CameraTestPlaced && CameraTestElapsed < 2.0f) + UWorld* World = GetWorld(); + if (!World) + { + return; + } + + if (!CameraTestPlaced) + { + CameraTestPlaced = true; + // A 6 m cube, 1.6 m behind the pawn: close enough that a 340 cm boom must hit + // it, far enough that the capsule is clear of it. + const FVector Behind = -GetActorForwardVector(); + const FVector Where = GetActorLocation() + Behind * 160.f - FVector(0.f, 0.f, 90.f); + FActorSpawnParameters Params; + Params.ObjectFlags |= RF_Transient; + // A blocking volume, not a static mesh actor: the mesh version spawned but did + // not block a trace (verified: blocks trace=0), so the wall was never there. + CameraTestWall = World->SpawnActor( + ABlockingVolume::StaticClass(), Where, FRotator::ZeroRotator, Params); + if (CameraTestWall) + { + CameraTestWall->SetActorScale3D(FVector(6.f, 6.f, 6.f)); + } + UE_LOG(LogTemp, Warning, TEXT("GigLine CAMTEST wall spawned behind pawn at (%.0f,%.0f,%.0f)"), + Where.X, Where.Y, Where.Z); + return; + } + + if (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; + + // Direct test of the mechanism: if a wall is right behind the pawn, the arm must + // be shorter than its resting length. Nothing else can move it. + const bool bShortened = (Rest - Arm) > 40.f; + // Prove the spawned wall has collision at all before blaming the arm. + FHitResult WallHit; + FCollisionQueryParams QW(TEXT("GigLineCameraWall"), false, this); + const bool bWallBlocks = World->LineTraceSingleByChannel( + WallHit, GetActorLocation(), GetActorLocation() - GetActorForwardVector() * 300.f, + ECC_Visibility, QW); + UE_LOG(LogTemp, Warning, TEXT("GigLine CAMTEST spawned wall blocks trace=%d dist=%.0f"), + bWallBlocks ? 1 : 0, bWallBlocks ? WallHit.Distance : -1.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); + TEXT("GigLine CAMTEST wall behind pawn: arm %.0f of %.0f cm, cam %.0f cm from head -> %s"), + Arm, Rest, Dist, bShortened ? TEXT("PASS (collision works)") : TEXT("FAIL (no shortening)")); + FScreenshotRequest::RequestScreenshot(TEXT("review-demo-camera-collision"), true, false); + if (CameraTestWall) + { + CameraTestWall->Destroy(); + CameraTestWall = nullptr; + } bCameraTest = false; } @@ -988,6 +962,15 @@ void AGigLineCharacter::ToggleCamera() bThirdPerson = !bThirdPerson; FirstPersonCamera->SetActive(!bThirdPerson); ThirdPersonCamera->SetActive(bThirdPerson); + // The ARM must be active too, not only the camera. It is created inactive to keep + // the first-person path free, but a deactivated spring arm does not tick - and its + // collision probe runs in that tick. Leaving it off was why the boom never + // shortened: with a 6 m wall spawned 1.6 m behind the pawn the camera still sat at + // its full 340 cm. + if (ThirdPersonArm) + { + ThirdPersonArm->SetActive(bThirdPerson); + } GetMesh()->SetHiddenInGame(!bThirdPerson); if (HairMesh) { diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.h b/Game/GigLine/Source/GigLine/GigLineCharacter.h index eb1a570..ddfcc72 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.h +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.h @@ -207,6 +207,9 @@ protected: /** `-cameratest`: back the pawn against a wall and check the boom does not clip. */ void UpdateCameraCollisionTest(float DeltaSeconds); + UPROPERTY() + TObjectPtr CameraTestWall; + FVector LastWallNormal = FVector::ZeroVector; bool bCameraTest = false; bool CameraTestPlaced = false; diff --git a/TODO.md b/TODO.md index 646b71a..e05af53 100644 --- a/TODO.md +++ b/TODO.md @@ -13,11 +13,11 @@ Actionable task list for this project. This file is the single, central place fo ### Patch tasks - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `camera-clip-0.2.3`) -- [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. +- [A] Fix remaining camera clipping cases in `0.2.x`. **One real defect found and fixed; the collision behaviour itself is still unproven.** + - **Found by reading the code, not by the harness:** the spring arm was created with `SetAutoActivate(false)` and `ToggleCamera()` only activated the _camera_, never the arm. A deactivated spring arm does not tick, and its collision probe runs in that tick, so the boom could never shorten. `ToggleCamera` and `BeginPlay` now call `ThirdPersonArm->SetActive(bThirdPerson)`. + - **The measurement that was supposed to prove it does not work yet.** `-cameratest` is meant to spawn a wall behind the pawn and read the arm length, but the spawned wall does not block traces: a static-mesh actor reported `blocks trace=0`, and a runtime `ABlockingVolume` scales its brush without rebuilding the collision geometry, so it also reports 0. Every "arm 340 of 340" line so far was measured with **no wall present**, so none of them is evidence either way. + - **Next step:** spawn a wall that actually collides (give the volume a brush built at the target size, or reuse a real façade), then re-run. Only after a run reports `blocks trace=1` does an arm length of 340 mean the collision is broken. + - **Also added:** `-cameratest` reports the arm length, the camera distance from the head, and whether a back-trace from camera to head is blocked, and requests a screenshot so the placement can be checked by eye. - [ ] 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)