Ch3 팀 프로젝트(5) - Debuff Icon(1)

yys·2026년 5월 14일

TIL

목록 보기
47/74

텍스처 찾기


이번엔 디버프 아이콘을 추출하려고 하나, 배경은 각자의 프롬프트가 달라서 일관성 없게 유지될 것 같아서 아이콘만 이미지로, 배경은 머터리얼로 구현하기로 했다.

먼저 "팔 부상"에 관련된 아이콘을 작성해달라고 했는데, 다음과 같이 구현해주었다.

텍스처 질감이라거나 표현 등은 아는 지식이 없다보니 프롬프트가 빈약한데, 이 정도면 괜찮은 것 같아서 나머지 아이콘(출혈, 굶주림, 발 골절, 갈증 등)을 유사한 텍스처로 작성해달라고 하였다.

다만, 이 상태 그대로 텍스처를 사용하려고 하면 1:1 비율로 텍스처를 사용하기 때문에 출혈 아이콘이나 갈증 아이콘은 특히 이미지가 뭉개지는 현상이 발생한다.

그렇기 때문에 Photopea에서 1대1 비율의 투명 레이어를 생성해준 후 아이콘의 크기를 재설정해주는 작업을 진행해주었다.

코드 작업


디버프 클래스의 흐름은 다음과 같다.

  • PDDebuffIconWidget: 디버프 아이콘의 이미지/머터리얼만 담은 클래스
  • PDDebuffIconBarWidget: 디버프 아이콘을 나열할 클래스
//PDDebuffIconWidget.h
class PROJECTD_API UPDDebuffIconWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	void SetIconData(UMaterialInterface* IconMaterial);

protected:
	UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional))
	TObjectPtr<UImage> Image_Background;
	
	UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
	TObjectPtr<UImage> Image_Icon;
};

// PDDebuffIconWidget.cpp
void UPDDebuffIconWidget::SetIconData(UMaterialInterface* IconMaterial)
{
	if (!Image_Icon || !IconMaterial) return;

	Image_Icon->SetBrushFromMaterial(IconMaterial);
}

PDDebuffIconWidget는 정말 단순하게 이미지로 무엇을 표현할지에 대한 정보만 저장해주고 특정 시점에 아이콘을 활성화하는 로직만 구현되어 있다.

PDDebuffIconBarWidget은 그냥 컨테이너에 Icon을 삽입하는 게 아니라 다음과 같은 규칙이 존재한다.

  1. 디버프가 걸린 순서대로 아이콘이 삽입이 된다.
  2. 디버프가 해제되면 위치와 상관 없이 아이콘이 삭제된다.
  3. 삭제된 디버프 자리를 채우기 위해 뒤에 있는 모든 아이콘을 앞으로 땡겨온다.
//PDDebuffIconBarWidget.h
USTRUCT(BlueprintType)

// 디버프 머터리얼 데이터
struct FPDDebuffIconData
{
	GENERATED_BODY()

	UPROPERTY(EditDefaultsOnly)
	TObjectPtr<UMaterialInterface> IconMaterial;
};

UCLASS(Abstract, BlueprintType, meta = (DisableNativeTick))
class PROJECTD_API UPDDebuffIconBarWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	// 버프 활성화 함수
	void SetDebuffActive(const FGameplayTag& DebuffTag, bool bActive);

protected:
	virtual void NativeConstruct() override;
	
    // Container는 Horizontal/Vertical로 구성
	UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
	TObjectPtr<UPanelWidget> Container_Icons;

	UPROPERTY(EditDefaultsOnly, Category = "PD|Debuff")
	TSubclassOf<UPDDebuffIconWidget> IconWidgetClass;
	
    // GameplayTag와 아이콘 머터리얼을 매핑하기 위한 컨테이너
	UPROPERTY(EditDefaultsOnly, Category = "PD|Debuff", meta = (ForceInlineRow))
	TMap<FGameplayTag, FPDDebuffIconData> TagToIconData;

private:
	// 런타임에서 현재 활성화 된 디버프 아이콘을 파악하기 위한 컨테이너
	UPROPERTY(Transient)
	TMap<FGameplayTag, TObjectPtr<UPDDebuffIconWidget>> ActiveIcons;
};

// PDDebuffIconBarWidget.cpp
void UPDDebuffIconBarWidget::NativeConstruct()
{
	Super::NativeConstruct();
	
    // 기본적으로 런타임에 아이콘을 추가하기 때문에 사전 클리어 작업
	if (Container_Icons)
	{
		Container_Icons->ClearChildren();
	}
	ActiveIcons.Reset();
}

void UPDDebuffIconBarWidget::SetDebuffActive(const FGameplayTag& DebuffTag, bool bActive)
{
	if (!Container_Icons) return;
	
    // 태그에 맞는 아이콘 머터리얼을 가져온 후
	const FPDDebuffIconData* Data = TagToIconData.Find(DebuffTag);
	if (!Data) return;
	
	const bool bCurrentlyActive = ActiveIcons.Contains(DebuffTag);

	// 해당 디버프가 활성화 상태가 아니라면
	if (bActive && !bCurrentlyActive)
	{
		if (!IconWidgetClass) return;
		
		UPDDebuffIconWidget* NewIcon = CreateWidget<UPDDebuffIconWidget>(this, IconWidgetClass);
		if (!NewIcon) return;
		
        // 아이콘을 생성한 뒤, 머터리얼도 같이 세팅해준다.
		NewIcon->SetIconData(Data->IconMaterial);
		
        // 이후 컨테이너의 자식으로 붙여준다.
		Container_Icons->AddChild(NewIcon);
		ActiveIcons.Add(DebuffTag, NewIcon);
	}
    // 해당 디버프가 수명을 다했다면
	else if (!bActive && bCurrentlyActive)
	{
    	// 아이콘을 컨테이너 자식으로 제거
		TObjectPtr<UPDDebuffIconWidget> Existing = ActiveIcons.FindRef(DebuffTag);
		if (Existing)
		{
			Container_Icons->RemoveChild(Existing);
		}
		ActiveIcons.Remove(DebuffTag);
	}
}

해당 클래스는 SetDebuffActive()가 핵심인데, 이는 HUD에서 호출되고 디버프가 보유/해제될 때마다 컨테이너에 할당/제거를 하는 모습을 볼 수 있다.

디버프 아이콘이 제거될 때 뒤에 있는 요소가 땡겨지는 것은 Horizontal/Vertical Box의 특성 상 자동으로 땡겨오기 때문에 다른 로직은 추가해주지 않았다.

// PDHUDWidget.h

protected:
    // 디버프 아이콘 바 위젯 바인딩
    UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional))
    TObjectPtr<UPDDebuffIconBarWidget> Bar_Debuffs;

private:
    // 디버프 태그 바인딩 초기화 함수
    void BindAllDebuffTags();
    
    // 개별 디버프 상태 처리 핸들러
    void HandleBleeding(const FGameplayTag& Tag, int32 NewCount);
    void HandleLegDamaged(const FGameplayTag& Tag, int32 NewCount);
    void HandleArmDamaged(const FGameplayTag& Tag, int32 NewCount);
    void HandleStarving(const FGameplayTag& Tag, int32 NewCount);
    void HandleDehydrated(const FGameplayTag& Tag, int32 NewCount);

    // 디버프 이벤트 바인딩을 위한 태그 델리게이트 함수
    void BindTagEvent(const FGameplayTag& Tag, TFunction<void(const FGameplayTag&, int32)> OnChanged);
    void UnbindAllTags();
    
// PDHUDWidget.cpp
void UPDHUDWidget::RebindToASC(UAbilitySystemComponent* NewASC)
{
    BindAllDebuffTags();
}

void UPDHUDWidget::BindAllDebuffTags()
{
    BindTagEvent(PDGameplayTags::State_Debuff_Bleeding,
       [this](const FGameplayTag& Tag, int32 NewCount) { HandleBleeding(Tag, NewCount); });

    /* 나머지 4개의 디버프도 똑같이 구현 */
}

void UPDHUDWidget::BindTagEvent(const FGameplayTag& Tag,
	TFunction<void(const FGameplayTag&, int32)> OnChanged)
{
	if (!CachedASC.IsValid()) return;
	UAbilitySystemComponent* ASC = CachedASC.Get();

	OnChanged(Tag, ASC->GetGameplayTagCount(Tag));

	TWeakObjectPtr<UPDHUDWidget> WeakSelf = this;
	FDelegateHandle Handle = ASC->RegisterGameplayTagEvent(Tag, EGameplayTagEventType::NewOrRemoved)
		.AddLambda([WeakSelf, OnChanged](const FGameplayTag CallbackTag, int32 NewCount)
		{
			if (!WeakSelf.IsValid()) return;
			OnChanged(CallbackTag, NewCount);
		});

	BoundTagHandles.Add({ Tag, Handle });
}

void UPDHUDWidget::HandleBleeding(const FGameplayTag& Tag, int32 NewCount)
{
    if (Bar_Debuffs)
    {
       Bar_Debuffs->SetDebuffActive(Tag, NewCount > 0);
    }
}
/* 나머지 4개의 디버프 핸들링도 똑같이 구현 */

코드만 보면 조금 난해할 수도 있는데, 내용의 흐름을 정리하자면 다음과 같다.

  1. PDDebuffIconBarWidget에서 태그와 머터리얼 매핑
  2. HUD 초기화 및 ASC 바인딩(RebindToASC)
  3. 디버프 태그가 활성화 되면 핸들링 함수를 수행하도록 리스너 등록(BindAllDebuffTags, BindTagEvent, 나머지 핸들링 함수)
  4. 핸들링 함수에서 아이콘을 컨테이너에 Push하는 SetDebuffActive()를 호출
  5. 유저는 해당 디버프 아이콘을 보게 됨

다음에는 머터리얼 작업을 하고 태그와 머터리얼을 매핑하는 작업을 할 예정이다.

profile
게임 개발 지망생

0개의 댓글