Game Develop

[UE5] C++로 위젯블루프린트 생성 후 부착하기 본문

UnrealEngine5/이것저것

[UE5] C++로 위젯블루프린트 생성 후 부착하기

MaxLevel 2023. 8. 16. 02:57

튜토리얼?이라던가 그런것들 보면 보통 생성자에서 부착해서 쓰고들 한다.

그래서 생성자에서만 호출 가능한 로드함수들을 사용하고는 한다.

CreateDefaultSubobject라던가 ConstructorHelpers::FClassFinder (혹은 FObjectFinder 등) 같은 걸 쓴다.

 

일단 기존의 생성자에서 호출하는 코드.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    UWidgetComponent* widgetComponent = CreateDefaultSubobject<UWidgetComponent>(*assetPath);
    widgetComponent->SetupAttachment(mesh);
    widgetComponent->SetRelativeLocation(relativeLocation);
    widgetComponent->SetWidgetSpace(EWidgetSpace::Screen);
 
    static ConstructorHelpers::FClassFinder<UUserWidget> UI_HUD(*assetPath);
    if (UI_HUD.Succeeded())
    {
        widgetComponent->SetWidgetClass(UI_HUD.Class);
        widgetComponent->SetDrawSize(drawSize);
    }
    UCharacterUpperHPBar* upperHPBar = Cast<UCharacterUpperHPBar>(widgetComponent->GetUserWidgetObject());
    if (upperHPBar != nullptr)
    {
        
        upperHPBar->BindStatComponent(castedStatusActor->GetStatComponent());
    }
cs

 

 

 

근데 UI관련해서 구조를 설계할 때 캐릭터클래스들 쪽에서 UI관련 변수를 멤버변수로 선언하고 싶지 않았다.

UI는 게임로직과 아무상관없이 철저히 분리해야한다고 알고있기도 하고, 그게 맞는 것 같다.

예를 들어 게임옵션에서 체력바를 보이게하는거에 대한 ON/OFF하는 기능을 구현해야한다고 생각하면 좀 더 와닿기 쉬운 것 같다.

그러한 기능들을 쉽게 구현하려면 결국 한쪽에서 메모리에 올려져 있는 캐릭터들의 체력바들을 전부 들고있는게 편하지 않을까... 

 

그래서 언리얼의 서브시스템을 사용해서 UI Manager를 따로 만들었다. 알아보는 과정에서 서브시스템을 있는걸 알게 됐다. 이전에 csv파일에서 공격정보들을 읽어오는거랑 액터풀을 커스텀한 게임인스턴스에다가 작성해놨는데 이것도 서브시스템으로 구분하는게 맞을 것 같다.

게임인스턴스 상속받아서 따로 정의한 커스텀인스턴스를 만들 경우 툭하면 pure VirtualFunction 어쩌고 저쩌고가 발생해서.. 구글링하니까 뭔가 확실한 해결법은 없는것 같다. 그래서 마음에 안들었었는데 서브시스템이 있어서 괜찮은 것 같다.

 

어쨌든 다시 본론으로 들어와서 기존의 생성자에서 위젯컴포넌트를 생성하고 내가 생성했던 위젯블루프린트를 더 나중에 호출하기위해 컴포넌트를 가져오는 CreateDefaultSubobject를 NewObject로 바꾸고 클래스를 로드하는 ConstructorHelpers::FClassFinder를 LoadClass로 바꿨다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    UWidgetComponent* widgetComponent = NewObject<UWidgetComponent>(this, UWidgetComponent::StaticClass(), subObjectName);
    widgetComponent->SetupAttachment(mesh);
    widgetComponent->SetRelativeLocation(relativeLocation);
    widgetComponent->SetWidgetSpace(EWidgetSpace::Screen);
 
    auto widgetClass = LoadClass<UUserWidget>(nullptr, TEXT("WidgetBlueprint'/Game/UI/Monster/UI_HPBar.UI_HPBar_C'"));
 
    if (widgetClass != nullptr)
    {
        widgetComponent->SetWidgetClass(widgetClass);
        widgetComponent->SetDrawSize(drawSize);
    }
 
    auto widgetObject = widgetComponent->GetUserWidgetObject(); // 여기가 nullptr임;;
    UCharacterUpperHPBar* upperHPBar = Cast<UCharacterUpperHPBar>(widgetObject);
    if (upperHPBar != nullptr)
    {
        
        upperHPBar->BindStatComponent(castedStatusActor->GetStatComponent());
    }
cs

 

딱 이정도만 하고 빌드하면 에러가 발생한다. 컴포넌트랑 widgetClass에는 값이 들어오긴 했는데, 14번째 줄의 widgetObject에서는 null값이 뜬다. 

 

이유를 바로 말하자면, 생성자에서 컴포넌트를 불러오면 해당 액터에 자동으로 붙지만 그 외의 곳에서 불러오면 수동으로 붙여줘야 한다는 것이다. 

생성자에서 CreateDefaultSubobject로 생성하면 CreationMehtod가 Native로 설정된다고 한다.

이 값에 따라서 컴포넌트가 관리되는 방식이 조금씩 달라진다.

블루프린트의 Construction Script에서 생성된 컴포넌트는 스크립트가 호출될 때 마다 삭제하고 새로 생성되는식이라고 한다.

 

어쨌든 생성자에서 생성한 컴포넌트는 자동으로 액터의 RegisterAllComponents에서 등록이 되지만, 동적으로 생성한 컴포넌트는 수동으로 호출(등록) 해줘야 한다고 한다.

두줄이 추가되는데 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    UWidgetComponent* widgetComponent = NewObject<UWidgetComponent>(actor, UWidgetComponent::StaticClass(), subObjectName);
    widgetComponent->SetupAttachment(mesh);
    widgetComponent->SetRelativeLocation(relativeLocation);
    widgetComponent->SetWidgetSpace(EWidgetSpace::Screen);
    widgetComponent->CreationMethod = EComponentCreationMethod::UserConstructionScript;
    widgetComponent->RegisterComponentWithWorld(GetWorld());
 
    UClass* widgetClass = LoadClass<UUserWidget>(nullptr, TEXT("WidgetBlueprint'/Game/UI/Monster/UI_HPBar.UI_HPBar_C'"));
 
    if (widgetClass != nullptr)
    {
        widgetComponent->SetWidgetClass(widgetClass);
        widgetComponent->SetDrawSize(drawSize);
    }
 
    UUserWidget* widgetObject = widgetComponent->GetUserWidgetObject();
    checkf(widgetObject != nullptr, TEXT("Failed to Get UserWidgetObject"));
 
    UCharacterUpperHPBar* upperHPBar = Cast<UCharacterUpperHPBar>(widgetObject);
    checkf(upperHPBar != nullptr, TEXT("Failed Cast To CharacterUpperHPBar"));
 
    if (upperHPBar != nullptr)
    {
        upperHPBar->BindStatComponent(castedStatusActor->GetStatComponent());
    }
cs

5번째줄에서 CreationMethod에 어떠한 값을 넣고 6번재줄에서 RegisterCompnentWithWorld(GetWorld())라는 함수를 호출해준다. 이 과정이 등록해주는 과정이다.

 

그리고 경우에 따라서 컴포넌트를 등록했을 때 BeginPlay를 호출해줘야 한다고 한다.

저 BeginPlay를 호출해야한다는 말은 최소 BeginPlay이후에 동적으로 부착해야한다는 말이다.

 

위 코드처럼 수동으로 등록해줬는데도 16번째줄에서 null이 떴었다.

원인은 PossessedBy에서 로직을 수행해서 그랬던 것이고 알려준대로 BeginPlay에서 수행하니까 비로소 전부 해결이 됐었다.