언리얼 부트캠프를 참여하며 UI를 학습하게 되었고, 그에 관련된 프로젝트가 주어졌습니다.

프로젝트는 UI를 통해 다양한 디자인을 하는것이였으며, 제출할 프로젝트는 조작키 변경에 대해 다뤄보고자합니다.

이번 프로젝트에 대한 제출 시 저의 목표는 이렇습니다.

  1. 빙의된 폰/캐릭터 기반의 클래스의 조작키를 런타임 환경에서도 변경, 적용, 기본값 복원
  2. 캐릭터 클래스의 IMC에대한 매핑에 대한 정보를 보관하는 데이터 에셋과 연동, 자동으로 관리
  3. UMG 형식의 UUserWidget기반의 다양한 팔레트/애니메이션 적용하기

 

이번 포스팅에서는 프로젝트 내 UI와 DataAsset의 연동과정에 대해 알아보겠습니다.

 

UMG (Unreal Motion Graphics)

  • 언리얼 엔진의 Widget Blueprint를 사용한 HUD 디자인 방식
  • 직관적이며, Text,Button,Image,Scroll,ListView등 다양한 위젯들이 존재한다.
  • 기본적으로 Animation 기능들을 가지고 있다.
  • 클릭시 색상 변경, 텍스트 무브 등 UI 내 이벤트 연출에 대한 자유도가 높다.

※ HUD(HeadUpDisplay) : 플레이어에게정보를 제공하기 위한 2D 형식의 화면. 

 

위젯 블루프린트 (Widget Blueprint)

  • UI(User Interface)를 시각적으로 설계할 수 있도록 제공되는 에디터용 블루프린트
  • 드래그앤 드롭으로 다양한 위젯들을 배치할 수 있다.
  • Designer : UI 배치 공간
  • Graph : 블루프린트를 사용한 이벤트 그래프를 작성하는 공간

 

연동과정 - 기존 위젯 블루프린트 UI

기존의 강의를 통해 학습받은 UI 적용 과정은 이렇습니다.

  1. UUserWidget를 상속받은 블루프린트 클래스를 생성한다.
  2. 위젯 블루프린트 클래스 내 디자이너에서 다양한 위젯들을 드래그앤 드롭을 통해 배치한다.
  3. HUD를 보여줄 로컬 플레이어의 컨트롤러에서 해당 위젯들을 리플렉션을 통해 바인딩, 로직을 구현한다.
  4. 로직 구현 중 버튼, 텍스트와 같은 기능들은 GetWidgetFromName 함수를 통해 캐스팅한다.

위젯 블루프린트 클래스 - 현재 점수, 레벨, 남은 시간을 나타낸다.

if (UUserWidget* HUDWidget = PlayerController->GetHUDWidget())
{
  if (UTextBlock* TimeText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Time"))))
  {
  //... Time 관련 로직
  }
  if (UTextBlock* ScoreText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Score"))))
  {
  //... Score 관련 로직
  }
  if (UTextBlock* LevelText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Level"))))
  {
  // ... Level 관련 로직
  }
 }

 

위젯 블루프린트 내 사용한 TextBlock들을 캐스팅하는 과정

 

강의를 통해 배운 과정에서는 불편한 점을 가지고 있었습니다.

GetWidgetFromName를 통해 가져올 경우 만약 Score라는 위젯의 이름이 변경시, 블루프린트와 C++클래스가 동시에 변경되어야합니다.

또한, 빌드시 문제가 발견되지않아 문제가 발생시 해당 원인을 명확하게 파악하기 어렵다.

현재 화면에서는 3개의 위젯만을 다루고 있지만, 100개의 위젯을 다루는 경우 동일 로직이 100개 있으며 문제점을 쉽게 파악하기 어렵고, 동일한 코드가 길어집니다. 즉, 유지보수성 및 확장성이 낮습니다.

해당 과정의 문제점을 해결하기 위한 리팩토링 과정에 대해 알아보겠습니다.

 

연동과정 - 리팩토링 후 UI

개선된 UI는 기존 위젯 블루프린트 클래스가 아닌 다른 클래스에서 작성되었습니다.

 

UI 개요

  • 사용자 설정시 키값을 변경하기 위한 UI
  • 현재 로컬 플레이어에 매핑되어있는 입력에 대한 정보들과 동기화.
  • 변경할 조작키의 이름 / [현재 설정된 조작키 / 변경 할 조작키]
  • 적용 버튼을 누를 시 변경된 내용 저장, 그 외는 변경된 내용은 적용하지 않는다.

UI 구조 형식을 개선한 이유

  • 블루프린트 클래스 내 디자인 후 바인딩하면 유지보수성이 낮다.
  • 또한 같은 로직을 가지고있는 UI의 경우에도 GetWidgetFromName을 통해 바인딩 해야하여 유연성 및 확장성이 낮다.

연동 결과 - IMC에 매핑되어있는 값들을 보여주는 UI

0. 기존의 입력관련된 정보를 가지고 있는 DataAsset

IMC에 매핑시 필요한 정보들 FKey, ETrigger, InputAction 및 메타정보들을 구조체 형식으로 보관중인 DataAsset.

 

1. C++ 클래스를 통한 UUserWidget 하위클래스 생성

가장 먼저 UI에 어떠한 데이터들을 노출 해야 할 지 정하는 과정입니다.

Text1 : 현재 InputAction에 대한 정보

Text2 : IMC내 IA와 매핑되어있는 FKey값

 

2. SRP원칙을 준수하여 하위클래스 분리

UI내 보여줄 Text1,Text2에 대한 값들을 저장할 클래스

해당 클래스들의 인스턴스들을 가지고있으며, 화면 출력 시 백그라운드 이미지 및 다른 UI와 연동예정인 클래스

 

FKeyBindingData

USTRUCT(BlueprintType)
struct FKeyBindingData
{
  // ...
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "KeyBinding")
	FString ActionName;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "KeyBinding")
	UInputAction* InputAction;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "KeyBinding")
	FKey DefaultKey;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "KeyBinding")
	FKey CurrentKey;
  // ...
};

위젯 내 필요한 정보들을 분리한 구조체 생성

 

UKeyBindWidget

	UPROPERTY(meta = (BindWidget))
	UTextBlock* ActionText;
	
	UPROPERTY(meta = (BindWidget))
	UButton* KeyButton;

	UPROPERTY(meta = (BindWidget))
	UTextBlock* KeyText;

	UPROPERTY(meta = (BindWidget))
	UBorder* ActionBorder;
	
	
	if (ActionText)
	{
		ActionText->SetText(FText::FromString(Data.ActionName));
	}
	if (KeyText)
	{
		KeyText->SetText(FText::FromString(Data.CurrentKey.ToString()));
	}

 

meta = (BindWidget) 

  • 블루프린트에 동일한 클래스와 동일한 이름을 가진 위젯이 있다면 해당 위젯 포인터를 통해 C++에서 런타임에 엑세스 할 수 있다.
  • 클래스와 동일한 위젯타입,위젯명이 없을 경우 컴파일 오류가 발생하여 문제를 쉽게 파악, 해결할 수 있다.

ActionText->SetText

  • ActionText는 어떠한 String명이 오는지, 변경되는지 확인하지 않는다.
  • 해당 클래스에 사용하려는 위젯들이 많아져도 유효성검사, SetText를 통해 쉽게 바인딩 할 수 있다.

 

UKeyBindWidget을 상속받은 위젯 블루프린트 클래스

BindWidget 리플렉션과 동일한 위젯이 없다면 컴파일 오류가 발생한다.

 

 

 

UMenuWidget

	UPROPERTY(meta = (BindWidget))
	UScrollBox* KeyBindingList;

	UPROPERTY(EditAnywhere, Category = "KeyBindings")
	UInputConfigPrimaryDataAsset* KeyDataAsset;

	UPROPERTY(EditAnywhere, Category = "KeyBindings")
	TSubclassOf<UOptionKeyBindWidget> KeyBindWidgetClass;
  
	void UOptionMenuWidget::NativeConstruct()
	{
		Super::NativeConstruct();
	  
		KeyBindingList->ClearChildren();
  
		// 리플렉션을 통해 등록한 데이터 에셋들을 전부 불러온다.
		for (const auto& KeyMapping : KeyDataAsset->KeyMappings)
		{
		// KeyWidget이 존재하는지
			UOptionKeyBindWidget* KeyWidget = CreateWidget<UOptionKeyBindWidget>(this, KeyBindWidgetClass);
			if (!KeyWidget) continue;
			
			// Enum To String
			FString ActionName = StaticEnum<EPlayableInputAction>()->GetNameStringByValue(static_cast<int64>(KeyMapping.InputActionEnum));
			
			// 구조체 생성, 기존의 KeyBindWidget클래스를통해 생성.
			FKeyBindingData NewData(ActionName, KeyMapping.InputAction, KeyMapping.DefaultKey, KeyMapping.CurrentKey);
			KeyWidget->SetKeyBindWidget(NewData);
			
			// ScrollBar에 자식으로 추가
			KeyBindingList->AddChild(KeyWidget);
		}
	}

 

PreConstruct()

  • UI생성 직전 호출된다.
  • 여러번 호출 할 수 있으며, UI 요소를 미리 설정할 시 사용한다.

 

NativeConstruct()

  • UUserWidget이 생성된 직후 (UI가 화면에 추가되기 전) 호출된다.
  • UI 위젯이 생성될 때 초기화 작업(버튼 바인딩, 데이터 로드, 변수 설정 등)시 사용한다.
  • 여러번 호출화 할 수 있으며, 화면에 추가될 때마다 실행된다.

 

OnInitialized()

  • 위젯이 처음 초기화 될 때 한번만 호출된다.

 

NaticeDestruct()

  • 런타임 내 위젯이 제거될 때 실행된다. 메모리 정리, 리소스 해제시 사용된다.

RemoveFromParent

  • UI가 제거될 때 호출된다.
  • 즉시 삭제되며 위젯을 강제로 제거하는 경우 사용된다.
  • EndPlay()가 Destroy()를 호출하듯이, NativeDestruct()를 호출한다.

 

 

 

해당 과정을 통해 기존의 IMC에 매핑 시 사용한 DataAsset들의 정보 (Key값, 바인딩된 IA, IA와 매핑된 액션의 이름 등)을 가져올 수 있었습니다.

이로인해 런타임 환경에서도 플레이어 컨트롤러 내 사용되는 IMC에 대한 정보에 대한 동기화를 할 수 있었으며, DataAsset에 IA들을 추가하더라도, 위젯에서도 자동으로 연동하여 보여줄 수 있게 되었습니다.

즉, 유지보수성 및 확장성을 향상시킬 수 있었습니다.

이번포스팅에서는 데이터에셋을 통해 UI를 연동한 과정에 대해 간략하게 알아보았으며,

다음 포스팅에서는 조작키를 동적환경에서 어떻게 변경할것인지에 대한 과정을 알아보겠습니다.

 

 

 

+ Recent posts