이번에는 전투 시스템에서 발생하는 체력, 스태미너, 탄약, 데미지 정보를 UI와 연결한 내용을 정리해보려고 한다.
HUD의 기본 화면 구성은 팀원이 작업했고,
나는 여기에 StatComponent, GunBase, WeaponComponent에서 발생하는 데이터를 연결하는 작업을 맡았다.
이번 작업에서 내가 구현한 부분은 다음과 같다.
UI 연동
- 체력 변경 시 HP UI 갱신
- 스태미너 변경 시 SP UI 갱신
- 총기 장착 / 탄약 변경 시 탄약 UI 갱신
- UI 생성 시점과 컴포넌트 초기화 순서 문제 해결
데미지 표시
- 데미지를 받았을 때 숫자 팝업 표시
- DamagePopupComponent
- DamagePopupActor
- WBP_DamagePopup
부서지는 오브젝트 연동
- StatComponent로 체력 관리
- DamagePopupComponent로 데미지 숫자 표시
- 사망 시 GeometryCollection으로 파괴 연출
이번 작업의 핵심은 UI를 새로 만드는 것보다,
전투 시스템에서 바뀐 값들을 델리게이트를 통해 UI에 반영하는 것이었다.
HUD에 전투 데이터 연결하기
기본 HUD 화면은 팀원이 먼저 작업해주었다.

HUD에는 왼쪽 아래에 체력과 스태미너가 표시되고,
조준 시 화면 중앙에 크로스헤어가 나타나며,
오른쪽 아래에는 현재 장전된 탄 수와 남은 탄 수가 표시된다.
여기에 StatComponent와 GunBase에서 변경되는 값을 연결했다.
체력과 스태미너는 StatComponent에서 관리하고, 탄약은 GunBase에서 관리한다.
그래서 UI가 값을 계속 확인하는 방식이 아니라,
각 컴포넌트에서 발생하는 이벤트에 바인딩해서 값이 바뀔 때만 갱신되도록 만들었다.
StatComponent와 체력 / 스태미너 UI 연결
이전에 StatComponent를 만들면서 체력, 스태미너, 데미지, 회복, 사망 이벤트를 분리해두었다.
OnHealthChanged
- 현재 체력 또는 최대 체력이 변경되었을 때 호출
OnStaminaChanged
- 현재 스태미너 또는 최대 스태미너가 변경되었을 때 호출
OnDamaged
- 실제 피격이 발생했을 때 호출
OnHealed
- 회복이 발생했을 때 호출
OnDead
- 사망했을 때 호출
체력 UI와 스태미너 UI는 이 중에서 OnHealthChanged, OnStaminaChanged에 바인딩했다.

위젯이 생성될 때 캐릭터를 가져오고,
캐릭터가 가지고 있는 StatComponent를 통해 체력과 스태미너 이벤트에 바인딩한다.
그리고 시작 시점에도 현재 값을 한 번 가져와 UI를 초기화했다.
WBP_Character 생성
→ PlayerCharacter 가져오기
→ StatComponent 가져오기
→ OnHealthChanged 바인딩
→ OnStaminaChanged 바인딩
→ 현재 체력 / 스태미너 값으로 UI 초기화
이렇게 하면 데미지를 받거나, 회복하거나, 최대 체력이 변경될 때 UI가 자동으로 갱신된다.
체력 / 스태미너 ProgressBar 갱신
체력과 스태미너는 ProgressBar와 Text를 함께 사용해서 표시했다.
ProgressBar는 0부터 1 사이의 값을 사용하기 때문에,
현재 값과 최대 값을 이용해서 비율을 계산해야 한다.
비율 = Current / Max

UpdateHealth 이벤트에서는 CurrentHealth와 MaxHealth를 받아서 체력바와 체력 텍스트를 갱신했다.
OnHealthChanged 이벤트 발생
→ UpdateHealth 호출
→ CurrentHealth / MaxHealth 계산
→ HP ProgressBar SetPercent
→ "현재 체력 / 최대 체력" 형태로 Text 갱신
스태미너도 같은 구조로 OnStaminaChanged 이벤트를 받아 ProgressBar와 Text를 갱신했다.
이렇게 구성하니 StatComponent는 UI를 직접 알 필요 없이 체력과 스태미너 변경 이벤트만 발생시키고,
위젯은 해당 이벤트를 받아 화면에 표시하는 역할만 담당하게 되었다.
탄약 UI 갱신
탄약 UI는 체력이나 스태미너와 조금 달랐다.
체력과 스태미너는 캐릭터가 항상 가지고 있는 StatComponent에서 가져올 수 있지만,
총은 게임 시작부터 가지고 있는 것이 아니라 진행 중에 획득해서 장착하는 구조였다.
따라서 UI 생성 시점에 바로 총에 바인딩할 수 없었다.
그래서 GunBase에는 탄약 변경 이벤트를 만들었다.
OnAmmoChanged(CurrentAmmo, RemainAmmo)
그리고 탄약이 변경되는 상황에서 이 이벤트를 호출했다.
사격으로 탄약 소모
→ OnAmmoChanged Broadcast
재장전으로 탄약 변경
→ OnAmmoChanged Broadcast
UI에서는 현재 장착된 총이 생겼을 때 해당 총의 OnAmmoChanged에 바인딩하도록 했다.
무기 장착 시점에 탄약 UI 바인딩하기
무기는 게임 도중 장착되기 때문에, 탄약 UI는 WeaponComponent의 무기 장착 이벤트를 기준으로 연결했다.

흐름은 다음과 같다.
무기 장착 이벤트 발생
→ 장착된 무기가 GunBase인지 확인
→ 기존에 바인딩된 총이 있다면 Unbind
→ 새 총을 CurrentBoundGun으로 저장
→ 새 총의 OnAmmoChanged에 Bind
→ 현재 탄약 값으로 UI 즉시 갱신
여기서 중요한 점은 기존 총에 바인딩된 이벤트를 해제하는 것이다.
총을 교체했는데 이전 총의 이벤트가 남아 있으면,
이전 총의 탄약 변경 이벤트가 UI에 영향을 줄 수 있다.
그래서 새로운 총에 바인딩하기 전에 기존 총의 이벤트를 먼저 해제했다.
기존 총 있음
→ 기존 OnAmmoChanged Unbind
새 총 있음
→ 새 OnAmmoChanged Bind
→ 현재 탄약으로 UI 초기화
이렇게 하니 무기를 중간에 획득하거나 교체해도 탄약 UI가 정상적으로 갱신되었다.
UI 초기화 시점 문제
UI를 연결하면서 Accessed None 문제가 발생했다.


WBP_Character에서 캐릭터의 StatComponent나 WeaponComponent를 가져와 이벤트에 바인딩하려고 했는데,
해당 컴포넌트 참조가 아직 비어 있었다.
원인은 캐릭터에서 주요 컴포넌트를 캐싱하는 시점과 UI가 생성되는 시점이 어긋났기 때문이었다.
UI도 BeginPlay 흐름에서 생성되면서,
위젯 쪽 Construct나 이벤트 그래프가 캐릭터의 컴포넌트 캐싱보다 먼저 실행되는 상황이 생겼다.
문제 흐름
Character BeginPlay
→ 컴포넌트 캐싱 예정
WBP Construct
→ Character 참조
→ Character의 StatComponent 참조
→ 아직 캐싱되지 않음
→ Accessed None 발생
이를 해결하기 위해 캐릭터의 컴포넌트 참조 초기화 시점을 PostInitializeComponents()로 앞당겼다.PostInitializeComponents()는 BeginPlay()보다 먼저 호출된다.
그래서 이 시점에 StatComponent, CombatComponent, WeaponComponent 등을 미리 찾아두면,
이후 UI가 생성될 때 캐릭터의 컴포넌트 참조가 이미 준비되어 있다.
PostInitializeComponents
- 캐릭터가 가진 컴포넌트 참조 캐싱
BeginPlay
- 게임 시작 시 필요한 바인딩
- UI 생성
- 입력 준비
WBP Construct
- 이미 준비된 캐릭터 / 컴포넌트 참조 사용
- UI 이벤트 바인딩
이렇게 수정한 뒤 UI 바인딩 시점의 Accessed None 문제가 사라졌다.
이번 문제를 통해 언리얼에서 UI와 Actor 초기화 순서가 항상 내가 예상한 순서대로만 흐르지는 않는다는 것을 알게 되었다.
데미지 팝업 구조
체력 UI는 화면에 고정된 HUD에 표시되지만 데미지 팝업은 조금 다르다.
데미지를 받을 때마다 대상 근처에 숫자가 떠야 하고,
여러 번 맞으면 여러 개의 데미지 숫자가 동시에 떠야 한다.
그래서 데미지 팝업은 일반 HUD 위젯에 하나만 표시하는 방식이 아니라,DamagePopupActor를 생성하는 방식으로 구현했다.
HUD UI
- 체력
- 스태미너
- 탄약
- 크로스헤어
- 화면에 고정된 정보 표시
DamagePopupActor
- 데미지를 받을 때마다 생성
- 여러 개를 동시에 표시 가능
- 월드 위치 기반으로 표시 가능
- 각 팝업이 독립적으로 애니메이션 재생 후 제거
데미지 팝업 구조는 다음과 같이 나누었다.
StatComponent
- 데미지 처리
- OnDamaged Broadcast
DamagePopupComponent
- StatComponent의 OnDamaged에 바인딩
- 데미지를 받으면 팝업 생성 이벤트 호출
DamagePopupActor
- WidgetComponent를 이용해 데미지 숫자 표시
- 일정 시간 후 제거
WBP_DamagePopup
- 데미지 숫자 텍스트 표시
- 위로 올라가면서 작아지는 애니메이션 재생
DamagePopup 위젯
데미지 숫자를 표시하기 위해 WBP_DamagePopup을 만들었다.

위젯 자체는 단순하다.
가운데에 데미지 숫자를 표시할 텍스트를 두고,
애니메이션으로 위로 올라가면서 작아지는 효과를 만들었다.
처음에는 위젯의 Construct에서 바로 애니메이션을 재생해도 되지 않을까 생각했다.
하지만 데미지 값이 세팅되기 전에 애니메이션이 먼저 실행될 수 있으므로,
데미지 값이 들어온 뒤 애니메이션을 재생하는 흐름으로 바꾸었다.
즉, DamagePopupActor가 데미지 값을 위젯에 전달하고,
그 이후에 위젯 애니메이션을 재생하도록 했다.
DamagePopupComponent
데미지 팝업은 DamagePopupComponent에서 생성하도록 했다.
이 컴포넌트는 Owner가 가진 StatComponent를 찾아 OnDamaged에 바인딩한다.


그리고 데미지를 받으면 블루프린트 이벤트를 통해 데미지 팝업 액터를 생성한다.

BeginPlay
→ Owner의 StatComponent 찾기
→ OnDamaged에 바인딩
OnDamaged 발생
→ ShowDamagePopup 이벤트 호출
→ DamagePopupActor Spawn
→ InitDamagePopupActor로 데미지 값 전달
팝업 액터를 C++에서 직접 생성할 수도 있었지만,
스폰 위치를 조정하거나 팝업 블루프린트를 바꾸는 작업은 블루프린트가 더 편하다고 판단했다.
그래서 C++에서는 데미지를 받았다는 이벤트까지만 처리하고,
실제 팝업 생성과 연출은 블루프린트에서 처리하도록 했다.
DamagePopupActor와 위젯 초기화
DamagePopupActor는 내부에 WidgetComponent를 가지고 있다.
이 WidgetComponent에 WBP_DamagePopup을 넣고,
데미지 값을 전달받으면 위젯의 텍스트를 변경한 뒤 애니메이션을 재생한다.

흐름은 다음과 같다.
DamagePopupActor 생성
→ WidgetComponent에서 WBP_DamagePopup 가져오기
→ 데미지 숫자 설정
→ 애니메이션 재생
→ 일정 시간 후 Actor 제거
처음에는 데미지 팝업 위젯 애니메이션이 끝났을 때 DestroyActor를 호출해서 팝업 액터를 제거하려고 했다.
블루프린트에서는 Play Animation with Finished Event 노드를 사용해서,
애니메이션이 끝나면 DestroyActor를 호출하는 방식으로 처리했다.
하지만 테스트 중 문제가 있었다.
데미지 팝업을 띄운 뒤 카메라를 반대 방향으로 돌리고,
잠시 기다렸다가 다시 데미지 팝업이 있던 방향을 보면 팝업이 아직 남아 있는 경우가 있었다.
원인을 찾아보니 화면 밖에 있는 위젯은 최적화 때문에 애니메이션 Tick이 기대한 대로 처리되지 않을 수 있었다.
그 결과 데미지 팝업이 화면 밖에 있으면 애니메이션 종료 후 호출하려던 DestroyActor도 실행되지 않을 수 있었다.
해결 방법은 크게 두 가지가 있었다.
1. 위젯 애니메이션이 화면 밖에서도 처리되도록 `Tick When Offscreen` 옵션을 켠다.
2. 위젯 애니메이션 종료에 의존하지 않고, 액터 수명으로 제거한다.
나는 두 번째 방법을 선택했다.
데미지 팝업은 어차피 일정 시간 뒤 사라지는 일회성 연출이기 때문에,
애니메이션 종료 이벤트에 의존하기보다 DamagePopupActor에 직접 수명을 설정하는 편이 더 단순하다고 판단했다.
그래서 InitDamagePopupActor에서 데미지 값을 초기화한 뒤 SetLifeSpan을 호출하도록 했다.
DamagePopupActor 생성
→ 데미지 값 설정
→ 위젯 애니메이션 재생
→ SetLifeSpan(LifeTime)
→ 시간이 지나면 Actor 자동 제거
이렇게 바꾸니 카메라 방향이나 위젯 애니메이션 처리 여부와 상관없이,
데미지 팝업 액터가 일정 시간 뒤 안정적으로 제거되었다.
부서지는 오브젝트에도 같은 구조 적용하기
이후 부서지는 오브젝트를 구현하면서,
앞에서 만든 StatComponent와 DamagePopupComponent 구조를 그대로 사용할 수 있는지 확인해보았다.
부서지는 오브젝트도 단순히 “몇 번 맞으면 부서진다”처럼 만들 수는 있다.
하지만 그렇게 하면 오브젝트마다 체력이나 방어력 같은 값을 다르게 주기 어렵고,
피해 처리 방식도 캐릭터와 별도로 다시 만들어야 한다.
이번 프로젝트에서는 오브젝트마다 내구도를 다르게 줄 수 있으면 좋겠다고 생각했다.
약한 오브젝트
- 낮은 체력
- 낮은 방어력
튼튼한 오브젝트
- 높은 체력
- 높은 방어력
그래서 부서지는 오브젝트에도 StatComponent를 붙이는 방식으로 구현했다.
StatComponent를 사용하면 오브젝트마다 최대 체력, 방어력 같은 값을 다르게 설정할 수 있고,
기존 캐릭터에서 사용하던 데미지 처리 흐름도 그대로 재사용할 수 있다.
또 데미지를 받을 때 숫자 팝업도 표시하기 위해 DamagePopupComponent도 함께 붙였다.

BreakableObject
- StatComponent
- DamagePopupComponent
이렇게 구성하면 별도의 데미지 처리 코드를 새로 많이 작성하지 않아도 된다.
기존에 DamagePopupComponent는 Owner의 StatComponent를 찾아 OnDamaged에 바인딩하도록 만들어두었기 때문에,
오브젝트에 두 컴포넌트만 붙여도 데미지를 받았을 때 자동으로 데미지 팝업이 표시된다.
흐름은 다음과 같다.
공격 받음
→ StatComponent에서 체력 감소
→ OnDamaged Broadcast
→ DamagePopupComponent가 데미지 팝업 표시
→ 체력이 0이 되면 OnDead Broadcast
→ 부서지는 연출 실행
즉, 데미지 처리와 데미지 표시 구조를 캐릭터 전용으로 만들지 않았기 때문에,
부서지는 오브젝트에도 같은 구조를 재사용할 수 있었다.
StaticMesh와 GeometryCollection 분리
부서지는 오브젝트는 평소에는 일반 StaticMesh로 보이도록 했다.
그리고 체력이 0이 되어 부서질 때만 GeometryCollection을 활성화해서 파괴 연출을 보여주도록 했다.

초기 상태에서는 다음처럼 처리했다.
StaticMesh
- 보이는 상태
- Collision 활성화
GeometryCollection
- 숨김 상태
- Collision 비활성화
사망 이벤트가 발생하면 반대로 전환한다.

OnDead 발생
→ StaticMesh 숨김
→ StaticMesh Collision 비활성화
→ GeometryCollection 표시
→ GeometryCollection Collision 활성화
→ Simulate Physics 활성화
→ Physics Field / Radial Impulse 적용
→ 일정 시간 후 제거되도록 Life Span 설정
이렇게 하면 평소에는 가벼운 StaticMesh로 표시하다가,
파괴되는 순간에만 GeometryCollection을 사용해 부서지는 연출을 보여줄 수 있다.
이 구조도 StatComponent의 OnDead 이벤트에 반응해서 실행되도록 만들었다.
결과적으로 적 캐릭터뿐만 아니라 오브젝트도 같은 데미지 처리 흐름을 사용할 수 있게 되었다.
실행 예시

정리
이번에는 전투 UI와 데미지 팝업을 연결한 내용을 정리했다.
전체 흐름은 다음과 같다.
StatComponent
- 체력 / 스태미너 관리
- OnHealthChanged
- OnStaminaChanged
- OnDamaged
- OnDead
GunBase
- 탄약 관리
- OnAmmoChanged
WBP_Character
- 체력 UI 갱신
- 스태미너 UI 갱신
- 탄약 UI 갱신
- 크로스헤어 표시
WeaponComponent
- 무기 장착 이벤트 발생
- 총 장착 시 탄약 UI 바인딩
DamagePopupComponent
- OnDamaged에 반응
- 데미지 팝업 생성 이벤트 호출
DamagePopupActor
- WidgetComponent로 데미지 숫자 표시
- SetLifeSpan으로 자동 제거
BreakableObject
- StatComponent로 체력 관리
- DamagePopupComponent로 피해 숫자 표시
- OnDead 시 GeometryCollection 파괴 연출 실행
이번 작업의 핵심은 UI가 전투 컴포넌트를 직접 계속 확인하는 것이 아니라,
각 컴포넌트에서 발생하는 델리게이트에 반응하도록 만든 것이다.
값 변경
→ 델리게이트 Broadcast
→ UI 또는 팝업 컴포넌트가 반응
→ 화면 갱신
이렇게 하니 체력, 스태미너, 탄약, 데미지 팝업을 각각 필요한 시점에만 갱신할 수 있었다.
마무리
이번 작업을 통해 델리게이트 기반으로 전투 로직과 UI를 분리해두면,
체력, 스태미너, 탄약 UI뿐만 아니라 데미지 팝업이나 부서지는 오브젝트에도
같은 흐름을 확장해서 사용할 수 있다는 것을 알 수 있었다.
다음 글에서는 AnimNotifyState를 이용해서 공격 모션의 타이밍을 어떻게 제어했는지 정리해보려고 한다.
'언리얼 엔진 > 프로젝트' 카테고리의 다른 글
| [언리얼 엔진] 팀 프로젝트 기록 11 - 레벨 스트리밍 동기화와 프로젝트 마무리 (0) | 2026.05.29 |
|---|---|
| [언리얼 엔진] 팀 프로젝트 기록 10 - AnimNotifyState로 전투 모션 타이밍 제어하기 (0) | 2026.05.27 |
| [UE5 C++] 팀 프로젝트 기록 08 - RangedCombatComponent로 사격과 재장전 구현하기 (0) | 2026.05.21 |
| [언리얼 엔진] 팀 프로젝트 기록 07 - MeleeCombatComponent로 근접 공격과 콤보 구현하기 (0) | 2026.05.20 |
| [언리얼 엔진] 팀 프로젝트 기록 06 - CombatComponent로 전투 행동 상태 관리하기 (0) | 2026.05.19 |