TECH.ART.FLOW.IO

[번역][연재물] 언리얼 엔진 개발 가이드. Slate part one

jplee 2025. 4. 24. 00:08

1 - Slate 개발

Slate는 UE에서 제공하는 UI 프레임워크로, 핵심 이념은 다음과 같습니다:

UI의 기본 개념에 대해서는 다음을 참고하세요:

기초

UE에서 Slate를 사용할 때는 세 가지 핵심 구조를 이해해야 합니다:

  • FSlateApplication : 전역 싱글톤으로, 모든 UI의 제어 센터입니다.
  • SWindow : 최상위 윈도우로, 크로스 플랫폼 윈도우 인스턴스(FGenericWindow)를 보유하며 윈도우 관련 설정과 조작을 제공합니다.
  • SWidget : 위젯으로, 윈도우 영역을 분할하고 해당 영역 내의 상호작용과 렌더링 이벤트를 처리합니다.

기본 사용법

UE에서 간단한 Slate 사용 예시는 다음과 같습니다:

auto Window = SNew(SWindow)                         //创建窗口
    .ClientSize(FVector2D(600, 600))                //设置窗口大小
    [                                               //填充窗口内容
        SNew(SHorizontalBox)                        //创建水平盒子
        + SHorizontalBox::Slot()                    //添加子控件插槽
        [                                           
            SNew(STextBlock)                        //创建文本框
            .Text(FText::FromString("Hello"))       //设置文本框内容
        ]
        + SHorizontalBox::Slot()                    //添加子控件插槽
        [
            SNew(STextBlock)                        //创建文本框
            .Text_Lambda([](){                      //设置文本框内容   
                return FText::FromString("Slate");
            })      
        ]
    ];
FSlateApplication::Get().AddWindow(Window, true);   //注册该窗口,并立即显示

Slate의 코드 스타일은 다음과 같습니다:

  • Slate 컨트롤의 클래스 이름은 일반적으로 S로 시작합니다
  • 다음 함수들을 통해 Slate 컨트롤을 빠르게 구축할 수 있습니다:
    • SNew( WidgetType, ... ): 일반적인 생성 방식
    • SAssignNew( ExposeAs, WidgetType, ... ): 생성 후 컨트롤을 ExposeAs에 할당
    • SArgumentNew( InArgs, WidgetType, ... ): 파라미터 집합을 사용하여 생성
  • 생성 코드 표현식에서는 여러 함수를 통해 컨트롤의 생성 파라미터를 설정할 수 있으며, 이 함수들은 컨트롤 자신의 참조를 반환하므로 체인 방식으로 호출할 수 있습니다
    • operatpr .를 통해 함수를 호출하여 컨트롤의 속성과 이벤트를 설정할 수 있습니다
    • SPanel 하위 클래스의 경우, operator +SPanelType::Slot을 사용하여 자식 컨트롤의 슬롯을 추가할 수 있습니다
    • SCompoundWidgetSPanel::Slot의 경우, operator []를 사용하여 자식 컨트롤을 채울 수 있습니다
    • Slate의 속성과 이벤트는 여러 방식으로 바인딩할 수 있습니다. 예를 들어 위의 .Text(...)는 정적 값을 설정하는 것이며, 함수 바인딩을 통해 동적으로 속성 값을 가져올 수도 있습니다:
      • Text_Lambda(...)
      • Text_Raw(...)
      • Text_Static(...)
      • Text_UObject(...)

컨트롤 개발 시에는 경험(합리적인 것은 존재한다는 원칙)과 IDE의 도움을 받아 코드를 작성할 수 있습니다.

Slate는 게임 UI와 에디터 UI로 동시에 사용할 수 있습니다
에디터에서는 다음과 같은 방식으로 UI를 추가할 수 있습니다:

`auto Window = SNew(SWindow)                         //必须具有一个顶层窗口
    .Title(FText::FromString("CustomWindow"))       //设置窗口标题
    .ClientSize(FVector2D(600, 600))                //设置窗口大小
    [                                               //填充窗口内容
        SNew(SSpacer)                               //自身控件,这里是一个空白填充                
    ];
//方法1:注册该窗口,并立即显示
FSlateApplication::Get().AddWindow(Window, true);   
//方法2:注册该窗口,不显示,手动调用ShowWindow来显示
FSlateApplication::Get().AddWindow(Window, false);  //注册该窗口,并立即显示
Window->ShowWindow();`

DockTab을 사용하여 창을 추가할 수도 있습니다:

// 注册Tab页面的生成器
FGlobalTabmanager::Get()->RegisterTabSpawner(FName("CustomTab"),FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& Args){
    return SNew(SDockTab)
        [
            SNew(SSpacer)
        ];
}));
FGlobalTabmanager::Get()->TryInvokeTab(FTabId("CustomTab"));        //尝试激活Tab页面

게임에서는 다음과 같은 방식으로 UI를 추가할 수 있습니다:

GEngine->GameViewport->AddViewportWidgetContent(
        SNew(SSpacer)
);

GameViewport에는 다른 인터페이스도 사용할 수 있습니다:

class UGameViewportClient : public UScriptViewportClient, public FExec
{
    virtual void AddViewportWidgetContent( TSharedRef<class SWidget> ViewportContent, const int32 ZOrder = 0 );
    virtual void RemoveViewportWidgetContent( TSharedRef<class SWidget> ViewportContent );
    virtual void AddViewportWidgetForPlayer(ULocalPlayer* Player, TSharedRef<SWidget> ViewportContent, const int32 ZOrder);
    virtual void RemoveViewportWidgetForPlayer(ULocalPlayer* Player, TSharedRef<SWidget> ViewportContent);
    void RemoveAllViewportWidgets();
    void RebuildCursors();
};

자세한 내용:https://docs.unrealengine.com/5.2/en-US/using-slate-in-game-in-unreal-engine/

기본 개념

UI 개발에 있어 다음과 같은 기본 개념들을 이해해야 합니다:

  • 윈도우의 기본 상태 : 활성화(Active), 포커스(Focus), 가시성(Visible), 모달(Modal), 변환(Transform)
  • 레이아웃 전략 및 관련 개념 :
    • 박스 레이아웃(HBox, VBox), 플로우 레이아웃(Flow), 그리드 레이아웃(Grid), 앵커 레이아웃(Anchors), 오버랩 레이아웃(Overlap), 캔버스(Canvas)
    • 패딩(Padding), 마진(Margin), 간격(Spacing), 정렬(Alignment)
  • 스타일 관리 : 아이콘(Icon), 스타일(Style), 브러시(Brush)
  • 폰트 관리 : 폰트 패밀리(Font Family), 텍스트 너비 측정(Font Measure)
  • 크기 계산 : 위젯 크기 계산 전략
  • 상호작용 이벤트 : 마우스, 키보드, 드래그 앤 드롭, 포커스 이벤트, 이벤트 처리 메커니즘, 마우스 캡처
  • 렌더링 이벤트 : 렌더링 요소, 영역 클리핑
  • 기본 위젯 : 레이블(Label), 버튼(Button), 체크박스(Check), 콤보박스(Combo), 슬라이더(Slider), 스크롤바(Scroll Bar), 텍스트박스(Text), 다이얼로그(Dialog), 컬러피커(Color), 메뉴바(Menu Bar), 메뉴(Menu), 상태바(Status Bar), 스크롤 패널(Scroll), 스택(전환) 패널(Stack/Switcher), 리스트 패널(List), 트리 패널(Tree)...
  • 국제화 : 텍스트 현지화 번역(Localization)

또한 UI 프레임워크에서 어떤 전역 데이터와 작업이 있는지 파악해야 합니다. UE에서는 Get, Set을 입력하여 IDE의 제안을 통해 간단히 살펴볼 수 있습니다:

이는 일정한 코딩 규칙을 따르는 것이 가져다주는 이점을 보여줍니다

SWidget

SWidget은 Slate UI 개발 과정에서 개발자가 가장 자주 접하는 타입으로, 모든 UI 컨트롤의 기본 클래스입니다
개발자는 이것의 기본 구조를 이해하고 어떤 속성들을 조정할 수 있는지 알아야 합니다:

TAttribute<bool> EnabledState,                          //开启状态,指定是否能够与控件交互,如果禁用,则显示为灰色
TAttribute<EVisibility> Visibility,                     //可见性策略
TSharedPtr<IToolTip> ToolTip,                           //提示框控件
TAttribute<FText> ToolTipText,                          //提示框文本内容
TAttribute<TOptional<EMouseCursor::Type> > Cursor,      //鼠标样式
float RenderOpacity,                                    //渲染透明度
TAttribute<TOptional<FSlateRenderTransform>> Transform, //渲染变换
TAttribute<FVector2D> TransformPivot,                   //渲染变换中心
FName Tag,                                              //标签
bool ForceVolatile,                                     //强制UI失效
EWidgetClipping Clipping,                               //裁剪策略
EFlowDirectionPreference FlowPreference,                //UI流向
TOptional<FAccessibleWidgetData> AccessibleData,        //存储数据
TArray<TSharedRef<ISlateMetaData>> MetaData             //元数据

자세한용:https://docs.unrealengine.com/5.2/zh-CN/slate-ui-widget-examples-for-unreal-engine/
호출할 수 있는 함수:

재정의할 수 있는 기능(오버라이드):

SWindow

SWindowSCompoundWidget을 상속받았는데, 이는 SWindowSWidget과 같은 코드 스타일을 사용할 수 있도록 하기 위한 것이며, 다음과 같은 속성들을 가지고 있습니다:

EWindowType Type;                               //窗口类型
FWindowStyle Style;                             //窗口样式
FText Title;                                    //窗口标题
float InitialOpacity;                           //初始透明度
FVector2D ScreenPosition                        //窗口坐标
FVector2D ClientSize;                           //窗口客户区域尺寸
TOptional<float> MinWidth;                      //最小宽度
TOptional<float> MinHeight;                     //最小高度
TOptional<float> MaxWidth;                      //最大宽度
TOptional<float> MaxHeight;                     //最大高度
FMargin LayoutBorder;                           //窗口内容的边距
FMargin UserResizeBorder;                       //调整窗口区域时的响应距离
EAutoCenter AutoCenter;                         //居中策略
ESizingRule SizingRule;                         //窗口尺寸处理策略
FWindowTransparency SupportsTransparency;       //窗口透明度策略
EWindowActivationPolicy ActivationPolicy;       //激活处理策略
bool IsInitiallyMaximized;                      //初始时显示为最大化
bool IsInitiallyMinimized;                      //初始时显示为最小化
bool IsPopupWindow;                             //是否是 Pop up 窗口(无任务栏图标)
bool IsTopmostWindow;                           //是否是置顶窗口
bool FocusWhenFirstShown;                       //首次预览时获得焦点
bool AdjustInitialSizeAndPositionForDPIScale;   //根据DPI调整窗口初始坐标和尺寸
bool UseOSWindowBorder;                         //使用操作系统自身的窗口边框
bool HasCloseButton;                            //是否带有关闭按钮
bool SupportsMaximize;                          //是否支持最大化
bool SupportsMinimize;                          //是否支持最小化
bool ShouldPreserveAspectRatio;                 //是否锁定宽高比
bool CreateTitleBar;                            //是否创建标题栏
bool SaneWindowPlacement;                       //是否将窗口约束到屏幕内
bool bDragAnywhere;                             //是否可拖拽到任意位置
bool bManualManageDPI;                          //是否手动调整DPI

일반적으로 사용되는 기능은 다음과 같습니다:

void MoveWindowTo( FVector2D NewPosition );
void ReshapeWindow( FVector2D NewPosition, FVector2D NewSize );
void ReshapeWindow( const FSlateRect& InNewShape );
void Resize( FVector2D NewClientSize );
void MorphToPosition( const FCurveSequence& Sequence, const float TargetOpacity, const FVector2D& TargetPosition );
void MorphToShape( const FCurveSequence& Sequence, const float TargetOpacity, const FSlateRect& TargetShape );
void ShowWindow();
void HideWindow();
void BringToFront( bool bForce = false );
void RequestDestroyWindow();
void EnableWindow( bool bEnable );
void Maximize();
void Restore();
void Minimize();

개발 프로세스

Slate 개발의 대부분 작업 내용은 다음과 같이 요약할 수 있습니다:

  • 위젯 속성 설정
  • 이벤트 로직 바인딩
  • 계층 구조 구성

SWidget은 추상 기본 클래스이며, UE는 사용 방식에 따라 이를 다음 네 가지 유형으로 파생시켰습니다:

  • SCompoundWidget : ChildSlot을 설정할 수 있음
  • Slate가 개발자에게 제공하는 주요 확장 방식으로, 일반적으로 이를 상속받아 기존 SWidget을 활용하여 일련의 위젯을 구성합니다.
  • SPanel : SWidget의 컨테이너로 볼 수 있으며, 하나 이상의 ChildSlot을 포함할 수 있어 SWidget을 추가하는 데 사용됩니다.
  • Slate는 이미 레이아웃 구성 등 다양한 작업을 위한 충분한 SPanel을 파생시켰기 때문에, 일반적으로 이를 상속받는 경우는 거의 없습니다.
  • SLeafWidget : 리프 위젯으로, ChildSlot을 포함하지 않음
  • 이것을 파생시키는 주된 목적은 독특한 렌더링 및 크기 처리 메커니즘을 가진 위젯을 커스터마이징하기 위해서입니다.
  • SWeakWidget : 이벤트가 아닌 논리적 소속 관계를 정의
  • SPanel 내의 SWidget은 이벤트 측면에서 계층 관계를 가지지만, 때로는 특수한 상황이 발생합니다. 예를 들어 버튼을 클릭하여 메뉴를 열 때, 메뉴는 해당 버튼에 속한 것으로 볼 수 있지만, 본질적으로는 새 창을 여는 것이므로 이벤트 전달 측면에서는 계층 관계가 없습니다. 이러한 문제를 해결하기 위해 SWeakWidget이 필요합니다. 일반적으로 거의 사용되지 않습니다.

대부분의 경우, 우리는 SCompoundWidget을 상속받는 새로운 C++ 클래스를 만들고, Construct 함수에서 자식 위젯을 채우는 방식으로 작업합니다. 다음과 같은 방식입니다:

class SCustomWidget: public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SCustomWidget) {}              //定义Slate参数
    SLATE_END_ARGS()
public:
    void Construct(const FArguments& InArgs){       //使用SNew本质上是调用该函数
        ChildSlot                                   //填充子控件
        [
            SNew(STextBlock)
            .Text(FText::FromString("This is body"))
        ];
    }
};

이를 통해 다음 코드를 사용하여 컨트롤을 만들 수 있습니다:

auto MyWidget = SNew(SCustomWidget);

예를 들어 텍스트 콘텐츠를 설정 가능하게 만들기 위해 인수를 추가하려는 경우 정의를 다음과 같이 변경할 수 있습니다:

class SCustomWidget : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SCustomWidget)                         
        : _Text(FText::FromString("Default")) {     //初始化参数默认值,变量名为参数定义时的变量名前加下划线
        }               
        SLATE_ARGUMENT(FText,Text)                  //使用宏SLATE_ARGUMENT定义参数
    SLATE_END_ARGS()
public:
    void Construct(const FArguments& InArgs) {      
        Text = InArgs._Text;                        //接收传递进来的参数
        ChildSlot                                   
        [
            SNew(STextBlock)
            .Text(Text)                             //传递文本内容
        ];
    }
private:
    FText Text;
};

다음 코드를 사용하여 컨트롤의 콘텐츠를 설정할 수 있습니다:

auto MyWidget = SNew(SCustomWidget)
                    .Text(FText::FromString("Hello"));

물론 이 작업을 FArguments 방식으로 수행하지 않고도 수행할 수 있습니다:

`class SCustomWidget : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SCustomWidget){}               
    SLATE_END_ARGS()
public:
    void Construct(const FArguments& InArgs, FText InText) {    //直接从Construct的函数参数中传入
        Text = InText;                      
        ChildSlot                                   
        [
            SNew(STextBlock)
            .Text(Text)
        ];
    }
private:
    FText Text;
};`

`auto MyWidget = SNew(SCustomWidget,FText::FromString("Hello"));   //从SNew的参数列表中传入Text`

위의 파라미터를 델리게이트에 바인딩하려면 슬레이트의 어트리뷰트 메커니즘을 사용해야 합니다:

class SCustomWidget : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SCustomWidget)                         
        : _Text(FText::FromString("Default")) {     
        }               
        SLATE_ATTRIBUTE(FText,Text)                         //使用宏SLATE_ATTRIBUTE定义参数
    SLATE_END_ARGS()
public:
    void Construct(const FArguments& InArgs) {      
        Text = InArgs._Text;                                //接收传递进来的参数
        ChildSlot                                   
            [
                SNew(STextBlock)
                .Text(Text)                                 //传递文本属性
            ];
    }
private:
    TAttribute<FText> Text;                                 //使用TAttribute包裹属性
};

그런 다음 다음과 같은 코드를 사용할 수 있습니다:

auto MyWidget = SNew(SCustomWidget)
        .Text_Lambda([](){
            return FText::FromString("Hello");
        });

위의 텍스트가 변경될 때와 같이 일부 이벤트 핸들러 콜백을 컨트롤에 추가하려면 슬레이트의 이벤트 메커니즘을 사용할 수 있습니다:

class SCustomWidget : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SCustomWidget)                         
        : _Text(FText::FromString("Default")) {     
        }               
        SLATE_ATTRIBUTE(FText,Text)                         
        SLATE_EVENT(FSimpleDelegate, OnTextChanged) //使用宏SLATE_EVENT声明事件
    SLATE_END_ARGS()
public:
    void Construct(const FArguments& InArgs) {      
        Text = InArgs._Text;    
        OnTextChanged = InArgs._OnTextChanged;      //传递事件委托
        ChildSlot                                   
        [
            SNew(STextBlock)
            .Text(Text)
        ];
    }
    void SetText(FText InText) {
        Text = InText;
        OnTextChanged.ExecuteIfBound();             //执行委托
    }
    FText GetText(){
        return Text.Get();
    }
private:
    FSimpleDelegate OnTextChanged;                  //定义委托
    TAttribute<FText> Text;
};

auto MyWidget = SNew(SCustomWidget)
    .Text_Lambda([](){
        return FText::FromString("Hello");
    })
    .OnTextChanged_Lambda([](){                     //当文字变动时打印日志
        UE_LOG(LogTemp,Warning,TEXT("Oh, Text is changed!"));
    });

또한, SLATE_BEGIN_ARGS(...)와 SLATE_END_ARGS() 사이에는 다른 매크로들도 사용할 수 있지만, 사용 빈도가 낮아 여기서는 자세히 다루지 않겠습니다. 자세한 내용은 다음을 참조하세요:

  • Engine\\Source\\Runtime\\SlateCore\\Public\\Widgets\\DeclarativeSyntaxSupport.h

이상으로 Slate의 기본적인 개발 과정을 살펴보았습니다.
 
... 파트 4에서 위젯 개요 계속...