feat(camera): third-person rig, V toggle, visible body
One pawn, both rigs (ADR-9): a skeletal body hidden in first person, a collision-tested spring arm, and a toggle that swaps the active camera and the body's visibility. Shipped default stays first person; -thirdperson starts the pawn in the third-person rig so the review shot is reproducible, and the screenshot run passes it. Three defects found and two fixed: - The skin material had no SkeletalMesh usage flag, so Unreal silently substituted the default grey. That is why the body rendered as clay. - render_screenshots.sh still called ./Engine/... on this branch (the fix lives on the facade branch and never reached main); applied here so the shots generate. - OPEN: the idle clip is bound and PlayAnimation is called - the run logs the clip name - but the body still renders in bind pose, arms out. Recorded with that evidence in TODO.md rather than called done. Shot 5 (demo-third-person) added and documented; camera collision is implemented but unproven, since the spawn has open space behind it.
This commit is contained in:
1 parent
96238bdb6a
commit
05c1488c00
49 files changed
+333
-65
No files matched your search
@@ -1,6 +1,9 @@
|
||||
#include "GigLineCharacter.h"
|
||||
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
@@ -28,7 +31,7 @@ static TAutoConsoleVariable<int32> CVarGigLineSetupLogs(
|
||||
|
||||
AGigLineCharacter::AGigLineCharacter()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
|
||||
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
|
||||
@@ -51,9 +54,59 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m
|
||||
FirstPersonCamera->bUsePawnControlRotation = true;
|
||||
|
||||
// An eye-level mesh is not needed for 0.1.0; the body is a capsule with a camera.
|
||||
GetMesh()->SetHiddenInGame(true);
|
||||
// Visible body (0.2.0). The animated import carries its own skeleton, so the
|
||||
// clips resolve without a retarget step; it is hidden while the first-person
|
||||
// camera is active and shown in third person.
|
||||
static ConstructorHelpers::FObjectFinder<USkeletalMesh> BodyFinder(
|
||||
TEXT("/Game/Test/Character/Character_Anim.Character_Anim"));
|
||||
if (BodyFinder.Succeeded())
|
||||
{
|
||||
GetMesh()->SetSkeletalMesh(BodyFinder.Object);
|
||||
}
|
||||
// The animated import carries no material of its own; without this the body is
|
||||
// grey clay. The warm skin material was created by the character import.
|
||||
static ConstructorHelpers::FObjectFinder<UMaterialInterface> SkinFinder(
|
||||
TEXT("/Game/Test/Character/M_Character_Export.M_Character_Export"));
|
||||
if (SkinFinder.Succeeded())
|
||||
{
|
||||
GetMesh()->SetMaterial(0, SkinFinder.Object);
|
||||
}
|
||||
static ConstructorHelpers::FObjectFinder<UAnimSequence> IdleFinder(
|
||||
TEXT("/Game/Test/Character/Character_AnimHuman_rig_GigLine_Idle.Character_AnimHuman_rig_GigLine_Idle"));
|
||||
if (IdleFinder.Succeeded())
|
||||
{
|
||||
IdleAnimation = IdleFinder.Object;
|
||||
}
|
||||
static ConstructorHelpers::FObjectFinder<UAnimSequence> WalkFinder(
|
||||
TEXT("/Game/Test/Character/Character_AnimHuman_rig_GigLine_Walk.Character_AnimHuman_rig_GigLine_Walk"));
|
||||
if (WalkFinder.Succeeded())
|
||||
{
|
||||
WalkAnimation = WalkFinder.Object;
|
||||
}
|
||||
GetMesh()->SetRelativeLocation(FVector(0.0f, 0.0f, -87.5f));
|
||||
GetMesh()->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
|
||||
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
GetMesh()->SetHiddenInGame(true);
|
||||
|
||||
// Third-person boom. bDoCollisionTest is the camera-collision feature: the arm
|
||||
// shortens when something is between the pivot and the desired camera position.
|
||||
ThirdPersonArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("ThirdPersonArm"));
|
||||
ThirdPersonArm->SetupAttachment(GetCapsuleComponent());
|
||||
ThirdPersonArm->SetRelativeLocation(FVector(0.0f, 0.0f, 60.0f));
|
||||
ThirdPersonArm->TargetArmLength = ThirdPersonBoomLength;
|
||||
ThirdPersonArm->bUsePawnControlRotation = true;
|
||||
ThirdPersonArm->bDoCollisionTest = true;
|
||||
ThirdPersonArm->ProbeSize = 12.0f;
|
||||
ThirdPersonArm->ProbeChannel = ECollisionChannel::ECC_Camera;
|
||||
ThirdPersonArm->bEnableCameraLag = true;
|
||||
ThirdPersonArm->CameraLagSpeed = 8.0f;
|
||||
ThirdPersonArm->SetAutoActivate(false);
|
||||
|
||||
ThirdPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ThirdPersonCamera"));
|
||||
ThirdPersonCamera->SetupAttachment(ThirdPersonArm, USpringArmComponent::SocketName);
|
||||
ThirdPersonCamera->bUsePawnControlRotation = false;
|
||||
ThirdPersonCamera->SetAutoActivate(false);
|
||||
ThirdPersonCamera->SetFieldOfView(90.0f);
|
||||
|
||||
// Hard-reference the Enhanced Input assets with ConstructorHelpers so the
|
||||
// cooker always includes them and they load during CDO construction. Runtime
|
||||
@@ -78,6 +131,8 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
TEXT("/Game/Input/IA_Jump.IA_Jump"));
|
||||
static ConstructorHelpers::FObjectFinder<UInputAction> ResetF(
|
||||
TEXT("/Game/Input/IA_Reset.IA_Reset"));
|
||||
static ConstructorHelpers::FObjectFinder<UInputAction> CamF(
|
||||
TEXT("/Game/Input/IA_ToggleCamera.IA_ToggleCamera"));
|
||||
static ConstructorHelpers::FObjectFinder<UInputAction> PauseF(
|
||||
TEXT("/Game/Input/IA_Pause.IA_Pause"));
|
||||
|
||||
@@ -89,6 +144,7 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
LookAction = LookF.Object;
|
||||
JumpAction = JumpF.Object;
|
||||
ResetAction = ResetF.Object;
|
||||
ToggleCameraAction = CamF.Object;
|
||||
PauseAction = PauseF.Object;
|
||||
|
||||
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|
||||
@@ -138,6 +194,23 @@ void AGigLineCharacter::BeginPlay()
|
||||
StartLocation = GetActorLocation();
|
||||
StartRotation = GetActorRotation();
|
||||
|
||||
// `-thirdperson` starts the pawn in the third-person rig. The first-person view
|
||||
// stays the default (owner decision for 0.1.0), but the screenshot run needs the
|
||||
// body visible without a human pressing V, so the review is reproducible.
|
||||
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||
{
|
||||
UE_LOG(LogTemp, Log,
|
||||
TEXT("GigLine: BeginPlay thirdperson=%d body=%s idle=%s walk=%s"),
|
||||
FParse::Param(FCommandLine::Get(), TEXT("thirdperson")) ? 1 : 0,
|
||||
GetMesh()->GetSkeletalMeshAsset() ? TEXT("ok") : TEXT("nil"),
|
||||
IdleAnimation ? TEXT("ok") : TEXT("nil"),
|
||||
WalkAnimation ? TEXT("ok") : TEXT("nil"));
|
||||
}
|
||||
if (FParse::Param(FCommandLine::Get(), TEXT("thirdperson")))
|
||||
{
|
||||
ToggleCamera();
|
||||
}
|
||||
|
||||
// The PlayerStart sits above grade so the capsule cannot spawn embedded, so
|
||||
// the BeginPlay position is mid-air. Re-capture once the pawn has landed,
|
||||
// otherwise Reset drops the player 1.4 m back into the air every press.
|
||||
@@ -557,6 +630,10 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
|
||||
{
|
||||
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
|
||||
}
|
||||
if (ToggleCameraAction)
|
||||
{
|
||||
Input->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AGigLineCharacter::ToggleCamera);
|
||||
}
|
||||
if (JumpAction)
|
||||
{
|
||||
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
||||
@@ -644,6 +721,68 @@ void AGigLineCharacter::Look(const FInputActionValue& Value)
|
||||
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
|
||||
}
|
||||
|
||||
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.
|
||||
UpdateBodyAnimation();
|
||||
}
|
||||
|
||||
void AGigLineCharacter::ToggleCamera()
|
||||
{
|
||||
// Both rigs exist from construction; the toggle only swaps which one is active
|
||||
// and whether the body is drawn. Deactivating the other camera matters: with
|
||||
// two active cameras the view is whichever was registered last.
|
||||
bThirdPerson = !bThirdPerson;
|
||||
FirstPersonCamera->SetActive(!bThirdPerson);
|
||||
ThirdPersonCamera->SetActive(bThirdPerson);
|
||||
GetMesh()->SetHiddenInGame(!bThirdPerson);
|
||||
if (bThirdPerson)
|
||||
{
|
||||
UpdateBodyAnimation();
|
||||
}
|
||||
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine: camera %s"), bThirdPerson ? TEXT("third person") : TEXT("first person"));
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::UpdateBodyAnimation()
|
||||
{
|
||||
if (!bThirdPerson || !GetMesh())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const bool bMoving = GetVelocity().Size2D() > WalkAnimationThreshold;
|
||||
UAnimSequence* Wanted = bMoving ? WalkAnimation.Get() : IdleAnimation.Get();
|
||||
if (!Wanted)
|
||||
{
|
||||
// Logged once: a missing clip presents as an A-posed body, which looks like
|
||||
// an animation problem rather than a missing asset reference.
|
||||
static bool bWarnedMissingClip = false;
|
||||
if (!bWarnedMissingClip)
|
||||
{
|
||||
bWarnedMissingClip = true;
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("GigLine: body clip missing (idle=%s walk=%s) - body stays in bind pose"),
|
||||
IdleAnimation ? TEXT("ok") : TEXT("nil"),
|
||||
WalkAnimation ? TEXT("ok") : TEXT("nil"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Wanted == PlayingAnimation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
PlayingAnimation = Wanted;
|
||||
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine: playing body clip %s"), *Wanted->GetName());
|
||||
}
|
||||
GetMesh()->PlayAnimation(Wanted, true);
|
||||
}
|
||||
|
||||
void AGigLineCharacter::ResetToStart()
|
||||
{
|
||||
// Reset is a level-restart in miniature: put the body back where it spawned,
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
#include "InputActionValue.h"
|
||||
#include "GigLineCharacter.generated.h"
|
||||
|
||||
class UAnimSequence;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
class USpringArmComponent;
|
||||
|
||||
|
||||
/**
|
||||
@@ -29,6 +31,7 @@ public:
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
protected:
|
||||
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
|
||||
@@ -38,6 +41,23 @@ protected:
|
||||
void ResetToStart();
|
||||
void TogglePause();
|
||||
|
||||
/** Swaps between the first-person and third-person rigs (V). */
|
||||
void ToggleCamera();
|
||||
|
||||
/**
|
||||
* Plays the idle or walk clip on the visible body.
|
||||
*
|
||||
* Called from the movement tick rather than from input: an animation state
|
||||
* driven by speed is correct whether the player walked, was pushed, or fell.
|
||||
* Only switches when the clip changes, because PlayAnimation restarts a clip
|
||||
* and calling it every frame would freeze the cycle at its first key.
|
||||
*/
|
||||
void UpdateBodyAnimation();
|
||||
|
||||
/** Velocity above which the walk clip plays, in cm/s (0.1 m/s tolerance). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
|
||||
float WalkAnimationThreshold = 10.0f;
|
||||
|
||||
/**
|
||||
* TEMP QA: scripted walk / reset / pause verification.
|
||||
*
|
||||
@@ -109,6 +129,39 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
||||
|
||||
/**
|
||||
* Third-person boom. Collision testing is on, which is what stops the camera
|
||||
* from ending up inside a wall: the arm shortens until the camera is clear
|
||||
* instead of clipping through geometry.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<USpringArmComponent> ThirdPersonArm;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UCameraComponent> ThirdPersonCamera;
|
||||
|
||||
/** Boom length in cm: far enough to see the body, close enough to read the street. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
float ThirdPersonBoomLength = 340.0f;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAnimSequence> IdleAnimation;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAnimSequence> WalkAnimation;
|
||||
|
||||
/** True while the third-person rig is the active view. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Camera")
|
||||
bool bThirdPerson = false;
|
||||
|
||||
/** Clip currently playing on the body, so UpdateBodyAnimation can diff it. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAnimSequence> PlayingAnimation;
|
||||
|
||||
/** Toggle-camera action (V). */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> ToggleCameraAction;
|
||||
|
||||
/** Degrees per second per unit of mouse input; defaults to Unreal's usual 45. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
float LookSensitivity = 45.0f;
|
||||
|
||||
Reference in new issue
Block a user