이번엔 디버프 아이콘을 추출하려고 하나, 배경은 각자의 프롬프트가 달라서 일관성 없게 유지될 것 같아서 아이콘만 이미지로, 배경은 머터리얼로 구현하기로 했다.
먼저 "팔 부상"에 관련된 아이콘을 작성해달라고 했는데, 다음과 같이 구현해주었다.

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

다만, 이 상태 그대로 텍스처를 사용하려고 하면 1:1 비율로 텍스처를 사용하기 때문에 출혈 아이콘이나 갈증 아이콘은 특히 이미지가 뭉개지는 현상이 발생한다.
그렇기 때문에 Photopea에서 1대1 비율의 투명 레이어를 생성해준 후 아이콘의 크기를 재설정해주는 작업을 진행해주었다.

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

//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을 삽입하는 게 아니라 다음과 같은 규칙이 존재한다.
//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개의 디버프 핸들링도 똑같이 구현 */
코드만 보면 조금 난해할 수도 있는데, 내용의 흐름을 정리하자면 다음과 같다.
다음에는 머터리얼 작업을 하고 태그와 머터리얼을 매핑하는 작업을 할 예정이다.