
HUD 액터는 게임 모드(Game Mode)에 의해 레벨에 스폰되는 액터입니다(직접 수동으로 스폰하는 것은 권장하지 않으며, 게임 모드 기능을 오버라이드하고 동작을 정확히 이해하는 경우에만 예외적으로 사용하세요).
이 액터들은 네트워크 복제가 되지 않으며, 기본적으로 화면에 보이지 않고, 각 로컬 플레이어 컨트롤러마다(분할 화면 지원을 위해) 1개씩만 스폰됩니다. 전용 서버(Dedicated Server)에서는 스폰되지 않습니다.
HUD 액터의 목적은 언리얼 엔진 3에서는 플레이어의 UI 요소를 담당하는 주요 기능 제공자였으나, 언리얼 엔진 4(그리고 5)부터는 UMG와 Slate의 도입으로 인해 UI의 모든 기능을 직접 처리하기보다는 UMG/Slate UI 요소를 관리하는 매니저 객체로 역할이 변경되었습니다. 즉, HUD 액터는 UI의 모든 동작을 직접 구현하는 것이 아니라, UMG/Slate 기반 UI를 관리하는 용도로 사용되며, 반드시 사용해야 하는 필수 요소는 아닙니다. 프로젝트 구조에 따라 자유롭게 설계할 수 있습니다.
HUD 액터는 특정 플레이어 컨트롤러에 연결되어 있으며, 디버그 모드 정보와 수동으로 UI 요소를 화면에 그릴 수 있는 기능을 제공합니다.
그릴 수 있는 UI 요소 목록은 다음과 같습니다:
중요한 점: HUD 액터가 이미지를 비롯한 UI 요소를 그리는 방식은, UMG가 사용하는 여러 단계를 건너뛰고 캔버스에 직접 그리도록 명령하는 방식입니다.
HUD 액터의 흥미로운 기능 중 하나는 화면의 특정 영역이 마우스 오버/클릭/프레스/릴리즈 되었는지 감지할 수 있다는 점입니다.
이 기능은 FHUDHitBox라는 클래스를 통해 구현됩니다.
FHUDHitBox는 C++에서만 사용할 수 있는 클래스이며, 주요 속성은 다음과 같습니다:
FVector2D: 히트 박스의 좌상단 좌표FVector2D: 히트 박스의 크기FName: 히트 박스의 이름boolean: 입력 소비 여부(실제 게임 입력을 소비하지는 않음)int32: 히트 박스의 우선순위(또는 Z-Order). 값이 높을수록 우선 처리됨FHUDHitBox를 사용하면 HUD에서 마우스 이벤트를 처리할 수 있으며, 각 이벤트는 해당 히트 박스의 이름을 제공합니다:
HUD 액터는 런타임에 히트 박스를 추가/제거할 수 있는 배열을 가지고 있습니다.
이 배열은 모든 HUD 액터에서 public 변수로 선언되어 있지만, C++에서만 접근할 수 있습니다:
HitBoxMap(이름은 Map이지만 실제로는 배열입니다).
히트 박스를 추가하는 방법은 두 가지가 있습니다:
AddHitBox 함수를 호출하여 파라미터를 전달하면 내부에서 FHUDHitBox 객체가 생성되어 추가됨FHUDHitBox 객체를 생성한 뒤 HitBoxMap 배열에 수동으로 추가히트 박스를 제거하려면 HitBoxMap 배열에서 해당 객체를 삭제하면 됩니다.
위젯 컴포넌트는 UMeshComponent(메시를 렌더링할 수 있는 액터 프리미티브 컴포넌트)로,
실제로는 위젯이 그려진 텍스처를 적용한 프로시저럴 스태틱 메시를 월드에 생성합니다.
이때 위젯의 렌더 타겟은 GetRenderTarget 함수로 접근할 수 있습니다.
위젯 컴포넌트는 전용 서버(Dedicated Server)에서는 Tick이 동작하지 않습니다.
이는 대부분의 기능이 User Widget의 렌더링 갱신에 집중되어 있기 때문입니다.
또한 컴포넌트의 충돌도 User Widget을 기반으로 처리되므로, 위젯이 서버에서 스폰되지 않으면 충돌도 정상 동작하지 않습니다.
위젯 컴포넌트 사용 시 성능에 큰 영향을 주는 점은, 각 위젯 컴포넌트마다 렌더 타겟이 생성되어 컴포넌트의 Tick마다 갱신된다는 것입니다.
예를 들어, 고해상도 위젯 컴포넌트 100개를 사용하면 매 프레임마다 100개의 렌더 타겟이 갱신됩니다.
(렌더 타겟 갱신을 "화면에 그릴 때만"으로 설정해도 GPU 메모리는 계속 사용됨).
이런 GPU 메모리 사용 문제를 피하기 위한 대표적인 상황별 대안(이외에도 다양한 방법이 존재함, 아래는 경험에 기반한 추천 예시임)은 다음과 같습니다:
위젯 컴포넌트와 호환되는 머티리얼의 텍스처 파라미터는 다음과 같습니다:
SlateUI: 위젯의 렌더 타겟이 입력됨TintColorAndOpacity: 위젯 컴포넌트의 TintColorAndOpacity 속성이 입력됨OpacityFromTexture: 위젯 컴포넌트의 OpacityFromTexture 속성이 입력됨중요한 점은, 위젯 컴포넌트는 실제로 User Widget을 생성한다는 것입니다.
User Widget 객체는 GetUserWidgetObject로 접근할 수 있고,
Slate 위젯(SWidget)은 GetSlateWidget으로 얻을 수 있습니다.
GetUserWidgetObject는 액터의 Construction Script에서는 사용할 수 없습니다.
위젯은BeginPlay이후에만 유효합니다.
기본적으로 위젯 컴포넌트는 게임 인스턴스에서 첫 번째 로컬 플레이어를 가져와 사용합니다:
if (UWorld* LocalWorld = GetWorld())
{
UGameInstance* GameInstance = LocalWorld->GetGameInstance();
check(GameInstance);
return GameInstance->GetFirstGamePlayer();
}
다른 로컬 플레이어를 사용하려면 SetOwnerPlayer로 ULocalPlayer를 지정할 수 있습니다.
월드 상의 위치를 위젯 컴포넌트의 2D 평면(위젯 공간) 좌표로 변환하려면 GetLocalHitLocation을 사용할 수 있습니다.
이 함수는 다음과 같은 수학적 연산을 통해 좌표를 계산합니다:
- 월드 위치를 위젯 컴포넌트의 상대 공간으로 변환(
InverseTransformPosition)- 상대 위치의 -Y값을 2D X축, -Z값을 2D Y축으로 사용해 2D 좌표 생성
- 2D X축에 현재 Draw Size의 X값 * Pivot의 X값을 더함
- 2D Y축에 현재 Draw Size의 Y값 * Pivot의 Y값을 더함
- 2D 좌표를 현재 Draw Size로 나누어 정규화된 위치를 캐싱
- 2D Y축을 Draw Size의 Y값 * 정규화된 Y값으로 갱신(포물선 왜곡 보정)
위젯 컴포넌트와 상호작용하려면 Widget Interaction Component를 사용해야 합니다.
이 컴포넌트는 레이저 포인터 방식으로 사용자 입력 및 마우스 포인터(또는 가상 손가락 끝) 입력을 시뮬레이션하도록 설계되었습니다.
각 Widget Interaction Component는 자체적으로 가상 유저(Virtual User)를 가지며, 이 유저가 Slate 위젯에 입력을 제공합니다.
컴포넌트가 활성화되면 실제로 FSlateUser가 생성되어 입력을 시뮬레이션합니다.
엔진은 기본적으로 Slate User Index 8(허용되는 최대 인덱스)부터 사용하며, 0부터 시작하지 않고 순차적으로 증가시켜 실제 Slate User와 가상 유저가 충돌하지 않도록 합니다.
Widget Interaction Component는 틱(Tick)마다 라인 트레이스를 통해 어떤 위젯과 상호작용 중인지 판단합니다.
UWidgetInteractionComponent::TickComponent에서 컴포넌트가 라인 트레이스를 수행하는 순서는 다음과 같습니다:
UWidgetInteractionComponent::SimulatePointerMovement
bEnableHitTesting이 활성화되어 있는지 확인하여 히트 테스트 가능 여부를 판단합니다.CanSendInput에서 입력을 보낼 수 있는지 확인합니다.
- Slate Application이 초기화되었는지, 가상 유저가 세팅되었는지 검사합니다.
UWidgetInteractionComponent::DetermineWidgetUnderPointer
- 이전에 호버 중이던 위젯 컴포넌트를 캐싱합니다.
UWidgetInteractionComponent::PerformTrace
InteractionSource타입에 따라 라인 트레이스를 수행합니다:
- World: 컴포넌트의 위치에서 전방 방향으로
TraceChannel을 사용해 멀티 라인 트레이스. 소유 액터의 컴포넌트(위젯 컴포넌트 제외)는 무시합니다.- Mouse: 마우스 위치를 스크린에서 월드로 디프로젝션하여
TraceChannel로 멀티 라인 트레이스.- Center Screen: 뷰포트 중앙을 스크린에서 월드로 디프로젝션하여
TraceChannel로 멀티 라인 트레이스.- Custom: 런타임에 설정 가능한
CustomHitResult의 속성을 사용합니다.
이 구조체는 첫 틱 프레임에 아직 값이 세팅되지 않았을 수 있으므로, BeginPlay에서 미리 세팅하는 것이 안전합니다.- Custom 타입이 아니라면, 비가시성 위젯을 필터링합니다.
- 히트된 위젯 컴포넌트와 히트 결과를 받아 위젯 공간(2D)상의 히트 위치를 반환합니다.
- 반환된 위젯 공간 위치로부터
FWidgetPath를 찾습니다.PerformTrace의 결과를 반환합니다.- 새로 호버된 위젯 컴포넌트에 다시 그리기를 요청합니다.
- 트레이스에서 얻은 위젯 경로의 위젯들을 순회하며, 최종 위젯에 대해 다음 플래그를 업데이트합니다:
bIsHoveredWidgetInteractable: 상호작용 가능한 위젯인지bIsHoveredWidgetFocusable: 키보드 포커스가 가능한 위젯인지bIsHoveredWidgetHitTestVisible: 히트 테스트가 가능한 위젯인지- 새로 호버된 위젯 컴포넌트가 이전과 다르다면, 이전 컴포넌트에도 다시 그리기를 요청하고,
OnHoveredWidgetChanged를 브로드캐스트합니다.- 트레이스에서 얻은 위젯 경로를 반환합니다.
- Slate Application에 입력이 시뮬레이션되고 있음을 알리거나, 더 이상 호버 중인 위젯이 없다면 포인터가 이전 위젯에서 벗어났음을 알립니다.
사용자 위젯을 UTextureRenderTarget2D에 그리는 일반적인 과정(Widget Component가 내부적으로 수행하는 방식과 동일하며, 컴포넌트이기 때문에 씬 프록시 등 추가 작업이 필요함)은
FWidgetRenderer를 생성한 뒤, 사용자 위젯에서 Slate 위젯을 얻어 위젯 렌더러로 텍스처로 그리는 것입니다.
아래는 FWidgetRenderer를 사용해 UUserWidget을 텍스처로 만드는 예시 코드입니다(FWidgetRenderer::DrawWindow와 FWidgetRenderer::DrawWidget에는 여러 구현이 있으니 참고용으로 보세요):
bool UExampleFunctionLibrary::DrawWidgetToTarget(UTextureRenderTarget2D*& DrawnWidgetRenderTarget,
UUserWidget* WidgetToRender, const FVector2D DrawSize, const float DeltaTime)
{
// 블루프린트 사용자들이 이전 호출의 값을 재사용하지 않도록 변수 초기화
DrawnWidgetRenderTarget = nullptr;
// 렌더 타겟에 사용할 유효한 위젯인지 확인
if(!IsValid(WidgetToRender))
{
UE_LOG(LogExampleFunctionLibrary, Error, TEXT("UExampleFunctionLibrary::DrawWidgetToTarget: Inputted NULL WidgetToRender"));
return false;
}
// 유효한 DrawSize인지 확인
if(DrawSize.X <= 0 || DrawSize.Y <= 0)
{
UE_LOG(LogExampleFunctionLibrary, Error, TEXT("UExampleFunctionLibrary::DrawWidgetToTarget: Inputted INVALID DrawSize(%s)"), *DrawSize.ToString());
return false;
}
// User Widget을 Outer로 하여 렌더 타겟 객체 생성(안전성 확보 목적)
DrawnWidgetRenderTarget = NewObject<UTextureRenderTarget2D>(WidgetToRender);
// 렌더 타겟의 크기와 포맷 설정
DrawnWidgetRenderTarget->InitCustomFormat(DrawSize.X, DrawSize.Y,
FSlateApplication::Get().GetRenderer()->GetSlateRecommendedColorFormat(),
true); // false로 하면 선형 감마를 사용하지 않아 색상이 정확하지 않을 수 있음
// Slate 렌더러와 통신하여 위젯을 텍스처로 그리는 객체
FWidgetRenderer* WidgetRenderer = new FWidgetRenderer(true, false); // FWidgetRenderer(bool bUseGammaCorrection = false, bool bInClearTarget = true)
WidgetRenderer->DrawWidget(DrawnWidgetRenderTarget,
WidgetToRender->TakeWidget(),
DrawSize,
DeltaTime,
false); // 이 파라미터가 매우 중요: 즉시 렌더 타겟을 업데이트할지 여부
// bDeferRenderTargetUpdate: true면 프레임 끝에 업데이트(성능 최적화), false면 즉시 업데이트
// GPU가 즉시 위젯을 그려서 텍스처 정보가 채워지도록 렌더링 명령을 강제로 플러시
FlushRenderingCommands(); // 또 다른 강제 렌더링 방법
// 위젯 렌더러를 렌더 명령 큐가 플러시된 후에 삭제하도록 지연 정리
BeginCleanup(WidgetRenderer);
return true;
}
월드 공간에 위젯을 그릴 때는 위 코드와 유사하지만, DrawWidget 대신 DrawWindow를 직접 사용합니다.
위젯을 스크린 공간에 그릴 때는, User Widget을 위젯 컴포넌트만을 처리하는 별도의 게임 레이어 위젯(FWorldWidgetScreenLayer)에 추가합니다.
이 레이어는 SWorldWidgetScreenLayer와 직접 통신하여 화면에 위젯을 표시하는 역할을 합니다.