Files
gig-line/Game/GigLine/Source/GigLine/GigLineShotActor.cpp
T
zwitschi dbb177da6f feat(walk): the stride is captured — walking shot 8, traversal check closed
Shot 8 shows the body mid-stride in profile: legs apart, arms counter-swinging. The
traversal release check for 0.2.0 is closed with that evidence.

The reason earlier attempts froze on one pose was not the clip. A probe of the
single-node instance showed the clip advancing normally (assetTime 0.19 -> 1.03,
rate 1.0, playing) while the pawn's speed stayed 0 and the clip stayed Idle: the
call that enables walking sat in the shot sequencer's BeginPlay, which runs before
the player pawn exists, so the cast found nothing and silently did nothing. Moved
into the per-frame follow update, the same run logs walk=1 speed=28 clip=GigLine_Walk
and the stride appears.

Also: the walk expectation was missing from visual_targets.json after the earlier
retirement, so --verify was checking 7 shots while 8 were captured. Restored; 8/8
verify.
2026-11-09 12:18:22 +01:00

160 lines
4.9 KiB
C++

#include "GigLineShotActor.h"
#include "GigLineCharacter.h"
#include "Kismet/GameplayStatics.h"
#include "Camera/CameraActor.h"
#include "Camera/CameraComponent.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "UnrealClient.h"
AGigLineShotActor::AGigLineShotActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
}
void AGigLineShotActor::BeginPlay()
{
Super::BeginPlay();
NextShot();
}
void AGigLineShotActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// QA mode: the -qaselftest run owns the process (it walks, resets, pauses,
// transects, then quits on its own). Skip the screenshot rounds so the game
// does not exit mid-test via bQuitWhenDone.
if (FParse::Param(FCommandLine::Get(), TEXT("qaselftest")))
{
return;
}
if (bWaitingForSettle)
{
// A follow shot tracks the pawn while it settles, otherwise the subject
// walks out of the frame between placement and capture.
if (CurrentShot < ShotFollow.Num() && ShotFollow[CurrentShot])
{
UpdateFollowCamera();
}
SettleCountdown -= DeltaSeconds;
if (SettleCountdown <= 0.f)
{
bWaitingForSettle = false;
// Use the stable custom name only (no list-index prefix): inserting or
// reordering cameras must not renumber files and orphan the old ones.
const FString Name = CurrentShot < ShotNames.Num()
? ShotNames[CurrentShot]
: FString::FromInt(CurrentShot);
const FString FileName = FString::Printf(TEXT("review-%s"), *Name);
// Resolution is the game window's; the runner passes -ResX/-ResY.
FScreenshotRequest::RequestScreenshot(FileName, /*bInShowUI=*/true, false);
bWaitingForScreenshot = true;
}
return;
}
if (bWaitingForScreenshot && !FScreenshotRequest::IsScreenshotRequested())
{
bWaitingForScreenshot = false;
UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: captured %s"), *FScreenshotRequest::GetFilename());
// Walking shots move the pawn; put it back before the fixed-camera shots run.
if (ShotFollow.IsValidIndex(CurrentShot) && ShotFollow[CurrentShot])
{
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(UGameplayStatics::GetPlayerPawn(this, 0)))
{
Character->ReturnToStartForReview();
}
}
++CurrentShot;
NextShot();
}
}
void AGigLineShotActor::UpdateFollowCamera()
{
if (!CurrentCamera)
{
return;
}
APawn* Pawn = UGameplayStatics::GetPlayerPawn(this, 0);
if (!Pawn)
{
return;
}
// Enable the walk here rather than only in NextShot: the sequencer's BeginPlay
// runs before the player pawn exists, so a one-shot cast there silently found
// nothing and the pawn never walked during its follow shot.
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(Pawn))
{
Character->SetReviewWalk(true);
}
// Offset in the pawn's frame: to its right, slightly above, looking at the
// pelvis. Right-of-pawn gives a profile view, which is where a stride reads.
const FVector Right = Pawn->GetActorRightVector();
const FVector Origin = Pawn->GetActorLocation();
const FVector CameraLocation = Origin + Right * 420.f + FVector(0.f, 0.f, 90.f);
const FVector Target = Origin + FVector(0.f, 0.f, 40.f);
CurrentCamera->SetActorLocation(CameraLocation);
CurrentCamera->SetActorRotation((Target - CameraLocation).Rotation());
}
void AGigLineShotActor::NextShot()
{
if (!PlayerController)
{
PlayerController = GetWorld()->GetFirstPlayerController();
}
if (CurrentShot >= ShotLocations.Num() || !PlayerController)
{
UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: done (%d shots)"), CurrentShot);
if (bQuitWhenDone)
{
PlayerController->ConsoleCommand(TEXT("quit"));
}
return;
}
if (CurrentCamera)
{
CurrentCamera->Destroy();
}
FActorSpawnParameters Params;
Params.ObjectFlags |= RF_Transient;
CurrentCamera = GetWorld()->SpawnActor<ACameraActor>(
ACameraActor::StaticClass(), ShotLocations[CurrentShot], ShotRotations[CurrentShot], Params);
if (ShotFieldOfView.IsValidIndex(CurrentShot) && ShotFieldOfView[CurrentShot] > 0.0f)
{
if (UCameraComponent* Cam = CurrentCamera->GetCameraComponent())
{
// Long lens for elevations: fills the frame from across the street
// without pitching the camera, so verticals stay parallel.
Cam->SetFieldOfView(ShotFieldOfView[CurrentShot]);
}
}
// Follow shots ignore their recorded transform after this point: the camera is
// rebuilt from the pawn every frame until the capture. They also own the walking:
// the pawn only holds forward input while a follow shot is being taken, so the
// fixed-camera shots later in the list still find it at the spawn point.
const bool bFollowThisShot = ShotFollow.IsValidIndex(CurrentShot) && ShotFollow[CurrentShot];
if (AGigLineCharacter* Character = Cast<AGigLineCharacter>(UGameplayStatics::GetPlayerPawn(this, 0)))
{
Character->SetReviewWalk(bFollowThisShot);
}
if (bFollowThisShot)
{
UpdateFollowCamera();
}
PlayerController->SetViewTarget(CurrentCamera);
SettleCountdown = SettleSeconds;
bWaitingForSettle = true;
}