qa: make the demo walkable — terrain collision root cause, spawn, scripted QA
The reported fall-through was a misdiagnosis: the player never fell. The imported Terrain_DEM had only a bounding-box simple collision proxy, so a downward trace anywhere in the block hit the terrain at Z 819 (the mesh bbox max) and the street sat under an invisible 8 m lid. Standing on that box looked plausible, which is why it read as working. - add UGigLineAssetTools (C++): force CTF_UseComplexAsSimple on a static mesh. UE 5.8 dropped bUseComplexAsSimpleCollision and Python cannot set the flag; the builder also saves the asset, since the flag lives there and a map-only save silently reverts it. - spawn (-13336, -1150, 600): inside the carved corridor (the cut is a 10.7 m half-width around the road centreline), lands on the road at 456. - fix ResetToStart returning to the mid-air PlayerStart captured in BeginPlay: StartLocation is now captured on the first grounded frame. - HighResShot 1280x720 instead of "1280 720", which is a multiplier. - add -qaselftest: scripted walk/reset/pause on a core ticker plus a swept ground transect. Results: walk 746 cm/2 s with Z steady, reset 0 cm from spawn, pause/unpause 1/0, transect 84 samples 0 holes. - rebase onto main, keeping the frontage materials and placement fixes.
This commit is contained in:
1 parent
fee26ade10
commit
52b1212cf3
34 files changed
+591
-92
No files matched your search
@@ -0,0 +1,65 @@
|
||||
#include "GigLineAssetTools.h"
|
||||
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "PhysicsEngine/BodySetup.h"
|
||||
#include "StaticMeshResources.h"
|
||||
|
||||
void UGigLineAssetTools::SetComplexCollisionAsSimple(UStaticMesh* Mesh, bool bEnable)
|
||||
{
|
||||
if (!Mesh)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UBodySetup* BodySetup = Mesh->GetBodySetup();
|
||||
if (!BodySetup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BodySetup->CollisionTraceFlag = bEnable ? CTF_UseComplexAsSimple : CTF_UseSimpleAndComplex;
|
||||
BodySetup->InvalidatePhysicsData();
|
||||
BodySetup->CreatePhysicsMeshes();
|
||||
Mesh->MarkPackageDirty();
|
||||
Mesh->PostEditChange();
|
||||
}
|
||||
|
||||
void UGigLineAssetTools::ClearSimpleCollision(UStaticMesh* Mesh)
|
||||
{
|
||||
if (!Mesh)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UBodySetup* BodySetup = Mesh->GetBodySetup();
|
||||
if (!BodySetup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BodySetup->AggGeom.EmptyElements();
|
||||
BodySetup->InvalidatePhysicsData();
|
||||
BodySetup->CreatePhysicsMeshes();
|
||||
Mesh->MarkPackageDirty();
|
||||
Mesh->PostEditChange();
|
||||
}
|
||||
|
||||
FString UGigLineAssetTools::GetCollisionComplexity(UStaticMesh* Mesh)
|
||||
{
|
||||
if (!Mesh)
|
||||
{
|
||||
return TEXT("no mesh");
|
||||
}
|
||||
|
||||
const UBodySetup* BodySetup = Mesh->GetBodySetup();
|
||||
if (!BodySetup)
|
||||
{
|
||||
return TEXT("no body setup");
|
||||
}
|
||||
|
||||
const int32 Flag = static_cast<int32>(BodySetup->CollisionTraceFlag.GetValue());
|
||||
if (Flag == CTF_UseComplexAsSimple) { return TEXT("UseComplexAsSimple"); }
|
||||
if (Flag == CTF_UseSimpleAsComplex) { return TEXT("UseSimpleAsComplex"); }
|
||||
if (Flag == CTF_UseSimpleAndComplex) { return TEXT("UseSimpleAndComplex"); }
|
||||
return TEXT("ProjectDefault");
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "GigLineAssetTools.generated.h"
|
||||
|
||||
class UStaticMesh;
|
||||
|
||||
/**
|
||||
* Small editor-side helpers for the headless asset pipeline scripts.
|
||||
*
|
||||
* Exposed to Python so tools can fix up imported meshes that the Interchange
|
||||
* importer gives unusable default collision. The terrain is the motivating
|
||||
* case: without complex-as-simple it gets a bounding-box proxy, so the whole
|
||||
* street sits under an invisible 8 m box and the player walks on the box lid.
|
||||
*/
|
||||
UCLASS()
|
||||
class GIGLINE_API UGigLineAssetTools : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Make collision trace against the mesh triangles instead of a proxy hull. */
|
||||
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
|
||||
static void SetComplexCollisionAsSimple(UStaticMesh* Mesh, bool bEnable = true);
|
||||
|
||||
/** Drop any generated simple collision (hulls/boxes) from the mesh. */
|
||||
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
|
||||
static void ClearSimpleCollision(UStaticMesh* Mesh);
|
||||
|
||||
/** Report the current collision complexity: "SimpleAndComplex", "Simple", ... */
|
||||
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
|
||||
static FString GetCollisionComplexity(UStaticMesh* Mesh);
|
||||
};
|
||||
@@ -7,8 +7,13 @@
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "InputAction.h"
|
||||
#include "GameFramework/PlayerController.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"
|
||||
|
||||
AGigLineCharacter::AGigLineCharacter()
|
||||
{
|
||||
@@ -39,51 +44,56 @@ AGigLineCharacter::AGigLineCharacter()
|
||||
GetMesh()->SetHiddenInGame(true);
|
||||
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
|
||||
// Input assets are created by Tools/ue_create_input_assets.py. They are loaded in
|
||||
// BeginPlay rather than the constructor: the constructor runs during CDO
|
||||
// construction where asset loads are unreliable, and a missing asset should be
|
||||
// a loud warning, not a silent no-op.
|
||||
}
|
||||
// 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"));
|
||||
|
||||
void AGigLineCharacter::LoadInputAssets()
|
||||
{
|
||||
auto LoadSoft = [](const TCHAR* Path) -> TObjectPtr<UObject>
|
||||
{
|
||||
return LoadObject<UObject>(nullptr, Path);
|
||||
};
|
||||
MappingContext = Cast<UInputMappingContext>(LoadSoft(TEXT("/Game/Input/IMC_Default.IMC_Default")));
|
||||
MoveAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Move.IA_Move")));
|
||||
StrafeAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Strafe.IA_Strafe")));
|
||||
MoveBackAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_MoveBack.IA_MoveBack")));
|
||||
StrafeLeftAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft")));
|
||||
LookAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Look.IA_Look")));
|
||||
JumpAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Jump.IA_Jump")));
|
||||
ResetAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Reset.IA_Reset")));
|
||||
PauseAction = Cast<UInputAction>(LoadSoft(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 missing (context=%s move=%s look=%s). ")
|
||||
TEXT("Run Tools/ue_create_input_assets.py to build /Game/Input."),
|
||||
MappingContext ? TEXT("ok") : TEXT("missing"),
|
||||
(MoveAction && MoveBackAction && StrafeAction && StrafeLeftAction) ? TEXT("ok")
|
||||
: TEXT("missing"),
|
||||
LookAction ? TEXT("ok") : TEXT("missing"));
|
||||
TEXT("GigLine: input assets failed to load at construction. ")
|
||||
TEXT("Run Tools/ue_create_input_assets.py to build /Game/Input."));
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::BeginPlay()
|
||||
void AGigLineCharacter::LoadInputAssets()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
LoadInputAssets();
|
||||
|
||||
StartLocation = GetActorLocation();
|
||||
StartRotation = GetActorRotation();
|
||||
|
||||
// Push the mapping context for this local player.
|
||||
// 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()))
|
||||
@@ -92,11 +102,250 @@ void AGigLineCharacter::BeginPlay()
|
||||
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
|
||||
{
|
||||
Subsystem->AddMappingContext(MappingContext, 0);
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: pushed mapping context %s"),
|
||||
*MappingContext->GetName());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no enhanced input subsystem on local player"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
// TEMP QA: one screenshot at +1.5s so the actual frame is reviewable.
|
||||
FTimerHandle ShotHandle;
|
||||
GetWorldTimerManager().SetTimer(ShotHandle, [this]()
|
||||
{
|
||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||
{
|
||||
PC->ConsoleCommand(TEXT("HighResShot 1280x720"));
|
||||
}
|
||||
}, 1.5f, false);
|
||||
|
||||
// TEMP QA: every second log world time + capsule Z + gravity + fall state so
|
||||
// we can see whether the world is simulating the pawn at all (symptom:
|
||||
// "spawns above ground but never falls, no input").
|
||||
FTimerHandle SimHandle;
|
||||
GetWorldTimerManager().SetTimer(SimHandle, [this]()
|
||||
{
|
||||
UCharacterMovementComponent* M = GetCharacterMovement();
|
||||
UE_LOG(LogTemp, Log,
|
||||
TEXT("GigLine SIM: t=%.2f XYZ=(%.0f,%.0f,%.0f) grav=%.1f fall=%d local=%d role=%d mode=%d ctrl=%s"),
|
||||
GetWorld() ? GetWorld()->GetTimeSeconds() : -1.f,
|
||||
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
|
||||
M ? M->GetGravityZ() : 0.f,
|
||||
(M && M->IsFalling()) ? 1 : 0,
|
||||
IsLocallyControlled() ? 1 : 0,
|
||||
(int)GetLocalRole(),
|
||||
M ? (int)M->MovementMode : -1,
|
||||
GetController() ? *GetController()->GetName() : TEXT("NONE"));
|
||||
if (UWorld* W = GetWorld())
|
||||
{
|
||||
const FVector L = GetActorLocation();
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Q(TEXT("GigLineGroundProbe"), false, this);
|
||||
const bool bHit = W->LineTraceSingleByChannel(Hit,
|
||||
FVector(L.X, L.Y, 3000.f), FVector(L.X, L.Y, -3000.f), ECC_Visibility, Q);
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine GROUND: top=%.0f actor=%s"),
|
||||
bHit ? Hit.ImpactPoint.Z : -9999.f,
|
||||
bHit && Hit.GetActor() ? *Hit.GetActor()->GetActorLabel() : TEXT("none"));
|
||||
}
|
||||
}, 1.0f, true);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
static const float Lanes[] = {-1400.0f, -167.0f, 1000.0f}; // N walk, road, S walk
|
||||
for (float LaneY : Lanes)
|
||||
{
|
||||
for (float X = -14900.0f; X <= -9420.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"));
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -108,6 +357,10 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
|
||||
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)
|
||||
@@ -143,6 +396,12 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
|
||||
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
||||
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &AGigLineCharacter::StopJump);
|
||||
}
|
||||
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
|
||||
@@ -169,6 +428,8 @@ namespace
|
||||
|
||||
void AGigLineCharacter::MoveForward(const FInputActionValue& Value)
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: MoveForward fired, axis=%.3f type=%d"),
|
||||
FMath::Abs(SignedAxis(Value, false)), (int)Value.GetValueType());
|
||||
ApplyMove(FMath::Abs(SignedAxis(Value, false)), 0.0f);
|
||||
}
|
||||
|
||||
@@ -190,11 +451,16 @@ void AGigLineCharacter::MoveLeft(const FInputActionValue& Value)
|
||||
void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
|
||||
{
|
||||
const AController* Controller = GetController();
|
||||
if (!Controller || (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe)))
|
||||
if (!Controller)
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: ApplyMove called but NO controller (fwd %.2f strafe %.2f)"),
|
||||
Forward, Strafe);
|
||||
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);
|
||||
@@ -202,6 +468,8 @@ void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
|
||||
|
||||
AddMovementInput(ForwardDir, Forward);
|
||||
AddMovementInput(RightDir, Strafe);
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine IN: ApplyMove fwd %.2f strafe %.2f controller=%s yaw=%.0f"),
|
||||
Forward, Strafe, *Controller->GetName(), Controller->GetControlRotation().Yaw);
|
||||
}
|
||||
|
||||
void AGigLineCharacter::Look(const FInputActionValue& Value)
|
||||
|
||||
@@ -38,6 +38,22 @@ protected:
|
||||
void ResetToStart();
|
||||
void TogglePause();
|
||||
|
||||
/**
|
||||
* TEMP QA: scripted walk / reset / pause verification.
|
||||
*
|
||||
* Enabled only by the `-qaselftest` command-line switch, so it never runs in
|
||||
* normal play. It exists because those three behaviours were listed as
|
||||
* "needs a human at the keyboard" and a repeatable headless check beats
|
||||
* waiting for one.
|
||||
*/
|
||||
void StartQASelfTest();
|
||||
|
||||
/** Set while the self-test is driving forward input. */
|
||||
bool bQATestWalking = false;
|
||||
|
||||
/** Repeating timer that captures StartLocation on first ground contact. */
|
||||
FTimerHandle SettleHandle;
|
||||
|
||||
/** Where the player started, so reset returns there rather than the origin. */
|
||||
UPROPERTY()
|
||||
FVector StartLocation = FVector::ZeroVector;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "GigLineGameModeBase.h"
|
||||
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GigLineCharacter.h"
|
||||
#include "GigLineHUD.h"
|
||||
|
||||
@@ -9,3 +10,12 @@ AGigLineGameModeBase::AGigLineGameModeBase()
|
||||
// ODbL attribution must be visible in the running demo, not only in docs.
|
||||
HUDClass = AGigLineHUD::StaticClass();
|
||||
}
|
||||
|
||||
void AGigLineGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
|
||||
{
|
||||
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
|
||||
UE_LOG(LogTemp, Log, TEXT("GigLine GM: player %s connected; pawn=%s input=%s"),
|
||||
NewPlayer ? *NewPlayer->GetName() : TEXT("null"),
|
||||
NewPlayer && NewPlayer->GetPawn() ? *NewPlayer->GetPawn()->GetName() : TEXT("NONE"),
|
||||
NewPlayer && NewPlayer->PlayerInput ? TEXT("yes") : TEXT("no"));
|
||||
}
|
||||
@@ -18,4 +18,8 @@ class GIGLINE_API AGigLineGameModeBase : public AGameModeBase
|
||||
|
||||
public:
|
||||
AGigLineGameModeBase();
|
||||
|
||||
protected:
|
||||
/** Logs whether a local player connected to a pawn with input (QA). */
|
||||
virtual void HandleStartingNewPlayer_Implementation(class APlayerController* NewPlayer) override;
|
||||
};
|
||||
Reference in new issue
Block a user