Deeper Learning

[UE5] User Widget을 사용한 다양한 레이아웃의 UI 개발 (ProjectVT 개발기) 본문

Game Development/Devlog

[UE5] User Widget을 사용한 다양한 레이아웃의 UI 개발 (ProjectVT 개발기)

Dlaiml 2024. 6. 19. 10:55

현재 진행 중인 프로젝트(ProjectVT)에서는 언리얼 엔진을 사용하고 있다. Widget Bluerpint(User Widget)의 사용이 매우 많고 그에 따라 각종 레이아웃을 만드는 일도 잦다.

Figma나 포토샵으로 미리 UI를 그려보고 이를 옮겨와서 User Widget에서 구현하는 방식으로 작업이 이루어지고 있는데 다양한 레이아웃을 User Widget에서 그대로 구현하는 것이 생각보다 쉽지 않았다. 

이번 포스팅은 메신저 느낌의 UI의 초안을 User Widget을 사용하여 만드는 방법에 대해 소개하려 한다.

 

 

Parent & Child

우선 User Widget의 Hierarchy에 대한 이해가 필요하다. 

Friend1Text->SetText(FText::FromString("친구1"));
Friend1->AddChild(Friend1Text);

 

AddChild와 RemoveFromParent와 같은 함수명에서 알 수 있듯이 User Widget은 계층구조를 가지고 있다. 

User Widget의 Designer 패널 좌측에 있는 Palette를 보면 각 위젯 컴포넌트들의 설명과 특징이 적혀있다. Single Child는 Child로 하나의 위젯 컴포넌트만 둘 수 있다는 것이다. 


주로 여러 컴포넌트를 Child로 둘 수 있는 배치와 관련된 컴포넌트는 Panel 탭에 있다.

하위 child를 Slot으로 관리하며 Z-Order 조정이 용이하며, 자유로운 배치에 적합한 Canvas Panel

가로로 children을 배치시켜 주는 Horizontal Box

세로로 children을 배치시켜주는 Vertical Box를 사용하려 한다.

 

 

Hierarchy Design

 

우선 크기를 원하는 해상도로 맞춰둔 Canvas Panel을 세팅한다.

// UChatUserWidget.h

protected:
    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UCanvasPanel* MainCanvas;

 

 

구현하고자 하는 UI를 보면 크게 세 파트로 나눌 수 있는 것을 볼 수 있다.

가장 좌측의 회색 영역, 친구 목록이 있는 흰색 배경, 우측 파란 배경의 대화 영역. 

가로로 늘어선 레이아웃에는 Horizontal Box가 적합하다.

 

좌측 영역 구현

 

// UChatUserWidget.h

UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
class UHorizontalBox* MainHorizontalBox;

 

가장 좌측 회색 영역은 배경 색을 설정할 수 있어야 하며, 내부의 컴포넌트들은 세로로 배치되어 있는 버튼이다.

우선 배경색을 설정하는 방법은 여러 가지가 있으나 Border를 사용하였다. 

 

Border는 child widget을 감싸는 역할을 하며 배경, 테두리, 패딩의 설정이 가능하다.

두 버튼을 포함하는 Vertical Box를 Border의 Component로 두었다. (single child)

 

Vertical Box에는 두 버튼을 배치하고 남은 영역은 Spacer를 두어 좌측 영역의 구현을 마쳤다.

 

// UChatUserWidget.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ChatWidget.generated.h"

UCLASS()
class TEST_API UChatUserWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    virtual void NativeConstruct() override;

protected:
    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UCanvasPanel* MainCanvas;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UImage* BackgroundImage;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UHorizontalBox* MainHorizontalBox;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UBorder* MainBorder;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UVerticalBox* MainVerticalBox;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UButton* Button1;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UButton* Button2;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class USpacer* MainSpacer;
};

 

선언한 멤버 클래스들의 계층구조 만들기, 위치설정 등 User Widget의 생성전 동작들은 NativeConstruct 시점에 구현한다.

 

// UChatUserWidget.cpp
// *블루프린트에서 수정 이전 코드라 예시 레이아웃과 일치하지 않을 수 있습니다

void UChatUserWidget::NativeConstruct()
{
    Super::NativeConstruct();

    if (MainCanvas)
    {
        if (BackgroundImage)
        {
            UCanvasPanelSlot* ImageSlot = MainCanvas->AddChildToCanvas(BackgroundImage);
            if (ImageSlot)
            {
                ImageSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
                ImageSlot->SetOffsets(FMargin(0.f));
            }
        }

        if (MainHorizontalBox)
        {
            UCanvasPanelSlot* HorizontalBoxSlot = MainCanvas->AddChildToCanvas(MainHorizontalBox);
            if (HorizontalBoxSlot)
            {
                HorizontalBoxSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
                HorizontalBoxSlot->SetOffsets(FMargin(0.f));
            }

            if (MainBorder)
            {
                UHorizontalBoxSlot* BorderSlot = MainHorizontalBox->AddChildToHorizontalBox(MainBorder);
                if (BorderSlot)
                {
                    BorderSlot->SetSize(ESlateSizeRule::Fill);
                }

                if (MainVerticalBox)
                {
                    UBorderSlot* VerticalBoxSlot = MainBorder->AddChildToBorder(MainVerticalBox);
                    if (VerticalBoxSlot)
                    {
                        if (Button1)
                        {
                            MainVerticalBox->AddChildToVerticalBox(Button1);
                        }

                        if (Button2)
                        {
                            MainVerticalBox->AddChildToVerticalBox(Button2);
                        }

                        if (MainSpacer)
                        {
                            MainVerticalBox->AddChildToVerticalBox(MainSpacer);
                        }
                    }
                }
            }
        }
    }
}

 

 

코드까지 매번 소개하기에 이번 포스팅이 너무 길어져서 나머지 영역은 어떤 컴포넌트를 사용하였고, 어떻게 예시와 같게 구현할 수 있었는지에 대해 주로 설명하겠다.

 

중앙 영역 구현


마찬가지로 중앙 영역도 배경색 설정을 위한 Border를 추가하였다. (Multi-Child가 가능한 컴포넌트 위에 이미지를 넣는 방식으로도 가능하다)

이번에도 세로로 늘어선 배치를 위해 Vertical Box를 추가하였다.

 

구현이 필요한 요소를 뜯어보면

  • 좌상단 버튼 1개
  • 텍스트 "친구"
  • 본인프로필 - 본인이름
  • 친구프로필 - 친구이름
  • 친구프로필 - 친구이름

좌상단 버튼과 텍스트의 경우 바로 Vertical Box에 추가하였다.

[프로필 - 이름]의 조합의 경우 후에 같이 관리하기 위해 Vertical Box로 한번 더 다시 묶고 [프로필-이름]이 가로로 배치되어 있기 때문에 각각 Horizontal Box를 만들고 버튼과 텍스트를 배치하였다.

 

 

우측 영역 구현

 

우측 영역도 배경이 필요하기 때문에 Border를 깔고 파란색으로 설정하였다.

구현이 필요한 요소는 아래와 같다.

  • 좌상단 프로필 - 텍스트
  • 중간 대화 영역
  • 하단 채팅 입력 영역(흰색)

전체 widget의 배치는 세로로 늘어서 있는 형태이기 때문에 우선 Vertical Box를 Border의 child로 추가하였다.

프로필-텍스트의 경우 가로로 배치되어 있기 때문에 Horizontal Box로 감싸서 배치하였다.

 

중간 대화영역의 경우 스크롤이 필요하고 내부에 실제 DataTable을 참고한 대화 내용이 들어간 Widget이 들어가야 한다.

우선 Scroll Box를 배치해 두었다.

 

하단 채팅 입력 영역은 세부기능은 아직 구현하지 않고 전체적인 레이아웃을 잡는 단계라 Border를 추가하고 흰색으로 설정하였다.

 

 


 

처음에는 여러 유튜브 영상이나, UI 구현 코드를 보아도 각각 사용하는 방법이 너무 달라 User Widget에서 원하는 레이아웃을 만드는 것이 어려웠고 시간도 오래 걸렸다.

아직 Palette에 있는 수십 가지의 widget의 사용법과 용도를 익히지 못하였으나 익숙하게 사용하는 widget이 늘어날수록 작업속도가 점점 빨라지는 것을 느끼고 있다.

 

UI/UX를 잘 만든 게임을 참고하고 여러 디자인 패턴에 대해서 공부하여 사용자가 불편을 느끼지 않으며 직관적인 UI를 만들기 위해 팀원 모두가 노력하고 있다.

Comments