UMG 시스템 소개와 위젯 구조
UMG의 Widget Blueprint 계층과 UserWidget 수명 주기를 이해하고 UI 요소의 속성과 이벤트를 구성합니다.
체력이 얼마나 남았는지, 탄약이 몇 발인지, 지금 어떤 버튼을 누를 수 있는지를 플레이어에게 알려 주는 것은 월드 안의 물체가 아니라 화면 위의 UI입니다. UI는 정보를 보여 주고, 클릭이나 키 입력을 받고, 그 결과를 게임에 전달합니다. 화면 크기가 달라져도 위치가 맞아야 하고, 게임의 값이 바뀌면 표시도 따라 바뀌어야 하므로, 단순히 그림을 그리는 것 이상의 구조가 필요합니다.
언리얼 엔진은 이를 위해 UMG(Unreal Motion Graphics) UI 디자이너를 제공합니다. 이 글에서는 UMG가 무엇이고 UI의 기본 단위인 위젯(widget)이 어떤 계층 구조를 이루는지 살펴본 뒤, 위젯 블루프린트와 그 부모인 UUserWidget, 그리고 C++에서 위젯을 참조하는 meta=(BindWidget)의 기본을 다룹니다.
UMG가 제공하는 것
UMG는 언리얼 엔진의 저수준 UI 프레임워크인 슬레이트(Slate) 위에 만든 시각적 UI 제작 도구입니다. 코드를 직접 작성하는 대신 UMG UI 디자이너에서 미리 만들어진 위젯을 조합해 UI를 만들 수 있어, 웹 개발에서 HTML 요소를 조합해 페이지를 만드는 것과 비슷한 방식으로 복잡한 화면을 빠르게 구성할 수 있습니다.
UMG의 디자이너에서는 위젯을 끌어다 놓아 배치하고 크기를 조절하며 레이아웃을 바로 확인할 수 있습니다. C++나 블루프린트의 게임 변수를 텍스트나 진행률 바 같은 위젯 속성에 연결하는 데이터 바인딩(data binding)도 지원해, UI가 값을 읽거나 변경 알림을 받아 표시하도록 구성할 수 있습니다. 다만 일반적인 속성 바인딩은 값이 바뀔 때만 계산되는 방식이 아니라는 점은 데이터 바인딩과 HUD 시스템에서 다시 봅니다.
UI 요소에 전환 효과나 시각적 반응을 주는 애니메이션도 UMG에 내장된 애니메이션 도구로 만들 수 있습니다. UI 로직은 블루프린트로 쉽게 만들고 C++와도 연결할 수 있으며, 새로운 커스텀 위젯을 C++로 만들어 UMG에 넣을 수도 있습니다. 화면 그리기에는 GPU 가속을 씁니다.
위젯과 그 계층 구조
위젯은 UMG UI를 이루는 가장 기본적인 단위입니다. 버튼, 텍스트, 이미지, 체크박스, 슬라이더처럼 UI 화면에 보이는 요소는 모두 위젯이며, 모든 위젯은 UObject를 상속한 UWidget 클래스를 상속합니다.
UMG의 UI는 웹 페이지의 DOM(문서 객체 모델)처럼 위젯이 위젯을 담는 트리 구조를 이룹니다. 자식을 담을 수 있는 패널이 위젯들을 배치하는데, 모든 위젯이 여러 자식을 가질 수 있는 것은 아닙니다. 예를 들어 버튼 같은 콘텐츠 위젯은 자식을 하나만 받습니다.
계층의 맨 위에는 루트 위젯(root widget)이 있으며, 흔히 UCanvasPanel 같은 패널을 루트로 둡니다. 패널 위젯(panel widget)은 자식 위젯들을 정해진 규칙에 따라 배치하는 위젯으로, UCanvasPanel, UHorizontalBox, UVerticalBox, UGridPanel, UScrollBox 등이 있습니다. 컨트롤 위젯(control widget)은 사용자 입력을 받거나 정보를 표시하는 개별 요소로, UButton, UTextBlock, UImage, UProgressBar, USlider 등이 있습니다.
간단한 HUD를 계층으로 그려 보면 다음과 같습니다.
- UMG_HUD (UserWidget)
- CanvasPanel (Root Widget, Panel Widget)
- VerticalBox (Panel Widget)
- TextBlock (PlayerName, Control Widget)
- ProgressBar (HealthBar, Control Widget)
- HorizontalBox (Panel Widget)
- Button (AttackButton, Control Widget)
- TextBlock (Button Text, Control Widget)
- Button (DefendButton, Control Widget)
- TextBlock (Button Text, Control Widget)
- Image (Crosshair, Control Widget)이런 계층 구조 덕분에 복잡한 UI를 나눠 관리하고, 레이아웃을 부분별로 조정하고, 특정 위젯에만 영향을 주는 애니메이션이나 상호작용을 만들 수 있습니다. 그런데 계층에서 자리가 다르면 설정할 수 있는 것도 달라집니다.
UNREAL C++ · CH6-1
위젯의 직접 부모가 배치 슬롯을 결정한다
CanvasPanel의 자식과 VerticalBox의 자식을 구별합니다. 화살표는 소속이며 각 위젯의 직접 부모에 맞는 슬롯 설정을 사용합니다.
원 전체 계층에서 일부를 발췌했습니다. Crosshair는 Canvas 슬롯, PlayerName과 HealthBar는 VerticalBox 슬롯에 속합니다. C++ 바인딩 예제의 텍스트 이름은 PlayerNameText이며 이 일반 계층 예시와 구별합니다.
위 그림은 앞 계층의 일부를 뽑아, 각 위젯의 직접 부모가 무엇인지 보여 줍니다. UMG_HUD 아래에 CanvasPanel이 있고, CanvasPanel의 직접 자식은 VerticalBox와 Crosshair입니다. VerticalBox의 직접 자식으로는 PlayerName과 HealthBar가 있습니다. 화살표는 부모에서 자식으로의 소속을 나타낼 뿐, 이벤트가 실행되는 순서나 생성 순서가 아닙니다. 그림에 빠진 HorizontalBox와 두 버튼은 위의 전체 계층에 그대로 있습니다.
중요한 점은 직접 부모가 그 위젯의 배치 슬롯(slot)을 정한다는 것입니다. 그림 아래의 설명처럼 Crosshair는 Canvas 슬롯에, PlayerName과 HealthBar는 VerticalBox 슬롯에 속합니다. 그래서 Crosshair의 Canvas 슬롯 앵커를 조절하는 일과 HealthBar의 VerticalBox 슬롯 정렬을 조절하는 일은 서로 다른 설정입니다. 또 뒤에 나올 C++ 바인딩 예제에서는 텍스트의 이름이 PlayerNameText이므로, 이 일반 계층 예의 PlayerName과 구별해야 합니다.
위젯 블루프린트와 UUserWidget
에디터에서 직접 만들고 디자인하는 UI는 위젯 블루프린트(Widget Blueprint) 에셋입니다. 이 에셋은 C++의 UUserWidget 클래스를 상속합니다. UUserWidget은 UMG의 중심이 되는 클래스로, UI를 화면에 표시하고 상호작용 로직을 처리하며 그 안에 들어 있는 위젯들을 관리합니다.
위젯 블루프린트는 콘텐츠 브라우저에서 마우스 오른쪽 버튼을 눌러 User Interface 메뉴의 Widget Blueprint를 고르고 이름(예: WBP_HUD)을 정해 만듭니다. 만든 에셋을 더블클릭하면 UMG UI 디자이너가 열립니다. 팔레트(Palette)에는 쓸 수 있는 기본 위젯이 모두 있어, 여기서 원하는 위젯을 디자인 화면으로 끌어다 놓습니다. 계층(Hierarchy) 패널은 현재 UI를 이루는 위젯의 트리를 보여 주므로 부모와 자식 관계를 확인하고 순서를 바꿀 수 있습니다. 디자이너(Designer) 탭은 위젯을 실제로 배치하고 레이아웃을 조절하는 작업 공간이고, 그래프(Graph) 탭은 클릭했을 때의 동작이나 데이터 갱신 같은 이벤트 로직을 블루프린트로 만드는 곳입니다.
위젯의 공통 속성과 이벤트
위젯에는 공통적으로 쓰이는 속성이 있습니다. 위치와 크기는 화면에서 위젯이 놓일 자리와 크기를 정합니다. 앵커(anchor)는 Canvas Panel의 자식 슬롯에서 기준점을 정하는 속성으로, 모든 패널에 있는 것은 아니며 다른 패널의 자식은 각 슬롯의 정렬과 크기 규칙을 씁니다. 패딩(padding)과 마진(margin)은 위젯 안쪽 내용과 테두리 사이, 또는 바깥의 다른 위젯과의 간격을 조절합니다.
가시성(Visibility)은 위젯이 보이는지, 숨겨져 있는지, 아예 자리도 차지하지 않는지를 정하고, Is Enabled는 위젯이 사용자 입력을 받을 수 있는지를 정합니다. 위젯은 버튼 클릭인 OnClicked, 마우스를 올렸을 때의 OnHovered, 텍스트가 바뀌었을 때의 OnTextChanged처럼 사용자의 상호작용에 응답하는 이벤트도 가지고 있으며, 그래프 탭에서 이 이벤트에 블루프린트 노드를 연결해 로직을 만듭니다.
C++와 UMG 연결의 기본
UMG는 블루프린트 중심의 시각적 디자인을 지향하지만, 복잡한 로직이나 성능이 중요한 부분, 여러 위젯이 함께 쓸 기반 위젯은 C++로 만드는 편이 효율적입니다. 방법은 UUserWidget을 상속하는 C++ 클래스(예: UMyBaseHUDWidget)를 만들고, 그 클래스를 부모로 하는 위젯 블루프린트를 만들어 기획자가 시각 요소를 더하고 세부 로직을 만들게 하는 것입니다.
C++ 위젯 클래스에서는 UPROPERTY와 UFUNCTION으로 변수와 함수를 블루프린트에 공개합니다. 특히 meta=(BindWidget) 지정자를 쓰면 위젯 블루프린트 안의 특정 위젯을 C++ 코드에서 직접 참조할 수 있습니다. 다음은 체력 바와 이름 텍스트를 C++에서 다루는 위젯입니다.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MyUserWidget.generated.h"
// UProgressBar와 UTextBlock 전방 선언 (헤더는 .cpp에서 포함)
class UProgressBar;
class UTextBlock;
UCLASS()
class MYPROJECT_API UMyUserWidget : public UUserWidget
{
GENERATED_BODY()
public:
// 생성자나 BeginPlay에서 위젯들을 가져오지 않고,
// meta=(BindWidget)을 사용하여 위젯 블루프린트의 이름과 일치하는 UPROPERTY를 선언하면,
// 자동으로 해당 위젯 인스턴스가 바인딩됩니다.
// 이 실습에서는 디자이너의 Is Variable도 활성화합니다.
UPROPERTY(meta = (BindWidget))
UProgressBar* HealthBar; // 위젯 블루프린트의 Progress Bar 위젯 이름이 'HealthBar'여야 함
UPROPERTY(meta = (BindWidget))
UTextBlock* PlayerNameText; // 위젯 블루프린트의 Text Block 위젯 이름이 'PlayerNameText'여야 함
// 플레이어의 체력을 업데이트하는 함수
UFUNCTION(BlueprintCallable, Category = "UI")
void UpdateHealth(float CurrentHealth, float MaxHealth);
protected:
virtual void NativeConstruct() override; // Slate 위젯 구성 시 호출되며 재구성 때 반복될 수 있음
};meta=(BindWidget)을 붙인 UPROPERTY는 위젯 블루프린트 안에서 같은 이름과 같은 타입을 가진 위젯에 자동으로 연결됩니다. 그래서 변수 이름이 위젯 블루프린트의 위젯 이름과 정확히 같아야 하고, 위젯 블루프린트의 부모 클래스도 이 C++ 클래스여야 합니다. 이 실습에서는 디자이너에서 각 위젯의 Is Variable도 켭니다.
#include "MyUserWidget.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
void UMyUserWidget::NativeConstruct()
{
Super::NativeConstruct();
// HealthBar와 PlayerNameText는 이미 meta=(BindWidget)으로 인해 자동으로 바인딩됩니다.
// 여기서 추가적인 초기화나 이벤트 바인딩을 할 수 있습니다.
if (HealthBar)
{
HealthBar->SetPercent(1.0f); // 초기 체력 100%
}
if (PlayerNameText)
{
PlayerNameText->SetText(FText::FromString(TEXT("PlayerOne"))); // 초기 플레이어 이름 설정
}
}
void UMyUserWidget::UpdateHealth(float CurrentHealth, float MaxHealth)
{
if (HealthBar)
{
// 진행률 바 업데이트 (0.0 ~ 1.0 사이의 값)
const float Percent = MaxHealth > 0.0f ? FMath::Clamp(CurrentHealth / MaxHealth, 0.0f, 1.0f) : 0.0f;
HealthBar->SetPercent(Percent);
}
if (PlayerNameText)
{
// 텍스트 업데이트 (예: 체력 수치도 같이 표시)
PlayerNameText->SetText(FText::Format(FText::FromString(TEXT("{0} ({1}/{2})")),
FText::FromString(TEXT("PlayerOne")),
FText::AsNumber(FMath::FloorToInt(CurrentHealth)),
FText::AsNumber(FMath::FloorToInt(MaxHealth))));
}
}NativeConstruct()는 위젯의 슬레이트 위젯이 구성될 때 불리는 함수로, 여기서는 체력 바를 100%로, 이름을 PlayerOne으로 초기화합니다. 이 함수는 슬레이트 위젯이 다시 만들어질 때 여러 번 불릴 수 있으므로, 인스턴스마다 한 번만 해야 하는 이벤트 바인딩은 NativeOnInitialized()에서 하고, NativeConstruct()에서는 화면을 다시 구성할 때 필요한 표시 갱신만 하는 편이 좋습니다.
UpdateHealth는 표시할 값이 유한한 실수라는 전제로 쓴 함수입니다. 최대 체력이 양수이면 현재 체력을 최대 체력으로 나눈 비율을 0에서 1 사이로 제한해 바에 넣고, 양수가 아니면 나눗셈을 하지 않고 0을 넣습니다. 텍스트에는 이름과 함께 두 체력 값을 내림한 정수로 표시합니다. 이 함수는 표시를 바꿀 뿐, 캐릭터의 실제 체력을 고치지 않습니다. 또 공개 헤더가 UUserWidget 타입을 쓰므로, 모듈의 .Build.cs 의존성 목록에 UMG를 추가해야 합니다.
예를 들어 현재 체력 75, 최대 체력 120을 UpdateHealth에 넘기면 비율은 75/120 = 0.625이므로 바는 62.5%를 표시하고, 텍스트는 PlayerOne (75/120)이 됩니다.
이 C++ 예를 따라 만들 때는 UMyUserWidget을 부모로 하는 위젯 블루프린트에 ProgressBar인 HealthBar와 TextBlock인 PlayerNameText를 같은 이름으로 배치하고 Is Variable을 켭니다. 앞의 일반 계층 예에 나온 PlayerName을 그대로 쓰면 이름이 맞지 않아 연결되지 않습니다. 이 위젯을 실제로 화면에 만들고 붙이는 방법은 다음 글에서 다룹니다.
정리
- UMG는 슬레이트 위에 만든 시각적 UI 제작 도구로, 위젯을 조합해 UI를 만들고 데이터 바인딩, 애니메이션, 블루프린트와 C++ 연결을 지원합니다.
- 위젯은
UWidget을 상속하며, 루트 패널 아래에 패널 위젯과 컨트롤 위젯이 트리 구조를 이룹니다. 버튼 같은 콘텐츠 위젯은 자식을 하나만 받습니다. - 위젯의 직접 부모가 배치 슬롯을 정하므로, Canvas 슬롯의 앵커와 VerticalBox 슬롯의 정렬은 서로 다른 설정입니다.
- 위젯 블루프린트는
UUserWidget을 상속하며, 디자이너의 팔레트, 계층, 디자이너 탭, 그래프 탭으로 UI와 로직을 만듭니다. meta=(BindWidget)은 같은 이름과 타입의 위젯에 C++ 변수를 연결하며, 부모 클래스와 이름이 정확히 맞아야 합니다.NativeConstruct()는 반복될 수 있으므로 한 번만 할 바인딩은NativeOnInitialized()에서 하고, 모듈에는UMG의존성을 추가합니다.
연습 문제
1. 최대 체력보다 큰 값
유한한 현재 체력 150과 최대 체력 100을 UpdateHealth에 넘기면 바와 텍스트는 각각 무엇을 표시할까요? 바가 100%라는 이유로 캐릭터의 현재 체력도 100으로 바뀐 것일까요?
해설 보기
비율 1.5가 1로 제한되므로 바는 100%를 표시하고, 텍스트는 넘긴 값을 그대로 내림해 PlayerOne (150/100)을 표시합니다.
이 함수에는 입력 값을 고치는 코드가 없으므로 캐릭터의 체력은 150 그대로이고, 100으로 바뀌지 않습니다. 바의 제한은 표시를 위한 것일 뿐입니다.
2. 최대 체력이 0이라면
최대 체력 0을 넘기면 UpdateHealth는 어느 분기를 탈까요? 이 분기가 잘못된 게임 설정을 고쳐 줄까요?
해설 보기
MaxHealth > 0.0f가 거짓이므로 나눗셈을 하지 않고 바에 0을 넣는 분기를 탑니다. 0으로 나누는 문제를 피할 뿐, 최대 체력이 0으로 설정된 원인을 고치지는 않습니다. 텍스트에는 여전히 (현재값/0)이 표시됩니다.
게임 상태가 유효한지는 위젯이 아니라 체력을 가진 쪽에서 따로 검사하고 처리해야 합니다.
3. 연결되지 않는 체력 바
UMyUserWidget을 부모로 위젯 블루프린트를 만들고 ProgressBar의 이름을 HPBar로 정했습니다. C++ 빌드는 성공했는데 위젯 블루프린트를 컴파일하니 오류가 납니다. 원인은 무엇일까요?
해설 보기
meta=(BindWidget)은 변수 이름 HealthBar와 같은 이름의 위젯을 찾아 연결하므로, 이름이 HPBar이면 연결할 위젯을 찾지 못합니다. 위젯 블루프린트의 ProgressBar 이름을 HealthBar로 바꾸고 타입이 ProgressBar인지, Is Variable이 켜져 있는지 확인합니다.
BindWidget은 반드시 있어야 하는 연결이므로, 같은 이름의 위젯이 없으면 위젯 블루프린트를 컴파일할 때 필요한 위젯을 찾지 못했다는 오류가 납니다. 위젯이 없어도 되는 선택적 연결이 필요하다면 BindWidgetOptional을 쓰는데, 이때는 포인터가 비어 있을 수 있으므로 코드의 if (HealthBar) 같은 검사가 꼭 필요합니다.
UMG와 위젯 블루프린트의 구성은 Epic의 Widget Blueprints 문서에서, UUserWidget의 함수는 UUserWidget API 문서에서 확인할 수 있습니다.
다음 글 C++에서 위젯 생성 및 제어에서는 C++에서 위젯을 만들어 화면에 붙이고, 표시를 바꾸고, 제거하는 방법을 살펴봅니다.