Files
gig-line/Game/GigLine/Source/GigLine/GigLineCharacter.cpp
T
zwitschi 160e9144f9 fix(hero): export smoothing groups; give the facade real collision
UE export hygiene (unreal-export skill checklist) plus a gameplay defect the new
probe caught:

- FBX export now uses mesh_smooth_type="FACE" (was OFF, which is why every import
  logged "No smoothing group information was found"), and the round parts are
  shaded with shade_smooth_by_angle(35 deg) before export, so columns and
  medallions read round instead of faceted.
- SM_HeroFacade had complex-only collision, so capsule sweeps and line traces
  passed through the wall: the player could walk through the hero facade.
  auto_generate_collision was tried and rejected (coarse hulls bulged into the
  street; the QA wall probe went 0/35 -> 2/35). It now uses UseComplexAsSimple,
  saved on the asset, like the terrain.
- New -qaselftest facade occlusion probe: 40 m traces sideways from the road
  centre at walking height, required only where a building is staged (explicit X
  spans mirroring ue_build_demo_map). Result 0/9 -> PASS, alongside transect
  0 holes and wall probe 0/35.

Docs: release-checks records the probe and both collision attempts; Tools/README
lists the new phase; MEMORY records the smoothing/collision lessons.
2026-11-08 17:53:31 +01:00

697 lines
24 KiB
C++

#include "GigLineCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "InputAction.h"
#include "InputMappingContext.h"
#include "Engine/World.h"
#include "TimerManager.h"
#include "Containers/Ticker.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "HAL/FileManager.h"
// Development-only diagnostics for the walkable demo. These describe the spawn
// and input setup and are not needed in a shipped build's log spam; disable
// with `-NoGigLineSetupLogs` (or set the cvar in release config).
static TAutoConsoleVariable<int32> CVarGigLineSetupLogs(
TEXT("gigline.SetupLogs"),
1,
TEXT("Log GigLine spawn/input setup diagnostics (0=off, 1=on)."));
AGigLineCharacter::AGigLineCharacter()
{
PrimaryActorTick.bCanEverTick = false;
// 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
// Move relative to where the player is looking, not where the actor points.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false;
UCharacterMovementComponent* Movement = GetCharacterMovement();
Movement->bOrientRotationToMovement = false;
Movement->MaxWalkSpeed = WalkSpeedMetresPerSecond * 100.0f;
Movement->JumpZVelocity = 420.0f;
Movement->AirControl = 0.2f;
Movement->BrakingDecelerationWalking = 2048.0f;
Movement->MaxStepHeight = 45.0f;
FirstPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
FirstPersonCamera->SetupAttachment(GetCapsuleComponent());
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);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Hard-reference the Enhanced Input assets with ConstructorHelpers so the
// cooker always includes them and they load during CDO construction. Runtime
// soft-loading them at BeginPlay was fragile: the assets were pruned from the
// pak unless /Game/Input was force-cooked, and a soft path can miss while the
// IoStore registry is still mounting. A constructor FObjectFinder is a hard
// reference (followed by the cooker) and loads deterministically.
// Assets are created by Tools/ue_create_input_assets.py.
static ConstructorHelpers::FObjectFinder<UInputMappingContext> CtxFinder(
TEXT("/Game/Input/IMC_Default.IMC_Default"));
static ConstructorHelpers::FObjectFinder<UInputAction> MoveF(
TEXT("/Game/Input/IA_Move.IA_Move"));
static ConstructorHelpers::FObjectFinder<UInputAction> StrafeF(
TEXT("/Game/Input/IA_Strafe.IA_Strafe"));
static ConstructorHelpers::FObjectFinder<UInputAction> BackF(
TEXT("/Game/Input/IA_MoveBack.IA_MoveBack"));
static ConstructorHelpers::FObjectFinder<UInputAction> LeftF(
TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft"));
static ConstructorHelpers::FObjectFinder<UInputAction> LookF(
TEXT("/Game/Input/IA_Look.IA_Look"));
static ConstructorHelpers::FObjectFinder<UInputAction> JumpF(
TEXT("/Game/Input/IA_Jump.IA_Jump"));
static ConstructorHelpers::FObjectFinder<UInputAction> ResetF(
TEXT("/Game/Input/IA_Reset.IA_Reset"));
static ConstructorHelpers::FObjectFinder<UInputAction> PauseF(
TEXT("/Game/Input/IA_Pause.IA_Pause"));
MappingContext = CtxFinder.Object;
MoveAction = MoveF.Object;
StrafeAction = StrafeF.Object;
MoveBackAction = BackF.Object;
StrafeLeftAction = LeftF.Object;
LookAction = LookF.Object;
JumpAction = JumpF.Object;
ResetAction = ResetF.Object;
PauseAction = PauseF.Object;
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|| !LookAction)
{
UE_LOG(LogTemp, Warning,
TEXT("GigLine: input assets failed to load at construction. ")
TEXT("Run Tools/ue_create_input_assets.py to build /Game/Input."));
}
}
void AGigLineCharacter::LoadInputAssets()
{
// The assets are hard-referenced and loaded in the constructor. This pushes
// the now-valid mapping context to the local player's subsystem; called from
// SetupPlayerInputComponent at possession, when the controller exists.
if (MappingContext)
{
if (const APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(MappingContext, 0);
if (CVarGigLineSetupLogs.GetValueOnGameThread())
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: pushed mapping context %s"),
*MappingContext->GetName());
}
}
else if (CVarGigLineSetupLogs.GetValueOnGameThread())
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no enhanced input subsystem on local player"));
}
}
else if (CVarGigLineSetupLogs.GetValueOnGameThread())
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no PlayerController at LoadInputAssets"));
}
}
}
void AGigLineCharacter::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
StartRotation = GetActorRotation();
// 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.
// Repeat until the first grounded frame. A single late timer is wrong: it can
// fire after the player (or the QA self-test) has already started walking,
// which recorded a mid-stride position as "start".
GetWorldTimerManager().SetTimer(SettleHandle, [this]()
{
if (bQATestWalking)
{
return; // keep waiting; the test walks almost immediately
}
if (const UCharacterMovementComponent* Mov = GetCharacterMovement())
{
if (Mov->IsMovingOnGround())
{
StartLocation = GetActorLocation();
StartRotation = GetActorRotation();
GetWorldTimerManager().ClearTimer(this->SettleHandle);
if (CVarGigLineSetupLogs.GetValueOnGameThread())
{
UE_LOG(LogTemp, Log, TEXT("GigLine: start captured on ground at Z %.0f"),
StartLocation.Z);
}
}
}
}, 0.25f, true);
// Force game input mode + focus + mouse capture so keyboard/mouse reach the
// pawn regardless of window focus quirks.
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
PC->SetInputMode(FInputModeGameOnly());
PC->bShowMouseCursor = false;
}
if (FParse::Param(FCommandLine::Get(), TEXT("qaselftest")))
{
StartQASelfTest();
}
}
void AGigLineCharacter::StartQASelfTest()
{
// Driven by a core ticker rather than a world timer on purpose: pausing the
// game stops world timers, so a timer-based test would freeze the moment it
// proved pause works.
struct FQAState
{
float Elapsed = 0.0f;
FVector Start = FVector::ZeroVector;
FVector WalkEnd = FVector::ZeroVector;
float Step = 0.0f;
bool bWalkMoved = false;
bool bResetReturned = false;
bool bPausedOn = false;
bool bUnpausedBack = false;
// Performance capture over the fixed walk route (1.0-3.0 s).
TArray<float> FrameMs;
bool bPerfLogged = false;
};
TSharedPtr<FQAState> State = MakeShared<FQAState>();
State->Start = GetActorLocation();
UE_LOG(LogTemp, Log, TEXT("GigLine QA: start XYZ=(%.0f,%.0f,%.0f)"),
State->Start.X, State->Start.Y, State->Start.Z);
FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateWeakLambda(this,
[this, State](float DeltaTime)
{
State->Elapsed += DeltaTime;
const float E = State->Elapsed;
// 1.0 - 3.0 s: hold forward. Reset sets the Step marker so each phase
// runs exactly once without an ever-growing if-chain.
if (E >= 1.0f && State->Step < 1.0f)
{
State->Step = 1.0f;
// Measure from the settled position, not the mid-air spawn point.
State->Start = GetActorLocation();
UE_LOG(LogTemp, Log, TEXT("GigLine QA: settled XYZ=(%.0f,%.0f,%.0f)"),
State->Start.X, State->Start.Y, State->Start.Z);
bQATestWalking = true;
}
if (bQATestWalking)
{
AddMovementInput(GetActorForwardVector(), 1.0f);
// Frame time for the perf capture (this ticker runs every frame).
State->FrameMs.Add(DeltaTime * 1000.0f);
}
// 3.0: stop and measure.
if (E >= 3.0f && State->Step < 2.0f)
{
State->Step = 2.0f;
bQATestWalking = false;
State->WalkEnd = GetActorLocation();
const float Walked = FVector::Dist2D(State->Start, State->WalkEnd);
State->bWalkMoved = Walked > 200.0f; // >2 m in 2 s at 4 m/s
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: walk moved %.0f cm in 2 s -> %s (Z %.0f -> %.0f)"),
Walked, State->bWalkMoved ? TEXT("PASS") : TEXT("FAIL"),
State->Start.Z, State->WalkEnd.Z);
// Perf: report frame-time stats over the fixed walk route.
if (!State->bPerfLogged && State->FrameMs.Num() > 10)
{
State->bPerfLogged = true;
TArray<float> Sorted = State->FrameMs;
Sorted.Sort();
const int32 N = Sorted.Num();
double Sum = 0.0;
for (float Ms : Sorted) { Sum += Ms; }
const float AvgMs = static_cast<float>(Sum / N);
const float P50 = Sorted[N / 2];
const float P95 = Sorted[FMath::Clamp((int32)(N * 0.95f), 0, N - 1)];
const float P99 = Sorted[FMath::Clamp((int32)(N * 0.99f), 0, N - 1)];
const float MaxMs = Sorted.Last();
FVector2D Res(0.0, 0.0);
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(Res);
}
// Record the upscaling/AA state so the check is reproducible.
auto CV = [](const TCHAR* Name)
{
IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(Name);
return Var ? Var->GetInt() : -999;
};
UE_LOG(LogTemp, Log,
TEXT("GigLine PERF: frames=%d avg=%.2fms (%.1f fps) p50=%.2f p95=%.2f p99=%.2f max=%.2f ms at %dx%d"),
N, AvgMs, AvgMs > 0.f ? 1000.f / AvgMs : 0.f, P50, P95, P99, MaxMs,
(int32)Res.X, (int32)Res.Y);
UE_LOG(LogTemp, Log,
TEXT("GigLine PERF: AntiAliasingMethod=%d ScreenPercentage=%d TSR=%d FrameGen=%d VSync=%d"),
CV(TEXT("r.AntiAliasingMethod")), CV(TEXT("r.ScreenPercentage")),
CV(TEXT("r.TSR.History.ScreenPercentage")),
CV(TEXT("r.Streamline.FrameGeneration.Mode")),
CV(TEXT("r.VSync")));
// Frame-time series, so the check has a graph source, not one sample.
{
const FString Dir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Profiling"));
IFileManager::Get().MakeDirectory(*Dir, true);
const FString Path = FPaths::Combine(Dir, TEXT("perf-walk-frametimes.csv"));
FString Csv(TEXT("frame,ms\n"));
for (int32 i = 0; i < N; ++i)
{
Csv += FString::Printf(TEXT("%d,%.3f\n"), i, State->FrameMs[i]);
}
FFileHelper::SaveStringToFile(Csv, *Path);
UE_LOG(LogTemp, Log, TEXT("GigLine PERF: frame times written to %s"), *Path);
}
}
}
// 3.5: reset should return to the spawn point.
if (E >= 3.5f && State->Step < 3.0f)
{
State->Step = 3.0f;
ResetToStart();
}
if (E >= 4.0f && State->Step < 4.0f)
{
State->Step = 4.0f;
const float Back = FVector::Dist(State->Start, GetActorLocation());
State->bResetReturned = Back < 100.0f;
UE_LOG(LogTemp, Log, TEXT("GigLine QA: reset returned %.0f cm from spawn -> %s"),
Back, State->bResetReturned ? TEXT("PASS") : TEXT("FAIL"));
}
// 4.5: pause, then confirm the world actually stopped.
if (E >= 4.5f && State->Step < 5.0f)
{
State->Step = 5.0f;
TogglePause();
State->bPausedOn = UGameplayStatics::IsGamePaused(this);
UE_LOG(LogTemp, Log, TEXT("GigLine QA: pause -> paused=%d %s"),
State->bPausedOn ? 1 : 0, State->bPausedOn ? TEXT("PASS") : TEXT("FAIL"));
}
// 5.5: unpause and confirm it comes back.
if (E >= 5.5f && State->Step < 6.0f)
{
State->Step = 6.0f;
TogglePause();
State->bUnpausedBack = !UGameplayStatics::IsGamePaused(this);
UE_LOG(LogTemp, Log, TEXT("GigLine QA: unpause -> paused=%d %s"),
UGameplayStatics::IsGamePaused(this) ? 1 : 0,
State->bUnpausedBack ? TEXT("PASS") : TEXT("FAIL"));
}
// 6.0: ground transect - sweep both sidewalks and the carriageway and
// count holes, so "can traverse the block without falling through"
// is measured rather than assumed.
if (E >= 6.0f && State->Step < 7.0f)
{
State->Step = 7.0f;
UWorld* W = GetWorld();
int32 Holes = 0, Samples = 0;
float Lowest = 1.0e9f, Highest = -1.0e9f;
// Probed world-space (2026-11-08): N sidewalk -1912..-1053, kerb
// -1382..-1053, road -1382..+1047, kerb +718..+1047, S sidewalk
// +718..+1447. Rows include the kerb/road/sidewalk boundary Ys so
// seam gaps are sampled, not just lane centres.
static const float Rows[] = {
-1750.0f, -1500.0f, -1382.0f, -1280.0f, -1053.0f, -900.0f,
-167.0f, 0.0f, 300.0f, 800.0f, 1047.0f, 900.0f, 718.0f,
900.0f, 1250.0f, 1447.0f
};
for (float LaneY : Rows)
{
for (float X = -20000.0f; X <= -2000.0f; X += 200.0f)
{
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineTransect"), false, this);
const bool bHit = W->LineTraceSingleByChannel(Hit,
FVector(X, LaneY, 3000.f), FVector(X, LaneY, -3000.f),
ECC_Visibility, Q);
++Samples;
if (!bHit)
{
++Holes;
continue;
}
Lowest = FMath::Min(Lowest, Hit.ImpactPoint.Z);
Highest = FMath::Max(Highest, Hit.ImpactPoint.Z);
}
}
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: transect %d samples, %d holes, ground Z %.0f..%.0f -> %s"),
Samples, Holes, Lowest, Highest, Holes == 0 ? TEXT("PASS") : TEXT("FAIL"));
// Invisible-wall / blocking-mesh probe: at each sample, first find
// the ground under the start point (vertical raycast), then run a
// horizontal trace 160 cm ABOVE that ground along the street row.
// A clean street should let the 30 m trace run clear; a short hit
// flags an invisible or misplaced blocker. Starting the horizontal
// trace at a fixed Z (e.g. 160) is wrong: the street is at Z ~366
// there, so a 160 cm probe starts inside the earth (dist 0).
int32 Walls = 0, WallSamples = 0;
static const float TraceYs[] = {-1400.0f, -167.0f, 1000.0f, -1382.0f, 1047.0f};
for (float WallY : TraceYs)
{
for (float X0 = -19500.0f; X0 <= -3000.0f; X0 += 2500.0f)
{
FHitResult GroundHit;
FCollisionQueryParams GQ(TEXT("GigLineWallGround"), false, this);
if (!W->LineTraceSingleByChannel(GroundHit,
FVector(X0, WallY, 3000.f), FVector(X0, WallY, -1000.f),
ECC_Visibility, GQ))
{
continue; // no ground under start -> skip
}
const float ProbeZ = GroundHit.ImpactPoint.Z + 160.f;
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineWallProbe"), false, this);
const bool bHit = W->LineTraceSingleByChannel(Hit,
FVector(X0, WallY, ProbeZ),
FVector(X0 + 3000.f, WallY, ProbeZ),
ECC_Visibility, Q);
++WallSamples;
if (!bHit) { continue; } // 30 m clear -> good
// Starting just above the ground, a hit within 21 m = blocker.
if (Hit.Distance < 2100.f)
{
++Walls;
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: wall probe short-hit at X %.0f Y %.0f dist %.0f actor=%s"),
X0, WallY, Hit.Distance,
Hit.GetActor() ? *Hit.GetActor()->GetName() : TEXT("none"));
}
}
}
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: wall probe %d/%d traces with blocking hit inside 21 m -> %s"),
Walls, WallSamples,
Walls == 0 ? TEXT("PASS") : TEXT("FAIL"));
// Facade occlusion probe: walkable street is only half the story —
// the player must also NOT be able to walk through the buildings.
// From the road centre, fire 40 m sideways at walking height toward
// each frontage and require a blocking hit. A miss means the
// building has no collision (or only non-blocking collision) and the
// player would pass through the wall.
// Expect a wall only where the map actually stages a building.
// Spans below mirror the actors placed by Tools/ue_build_demo_map.py
// (frontage + hero) and the two north buildings; update them if
// that list changes. Gaps between buildings are legitimate misses.
struct FSpan { float Min, Max; };
static const FSpan SouthSpans[] = {
{-24276.f, -17700.f}, // 6922
{-17749.f, -14591.f}, // 6902
{-14638.f, -12034.f}, // 6840 hero
{-12082.f, -8210.f}, // 6838 El Capitan Theatre
{-7854.f, -7252.f}, // 6822 Budget
{-6608.f, -3307.f}, // 6806-6820
{-3320.f, -1437.f}, // 6800 Lee Drug
};
static const FSpan NorthSpans[] = {
{-27071.f, -22799.f}, // Madame Tussaud's
{-22889.f, -18442.f}, // TCL Chinese Theatre
};
auto InSpans = [](const FSpan* Spans, int32 Count, float X)
{
for (int32 i = 0; i < Count; ++i)
{
if (X >= Spans[i].Min && X <= Spans[i].Max) { return true; }
}
return false;
};
int32 Facades = 0, FacadeSamples = 0;
for (float X0 = -19000.0f; X0 <= -3000.0f; X0 += 2000.0f)
{
for (float Dir : {1.0f, -1.0f}) // +Y south frontage, -Y north side
{
const bool bExpectWall = (Dir > 0.0f)
? InSpans(SouthSpans, UE_ARRAY_COUNT(SouthSpans), X0)
: InSpans(NorthSpans, UE_ARRAY_COUNT(NorthSpans), X0);
if (!bExpectWall)
{
continue;
}
FHitResult GroundHit2;
FCollisionQueryParams GQ2(TEXT("GigLineFacadeGround"), false, this);
if (!W->LineTraceSingleByChannel(GroundHit2,
FVector(X0, 0.0f, 3000.f), FVector(X0, 0.0f, -1000.f),
ECC_Visibility, GQ2))
{
continue;
}
const float ProbeZ2 = GroundHit2.ImpactPoint.Z + 160.f;
FHitResult Hit2;
FCollisionQueryParams Q2(TEXT("GigLineFacadeProbe"), false, this);
const bool bHit2 = W->LineTraceSingleByChannel(Hit2,
FVector(X0, 0.0f, ProbeZ2),
FVector(X0, Dir * 4000.f, ProbeZ2),
ECC_Visibility, Q2);
++FacadeSamples;
const bool bBlocking = bHit2 && Hit2.GetActor() != nullptr;
if (!bBlocking)
{
++Facades;
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: facade probe miss at X %.0f dir %.0f (no blocking hit in 40 m)"),
X0, Dir);
}
}
}
UE_LOG(LogTemp, Log,
TEXT("GigLine QA: facade probe %d/%d directions without a blocking wall -> %s"),
Facades, FacadeSamples,
Facades == 0 ? TEXT("PASS") : TEXT("FAIL"));
}
// 7.0: verdict, then stop ticking.
if (E >= 7.0f)
{
const bool bAllPass = State->bWalkMoved && State->bResetReturned
&& State->bPausedOn && State->bUnpausedBack;
UE_LOG(LogTemp, Log, TEXT("GigLine QA: RESULT walk=%d reset=%d pause=%d unpause=%d -> %s"),
State->bWalkMoved ? 1 : 0, State->bResetReturned ? 1 : 0,
State->bPausedOn ? 1 : 0, State->bUnpausedBack ? 1 : 0,
bAllPass ? TEXT("PASS") : TEXT("FAIL"));
return false;
}
return true;
}));
}
void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
if (!Input)
{
UE_LOG(LogTemp, Error, TEXT("GigLine: player input component is not Enhanced Input"));
return;
}
// Push the mapping context for this controller's local player so the bound
// actions receive input (runs at possession, when the controller exists).
LoadInputAssets();
// One action per direction keeps the sign explicit: W and S differ only by
// which handler runs, not by a modifier in the asset.
if (MoveAction)
{
Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveForward);
}
if (MoveBackAction)
{
Input->BindAction(MoveBackAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveBack);
}
if (StrafeAction)
{
Input->BindAction(StrafeAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveStrafe);
}
if (StrafeLeftAction)
{
Input->BindAction(StrafeLeftAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveLeft);
}
if (LookAction)
{
Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::Look);
}
if (ResetAction)
{
Input->BindAction(ResetAction, ETriggerEvent::Started, this, &AGigLineCharacter::ResetToStart);
}
if (PauseAction)
{
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
}
if (JumpAction)
{
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &AGigLineCharacter::StopJump);
}
if (CVarGigLineSetupLogs.GetValueOnGameThread())
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: bound %d actions; ctx=%s move=%s look=%s jmp=%s"),
(Input ? 1 : 0),
MappingContext ? TEXT("ok") : TEXT("nil"),
MoveAction ? TEXT("ok") : TEXT("nil"),
LookAction ? TEXT("ok") : TEXT("nil"),
JumpAction ? TEXT("ok") : TEXT("nil"));
}
}
namespace
{
/** One-dimensional action value; S and A arrive positive and are negated here. */
float SignedAxis(const FInputActionValue& Value, bool bNegativeKey)
{
float Axis = 0.0f;
switch (Value.GetValueType())
{
case EInputActionValueType::Boolean:
Axis = Value.Get<bool>() ? 1.0f : 0.0f;
break;
case EInputActionValueType::Axis1D:
Axis = Value.Get<float>();
break;
default:
Axis = Value.Get<FVector2D>().Y;
break;
}
return bNegativeKey ? -Axis : Axis;
}
}
void AGigLineCharacter::MoveForward(const FInputActionValue& Value)
{
ApplyMove(FMath::Abs(SignedAxis(Value, false)), 0.0f);
}
void AGigLineCharacter::MoveBack(const FInputActionValue& Value)
{
ApplyMove(-FMath::Abs(SignedAxis(Value, false)), 0.0f);
}
void AGigLineCharacter::MoveStrafe(const FInputActionValue& Value)
{
ApplyMove(0.0f, FMath::Abs(SignedAxis(Value, false)));
}
void AGigLineCharacter::MoveLeft(const FInputActionValue& Value)
{
ApplyMove(0.0f, -FMath::Abs(SignedAxis(Value, false)));
}
void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
{
const AController* Controller = GetController();
if (!Controller)
{
return;
}
if (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe))
{
return;
}
// Forward/right on the controller yaw keeps the capsule upright on slopes.
const FRotator YawRotation(0.0f, Controller->GetControlRotation().Yaw, 0.0f);
const FVector ForwardDir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDir, Forward);
AddMovementInput(RightDir, Strafe);
}
void AGigLineCharacter::Look(const FInputActionValue& Value)
{
// Enhanced Input applies the mapping's scalar modifier, which already scales by
// DeltaTime; scaling again by DeltaTime would make the mouse frame-rate dependent.
const FVector2D Axis = Value.Get<FVector2D>();
AddControllerYawInput(Axis.X * LookSensitivity);
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
}
void AGigLineCharacter::ResetToStart()
{
// Reset is a level-restart in miniature: put the body back where it spawned,
// clear momentum so a fall does not carry over, and hand control back to the
// player if the pause menu was open.
if (UWorld* World = GetWorld())
{
SetActorLocationAndRotation(StartLocation, StartRotation, false, nullptr, ETeleportType::TeleportPhysics);
GetCharacterMovement()->StopMovementImmediately();
if (AController* Controller = GetController())
{
Controller->SetControlRotation(StartRotation);
}
UE_LOG(LogTemp, Log, TEXT("GigLine: reset to start"));
}
}
void AGigLineCharacter::TogglePause()
{
// Pause is a game-pause, not just an input mute: the world stops so physics and
// timers do not keep running behind the menu.
UWorld* World = GetWorld();
if (!World)
{
return;
}
bPaused = !bPaused;
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
PC->SetPause(bPaused);
PC->bShowMouseCursor = bPaused;
// The two input-mode structs are unrelated types, so branch rather than
// using a conditional expression.
if (bPaused)
{
PC->SetInputMode(FInputModeUIOnly());
}
else
{
PC->SetInputMode(FInputModeGameOnly());
}
}
UE_LOG(LogTemp, Log, TEXT("GigLine: paused=%s"), bPaused ? TEXT("true") : TEXT("false"));
}
void AGigLineCharacter::StartJump()
{
Jump();
}
void AGigLineCharacter::StopJump()
{
StopJumping();
}