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:
zwitschi committed 2026-11-08 11:56:19 +01:00
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);
};
+306 -38
View File
@@ -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;
};