캐릭터가 공격을 받으면 체력이 감소하고, 체력이 0이 되면 사망하는 기능은 액션 게임에서 가장 기본적인 시스템입니다. 처음에는 공격 블루프린트에서 상대 체력 변수를 직접 가져와 빼는 방식으로 구현하기 쉽지만, 프로젝트가 커지면 공격마다 체력 감소 로직이 반복되고 플레이어와 적의 처리도 뒤섞이게 됩니다.
언리얼 엔진은 이러한 문제를 줄일 수 있도록 Apply Damage, Event AnyDamage, Damage Type 같은 기본 데미지 기능을 제공합니다. 공격하는 쪽은 “누구에게 얼마의 피해를 줄 것인지”만 전달하고, 공격받는 쪽은 방어력 계산과 체력 감소, 피격 반응 및 사망 처리를 담당하도록 역할을 나눌 수 있습니다.
💡 한 줄 정답 — 언리얼 엔진 Damage System은 공격하는 블루프린트에서 Apply Damage로 피해량과 공격자 정보를 전달하고, 공격받는 Actor의 Event AnyDamage에서 최종 체력 감소와 피격·사망 처리를 실행하는 구조로 만들면 플레이어와 적에게 공통으로 적용하면서도 각 캐릭터의 반응을 독립적으로 관리할 수 있습니다.
목차
- 언리얼 엔진 Damage System이란?
- Apply Damage와 Event AnyDamage의 차이
- Damage System에 필요한 변수 만들기
- 공격하는 쪽에서 Apply Damage 호출하기
- 공격받는 쪽에서 체력 감소 처리하기
- 체력이 0이 되었을 때 사망 처리하기
- 플레이어와 적이 같은 데미지 시스템을 사용하는 방법
- 중복 타격을 방지하는 방법
- 방어력과 무적 상태 적용하기
- Damage Type은 언제 사용해야 할까?
- UI와 피격 효과 연결하기
- 자주 겪는 오류
- 마무리
언리얼 엔진 Damage System이란?
언리얼 엔진의 기본 데미지 시스템은 공격하는 Actor와 공격받는 Actor 사이에서 피해 정보를 전달하는 기능입니다.
여기서 중요한 점은 Apply Damage가 상대 체력을 자동으로 줄여주는 노드는 아니라는 것입니다. 이 노드는 피해량, 공격자, 피해를 발생시킨 Actor와 Damage Type을 대상에게 전달합니다.
전달받은 피해를 실제 체력에 적용하는 일은 공격받는 캐릭터의 Event AnyDamage에서 직접 구현해야 합니다.
전체적인 구조는 다음과 같습니다.
공격 판정 성공→ Apply Damage 호출→ 공격 대상의 Event AnyDamage 실행→ 방어력과 무적 상태 확인→ 체력 감소→ 피격 반응 또는 사망 처리
이렇게 공격과 체력 처리를 분리하면 검, 총알, 스킬과 폭발 공격이 모두 같은 Damage System을 사용할 수 있습니다.

[공격자 블루프린트의 Apply Damage에서 피해 대상의 Event AnyDamage로 연결되는 전체 흐름 이미지]
Apply Damage와 Event AnyDamage의 차이
두 노드는 이름이 비슷하지만 담당하는 역할이 완전히 다릅니다.
| 노드 | 실행 위치 | 역할 |
|---|---|---|
| Apply Damage | 공격하는 쪽 | 대상에게 데미지 정보 전달 |
| Event AnyDamage | 공격받는 쪽 | 전달받은 피해 처리 |
| Apply Point Damage | 공격하는 쪽 | 피격 위치와 방향이 필요한 단일 지점 공격 |
| Event PointDamage | 공격받는 쪽 | 부위와 충돌 정보를 포함한 피해 처리 |
| Apply Radial Damage | 폭발을 발생시키는 쪽 | 일정 범위 안의 Actor에게 피해 전달 |
| Event RadialDamage | 공격받는 쪽 | 범위 피해 정보 처리 |
근접 공격을 처음 구현한다면 Apply Damage와 Event AnyDamage만으로도 충분합니다. 헤드샷이나 총알의 충돌 위치가 중요하다면 Apply Point Damage, 폭탄처럼 범위 공격이 필요하면 Apply Radial Damage를 사용할 수 있습니다.
공식 문서에 따르면 Event AnyDamage는 대상 Actor가 어떤 형태로든 데미지를 받았을 때 호출됩니다. 멀티플레이 게임에서는 서버에서만 실행되는 Authority Only 이벤트라는 점도 기억해야 합니다.
Damage System에 필요한 변수 만들기
먼저 데미지를 받을 캐릭터 블루프린트에 다음 변수를 만들어 줍니다.
| 변수 이름 | 자료형 | 기본값 예시 | 역할 |
|---|---|---|---|
| MaxHealth | Float | 100 | 최대 체력 |
| CurrentHealth | Float | 100 | 현재 체력 |
| Defense | Float | 0 | 피해 감소에 사용할 방어력 |
| IsDead | Boolean | False | 사망 중복 처리 방지 |
| IsInvincible | Boolean | False | 무적 상태 확인 |
| LastDamageCauser | Actor Reference | None | 마지막으로 피해를 준 Actor |
Event BeginPlay에서는 CurrentHealth를 MaxHealth 값으로 초기화합니다.
Event BeginPlay→ Set CurrentHealth = MaxHealth
기본값을 모두 100으로 설정했더라도 초기화 로직을 따로 만드는 것이 좋습니다. 나중에 캐릭터 레벨이나 장비에 따라 MaxHealth가 변경될 때 현재 체력도 정확하게 맞출 수 있기 때문입니다.
체력이 음수가 되거나 최대 체력을 넘어가지 않도록 계산 결과에는 Clamp를 적용하는 것이 안전합니다.
공격하는 쪽에서 Apply Damage 호출하기
근접 공격이라면 무기 충돌, Overlap 또는 Trace로 공격 대상을 찾은 다음 Apply Damage를 호출합니다.
노드 연결은 다음과 같이 구성할 수 있습니다.
공격 Trace 성공→ Break Hit Result→ Hit Actor→ Is Valid→ Apply Damage
Apply Damage의 주요 입력값은 다음과 같습니다.
| 입력값 | 넣어야 하는 값 |
|---|---|
| Damaged Actor | 공격에 맞은 Actor |
| Base Damage | 기본 공격 피해량 |
| Event Instigator | 공격 캐릭터를 조종하는 Controller |
| Damage Causer | 피해를 직접 발생시킨 Actor |
| Damage Type Class | 공격 종류를 구분할 Damage Type |
플레이어 캐릭터가 주먹으로 적을 공격한다면 보통 다음처럼 설정합니다.
Damaged Actor = Hit ActorBase Damage = AttackDamageEvent Instigator = Get ControllerDamage Causer = Self
무기 Actor가 별도로 존재한다면 Damage Causer에 무기 또는 투사체를 전달할 수도 있습니다. 누가 공격했는지 확인해야 한다면 Event Instigator와 Damage Causer를 목적에 맞게 구분해야 합니다.

[Sphere Trace 또는 Box Trace 성공 후 Apply Damage를 호출하는 실제 Blueprint Graph]
공격받는 쪽에서 체력 감소 처리하기
데미지를 받을 캐릭터 블루프린트에서 Event AnyDamage를 추가합니다. 이 이벤트의 Damage 출력에는 공격하는 쪽에서 전달한 피해량이 들어옵니다.
가장 기본적인 체력 감소 구조는 다음과 같습니다.
Event AnyDamage→ Branch: IsDead가 False인가?→ Branch: IsInvincible이 False인가?→ FinalDamage 계산→ CurrentHealth - FinalDamage→ Clamp(0, MaxHealth)→ Set CurrentHealth
방어력을 단순하게 수치 차감 방식으로 처리한다면 다음 공식을 사용할 수 있습니다.
FinalDamage = Max(Damage - Defense, 0)
예를 들어 공격력이 30이고 방어력이 8이면 실제 피해는 22가 됩니다. Max를 사용하지 않으면 방어력이 공격력보다 높을 때 피해량이 음수가 되어 오히려 체력이 회복되는 문제가 생길 수 있습니다.
초반 프로젝트에서는 이 정도만으로도 충분합니다. 방어력 계산이 복잡해진다면 별도의 CalculateFinalDamage 함수로 분리하는 것이 좋습니다.
CalculateFinalDamage입력: BaseDamage출력: FinalDamage
피해 계산을 함수로 분리하면 치명타, 버프, 속성 저항과 난이도 보정도 한곳에서 관리할 수 있습니다.
체력이 0이 되었을 때 사망 처리하기
체력을 변경한 다음에는 CurrentHealth <= 0인지 확인합니다.
Set CurrentHealth→ CurrentHealth <= 0→ Branch True → Die False → Play Hit Reaction
사망 처리는 Die라는 함수나 커스텀 이벤트로 분리하는 것이 좋습니다.
Die→ Branch: IsDead가 False인가?→ Set IsDead = True→ 이동과 공격 입력 중지→ 충돌 설정 변경→ 사망 애니메이션 재생→ AI 로직 중지→ 보상 또는 게임오버 처리
IsDead를 먼저 True로 바꾸는 이유는 같은 프레임이나 짧은 시간 안에 여러 공격이 들어왔을 때 사망 로직이 반복되는 것을 막기 위해서입니다.
사망 몽타주, 아이템 드롭, 점수 증가와 Actor 제거를 Event AnyDamage에 전부 연결하면 그래프가 복잡해집니다. Event AnyDamage는 사망 여부만 판단하고 실제 처리는 Die에서 담당하도록 나누는 편이 관리하기 쉽습니다.

[사망 처리하기]
플레이어와 적이 같은 데미지 시스템을 사용하는 방법
플레이어와 적 모두 데미지를 받는다고 해서 두 캐릭터의 Event AnyDamage를 완전히 똑같이 구성할 필요는 없습니다.
공통으로 필요한 부분은 다음과 같습니다.
- 최대 체력과 현재 체력 관리
- 최종 피해량 계산
- 무적 상태 확인
- 사망 여부 확인
- 체력 변경 알림
반면 캐릭터별로 다른 부분도 있습니다.
| 플레이어 | 적 캐릭터 |
|---|---|
| 게임오버 또는 부활 처리 | 경험치와 아이템 보상 |
| 플레이어 HUD 갱신 | 적 머리 위 체력바 갱신 |
| 입력 잠금 | AI Behavior Tree 정지 |
| 체크포인트 이동 | 일정 시간 후 Actor 제거 |
프로젝트 규모가 작다면 부모 캐릭터인 BP_CharacterBase에 공통 체력 로직을 만들고 플레이어와 적이 이를 상속받는 방법이 간단합니다.
플레이어와 적의 구조가 많이 다르다면 BPC_HealthComponent 같은 Actor Component를 만들어 체력 계산만 공통으로 관리할 수 있습니다. 이때 플레이어 부활이나 적 보상처럼 각 캐릭터에만 필요한 기능은 컴포넌트 내부에 억지로 넣지 않는 것이 좋습니다.
체력이 0이 됐다는 사실은 이전 글에서 만든 Event Dispatcher로 알릴 수 있습니다.
CurrentHealth <= 0→ Call OnDeath
플레이어, 적 AI, UI와 GameMode가 필요한 방식으로 OnDeath에 반응하도록 연결하면 체력 컴포넌트가 다른 시스템을 직접 참조하는 문제를 줄일 수 있습니다.
중복 타격을 방지하는 방법
근접 공격을 만들 때 가장 자주 발생하는 문제는 한 번의 공격 모션으로 같은 적이 여러 번 피해를 받는 것입니다.
Sphere Trace Multi, Box Trace 또는 무기 Overlap을 Tick이나 Anim Notify State에서 반복 실행하면 같은 적이 여러 프레임에 걸쳐 감지될 수 있습니다.
이 문제는 HitActors라는 Actor 배열을 만들어 해결할 수 있습니다.
공격 시작→ Clear HitActorsTrace에서 적 감지→ HitActors에 대상이 포함되어 있는가? False → Add Unique → Apply Damage True → 아무 처리하지 않음공격 종료→ Clear HitActors
Add Unique를 사용하면 같은 Actor가 배열에 중복으로 들어가는 것을 막을 수 있습니다. 하지만 공격이 끝났을 때 배열을 비우지 않으면 다음 공격에서도 해당 적에게 피해가 들어가지 않습니다.
따라서 공격 몽타주의 시작 또는 공격 판정이 활성화되는 Anim Notify에서 배열을 초기화하고, 한 번의 공격 구간이 끝난 뒤 다시 정리해야 합니다.

[hit reaction후 apply damage 시키는 블루프린트]
방어력과 무적 상태 적용하기
회피, 가드 또는 부활 직후처럼 피해를 받지 않아야 하는 상황에서는 IsInvincible 변수를 사용할 수 있습니다.
Event AnyDamage→ Branch: IsInvincible True → 종료 False → 피해 계산
가드는 무적과 조금 다르게 처리할 수 있습니다. 완전히 피해를 막는다면 0으로 만들고, 일부만 막는다면 전달받은 피해에 감소 비율을 적용합니다.
가드 중 실제 피해 = Damage × 0.3일반 상태 피해 = Damage
무적 여부를 공격하는 쪽에서 확인하지 말고 피해를 받는 쪽에서 판단하는 것이 좋습니다. 공격자가 여러 종류일 때마다 무적 검사 로직을 반복하지 않아도 되기 때문입니다.
다만 피격 이펙트나 공격 성공 판정이 무적 상태에서도 발생해야 하는지는 게임 디자인에 따라 별도로 결정해야 합니다.
Damage Type은 언제 사용해야 할까?
Damage Type은 피해 종류를 구분하기 위한 클래스입니다.
예를 들어 다음과 같이 만들 수 있습니다.
DT_Normal: 일반 공격DT_Fire: 화염 피해DT_Poison: 독 피해DT_Explosion: 폭발 피해DT_Execution: 처형 공격
공식 문서에서도 Damage Type은 특정 형태의 피해를 설명하고, 피해 종류에 따라 다른 반응을 만들 수 있도록 제공되는 기능이라고 설명합니다.
예를 들어 화염 Damage Type이면 캐릭터에게 불 이펙트를 붙이고 일정 시간 추가 피해를 줄 수 있습니다. 처형 공격이라면 일반 방어력을 무시하도록 설정할 수도 있습니다.
하지만 공격 하나마다 무조건 Damage Type을 만들 필요는 없습니다. 단순한 프로젝트라면 일반 공격과 특수 피해 정도만 구분하고, 실제로 다른 반응이 필요한 시점에 종류를 늘리는 편이 좋습니다.

[DamageType 구분하기]
UI와 피격 효과 연결하기
체력이 변경되면 체력바, 피격 이펙트와 사운드도 갱신해야 합니다. 이때 위젯이 매 프레임 체력을 확인하도록 Event Tick을 사용하는 방식은 피하는 것이 좋습니다.
대신 OnHealthChanged라는 Event Dispatcher를 만들고 체력이 변경될 때만 호출합니다.
Set CurrentHealth→ Call OnHealthChanged CurrentHealth MaxHealth
체력 위젯은 게임 시작 시 OnHealthChanged에 이벤트를 Bind하고, 호출될 때 Progress Bar를 갱신합니다.
HealthPercent = CurrentHealth / MaxHealth
피격 반응은 체력 UI와 분리하는 것이 좋습니다. 캐릭터 블루프린트에서는 몽타주와 이펙트를 실행하고, 위젯은 체력 표시만 담당하도록 구성하면 역할이 명확해집니다.
이 구조는 앞에서 작성한 Blueprint Event Dispatcher 사용법과 자연스럽게 연결됩니다.
자주 겪는 오류
Apply Damage를 호출했는데 체력이 줄지 않는 경우
Apply Damage만 배치하고 공격받는 Actor에 Event AnyDamage와 체력 감소 로직을 만들지 않았을 가능성이 큽니다.
또한 대상 Actor의 Can Be Damaged 설정이 꺼져 있으면 데미지 이벤트가 호출되지 않을 수 있습니다.
Event AnyDamage가 실행되지 않는 경우
Damaged Actor에 실제 공격 대상이 들어가는지 확인합니다. Trace의 Hit Actor가 None이거나 공격자 자신이 들어가면 원하는 결과가 나오지 않습니다.
멀티플레이에서는 Event AnyDamage가 서버에서 실행되는 이벤트라는 점도 확인해야 합니다.
한 번 공격했는데 체력이 여러 번 감소하는 경우
공격 판정을 여러 프레임 동안 반복하면서 동일한 적에게 계속 Apply Damage를 호출하고 있을 가능성이 큽니다. 공격 한 번에 맞은 Actor를 저장하는 HitActors 배열을 사용합니다.
플레이어 공격으로 플레이어 자신이 피해를 받는 경우
Trace 또는 Overlap의 대상에서 Self를 제외하지 않았을 수 있습니다. Actors to Ignore에 공격자 자신을 추가하거나 Hit Actor != Self를 검사합니다.
체력이 0인데 사망 로직이 계속 실행되는 경우
IsDead 검사가 없거나 값을 너무 늦게 변경하고 있을 가능성이 큽니다. Die가 시작되는 순간 가장 먼저 IsDead = True로 설정합니다.
적을 공격했는데 플레이어용 로직까지 실행되는 경우
플레이어와 적이 같은 Take Damage 흐름을 사용하면서 UI, 부활과 AI 사망 처리를 하나의 공통 로직에 모두 넣은 경우입니다. 체력 감소만 공통으로 관리하고 캐릭터별 사망 처리는 분리해야 합니다.
마무리
언리얼 엔진의 기본 Damage System은 구조 자체는 어렵지 않습니다. 공격하는 쪽에서 Apply Damage를 호출하고 공격받는 쪽의 Event AnyDamage에서 체력을 감소시키는 것이 핵심입니다.
중요한 것은 모든 기능을 하나의 이벤트에 연결하지 않는 것입니다. 피해 계산은 함수로 분리하고, 사망 처리는 Die, UI 갱신은 OnHealthChanged, 중복 타격 방지는 HitActors 배열처럼 역할별로 나누는 것이 좋습니다.
처음에는 체력 100에서 공격력 20을 빼는 작은 기능부터 시작해 보세요. 정상적으로 동작하는 것을 확인한 다음 방어력, 무적, 치명타, Damage Type과 부활 시스템을 하나씩 추가하면 오류가 발생했을 때 원인을 찾기도 쉬워집니다.
다음 글에서는 Damage System을 만들 때 자주 발생하는 Accessed None 오류의 원인과 해결 방법을 실제 블루프린트 상황을 예로 들어 알아보겠습니다.
참고 사이트
- Event AnyDamage 공식 문서
Actor가 데미지를 받았을 때 호출되는 Event AnyDamage의 출력값과 서버 권한 실행 여부를 확인할 수 있습니다. - GameplayStatics Apply Damage API
Apply Damage, Apply Point Damage를 비롯해 언리얼 엔진이 제공하는 기본 피해 전달 기능을 확인할 수 있습니다. - Damage Type 공식 API 문서
화염, 폭발과 같은 피해 종류를 구분하고 공격별 반응을 확장할 때 참고할 수 있습니다. - 언리얼 엔진 타이머 사용법
화상이나 독처럼 일정 시간마다 반복 피해를 주는 시스템을 구현할 때 참고하기 좋은 공식 예제입니다.