학습일지/언리얼

UE5 실시간 HUD 연동 & 로딩 화면 구현 TIL

Tsukino Ren 2026. 5. 26. 23:00

실시간 HUD 연동

목표

AgentDataLogger 컴포넌트에서 수집하는 차량 데이터(속도, 조향각, 가속도, UTM 좌표 등)를 UMG 위젯에 실시간으로 표시


구조 설계

처음에는 AHUD 클래스를 사용하려 했으나, UE5 실무에서는 AHUD 를 거의 사용하지 않는다.
현재 표준은 UUserWidget 단독으로 HUD를 처리하는 방식이다.

또한 위젯에서 직접 FindComponentByClass 로 DataLogger를 찾는 방식보다,
GameInstanceSubsystem 에 레퍼런스를 등록해두고 어디서든 꺼내쓰는 방식이 실무 표준이다.

최종 구조:

UAgentDataLogger (BeginPlay)
 └─ Subsystem에 자신을 Register

UAgentLoggerSubsystem (GameInstance 수명)
 └─ TWeakObjectPtr<UAgentDataLogger> 보관

UAgentHudWidget (NativeTick)
 └─ Subsystem → GetLogger() → 데이터 폴링
 └─ OnDataUpdated() → 블루프린트에서 텍스트 갱신
 └─ RestartLevel() / GoToLobby() 버튼 연결

AgentLoggerSubsystem 구현

// AgentLoggerSubsystem.h
UCLASS()
class ANTARCITCKIDS_API UAgentLoggerSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category = "AgentLogger")
    void RegisterLogger(UAgentDataLogger* InLogger);

    UFUNCTION(BlueprintCallable, Category = "AgentLogger")
    void UnregisterLogger(UAgentDataLogger* InLogger);

    UFUNCTION(BlueprintPure, Category = "AgentLogger")
    UAgentDataLogger* GetLogger() const { return ActiveLogger.Get(); }

private:
    TWeakObjectPtr<UAgentDataLogger> ActiveLogger;
};
// AgentLoggerSubsystem.cpp
void UAgentLoggerSubsystem::RegisterLogger(UAgentDataLogger* InLogger)
{
    if (InLogger)
    {
        ActiveLogger = InLogger;
    }
}

void UAgentLoggerSubsystem::UnregisterLogger(UAgentDataLogger* InLogger)
{
    if (ActiveLogger.Get() == InLogger)
    {
        ActiveLogger = nullptr;
    }
}

TWeakObjectPtr 을 쓰는 이유
일반 포인터로 보관하면 액터가 삭제됐을 때 댕글링 포인터가 되어 크래시 발생
WeakObjectPtr 은 참조 대상이 사라지면 자동으로 nullptr 반환


AgentDataLogger 수정 - Subsystem 등록/해제

// BeginPlay() 에 추가
if (const UGameInstance* GI = GetWorld()->GetGameInstance())
{
    if (UAgentLoggerSubsystem* Sub = GI->GetSubsystem<UAgentLoggerSubsystem>())
    {
        Sub->RegisterLogger(this);
    }
}

// EndPlay() 에 추가
if (const UGameInstance* GI = GetWorld()->GetGameInstance())
{
    if (UAgentLoggerSubsystem* Sub = GI->GetSubsystem<UAgentLoggerSubsystem>())
    {
        Sub->UnregisterLogger(this);
    }
}

AppendRow() 에 캐시 변수 추가:

CachedSpeedKmh    = SpeedKmh;       // SpeedKmh 계산 직후
CachedUtmEasting  = UtmEasting;     // WorldToUtm() 호출 직후
CachedUtmNorthing = UtmNorthing;

public getter 추가:

UFUNCTION(BlueprintPure, Category = "Data Logger|Runtime")
double GetCurrentSpeedKmh()      const { return CachedSpeedKmh; }

UFUNCTION(BlueprintPure, Category = "Data Logger|Runtime")
double GetCurrentUtmEasting()    const { return CachedUtmEasting; }

UFUNCTION(BlueprintPure, Category = "Data Logger|Runtime")
double GetCurrentUtmNorthing()   const { return CachedUtmNorthing; }

UFUNCTION(BlueprintPure, Category = "Data Logger|Runtime")
int32  GetOriginUtmZone()        const { return OriginUtmZone; }

UFUNCTION(BlueprintPure, Category = "Data Logger|Runtime")
float  GetElapsedRecordingTime() const { return ElapsedRecordingTime; }

AgentHudWidget 구현

// AgentHudWidget.h
USTRUCT(BlueprintType)
struct FAgentHudData
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadOnly) double SpeedKmh         = 0.0;
    UPROPERTY(BlueprintReadOnly) double SteeringLeft     = 0.0;
    UPROPERTY(BlueprintReadOnly) double SteeringRight    = 0.0;
    UPROPERTY(BlueprintReadOnly) double AccelerationMps2 = 0.0;
    UPROPERTY(BlueprintReadOnly) double DecelerationMps2 = 0.0;
    UPROPERTY(BlueprintReadOnly) double TotalDistanceM   = 0.0;
    UPROPERTY(BlueprintReadOnly) double UtmEasting       = 0.0;
    UPROPERTY(BlueprintReadOnly) double UtmNorthing      = 0.0;
    UPROPERTY(BlueprintReadOnly) int32  UtmZone          = 0;
    UPROPERTY(BlueprintReadOnly) float  ElapsedTime      = 0.0f;
    UPROPERTY(BlueprintReadOnly) bool   bIsRecording     = false;
};

UCLASS()
class ANTARCITCKIDS_API UAgentHudWidget : public UUserWidget
{
    GENERATED_BODY()
protected:
    virtual void NativeConstruct() override;
    virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;

public:
    UPROPERTY(BlueprintReadOnly, Category = "HUD")
    FAgentHudData CurrentData;

    UFUNCTION(BlueprintCallable, Category = "HUD|Navigation")
    void RestartLevel();

    UFUNCTION(BlueprintCallable, Category = "HUD|Navigation")
    void GoToLobby();

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HUD|Navigation")
    FName LobbyLevelName = TEXT("MainMenu");

protected:
    UFUNCTION(BlueprintImplementableEvent, Category = "HUD")
    void OnDataUpdated(const FAgentHudData& NewData);

private:
    UPROPERTY()
    UAgentLoggerSubsystem* LoggerSubsystem = nullptr;

    void PollDataLogger();
};
// AgentHudWidget.cpp
void UAgentHudWidget::NativeConstruct()
{
    Super::NativeConstruct();

    if (const UGameInstance* GI = GetGameInstance())
    {
        LoggerSubsystem = GI->GetSubsystem<UAgentLoggerSubsystem>();
    }
}

void UAgentHudWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
    Super::NativeTick(MyGeometry, InDeltaTime);
    PollDataLogger();
}

void UAgentHudWidget::PollDataLogger()
{
    if (!LoggerSubsystem) return;

    UAgentDataLogger* Logger = LoggerSubsystem->GetLogger();
    if (!Logger || !Logger->IsRecording())
    {
        CurrentData.bIsRecording = false;
        OnDataUpdated(CurrentData);
        return;
    }

    CurrentData.SpeedKmh         = Logger->GetCurrentSpeedKmh();
    CurrentData.SteeringLeft     = Logger->GetSteeringLeftValue();
    CurrentData.SteeringRight    = Logger->GetSteeringRightValue();
    CurrentData.AccelerationMps2 = Logger->GetAccelerationsMps2();
    CurrentData.DecelerationMps2 = Logger->GetDecelerationMps2();
    CurrentData.TotalDistanceM   = Logger->GetTotalDistanceM();
    CurrentData.UtmEasting       = Logger->GetCurrentUtmEasting();
    CurrentData.UtmNorthing      = Logger->GetCurrentUtmNorthing();
    CurrentData.UtmZone          = Logger->GetOriginUtmZone();
    CurrentData.ElapsedTime      = Logger->GetElapsedRecordingTime();
    CurrentData.bIsRecording     = true;

    OnDataUpdated(CurrentData);
}

void UAgentHudWidget::RestartLevel()
{
    UGameplayStatics::OpenLevel(this,
        *UGameplayStatics::GetCurrentLevelName(this, true));
}

void UAgentHudWidget::GoToLobby()
{
    if (!LobbyLevelName.IsNone())
    {
        UGameplayStatics::OpenLevel(this, LobbyLevelName);
    }
}

블루프린트 연동 (WBP_AgentHUD)

OnDataUpdated 이벤트 구현:

  1. Event OnDataUpdatedNew Data 핀 → Break FAgentHudData
  2. Event OnDataUpdatedSequence (실행 흐름)
  3. Then 핀 → Set TextTarget 에 각 TextBlock 변수 연결
  4. Break 에서 나온 값 → To Text (Float/Integer)Set TextIn Text 연결

핵심: 실행 흐름(흰 핀)은 Sequence, 데이터 흐름(색깔 핀)은 Break 에서 직접 연결

HUD 모드 분기 (로그 전용 / 게임 오버):

  • E_HudMode Enumeration 생성 (LogOnly, GameOver)
  • WBP 변수 HudMode 추가
  • Event ConstructSwitch on E_HudMode → 버튼 SetVisibility 분기

로딩 화면 구현

목표

레벨 전환 시 로딩 화면을 표시하여 나이아가라 등 에셋 로딩 시간 동안 화면이 튀는 현상 방지


GameInstance 에 Movie Player 연동

기존 UDigitalTwinConfiguration (GameInstance) 에 로딩 화면 기능 추가

// DigitalTwinConfiguration.h 추가
class ULoadingWidget; // 전방선언

virtual void Init() override;

UPROPERTY(EditDefaultsOnly, Category = "Loading")
TSubclassOf<ULoadingWidget> LoadingWidgetClass;

void BeginLoadingScreen(const FString& MapName);
void EndLoadingScreen(UWorld* InLoadedWorld);
// DigitalTwinConfiguration.cpp
#include "MoviePlayer.h"
#include "Blueprint/UserWidget.h"
#include "Widget/LoadingWidget.h"
#include "Widgets/SWeakWidget.h"

void UDigitalTwinConfiguration::Init()
{
    Super::Init();

    FCoreUObjectDelegates::PreLoadMap.AddUObject(
        this, &UDigitalTwinConfiguration::BeginLoadingScreen);
    FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(
        this, &UDigitalTwinConfiguration::EndLoadingScreen);
}

void UDigitalTwinConfiguration::BeginLoadingScreen(const FString& MapName)
{
    FLoadingScreenAttributes LoadingScreen;
    LoadingScreen.bAutoCompleteWhenLoadingCompletes = true;
    LoadingScreen.MinimumLoadingScreenDisplayTime = 2.0f;

    if (LoadingWidgetClass)
    {
        TSharedRef<SWidget> Widget =
            SNew(SWeakWidget).PossiblyNullContent(
                CreateWidget<ULoadingWidget>(this, LoadingWidgetClass)->TakeWidget()
            );
        LoadingScreen.WidgetLoadingScreen = Widget;
    }

    GetMoviePlayer()->SetupLoadingScreen(LoadingScreen);
}

void UDigitalTwinConfiguration::EndLoadingScreen(UWorld* InLoadedWorld)
{
    // 추후 필요 시 구현
}

Build.cs 에 MoviePlayer 모듈 추가 필요

"MoviePlayer"

WBP_LoadingWidget 구성

[Root] Overlay (Fill Screen)
 ├─ Image (배경 - Fill)
 └─ Vertical Box (Center 정렬)
     ├─ SizeBox → Image (로고)
     ├─ Spacer
     ├─ SizeBox → Image (트럭 이미지)
     ├─ Spacer
     └─ SizeBox (Width 800, Height 30) → ProgressBar
 └─ TextBlock "Loading..." (Bottom 고정, Padding 으로 위치 조정)
 └─ Circular Throbber (스피너)

애니메이션 (LoadingAnim):

  • ProgressBar Percent: 0초 → 0.0 / 5초 → 0.9
  • 트럭 이미지 Translation X: 왼쪽 → 오른쪽 이동

Loading... 텍스트 점 애니메이션 (블루프린트):

Integer 변수 DotCount
Event Tick → Delay 0.5 → DotCount + 1 → % 4 → Set DotCount
DotCount → Switch on Int
 0 → Set Text "Loading"
 1 → Set Text "Loading."
 2 → Set Text "Loading.."
 3 → Set Text "Loading..."

트러블슈팅

1. GameInstance Init() 미호출

증상: UE_LOG 가 전혀 안 찍힘
원인: BP_DigitalTwinConfiguration 에서 Event InitParent: Init 을 호출하지 않아 C++ Init() 미실행
해결: BP 그래프 → Event Init 우클릭 → Add call to Parent FunctionParent: Init 연결


2. Movie Player 로딩 화면이 에디터에서 안 뜸

증상: BeginLoadingScreen, SetupLoadingScreen 모두 호출되는데 화면 미표시
원인: Movie Player 로딩 화면은 에디터 PIE 환경에서 Epic 이 의도적으로 비활성화
결론: 패키징 전용 기능. 에디터 테스트는 불가


3. Load Stream Level 방식으로 로딩화면 시도

시도: Open Level 대신 Load Stream Level 로 교체하면 현재 레벨 유지되어 위젯 표시 가능
문제: Load Stream Level 은 서브레벨 방식이라 완전한 레벨 전환이 안 됨
결론: 완전한 레벨 전환이 필요하면 Open Level 사용. 에디터 로딩 화면은 BeginPlay + Delay 방식으로 임시 구현


4. Niagara Stateless 에러로 디버거 멈춤

증상: Ensure condition failed: CountOffset != INDEX_NONE [NiagaraStatelessComputeManager.cpp]
원인: Rider 디버거가 붙은 상태에서 Ensure 오류 발생 시 강제로 멈춤
해결: Rider 디버거 없이 언리얼 에디터 플레이 버튼으로 실행하면 정상 동작
참고: UE 5.5 Stateless Niagara 관련 알려진 이슈


5. UnrealBuildTool failed with exit code 0xe0434352

원인: UnrealBuildTool 프로세스가 이미 실행 중인 상태에서 빌드 시도
해결: 작업 관리자에서 UnrealBuildTool 프로세스 강제 종료 후 재빌드


정리

기능 구현 방식 에디터 테스트
HUD 데이터 연동 Subsystem + NativeTick 가능
HUD 버튼 (Quit/Lobby) BlueprintCallable 함수 가능
로딩 화면 Movie Player 패키징 전용
로딩 화면 (임시) BeginPlay + Delay 가능