
UMG 프레임워크는 UObject 기반으로 동작하며, 반드시 특정 Player Controller(분할 화면을 고려해 Owning Player라고 부름)에 연결되어야 합니다.
Owning Player를 지정하지 않으면 자동으로 레벨의 첫 번째 로컬 플레이어에 연결됩니다.
UMG의 객체 계층 구조는 다음과 같습니다:
User Widget은 여러 위젯으로 구성되지만, 반드시 루트 위젯이 필요하지는 않습니다.
(참고: Actor가 여러 Actor Component로 구성되지만 반드시 루트 컴포넌트가 필요한 것과는 다름)
User Widget은 Actor가 컴포넌트 계층 구조를 상속받는 것과 달리 Widget Hierarchy(위젯 계층 구조)를 상속받을 수는 없습니다.
하지만 클래스 기능은 상속할 수 있으므로, User Widget을 추상 클래스로 만들어 다른 클래스가 상속받게 하거나, C++로 클래스를 만들어 상속 구조를 설계할 수 있습니다.
각 User Widget은 설계상 루트 UWidget입니다. 즉, User Widget은 내부에 위젯이 하나도 없을 수도 있으며, 기본적으로 Compound Widget(복합 위젯)으로 1개의 자식만 가질 수 있습니다.
하지만 그 자식 위젯은 또 다른 자식들을 가질 수 있으므로, 각 User Widget의 트리 계층 구조(Tree Hierarchy) 내에서 자식 위젯들이 연쇄적으로 포함되는 구조가 만들어집니다.
| 디자이너/계층 구조 에디터 뷰 | 런타임 결과 |
|---|---|
Health_Bar와 Health_Text는 굵게 표시되어 있는데, 이는 Is Variable 플래그가 활성화되어 있기 때문입니다![]() | ![]() |
해당 계층 구조를 다이어그램으로 나타낸 예시
모든 User Widget은 Sequencer를 통해 해당 User Widget 내부의 위젯들을 활용한 커스텀 애니메이션을 만들 수 있습니다.
Widget Designer(위젯 디자이너)에서 애니메이션을 생성할 수 있으며, 각 위젯의 렌더 트랜스폼, 위젯 가시성 등 다양한 속성을 애니메이션으로 제어할 수 있습니다.
또한, 위젯의 머티리얼 파라미터나 런타임 값 등도 애니메이션에서 수정할 수 있습니다.
UMG의 애니메이션 디자이너 예시
User Widget의 Tick Frequency(틱 빈도)가 클래스 기본값에서 Auto가 아닌 Never로 설정되어 있으면
애니메이션이 전혀 실행되지 않습니다.
애니메이션이 재생되려면 User Widget이 애니메이션을 틱할 수 있어야 하며,
Tick Frequency를 Never로 설정하면 애니메이션 객체도 틱되지 않아 애니메이션이 동작하지 않습니다.
모든 User Widget에는 직접 구현하여 원하는 기능을 추가할 수 있는 내장 이벤트들이 있습니다.
virtual void UUserWidget::NativePreConstruct()
{
// 블루프린트(BP) 버전 호출
PreConstruct(IsDesignTime());
}

virtual void UUserWidget::NativeOnInitialized()
{
// 이 위젯에 바인딩된 입력 델리게이트가 있다면 소유 플레이어 컨트롤러에 바인딩합니다.
if(APlayerController* PC = GetOwningPlayer())
{
UInputDelegateBinding::BindInputDelegates(GetClass(), PC->InputComponent, this);
}
OnInitialized();
}

virtual void UUserWidget::NativeConstruct()
{
// 블루프린트(BP) 버전 호출
Construct();
UpdateCanTick();
}

virtual void UUserWidget::NativeDestruct()
{
StopListeningForAllInputActions();
// BP(블루프린트) 버전 호출
Destruct();
}

virtual int32 UUserWidget::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
// BP에서 함수가 구현되어 있다면
if ( bHasScriptImplementedPaint )
{
FPaintContext Context(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
// BP 버전 호출
OnPaint( Context );
return FMath::Max(LayerId, Context.MaxLayer);
}
return LayerId;
}

virtual void UUserWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
//...
// BP에서 이벤트 그래프로 구현되어 있다면
if (bHasScriptImplementedTick)
{
// BP 버전 호출
Tick(MyGeometry, InDeltaTime);
}
}

virtual void UUserWidget::OnAnimationStartedPlaying(UUMGSequencePlayer& Player)
{
// BP 버전 호출
OnAnimationStarted(Player.GetAnimation());
BroadcastAnimationStateChange(Player, EWidgetAnimationEvent::Started);
}

virtual void UUserWidget::OnAnimationFinishedPlaying(UUMGSequencePlayer& Player)
{
// 이 이벤트는 애니메이션이 끝날 때 시퀀스 플레이어에서 직접 호출됩니다.
// BP 버전 호출
OnAnimationFinished(Player.GetAnimation());
BroadcastAnimationStateChange(Player, EWidgetAnimationEvent::Finished);
if ( Player.GetPlaybackStatus() == EMovieScenePlayerStatus::Stopped )
{
StoppedSequencePlayers.Add(&Player);
if (AnimationTickManager)
{
AnimationTickManager->AddLatentAction(FMovieSceneSequenceLatentActionDelegate::CreateUObject(this, &UUserWidget::ClearStoppedSequencePlayers));
}
}
UpdateCanTick();
}

virtual FReply UUserWidget::NativeOnFocusReceived( const FGeometry& InGeometry, const FFocusEvent& InFocusEvent )
{
// BP 버전 호출 및 반환값 반환
return OnFocusReceived( InGeometry, InFocusEvent ).NativeReply;
}


virtual void UUserWidget::NativeOnAddedToFocusPath(const FFocusEvent& InFocusEvent)
{
// BP 버전 호출
OnAddedToFocusPath(InFocusEvent);
}

virtual void UUserWidget::NativeOnFocusLost( const FFocusEvent& InFocusEvent )
{
// BP 버전 호출
OnFocusLost( InFocusEvent );
}

virtual void UUserWidget::NativeOnRemovedFromFocusPath(const FFocusEvent& InFocusEvent)
{
// BP 버전 호출
OnRemovedFromFocusPath(InFocusEvent);
}
