Game Development/Devlog

[UE5] User Widget Scroll Box를 활용하여 Log UI 구현 (ProjectVT 개발기)

Dlaiml 2024. 6. 20. 16:03

 

 

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

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

dlaiml.tistory.com

 

저번 포스팅에 이어서 대화 내용(Log)을 구현하려 한다. 스크롤이 가능한 박스안에 컨텐츠를 담은 위젯 컴포넌트가 차곡차곡 쌓이는 형태의 UI는 게임의 채팅창, 퀘스트 대화 로그, 세이브 로드 슬롯 등 게임에서 매우 흔하게 쓰이는 UI이다.

 

발더스게이트3 Dialog

 

 

 

저번 포스팅에 예시로 작업하였던 user widget에서 이어 개발을 진행해보려 한다.

우측 영역에서 가장 위 상대 프로필 부분을 제외한 파란색 영역이 저번 Scroll box을 배치하였던 부분이다.

 

Scroll Box

Scroll Box의 Orientation을 Vertical로 두어 내부의 위젯들이 세로로 쌓일 수 있도록 하였다.

Scroll Box 안에 들어갈 User Widget을 따로 만들어서 대화 로그라면 대화가 진행될 때마다 해당 User Widget을 Create하여 대화 텍스트, 화자 이름 등 내용을 채워 넣고 이를 Scoll Box에 추가하도록 구현하려 한다.

 

 

메신저 용도의 UI를 만들고 있기 때문에 아래와 같이 프로필, 대화 상대 이름, 대화 박스, 대화 텍스트, 전송 시각으로 구성된 User Widget을 새로 만들어보려 한다.

 

 

 

#pragma once

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

UCLASS()
class TestChat_API UWBP_ChatLeft : public UUserWidget
{
    GENERATED_BODY()

public:
    virtual void NativeConstruct() override;

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

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

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

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UTextBlock* FriendName;

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

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UOverlay* OverlayBox;

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

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UScaleBox* ScaleBox;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UTextBlock* ChatLeftText;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UTextBlock* ChatLeftTime;

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

    void InitializeChatWidget();
};

 

필요한 위젯들을 헤더에서 선언하고 이들의 초기값을 정하는 InitializaChatWidget 함수를 구현하였다.

initializeChatWidget 함수는 NativeConstruct 시점에 호출된다.

 

void UWBP_ChatLeft::InitializeChatWidget()
{
    CanvasPanel = WidgetTree->ConstructWidget<UCanvasPanel>(UCanvasPanel::StaticClass(), TEXT("CanvasPanel"));
    WidgetTree->RootWidget = CanvasPanel;

    HorizontalBox = WidgetTree->ConstructWidget<UHorizontalBox>(UHorizontalBox::StaticClass(), TEXT("HorizontalBox"));
    CanvasPanel->AddChild(HorizontalBox);

    VerticalBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("VerticalBox"));
    HorizontalBox->AddChildToHorizontalBox(VerticalBox);

    FriendName = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("FriendName"));
    FriendName->SetText(FText::FromString(TEXT("친구1")));
    Profile->AddChildToVerticalBox(FriendName);

    ChatBox = WidgetTree->ConstructWidget<UHorizontalBox>(UHorizontalBox::StaticClass(), TEXT("ChatBox"));
    HorizontalBox_23->AddChildToHorizontalBox(ChatBox);

    OverlayBox = WidgetTree->ConstructWidget<UOverlay>(UOverlay::StaticClass(), TEXT("OverlayBox"));
    ChatBox->AddChildToHorizontalBox(OverlayBox);

    ProfileImage = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass(), TEXT("ProfileImage"));
    OverlayBox->AddChild(ProfileImage);

    ScaleBox = WidgetTree->ConstructWidget<UScaleBox>(UScaleBox::StaticClass(), TEXT("ScaleBox"));
    ChatBox->AddChildToHorizontalBox(ScaleBox);

    ChatLeftText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("ChatLeftText"));
    ChatLeftText->SetText(FText::FromString(TEXT("안녕")));
    ScaleBox->AddChild(ChatLeftText);

    ChatLeftTime = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("ChatLeftTime"));
    ChatLeftTime->SetText(FText::FromString(TEXT("오전 15:21")));
    ChatBox->AddChildToHorizontalBox(ChatLeftTime);

    Spacer = WidgetTree->ConstructWidget<USpacer>(USpacer::StaticClass(), TEXT("Spacer"));
    ChatBox->AddChildToHorizontalBox(Spacer);
}

 

 

이번 포스팅에서는 저번 포스팅과 다르게 c++로 위젯 생성뿐 아니라 계층 구조까지 초기값을 지정하는 방식을 소개하려 한다. (위젯의 생성, 버튼 같은 상호작용 로직 구현을 제외하고는 눈으로 보면서 작업이 편한 블루프린트로 주로 작업을 하고 있다)

 

Widget Blueprint를 생성하고 Designer 탭에서 위젯들을 배치할 때, Tree 구조로 Parent, Child를 가지도록 하여 위젯들을 계층적으로 배치하곤 한다.

 

보통 가장 위(Root)로 Canvas Panel을 설정하고 하위에 위젯들을 배치하며, 이를 C++로 작업을 할 때는 이를 WidgetTree를 사용하여 관리할 수 있다.

 

https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/UMG/Blueprint/UWidgetTree?application_version=5.4

 

 

 

Horizontal Box로 프로필 사진 부분과 우측 부분을 나누고 

우측 부분은 Vertical Box로 대화 상대 이름 | 대화 박스, 대화 시각을 나누도록 하였다.

대화 박스(말풍선 + 대화내용) | 대화 시각을 다시 Horizontal Box로 나누고 우측 여백은 Spacer로 처리하였다.

대화 박스는 Overlay안에 배경 이미지, Scale Box를 넣고 Scale Box 내부에 대화 텍스트인 Text 위젯을 넣어 전체 위젯 렌더링 비율, 사이즈 변화에 대응할 수 있도록 설계하였다.

 

 

비슷한 방식으로 자신이 보내는 메시지를 위한 WBP_ChatRight도 구현을 하고 이 두 Widget Blueprint를 저번 포스팅에 만든 레이아웃의 Scroll box에 추가하면 스크롤 가능한 대화 UI가 완성된다.

 

 

게임 내에서 퀘스트 로그 기능을 구현하려면 퀘스트 대화가 끝나거나 대사가 넘어가는 시점에 위에서 만든 ChatWidget을 Create 하고 Text, Time 등에 알맞은 값을 넣은 뒤, 이를 퀘스트 로그 기능 Widget(ChatWidget Scroll Box를 포함하는 Widget)의 Scroll Box에 Child로 연결해 주면 된다.

 

언리얼 엔진의 User Widget을 활용해 게임에서 사용할 수 있는 세로로 스크롤되는 대화 기록 UI를 간단하게 만들어보았다.

 

Reference

[0] https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/UMG/Blueprint/UWidgetTree?application_version=5.4