[번역][연재물] 언리얼 엔진 개발 가이드. Slate part two
기본
- SButton :버튼

- SCheckBox:체크박스

- SComboBox<OptionType> :사용자 정의 요소의 콤보 박스, 메뉴 항목의 위젯 생성기 제공 필요

- STextComboBox :텍스트 콤보 박스

- SComboButton :콤보 버튼, ComboBox와 다른 점은 ComboButton은 메뉴 항목 생성기가 아닌 전체 메뉴 생성기를 제공해야 함

- SInputKeySelector :키 입력 선택기

- SNumericDropDown<NumType> :숫자 드롭다운 위젯

- SNumericEntryBox<NumType> :숫자 선택 상자

- SRotatorInputBox = SNumericRotatorInputBox<float> :회전 입력 상자

- SSlider :슬라이더

- SSpinBox<NumericType> :사용자 정의 숫자 타입의 스핀 박스

- SVectorInputBox = SNumericVectorInputBox<float, UE::Math::TVector<float>, 3> :벡터 입력 상자

- SVolumeControl :볼륨 조절 위젯

텍스트
- STextBlock :텍스트 표시 블록

- SRichTextBlock :리치 텍스트 표시 블록
- SEditableText :편집 가능한 텍스트

- SInlineEditableTextBlock :한 줄 텍스트 편집 블록(편집 시에만 테두리 표시)

- SEditableTextBox :편집 가능한 텍스트 상자

- SSuggestionTextBox :자동 완성 및 입력 기록이 있는 텍스트 상자

- SSearchBox :검색 상자

- SMultiLineEditableText :여러 줄 텍스트 편집 블록

- SMultiLineEditableTextBox :여러 줄 편집 가능한 텍스트 상자

- SHyperlink :하이퍼링크

색상
- SColorBlock :단일 색상 표시 위젯

- SSimpleGradient :단순 그라데이션 색상 표시 위젯, 시작 색상과 끝 색상만 설정 가능

- SComplexGradient :복잡한 그라데이션 색상 표시 위젯, 임의의 색상 수로 구성된 그라데이션 지원

- SColorPicker :단일 색상 조절 통합 위젯(모듈 AppFramework 필요)

이미지
- SImage:이미지 표시용

내비게이션
- SBreadcrumbTrail

알림
- SErrorHint:에러 아이콘, ToolTip 설정 가능

- SErrorText:빨간색 배경의 텍스트 블록

- FNotificationInfo:컨트롤이 아닌, 메시지 알림용

FNotificationInfo NotificationInfo(FText::FromString("FNotificationInfo"));
NotificationInfo.Text = FText::FromString("This Is Notification Info");
NotificationInfo.bFireAndForget = true;
NotificationInfo.ExpireDuration = 100.0f;
NotificationInfo.bUseThrobber = true;
FSlateNotificationManager::Get().AddNotification(NotificationInfo);
- SProgressBar:진행 바

- SHeader:제목 컨트롤

- SScrollBar:스크롤 바

- SSpacer:빈 컨트롤, 일반적으로 레이아웃에서 채우기용으로 사용
컨트롤 컨테이너(SPanel)
레이아웃과 크기
- SVerticalBox:수직 레이아웃
- SHorizontalBox:수평 레이아웃
- SOverlay:중첩 레이아웃
- SGridPanel:그리드 레이아웃

- SUniformGridPanel:균일한 그리드 크기를 사용하는 그리드 레이아웃 패널
- SWrapBox:플로우 레이아웃, 배열 방향 설정 가능, 컨트롤이 크기를 초과할 때 줄바꿈

- SUniformWrapPanel:균일한 크기의 플로우 레이아웃
- SCanvas:임의의 좌표와 크기로 Child를 배치할 수 있음
- SConstraintCanvas:정적
- SSplitter:분할선을 드래그하여 위젯 간 크기를 조절할 수 있음

- SScrollBox:Content에 스크롤 바 추가

- SWidgetSwitcher:위젯 전환 컨트롤, 위젯을 모두 Slot에 추가하고 Slot Index 설정으로 해당 위젯 표시

- SDPIScaler:DPI 스케일 조정용
- SBox:Content의 고정 크기, 최소 크기 또는 최대 크기 제어

- SScaleBox:Content의 크기를 균일하게 스케일링

뷰
- SListView:리스트 뷰

- STileView:타일 뷰

- STreeView:트리 뷰

도킹 윈도우
- SDockTab

const TSharedRef<SDockTab> MajorTab = SNew(SDockTab).TabRole(ETabRole::MajorTab);
TabManager = FGlobalTabmanager::Get()->NewTabManager(MajorTab);
TabManager->RegisterTabSpawner(FName("MyDockTab"), FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& Args) {
TSharedRef<SDockTab> Tab = SNew(SDockTab).Content()[SNew(STextBlock).Text(FText::FromString("MyDockContent"))];
return Tab;
})).SetDisplayName(FText::FromString("MyDock"))
.SetMenuType(ETabSpawnerMenuType::Hidden);
TabManager->TryInvokeTab(FName("MyDockTab"));
기타
- SBackgroundBlur:Content의 배경 블러 처리

- SBorder:Content에 테두리 추가

- STooltipPresenter:Content에 ToolTip 추가
- SExpandableArea:Content를 확장하거나 축소 가능

- SFxWidget
- SMenuAnchor:영역에 컨텍스트 메뉴 추가
코드 참조
UE는 위젯 리플렉터를 제공하여 해당 UI의 생성 코드를 빠르게 찾을 수 있습니다. 시작점은 에디터 메인 패널의 상단 메뉴 바 - 도구 - 디버그 - 위젯 리플렉터입니다:

또한, 공식 엔진 소스 코드에는 대부분의 Slate 컨트롤의 기본 사용법을 보여주는 SlateViewer 프로젝트가 포함되어 있습니다:


IDE의 코드 검색 도구를 사용하여 엔진에서 해당 컨트롤의 매개변수 예제를 찾을 수 있으며, Visual Studio의 솔루션 탐색기에서 클래스 파생 관계를 통해 모든 컨트롤을 개괄적으로 볼 수 있습니다:

고급 사용자화
위에서 설명한 기능들은 UI 개발의 대부분의 요구사항을 충족시킬 수 있지만, 복잡한 상호작용 메커니즘과 렌더링 로직이 필요한 경우에는 SLeafWidget을 사용자 정의해야 합니다.
여기 드래그하여 이동할 수 있는 위젯을 구현한 좋은 예시가 있습니다:

코드는 다음과 같습니다:
#pragma once
#include "Styling/StyleColors.h"
#include "Brushes/SlateColorBrush.h"
class SCustomSlateWidget :public SLeafWidget {
SLATE_DECLARE_WIDGET(SCustomSlateWidget, SLeafWidget) //매크로 SLATE_DECLARE_WIDGET으로 Slate 컨트롤 선언
public:
SLATE_BEGIN_ARGS(SCustomSlateWidget) {}
SLATE_END_ARGS()
void Construct(const SCustomSlateWidget::FArguments InArgs) {
SetCursor(EMouseCursor::GrabHand); //컨트롤 영역에 마우스가 들어왔을 때의 스타일 설정
}
protected:
FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override {
if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) {
LastMouseDownPosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());//마우스 위치를 스크린 공간에서 컨트롤의 로컬 공간으로 변환
return FReply::Handled().CaptureMouse(SharedThis(this)); //전역 마우스 이동의 고주파 캡처 시작
}
return FReply::Handled();
}
FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override{
if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) {
auto Window = FSlateApplication::Get().FindWidgetWindow(SharedThis(this)); //해당 컨트롤의 윈도우 가져오기
auto CurrentMousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
auto CurrWindowPosition = Window->GetPositionInScreen();
auto TargetPosition = CurrentMousePosition + CurrWindowPosition - LastMouseDownPosition; //위치 이동 계산
Window->MoveWindowTo(TargetPosition);
}
return FReply::Handled();
}
FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override {
LastMouseDownPosition = FVector2D::ZeroVector;
return FReply::Handled().ReleaseMouseCapture(); //마우스 캡처 해제
}
int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override {
static FSlateFontInfo FontInfo = FCoreStyle::GetDefaultFontStyle("Bold",30);
static const FSlateBrush* Bursh = FAppStyle::GetBrush("WhiteBrush");
FLinearColor BackgroundColor = IsHovered()? FLinearColor(0.6f, 0.5f, 0.9f) : FLinearColor(0.1f, 0.5f, 0.9f);
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Bursh, ESlateDrawEffect::None, BackgroundColor); //배경 그리기
FSlateDrawElement::MakeText(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), FText::FromString("Hello Slate"), FontInfo); //텍스트 그리기
return LayerId;
}
FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override {
return FVector2D::ZeroVector; //제로 벡터를 사용하면 부모 컨트롤의 내용을 채움
}
private:
FVector2D LastMouseDownPosition = FVector2D::ZeroVector;
};
// 아래 코드는 cpp에 배치해야 함
SLATE_IMPLEMENT_WIDGET(SCustomSlateWidget) //매크로 SLATE_IMPLEMENT_WIDGET으로 코드 구현 생성
void SCustomSlateWidget::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer) //이 함수는 반드시 구현해야 함
{
}`
`FSlateApplication::Get().AddWindow(
SNew(SWindow)
.ClientSize(FVector2D(200,200)) //윈도우 크기를 200*200으로 설정
.SizingRule(ESizingRule::FixedSize) //윈도우 크기 고정
.SupportsMaximize(false) //최대화 비활성화
.SupportsMinimize(false) //최소화 비활성화
.CreateTitleBar(false) //타이틀 바 생성 안 함
.AutoCenter(EAutoCenter::PreferredWorkArea) //중앙 정렬 표시
.IsTopmostWindow(true) //최상위 윈도우로 설정
[
SNew(SCustomSlateWidget)
]
);
사용자 정의 SLeafWidget의 작업 순서는 다음과 같습니다:
- 매크로 **SLATE_DECLARE_WIDGET(WidgetType, ParentType)**를 사용하여 컨트롤 선언
- 매크로 **SLATE_IMPLEMENT_WIDGET(WidgetType)**를 cpp에서 코드 구현을 정의하고, 다음 함수를 구현:
- void WidgetType::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer)
- 컨트롤 크기 계산 함수 재정의:
- virtual FVector2D SWidget::ComputeDesiredSize(float LayoutScaleMultiplier) const = 0
- 각종 상호작용 이벤트 재정의
- 컨트롤의 속성 상태 기계 조정
- 속성 상태 기계에 따라 그리기 함수(OnPaint)에서 그리기 요소 추가
상호작용 이벤트
SWidget은 개발자가 오버라이드할 수 있는 다양한 상호작용 이벤트(On으로 시작하는 가상 함수)를 제공합니다:
- OnFocusReceived: 포커스 획득
- OnFocusLost: 포커스 상실
- OnFocusChanging: 포커스 변경
- OnKeyChar: 키보드 입력 시
- OnKeyDown: 키보드 키 누름
- OnKeyUp: 키보드 키 뗌
- OnMouseButtonDown: 마우스 버튼 누름
- OnMouseButtonUp: 마우스 버튼 뗌
- OnMouseMove: 마우스 이동
- OnMouseEnter: 마우스가 위젯 영역에 들어가는 순간
- OnMouseLeave: 마우스가 위젯 영역을 떠나는 순간
- OnMouseButtonDoubleClick: 마우스 더블 클릭
- OnMouseWheel: 마우스 휠 스크롤
- OnDragEnter: 드래그한 항목이 위젯 영역에 들어가는 순간
- OnDragLeave: 드래그한 항목이 위젯 영역을 떠나는 순간
- OnDragOver: 드래그한 항목이 위젯 영역을 덮을 때
- OnDrop: 위젯 위에서 드래그한 항목을 놓을 때
- ...
자세한 설명은 SWidget의 정의를 참고하세요. 각 이벤트의 설명과 트리거 시점이 있으며, 사용법은 UE 소스 코드에서 해당 예제를 검색할 수 있습니다.
사용 설명:
- 이러한 이벤트들은 FSlateApplication이 호출을 담당하며, 함수 매개변수는 일반적으로 해당 이벤트가 가진 모든 정보를 포함합니다. 우리는 이를 오버라이드(override)하고 이벤트 데이터에 따라 자체 로직을 수행하기만 하면 됩니다.
- 이벤트의 반환 타입은 FReply로, 이벤트의 후속 처리를 정의하는 데 사용됩니다. 생성자가 private이므로 다음과 같은 정적 함수를 사용하여 인스턴스를 생성해야 합니다:
- FReplay::Handled(): 해당 이벤트가 처리되었음을 나타내며, 이벤트는 더 이상 하위 레벨로 전달되지 않습니다.
- FReplay::Unhandled(): 해당 컨트롤이 이벤트를 처리하지 않음을 나타내며, 이벤트는 하위 레벨로 계속 전달됩니다.
- FReply는 이벤트 종료 시 처리를 위한 일련의 작업을 제공합니다:
그리기 이벤트
SWidget의 실제 그리기 함수는 Paint이지만, 그리기 과정에 몇 가지 고정된 단계가 있어 Slate는 그리기 이벤트 OnPaint를 제공하여 사용자 정의 그리기 로직을 구현할 수 있게 합니다. 정의는 다음과 같습니다:
virtual int32 OnPaint(const FPaintArgs& Args,
const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements,
int32 LayerId,
const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const = 0;
매개변수 설명:
- const FPaintArgs& Args: 그리기 이벤트의 매개변수로, 현재 시간, 간격, 부모 창 등을 얻을 수 있음
- const FGeometry& AllottedGeometry: 이 SWidget에 할당된 기하학적 크기(부모 창 기준)
- const FSlateRect& MyCullingRect: 이 SWidget의 클리핑 영역으로, 그리기 중 이 사각형을 기준으로 그리기 요소를 제외할 수 있음
- FSlateWindowElementList& OutDrawElements: 전체 SWindow의 그리기 요소 목록으로, 우리는 이 객체를 수정하여 그리기를 수행
- int32 LayerId: 그리기의 LayerID로, 일반적으로 UI 레이어를 구분하여 같은 레벨의 그리기 요소를 통합 처리하는 데 사용
- const FWidgetStyle& InWidgetStyle: 부모 창에서 전달된 Style
- bool bParentEnabled: 부모 창이 활성화 상태인지 여부
OnPaint 함수에서는 주로 FSlateDrawElement의 Make로 시작하는 다양한 정적 함수를 통해 그리기 요소를 생성합니다. 다음과 같은 함수들이 있습니다:
static void MakeBox( //박스 그리기, Brush 설정을 통해 색상 블록이나 이미지가 될 수 있음
FSlateWindowElementList& ElementList,
uint32 InLayer,
const FPaintGeometry& PaintGeometry,
const FSlateBrush* InBrush,
ESlateDrawEffect InDrawEffects = ESlateDrawEffect::None,
const FLinearColor& InTint = FLinearColor::White );
static void MakeRotatedBox(...); //회전된 박스 그리기
static void MakeGradient(...); //그라데이션 박스 그리기
static void MakeText(...); //텍스트 그리기
static void MakeShapedText(...); //형상 텍스트 그리기
static void MakeLines(...); //선 그리기
static void MakeSpline(...) //곡선 그리기
static void MakeDrawSpaceSpline(...); //그리기 공간에서 곡선 그리기
static void MakeCubicBezierSpline(...); //3차 베지어 곡선 그리기
static void MakeViewport(...) //FSlateViewport 그리기
static void MakeCustom(...); //사용자 정의 요소 그리기, 요소의 Render 함수를 직접 구현 가능
static void MakeCustomVerts(...); //정점을 기반으로 그리기
static void MakePostProcessPass(...); //후처리 필터 그리기(블러)
이러한 함수들은 UE에서 매우 많이 사용되며, 관련 코드를 검색하여 참고할 수 있습니다.
자세한 내용은 Runtime\SlateCore\Public\Rendering\DrawElements.h를 참조하세요
FSlateBrush
FSlateBrush는 채우기 영역을 제어하는 데 사용되며, 인터페이스에서 사용 빈도가 높기 때문에 Slate는 다음과 같은 편리한 확장을 제공합니다:

- FSlateBorderBrush: 테두리 채우기
- FSlateBoxBrush: 박스 브러시
- FSlateRoundedBoxBrush: 둥근 사각형 브러시
- FSlateColorBrush: 색상 브러시
- FSlateImageBrush: 이미지
- FSlateVectorImageBrush: 벡터 이미지
- FSlateDynamicImageBrush: 동적으로 이미지를 전환할 수 있는 브러시
- FSlateNoResource: 리소스가 없는 브러시
위의 브러시들은 본질적으로 특정 매개변수를 통해 FSlateBrush를 구성하는 것입니다. 위의 브러시들을 조합해야 하는 경우, 해당 생성자 매개변수를 기반으로 직접 조합할 수 있습니다.
UE는 몇 가지 내장된 FSlateBrush를 제공하며, 소스 코드에서 ::Get().GetBrush를 검색하여 관련 사용법을 찾을 수 있습니다:

예시
SWidget 전체를 빨간색으로 채우려면 다음과 같은 코드를 사용할 수 있습니다:
int32 SCustomSlateWidget::OnPaint(const FPaintArgs& Args,
const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements,
int32 LayerId,
const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const {
static FSlateColorBrush ColorBrush(FColor(255,0,0));
FSlateDrawElement::MakeBox(OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
&ColorBrush,
ESlateDrawEffect::None,
ColorBrush.TintColor.GetColor(InWidgetStyle));
return LayerId;
}
ESlateDrawEffect
ESlateDrawEffect는 Slate에 몇 가지 효과를 제공하며, 다음과 같은 값을 가질 수 있습니다:
enum class ESlateDrawEffect : uint8
{
None = 0, //효과 없음
NoBlending = 1 << 0, //블렌딩 없음
PreMultipliedAlpha = 1 << 1, //사전 곱셈된 알파
NoGamma = 1 << 2, //감마 보정 없음
InvertAlpha = 1 << 3, //알파 반전
// ^^ These Match ESlateBatchDrawFlag ^^
NoPixelSnapping = 1 << 4, //픽셀 스냅핑 끄기
DisabledEffect = 1 << 5, //비활성화 상태 효과
IgnoreTextureAlpha = 1 << 6, //텍스처 알파 무시
ReverseGamma = 1 << 7 //감마 반전
};
FSlateFontInfo
FSlateFontInfo는 폰트 정보를 지정하는 데 사용되며, UE의 폰트 에셋에 해당합니다. 다음은 그 일부 정의입니다:
struct SLATECORE_API FSlateFontInfo
{
/**The font object (valid when used from UMG or a Slate widget style asset) */
TObjectPtr<const UObject> FontObject;
/**The material to use when rendering this font */
TObjectPtr<UObject> FontMaterial;
/**Settings for applying an outline to a font */
FFontOutlineSettings OutlineSettings;
/**The composite font data to use (valid when used with a Slate style set in C++) */
TSharedPtr<const FCompositeFont> CompositeFont;
/**The name of the font to use from the default typeface (None will use the first entry) */
FName TypefaceFontName;
/**
* The font size is a measure in point values. The conversion of points to Slate Units is done at 96 dpi. So if
* you're using a tool like Photoshop to prototype layouts and UI mock ups, be sure to change the default dpi
* measurements from 72 dpi to 96 dpi.
*/
int32 Size;
/**The uniform spacing (or tracking) between all characters in the text. */
int32 LetterSpacing = 0;
/**The font fallback level. Runtime only, don't set on shared FSlateFontInfo, as it may change the font elsewhere (make a copy). */
EFontFallback FontFallback;
};
UE에는 전용 폰트 편집기가 있어 C++에서 직접 구성하기는 까다롭지만, UE는 다음과 같은 내장 FSlateFontInfo를 제공합니다:
- FCoreStyle::Get().GetFontStyle("NormalFont")
- FCoreStyle::Get().GetFontStyle("SmallFont")
- FCoreStyle::GetDefaultFontStyle("Regular",FontSize);
- FCoreStyle::GetDefaultFontStyle("Bold",FontSize)
- FCoreStyle::GetDefaultFontStyle("Italic",FontSize);
- FCoreStyle::GetDefaultFontStyle("Mono",FontSize);
FontMeasure
대부분의 GUI 프레임워크에는 **폰트 측정(FontMeasure)**이라는 개념이 있습니다: 특정 폰트 형식의 텍스트의 그리기 너비와 높이를 측정합니다.
UE에서는 다음과 같은 방법으로 측정할 수 있습니다(의사 코드):
FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure( const FText& Text, //텍스트 const FSlateFontInfo& InFontInfo, //폰트 float FontScale //폰트 크기 조정 ) -> FVector2D //너비(X)와 높이(Y) 반환
성능 최적화
Slate는 UI의 Paint와 Tick 성능을 최적화하기 위해 SInvalidationPanel 컨트롤을 제공합니다. 이 컨트롤을 사용하면 자식 컨트롤에서 리소스를 많이 소모하는 작업을 캐시할 수 있으며, 특정 작업으로 인해 **캐시가 무효화(Invalidate)**될 때만 다시 실행됩니다.
자세한 내용은 다음을 참조하세요: 언리얼 엔진 Slate 아키텍처 이해하기 - 폴링 데이터 흐름과 위임 | 언리얼 엔진 5.2 문서
자식 컨트롤의 Invalidate() 함수를 수동으로 호출하여 캐시를 무효화할 수 있습니다:
void SWidget::Invalidate(EInvalidateWidgetReason InvalidateReason);
EInvalidateWidgetReason의 값은 다음과 같을 수 있습니다:
enum class EInvalidateWidgetReason : uint8
{
None = 0,
Layout = 1 << 0, //위젯의 크기를 변경해야 하는 경우 레이아웃을 무효화합니다. 이는 비용이 많이 드는 작업이므로, 단순히 위젯을 다시 그리기만 하면 되는 경우에는 사용하지 마세요
Paint = 1 << 1, //그리기 무효화
Volatility = 1 << 2, //Volatility가 변경될 때 트리거됨
ChildOrder = 1 << 3, //자식 위젯이 추가되거나 제거될 때 트리거되며, 무효화하면 Child의 순서가 재구성됨
RenderTransform = 1 << 4, //RenderTransform이 변경될 때 트리거됨
Visibility = 1 << 5, //가시성이 변경될 때 트리거됨
AttributeRegistration = 1 << 6, //속성이 바인딩될 때 트리거됨
Prepass = 1 << 7, //Prepass 무효화
/**
* 페인팅이나 크기 조정과 관련된 일반 속성을 변경할 때는 Paint 무효화를 사용하세요.
* 또한 변경된 속성이 Volatility에 영향을 미치는 경우, Volatility를 무효화하여
* 재계산되고 캐시되도록 하는 것이 중요합니다.
*/
PaintAndVolatility = Paint | Volatility,
/**
* 페인팅이나 크기 조정과 관련된 일반 속성을 변경할 때는 Layout 무효화를 사용하세요.
* 또한 변경된 속성이 Volatility에 영향을 미치는 경우, Volatility를 무효화하여
* 재계산되고 캐시되도록 하는 것이 중요합니다.
*/
LayoutAndVolatility = Layout | Volatility,
};
Slate는 속성(SlateAttribute) 기반의 방식을 제공합니다: 속성의 변경을 캐시 재구성과 연결합니다
구체적인 방법은 다음과 같습니다:
- TSlateAttribute로 속성을 감싸고(TAttribute와는 용도가 다름), TSlateAttribute로 감싼 속성은 생성자 초기화 시 (*this) 매개변수를 전달해야 합니다
- void WidgetType::PrivateRegisterAttributes(...) 함수에서 SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(_Initializer, _Property, _Reason) 매크로를 사용하여 속성을 등록합니다
다음은 간단한 예시입니다:
class SCustomSlateWidget :public SLeafWidget {
SLATE_DECLARE_WIDGET(SCustomSlateWidget, SLeafWidget)
public:
SLATE_BEGIN_ARGS(SCustomSlateWidget){}
SLATE_END_ARGS()
void Construct(const SCustomSlateWidget::FArguments InArgs){
}
protected:
SCustomSlateWidget()
: ClickedCounter(*this) { //SlateAttribute 초기화
}
FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override {
ClickedCounter.Set(*this, ClickedCounter.Get() + 1); //카운터 +1, 여기서는 마우스 클릭으로 간주
return FReply::Handled();
}
int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override {
static FSlateFontInfo FontInfo = FCoreStyle::GetDefaultFontStyle("Bold",30);
FSlateDrawElement::MakeText(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), FText::FromString(FString::FromInt(ClickedCounter.Get())), FontInfo); //현재 마우스 클릭 횟수 그리기
return LayerId;
}
FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override {
return FVector2D::ZeroVector;
}
private:
TSlateAttribute<int> ClickedCounter; //마우스 클릭 횟수를 기록하는 카운터
};
SLATE_IMPLEMENT_WIDGET(SCustomSlateWidget)
void SCustomSlateWidget::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer) {
//ClickedCounter의 값 변경을 다시 그리기 이벤트와 연결
//SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(AttributeInitializer, ClickedCounter, EInvalidateWidgetReason::Paint);
}
FSlateApplication::Get().AddWindow(
SNew(SWindow)
.SupportsMaximize(false)
.SupportsMinimize(false)
.IsTopmostWindow(true)
.ClientSize(FVector2D(200,200))
.SizingRule(ESizingRule::FixedSize)
.CreateTitleBar(false)
.AutoCenter(EAutoCenter::PreferredWorkArea)
[
SNew(SInvalidationPanel) // SInvalidationPanel 사용
[
SNew(SCustomSlateWidget)
]
]
);
이 코드는 클릭 가능한 블록을 생성합니다:

블록을 클릭하면 ClickedCounter+1이 되지만, 화면에는 변화가 없습니다. PrivateRegisterAttributes의 주석을 해제하면:
Slate에서 UMG로
UE의 로직 프로그래밍은 UObject 위에 구축되어 있으며, Slate는 극한의 성능을 추구하기 위해 UI 객체를 관리하는데 UObject와 같은 무거운 메커니즘을 사용하지 않았습니다
Slate에서는 **공유 포인터(Shared Pointer)**를 사용하여 UI 객체의 수명 주기를 관리합니다
게임 개발자의 UI 개발 효율성을 높이기 위해, UE는 UObject로 Slate 위젯을 감싸고, 리플렉션을 통해 Slate 위젯의 속성과 함수를 노출시켰으며, 동시에 시각적인 UI 디자인을 지원합니다. 이 인터페이스 레이어가 바로 우리가 잘 알고 있는 UMG입니다
UMG에 대해서는 공식 문서에 자세한 설명이 있습니다:
여기 Slate와 UMG의 관계를 잘 보여주는 간단한 코드가 있습니다:
#pragma once
#include "UObject/ObjectMacros.h"
#include "Widgets/Text/STextBlock.h"
#include "Components/Widget.h"
#include "CustomUWidget.generated.h"
UCLASS(meta = (DisplayName = "CustomWidget")) //DisplayName은 위젯 이름입니다
class UMGEXTENDUTILITIES_API UCustomWidget : public UWidget
{
GENERATED_BODY()
protected:
virtual TSharedRef<SWidget> RebuildWidget() override { //위젯을 재구축하고 멤버 변수에 할당합니다
return SAssignNew(MyTextBlock,STextBlock);
}
virtual void SynchronizeProperties() override { //UObject의 속성을 Slate 위젯에 동기화합니다
MyTextBlock->SetText(Text);
Super::SynchronizeProperties();
}
virtual void ReleaseSlateResources(bool bReleaseChildren) override { //Slate 리소스를 해제합니다
MyTextBlock.Reset();
}
#if WITH_EDITOR
virtual const FText GetPaletteCategory() override { //에디터에서의 위젯 카테고리
return FText::FromString("CustomCategory");
}
#endif
public:
UFUNCTION(BlueprintCallable) //블루프린트에 함수 공개
FText GetText() const {
return Text;
}
UFUNCTION(BlueprintCallable)
void SetText(FText InText) {
Text = InText;
if (MyTextBlock) { //속성 설정, 위젯이 이미 생성되었을 때 직접 값을 위젯에 설정합니다
MyTextBlock->SetText(Text);
}
}
protected:
UPROPERTY(EditAnywhere, BlueprintGetter = "GetText", BlueprintSetter = "SetText")
FText Text; //에디터에 속성 공개
TSharedPtr<STextBlock> MyTextBlock; //실제 Slate 위젯
};

C++에서 UMG를 사용하는 방법은 다음과 같습니다:
UWidget* UMGWidget = LoadObject<UWidget>(nullptr, TEXT("{AssetPath}")); //에셋에서 로드, NewObject로도 가능
TSharedPtr<SWidget> SlateWidget = UMGWidget->TakeWidget(); //이 작업은 새로운 SWidget을 생성합니다
TSharedPtr<SWidget> CachedSlateWidget = UMGWidget->GetCachedWidget(); //마지막으로 생성된 SWidget을 가져옵니다
//이후 Slate 사용 방식으로 사용
생명 주기 관리
UWidget은 UObject의 가비지 컬렉션을 따르며, SWidget의 공유 포인터를 보유합니다. 컨트롤이 표시될 때(TSharedPtr<SWidget>가 여전히 존재할 때) UWidget이 가비지 컬렉션되지 않도록 하기 위해, UMG는 FGCObject를 상속받은 SObjectWidget 컨트롤을 사용하여 UWidget에 대한 참조를 추가합니다.

위의 예제 코드로 돌아가보면, UWidget::TakeWidget()을 호출해서 얻는 것은 UWidget::RebuildWidget()에서 생성된 컨트롤이 아니라, SObjectWidget으로 한 번 더 감싼 것임을 알 수 있습니다. 생성 과정에서의 계층 관계는 다음과 같습니다:

소멸 시 해제 순서는 다음과 같습니다:

- SObjectWidget은 FGCObject를 상속받으며, UWidget에 대한 참조를 추가하여 SObjectWidget이 존재하는 동안 UWidget이 GC되지 않도록 보장합니다
- 컨트롤이 소멸을 시작할 때, SObjectWidget의 참조 카운트가 0이 되어 소멸되며, 이때 UWidget에 대한 참조가 해제됩니다
- UWidget의 참조가 0이 되면, 다음 가비지 컬렉션 시에 해제되며, 이때 보유하고 있던 SWidget 공유 포인터도 해제됩니다
... 파트 4부터는 플러그인 개발로 계속...