구현내용
플레이어의 HP가 일정 이하로 떨어지면, 화면에 붉은색이 번쩍이는
데미지 오버레이 효과를 구현하였습니다
사용한 클래스
| HpState | HP 퍼센트 구간에 따른 단계 를 정의한 Enum 클래스 |
| MainWidget | 전달받은 HpState 단계에 따라 화면 데미지 오버레이 연출 |
| StatComponent | 현재 HP 변화를 기반으로 HpState를 계산하여 UI에 전달 |
구현
HpState
먼저 상태를 다른 시스템에서 공통으로 사용하기 위해 별도의 EnumClass를 추가해주었습니다
#pragma once
#include "CoreMinimal.h"
#include "HpState.generated.h"
UENUM(BlueprintType)
enum class EHpState : uint8
{
normal UMETA(DisplayName = "Normal"),
danger UMETA(DisplayName = "Danger"),
critical UMETA(DisplayName = "Critical")
};
MainWidget
메인위젯에서 표현될 데미지 오버레이 이미지를 추가하였습니다

public:
UPROPERTY(meta = (BindWidget))
class UImage* Image_EffectBlink;
UPROPERTY(meta = (BindWidget))
class UImage* Image_EffectDamage;
UPROPERTY(meta = (BindWidget))
class UImage* Image_EffectHugeDamage;
UPROPERTY(meta = (BindWidgetAnim), Transient)
class UWidgetAnimation* BlinkAnim;
UPROPERTY(meta = (BindWidgetAnim), Transient)
class UWidgetAnimation* DamageAnim;
UPROPERTY(meta = (BindWidgetAnim), Transient)
class UWidgetAnimation* HugeDamageAnim;
public:
EHpState currentHpState = EHpState::normal;
TMap<EHpState, UWidgetAnimation*> effectAnimations;
UFUNCTION()
void ApplyHpState(EHpState newState);
UFUNCTION()
void PlayBlinkEffect();
UFUNCTION()
void OnBlinkAnimFinished();
처음에 이미지를 숨기고 effectAnimations Map에 애니메이션을 추가하여주었습니다
void UMainWidget::NativeConstruct()
{
Super::NativeConstruct();
Image_EffectBlink->SetVisibility(ESlateVisibility::Collapsed);
Image_EffectDamage->SetVisibility(ESlateVisibility::Collapsed);
Image_EffectHugeDamage->SetVisibility(ESlateVisibility::Collapsed);
effectAnimations.Add(EHpState::danger, DamageAnim);
effectAnimations.Add(EHpState::critical, HugeDamageAnim);
effectAnimations.Add(EHpState::normal, nullptr);
}
플레이어의 Hp상태가 바뀔 때 마다 이전 효과를 정리하고 새로운 데미지 오버레이 이미지를 UI에 적용시켜줍니다
void UMainWidget::ApplyHpState(EHpState newState)
{
if (newState == currentHpState)
return;
if (UWidgetAnimation** beforeAnim = effectAnimations.Find(currentHpState))
{
if (beforeAnim && *beforeAnim)
StopAnimation(*beforeAnim);
}
switch (currentHpState)
{
case EHpState::danger:
if (Image_EffectDamage) Image_EffectDamage->SetVisibility(ESlateVisibility::Collapsed);
break;
case EHpState::critical:
if (Image_EffectHugeDamage) Image_EffectHugeDamage->SetVisibility(ESlateVisibility::Collapsed);
break;
}
currentHpState = newState;
switch (newState)
{
case EHpState::danger:
if (Image_EffectDamage)
Image_EffectDamage->SetVisibility(ESlateVisibility::Visible);
if (UWidgetAnimation** afterAnim = effectAnimations.Find(newState))
{
if (afterAnim && *afterAnim)
PlayAnimation(*afterAnim, 0.f, 0);
}
break;
case EHpState::critical:
if (Image_EffectHugeDamage) Image_EffectHugeDamage->SetVisibility(ESlateVisibility::Visible);
if (UWidgetAnimation** afterAnim = effectAnimations.Find(newState))
{
if (afterAnim && *afterAnim)
PlayAnimation(*afterAnim, 0.f, 0);
}
break;
case EHpState::normal:
if (Image_EffectDamage)
Image_EffectDamage->SetVisibility(ESlateVisibility::Collapsed);
if (Image_EffectHugeDamage)
Image_EffectHugeDamage->SetVisibility(ESlateVisibility::Collapsed);
break;
}
}
void UMainWidget::PlayBlinkEffect()
{
Image_EffectBlink->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
FWidgetAnimationDynamicEvent blinAnimFinishEv;
blinAnimFinishEv.BindDynamic(this, &UMainWidget::OnBlinkAnimFinished);
BindToAnimationFinished(BlinkAnim, blinAnimFinishEv);
PlayAnimation(BlinkAnim, 0.f, 1, EUMGSequencePlayMode::Forward, 1.f);
}
void UMainWidget::OnBlinkAnimFinished()
{
Image_EffectBlink->SetVisibility(ESlateVisibility::Collapsed);
}
StatComponent
델리게이트를 선언해줍니다
.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHpStateChanged, EHpState, newState);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnBlink);
public:
UPROPERTY()
FOnHpStateChanged onHpStateChanged;
UPROPERTY()
FOnBlink onBlink;
EHpState currentHpState = EHpState::normal;
TArray<FHpStateRule> hpStateRules;
EHpState CalculateHpState(float hpPercent) const;
UFUNCTION()
void UpdateHpState();
UFUNCTION()
void TriggerBlink();
Hp 퍼센트를 보고 어떤 State인지 판단하여 반환하며
UpdateHpState에서는 Hp변화에 따라 상태가 달라졌으면 이벤트를 브로드캐스트합니다
EHpState UStatComponent::CalculateHpState(float hpPercent) const
{
for (const FHpStateRule& rule : hpStateRules)
{
if (hpPercent <= rule.hpThreshold)
return rule.State;
}
return EHpState::normal;
}
void UStatComponent::UpdateHpState()
{
if (!stats.Contains(EBaseStatType::Hp))
return;
float maxHp = stats[EBaseStatType::Hp].EntryTotal();
if (maxHp <= 0.f)
return;
float hpPercent = currentHp / maxHp;
EHpState newState = CalculateHpState(hpPercent);
if (newState != currentHpState)
{
currentHpState = newState;
onHpStateChanged.Broadcast(newState);
}
}
void UStatComponent::TriggerBlink()
{
if (onBlink.IsBound())
onBlink.Broadcast();
}
또한 BeginPlay에서 반드시 규칙을 초기화해줍니다
3할일때 Critical
4.5할일때 Danger State를 반환합니다
if (hpStateRules.Num() == 0)
{
hpStateRules.Add({ 0.3f, EHpState::critical });
hpStateRules.Add({ 0.45f, EHpState::danger });
}
MainWidget - 2
다시 돌아와서 메인위젯에서 델리게이트를 구독해줍니다
필자는 BeginPlay에서 설정해주었습니다
statComp->onHpStateChanged.AddDynamic(this, &UMainWidget::ApplyHpState);
statComp->onBlink.AddDynamic(this, &UMainWidget::PlayBlinkEffect);
이렇게 구현한건 UStatComponent가 플레이어의 HP를 관리함으로 상태가 바뀌면 이벤트를 브로드캐스트하고
MainWidget에서 statcomponent의 이벤트를 구독하여
델리게이트로 구독자에게 알려서 동작 흐름이 발견되면 UI를 업데이트시킵니다
이렇게 하면 StatComponent가 UI를 직접 몰라도되며 확장성도 좋고 재사용성도 넓어집니다
(혹시 더 좋은 방법 있으면 댓글로 알려주세요!)
결과
4.5할의 Danger 부터 3할의 Critical로 변하는 모습이다

Critical 데미지 오버레이

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - 세이브 게임 (0) | 2025.12.02 |
|---|---|
| Unreal - 경험치, 레벨업 구현하기 (0) | 2025.12.02 |
| Unreal - 인벤토리(5-2) (UI 디테일 추가) (0) | 2025.11.29 |
| Unreal - 인벤토리(5) ( 아이템 정보 ) (0) | 2025.11.28 |
| Unreal - 인벤토리(4) ( 정렬, 삭제 ) (0) | 2025.11.26 |