본문 바로가기
언리얼_엔진_게임개발_공부/언리얼 C++

[언리얼/C++] 디버프 - 코드로 UTextBlock 동적 생성 및 추가

by jaboy 2025. 2. 13.

디버프 효과가 적용될 때 디버프 이름과 남은 시간을 UTextBlock 으로 생성하여 HUD 위젯 블루프린트에서 만들어 놓은 Vertical Box 에 추가하는 과정을 기록한다.

 

프로세스

아래의 내용을 0.1초마다 호출되어 HUD 를 업데이트하는 함수에 구현한다.

1. 만들어 놓은 Vertical Box 가져오기

--> GetWidgetFromName 함수로 가져온 후 UVerticalBox 로 캐스팅

if (UVerticalBox* StatusBox = Cast<UVerticalBox>(HUDWidget->GetWidgetFromName(TEXT("StatusBox"))))
{
//...

2. 캐릭터의 현재 상태

--> 캐릭터 클래스의 퍼블릭 멤버 변수 가져오기

if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(PlayerController->GetCharacter()))
{
	uint8 CharacterStatus = PlayerCharacter->CharacterStatus;

3. 구현한 상태별 체크

--> 캐릭터의 상태는 비트 연산으로 적용/해제/확인할 수 있도록 Enum 을 만들었다. 이에 대한 자세한 내용은 다른 글에서 다뤄보자. (사실 이와 관련한 글은 인터넷에 많이 있다! 나도 참고했다.)

if ((CharacterStatus & static_cast<uint8>(ECharacterStatus::Slow)) == static_cast<uint8>(ECharacterStatus::Slow))
{
// 상태 해당
}
else
{
// 상태 미해당
}

* 주의 1. Bitwise 연산(&)은 비교 연산(==) 이후에 이루어지므로 괄호를 쳐주어야 한다.

* 주의 2. enum 값은 strongly typed 이므로 정수형으로 캐스팅이 필요하다. (귀찮..)

4. (상태 해당) 텍스트가 이미 위젯에 추가되었는지 확인

--> TMap<FName, UTextBlock*> 으로 <상태 이름, 상태 텍스트> 를 관리 추적하도록 했다.

...ㅋㅋㅋ...

// 헤더 파일 선언
TMap<FName, class UTextBlock*> StatusTextMap;

//...
UTextBlock* SlowText;
if (UTextBlock** SlowTextPtr = StatusTextMap.Find(FName(TEXT("Slow"))))
{
// 상태 텍스트가 이미 추가되어 있는 경우
	SlowText = *SlowTextPtr;
}
else
{
// 상태 텍스트가 추가되어 있지 않은 경우
}

--> 위젯에 추가 시 맵에도 추가, 삭제 시 맵에서도 삭제가 필요하겠다.

 

5. TMap 에 없는 경우 UTextBlock 생성 및 UVerticalBox 와 TMap에 추가

--> NewObject<UTextBlock>(Outer) 로 HUD에 표시할 상태텍스트를 생성한다.

* Outer 는 위에서 말한 Vertical Box 로 넘겼다.

--> TMap 에 Add(TTuple<FName, UTextBlock*>(상태이름, 상태텍스트) 

--> Vertical Box 에 AddChildToVerticalBox(상태텍스트) 로 추가한다.

else
{
	// 상태 텍스트가 맵에 추가되어 있지 않은 경우
	SlowText = NewObject<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("StatusBox")));
	StatusBox->AddChildToVerticalBox(SlowText);
	StatusTextMap.Add(TTuple<FName, UTextBlock*>(FName(TEXT("Slow")), SlowText));
}

* Vertical Box 는 위에서부터 아래로 자손 위젯을 주루룩 추가한다.

6. 상태 텍스트에 표시할 텍스트 지정

--> 표시할 텍스트는 "상태 이름 (남은 시간)"

--> 남은 시간은 상태에 해당하는 FTimerHandle 을 캐릭터에서 가져와 월드 타이머 매니저의 GetTimerRemaining 로 확인

--> 상태 텍스트에 SetText 로 위의 정보를 표시하게 한다.

if (IsValid(StatusText))
{
    float SlowRemainingTime = GetWorldTimerManager().GetTimerRemaining(PlayerCharacter->SlowTimerHandle);
    SlowText->SetText(FText::FromString(
        FString::Printf(TEXT("Slow (%.0f s)"), SlowRemainingTime)));
}

7. (상태 미해당) TMap에 해당 상태 텍스트가 있는 경우 맵과 VerticalBox에서 삭제

--> TMap 의 RemoveAndCopyValue 로 삭제 및 값(상태 텍스트) 복사

--> 상태 텍스트에 RemoveFromParent() 로 위젯에서 삭제 및 메모리 해제

else
{
	// 상태 미해당
    if (StatusTextMap.Find(FName(TEXT("Slow"))))
    {
        UTextBlock* SlowText;
        if (StatusTextMap.RemoveAndCopyValue(StatusName, SlowText))
        {
            SlowText->RemoveFromParent();
        }
    }
}

* RemoveFromParent() 가 메모리 해제하는 것 관련해서는 아래에 정리한 적이 있다.

 

[언리얼/C++] UUserWidget 인스턴스에 RemoveFromParent 호출 시 메모리 해제?

물론 알아서 잘 해주는 언리얼 엔진이지만...언리얼 엔진의 UMG 를 활용해 게임 UI 를 만드는 것에 대한 강의를 들었다.예제를 다루며 특정 상황에 위젯 객체를 RemoveFromParent 함수를 호출하여 화면

jaboy.tistory.com

 

 

그러나 이것을 각 상태 효과마다 해야한다고?!

지금이야 과제로 미니 프로젝트하는 느낌이라 3개 정도 구현했기에 괜찮지만...

만약 게임에 구현된 상태 효과가 수십개라면?

마비노기의 버프/디버프...

출처 : https://x.com/som_wf/status/1745728472809607502?mx=2

 

어차피 똑같은 짓을 여러 번 하는 거니까... 반복문을 써보기로 했다.

상태효과를 Bitwise 연산으로 관리하기 위해 Enum 의 각 상태에 해당하는 값은 2의 거듭제곱이다.

그러니 반복문을 써서 인덱스가 1부터 2씩 곱해진다면 각 상태에 대해 확인할 수 있다. (0은 상태 효과 없음)

for (uint8 ECharacterStatusValue = 1; ECharacterStatusValue <= PlayerCharacter->ECharacterStatusMax; ECharacterStatusValue *= 2)
{
	FName StatusName = PlayerCharacter->GetStatusName(ECharacterStatusValue);
	if ((CharacterStatus & ECharacterStatusValue) == ECharacterStatusValue)
	{

이렇게!

 

단! enum 에 정의된 최댓값은 간단히 알 수 없기에 (enum 값이 연속하는 수가 아니므로) 캐릭터 클래스에 별도의 상수로 정의해두었다.

단! enum 에 어떤 이름으로 정의되었는지 알 수 없기에 캐릭터 클래스에 인자로 받은 값에 따라 이름을 FName으로 반환하는 헬퍼 함수를 만들었다.

단! enum 값이나 이름으로 특정 타이머 핸들을 가져올 수 없기에 캐릭터 클래스에 인자로 받은 값에 따라 해당 FTimerHandle 주솟값을 반환하는 헬퍼 함수를 만들었다.

FName ASpartaCharacter::GetStatusName(uint8 ECharacterStatusValue)
{
	switch (ECharacterStatusValue)
	{
	case static_cast<uint8>(ECharacterStatus::Reverse):		return FName(TEXT("Reverse"));
	case static_cast<uint8>(ECharacterStatus::Slippery):	return FName(TEXT("Slippery"));
	case static_cast<uint8>(ECharacterStatus::Slow):		return FName(TEXT("Slow"));
	default:												return FName(TEXT("None"));
	}
}

FTimerHandle* ASpartaCharacter::GetStatusTimerHandle(uint8 ECharacterStatusValue)
{
	switch (ECharacterStatusValue)
	{
	case static_cast<uint8>(ECharacterStatus::Reverse):		return &ReverseTimerHandle;
	case static_cast<uint8>(ECharacterStatus::Slippery):	return &SlipperyTimerHandle;
	case static_cast<uint8>(ECharacterStatus::Slow):		return &SlowTimerHandle;
	default:												return nullptr;
	}
}

(왜 비주얼 스튜디오에서 복사해 여기에 붙여넣으면 가끔 들여쓰기가 엉망이 되어 버리는 걸까? 귀찮으니 내비둘래)

 

이렇게 되면 필요한 데이터가 모두 있으므로 위에 적은 프로세스를 for loop 으로 만들 수 있었다.

상태 효과가 추가되는 경우 캐릭터 클래스에서 몇가지만 수정하면 된다.

- 상태 효과 enum 에 정의된 최댓값

- 상태 이름과 타이머 핸들 헬퍼 함수의 switch 문에 case 추가

 

공부하고 있는 강의에서는 에디터에서 Widget Blueprint 로 Text Block 을 생성하고, 코드에서는 Get 만 하는 방식이었는데, 이렇게 게임/플레이어의 상태에 따라 추가하는 방법을 스스로 학습해 볼 수 있어서 뿌듯했고,

간단한 것이었지만 어쨌든 이런 저런 고민을 해보며 장황한 코드를 압축할 수 있었던 것도 기분이 좋다.

 

새벽 4시 반이 다되어가네... 언제 자....