실시간 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 이벤트 구현:
Event OnDataUpdated→New Data핀 →Break FAgentHudDataEvent OnDataUpdated→Sequence(실행 흐름)- 각
Then핀 →Set Text→Target에 각 TextBlock 변수 연결 Break에서 나온 값 →To Text (Float/Integer)→Set Text의In Text연결
핵심: 실행 흐름(흰 핀)은 Sequence, 데이터 흐름(색깔 핀)은 Break 에서 직접 연결
HUD 모드 분기 (로그 전용 / 게임 오버):
E_HudModeEnumeration 생성 (LogOnly, GameOver)- WBP 변수
HudMode추가 Event Construct→Switch 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 Init 이 Parent: Init 을 호출하지 않아 C++ Init() 미실행
해결: BP 그래프 → Event Init 우클릭 → Add call to Parent Function → Parent: 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 | 가능 |
'학습일지 > 언리얼' 카테고리의 다른 글
| 오브젝트 풀링(Object Pooling) (0) | 2026.05.29 |
|---|---|
| UE5 패키징 & 로딩 스크린 (0) | 2026.05.27 |
| 차량 AI 카메라 무빙 + 사운드 구현기 (0) | 2026.05.19 |
| 차량 AI 스플라인 추종 + 브레이크 등 + 헤드라이트 + 사이드미러 구현 (0) | 2026.05.18 |
| 언리얼 프레임워크 (0) | 2026.05.04 |