[UE5/GAS] 데미지 플래시를 멀티플레이에서 모두에게 보이게 하기 — GameplayEffect와 GameplayCue를 다리로
현재 개발 중인 프로젝트에서는 데미지를 받으면 메시가 체력 구간별 색(흰→노랑→빨강)으로 잠깐 번쩍이는 피격 연출을 GAS 기반으로 구현하고자 했다.
결과적으로 적(SkeletalMesh)과 파괴 가능 오브젝트(StaticMesh)에 공용으로 붙을 수 있는 UNSDamageFlashComponent(UActorComponent)을 구현하는 것으로 결정했다.
기능 자체는 단순하지만, 멀티플레이 환경이라 두 가지를 고민해야 했다.
- "이 번쩍임을 어떻게 모든 클라이언트에게 똑같이 보여줄 것인가"
- "이 연출을 어디에 담아야 여러 액터에 재사용되면서도 깔끔할 것인가"
1. 요구사항
- 서버에서 데미지가 적용되는 순간, 호스트와 모든 클라이언트 화면에서 똑같이 번쩍여야 한다.
- 번쩍임은 게임 시뮬레이션에 영향을 주지 않는 순수 연출(비주얼)이다.
- 적, 파괴 오브젝트 등 메시 종류가 다른 여러 액터에 같은 방식으로 붙어야 한다.
2. 비주얼을 어떻게 복제할 것인가
가장 단순하게 생각하면 "번쩍여라"라는 RPC를 멀티캐스트로 쏘는 방법이 있다.
하지만 GAS에는 정확히 이 용도로 설계된 채널이 이미 존재한다. 바로 GameplayCue다.
그래서 직접 RPC를 작성하는 대신 GameplayEffect(GE) + GameplayCue 조합을 택했다.
전체 흐름은 다음과 같다.
[서버] 데미지 GameplayEffect 적용
│ (GE의 GameplayCues 배열에 GameplayCue.Damage.Flash 태그 부착)
▼
ASC가 Cue를 전 클라이언트로 멀티캐스트 (호스트 로컬 포함)
│
▼
[각 머신] GameplayCueNotify::OnExecute(MyTarget) 실행
│
▼
MyTarget->FindComponentByClass<UNSDamageFlashComponent>()->PlayFlash()
│
▼
컴포넌트가 로컬에서 메시에 오버레이 머티리얼로 번쩍임 재생
2-1. GE에 Cue를 다는 것만으로 복제가 되는 이유
GameplayEffect는 서버에서 권위적으로 적용된다.
그리고 GE의 GameplayCues 배열에 태그를 넣어두면, GE가 적용되는 순간 ASC가 그 Cue를 NetMulticast(언리라이어블)로 관련 클라이언트 전원에게 전파한다.
즉 별도의 복제 코드나 RPC를 한 줄도 작성하지 않아도, "데미지가 들어갔다"는 서버 이벤트에 번쩍임이 자동으로 따라붙어 모두에게 전달된다. Cue는 원래 "전원이 보지만 시뮬레이션에는 영향이 없고, 누락돼도 괜찮은 연출"을 위한 채널이라 transient한 플래시와 성격이 정확히 맞는다.
추가로, 프로젝트의 모든 데미지 GE가 공용 부모 GE(GE_BaseDamage)를 상속하도록 reparent했다. 부모에 Cue 태그를 한 번만 달면 모든 데미지 경로에서 자동으로 번쩍이게 된다.
2-2. GameplayCue를 다리로 쓴 이유
CueNotify는 무상태(UGameplayCueNotify_Static)로 두었고,
하는 일은 단 하나, 타겟 액터에서 플래시 컴포넌트를 찾아 PlayFlash()를 호출하는 것뿐이다.
bool UNSGameplayCueNotify_DamageFlash::OnExecute_Implementation(
AActor* MyTarget, const FGameplayCueParameters& Parameters) const
{
Super::OnExecute_Implementation(MyTarget, Parameters);
if (!MyTarget) return false;
if (UNSDamageFlashComponent* Flash =
MyTarget->FindComponentByClass<UNSDamageFlashComponent>())
{
Flash->PlayFlash();
}
return true;
}
이렇게 하면 "데미지 발생(게임 로직, 서버 권위)"과 "번쩍임 재생(클라 비주얼)"이 완전히 분리된다. Cue가 다리 역할을 하면서, 컴포넌트는 멀티플레이를 전혀 신경 쓰지 않는 순수 로컬 비주얼 코드로 남는다.
3. 연출을 어디에 담을 것인가
번쩍임 자체를 어디에 둘지도 따로 고민했다.
CueNotify 안에서 직접 메시를 번쩍여도 되고, 각 액터에 메서드를 넣어도 된다.
하지만 둘 다 문제가 있었다.
- CueNotify에 직접 구현: CueNotify는 여러 타겟이 공유하는 무상태 객체라, 페이드 진행도나 MID 핸들 같은 인스턴스별 상태를 둘 곳이 없다.
- 각 액터에 메서드로 구현: 적, 엄폐물, 폭발통마다 같은 코드가 중복된다. 메시 타입(Skeletal/Static)도 제각각이라 복붙이 어긋난다.
따라서 연출을 UNSDamageFlashComponent라는 UActorComponent로 캡슐화하고, 붙이기만 하면 동작하도록 했다.
컴포넌트로 분리하면서 신경 쓴 점들은 다음과 같다.
3-1. 메시 종류를 가리지 않게 - UMeshComponent 기준
적은 SkeletalMesh, 파괴 오브젝트는 StaticMesh다.
둘의 공통 부모인 UMeshComponent를 기준으로 동작하게 하니 한 컴포넌트가 양쪽을 모두 커버한다.
컴포넌트는 BeginPlay에서 소유자의 메시를 찾거나 지정받아 거기에 연출을 건다.
3-2. 베이스 머티리얼을 건드리지 않는 오버레이 방식
처음에는 머티리얼에 FlashColor/FlashAmount 파라미터를 넣고 MID로 제어하려 했다.
하지만 적과 오브젝트가 제각각 다른 머티리얼을 쓰기 때문에, 모든 머티리얼에 같은 파라미터를 심어야 하는 부담이 있었다.
머티리얼마다 FlashColor / FlashAmount 파라미터 삽입 (모든 머티리얼 수정 필요)
│
▼
SetOverlayMaterial()로 전용 플래시 머티리얼 덧씌움 (베이스 머티리얼 무관)
따라서 UE5의 SetOverlayMaterial()로 전용 플래시 머티리얼을 덧씌우는 방식을 택했다.
베이스 머티리얼이 무엇이든 상관없고, 슬롯이 여러 개여도 오버레이는 메시 전체에 한 번에 적용돼서 슬롯별 MID 생성 같은 번거로움도 사라졌다.
3-3. Tick 없이 타이머로 연출
번쩍임은 확 켜졌다가 짧게 사라지는 연출이다. 이를 위해 매 프레임 Tick을 켜는 것은 낭비라고 생각했다.
PrimaryComponentTick은 끈 채로, PlayFlash()에서 강도를 1로 올리고 짧은 주기 타이머로 강도를 0까지 감소시켜 페이드한다. 평상시(피격이 없을 때) 비용이 0이다.
3-4. FindComponentByClass - opt-in 방식이라 누락돼도 안전
CueNotify가 FindComponentByClass()로 컴포넌트를 찾고, 없으면 그냥 무시한다.
덕분에 플래시를 원하는 액터만 컴포넌트를 붙이면 되고, 컴포넌트가 없는 액터에 데미지가 들어와도 에러 없이 조용히 지나간다. "기능 추가 = 컴포넌트 부착"이라는 한 동작으로 끝난다.
3-5. 색 정책도 컴포넌트가 소유
체력 구간별 색 규칙(임계값과 색상)을 컴포넌트 안에 두어, 색을 바꾸고 싶으면 컴포넌트만 고치면 된다. CueNotify도, 액터도, GE도 색을 모른다.
결과적으로 GE는 "맞았다"는 이벤트만, CueNotify는 "컴포넌트를 찾아 호출"만, 컴포넌트는 "어떻게 보일지"만 안다. 책임이 한 줄씩 깔끔하게 나뉜다.
4. 고려했지만 선택하지 않은 대안
4-1. GameplayAbility(GA)에 구현
GA는 능력을 발동하는 쪽의 개념이다. 데미지를 받는 대상은 피격 시 GA를 돌리지 않는다.
번쩍임을 GA에 넣으면 로직이 맞은 피해자가 아니라 때린 공격자 쪽에 얹히게 되어 대상 메시를 다루기 어색해진다.
또한 모든 데미지가 GA에서 나오는 것도 아니다. 환경 데미지나 GE를 직접 적용하는 경우는 빠진다.
특히 투사체는 GA가 발사 순간 종료되고 한참 뒤 투사체 액터가 충돌하므로, GA는 히트 시점을 알지도 못한다.
게다가 GA는 코스트, 쿨다운, 프리딕션, 어빌리티 태스크까지 딸린 무거운 시스템이라 단순 피격 연출에는 과하다.
데미지는 이미 피해자에게 적용되는 GE로 모델링돼 있으니, 자연스러운 부착 지점은 GA가 아니라 GE다.
그래야 히트스캔, 투사체, AoE, 환경 데미지가 한 곳에서 일괄로 플래시를 얻는 것이 가능하다.
4-2. Health 어트리뷰트 변화 델리게이트 구독
GetGameplayAttributeValueChangeDelegate(Health)에 바인딩해서 "Health가 줄면 번쩍여라"도 가능하다.
실제로 처음에는 이 방법도 떠올렸다.
하지만 문제가 있었다.
- 이벤트 의미가 흐려진다. 델리게이트는 "값이 변했다"만 알려줄 뿐, 데미지/힐/리젠/초기화/클램프를 구분하지 못한다. 힐이나 리젠에도 번쩍이게 되어 추가 필터링이 필요하다.
- 타이밍과 입자(granularity)가 복제 케이던스에 종속된다. 클라에서는 Health가 복제될 때 델리게이트가 뜨는데, 여러 작은 피격이 한 번의 복제 값으로 합쳐지면 개별 피격 번쩍임을 놓칠 수 있다. "한 대당 한 번 번쩍"이라는 피격 단위 피드백이 깨진다.
- 반면 Cue는 "이 데미지 인스턴스가 발생했다"는 명시적 이벤트다. 피격 한 번이 Cue 한 번으로 깔끔하게 매칭되고, instigator나 location 같은 파라미터도 함께 온다.
요컨대 어트리뷰트 델리게이트로 가면 GAS가 이미 제공하는 Cue 채널을 더 나쁜 형태로 재발명하는 셈이었다.
4-3. 다만, 색 결정에는 Health를 읽는다
오해를 피하기 위해 덧붙인다. 번쩍임의 트리거는 Cue지만, 번쩍임의 색은 컴포넌트가 PlayFlash() 시점에 ASC에서 Health/MaxHealth를 한 번 스냅샷으로 읽어 정한다.
const float Ratio = Health / MaxHealth;
if (Ratio >= High) return White; // 흰색
if (Ratio >= Low) return Yellow; // 노랑
return Red; // 빨강
즉 트리거(이벤트 = Cue)와 상태 조회(색 = 어트리뷰트 스냅샷)를 분리했다. 어트리뷰트는 결과를 읽는 데만 쓰고, 흐름을 모는 트리거로는 쓰지 않는다. 매 프레임 연산이 없어 가볍고, 멀티에서 경계값 근처의 미세한 색 오차는 짧은 번쩍임이라 체감되지 않는 선에서 허용했다.
5. 결론
데미지 플래시를 GameplayEffect + GameplayCue 구조로 구현한 결과, 별도의 복제 코드 없이도 서버 데미지 적용 한 번으로 호스트와 모든 클라이언트에서 동일하게 번쩍임이 재생되는 것을 확인했다.
공용 부모 GE에 Cue 태그를 한 번 단 것만으로 플레이어, 포탑, 드론 등 모든 데미지 경로가 자동으로 플래시를 얻었고, 새 공격 주체를 추가해도 별도 작업이 필요 없었다.
정리하면 이번 작업의 핵심은 다음과 같다.
- 연출의 멀티플레이 복제는 직접 RPC보다 GameplayCue가 정석이다. GE의 GameplayCues에 태그만 달면 서버 적용에서 전 클라 멀티캐스트가 따라온다.
- GameplayCue는 게임 로직과 비주얼을 잇는 다리다. CueNotify는 컴포넌트를 찾아 호출만 하고, 컴포넌트는 멀티플레이를 모르는 순수 로컬 비주얼로 남는다.
- GA는 발동하는 쪽, GE는 적용받는 쪽이다. 피격 연출은 피해자에게 적용되는 GE에 거는 것이 자연스럽고, 투사체와 AoE, 환경 데미지까지 한 곳에서 커버된다.
- 어트리뷰트 변화 구독은 트리거로 부적합하다. 이벤트 의미와 피격 단위, 타이밍을 잃는다. 어트리뷰트는 트리거가 아니라 상태 조회로만 쓴다.
- 연출은 액터에 직접 박지 말고 컴포넌트로 빼는 것이 좋다. UMeshComponent 기준과 오버레이 머티리얼로 메시 종류와 머티리얼을 가리지 않게 하니, 여러 액터에 붙이기만 하면 되는 재사용 단위가 됐다.
'언리얼 게임개발' 카테고리의 다른 글
| [TIL] 2026-06-08 | 최종 팀 프로젝트 4 - Procedural Dungeon 환경에서 Packed Level Actor(PLA)를 이용한 맵 최적화 (0) | 2026.06.08 |
|---|---|
| [TIL] 2026-05-21 | 최종 팀 프로젝트 3 - 전투 맵 디자인 및 NavMesh 설정 (1) | 2026.05.21 |
| [TIL] 2026-05-20 | 최종 팀 프로젝트 2 - 기획서 마일스톤 단위로 분리 (1) | 2026.05.20 |
| [TIL] 2026-05-15 | 최종 팀 프로젝트 1 - 프로토타입 기획서 (0) | 2026.05.15 |
| [TIL] 2026-04-29 | AI를 활용한 나만의 3D 게임 캐릭터 만들기 2 (Kawaii Physics를 활용한 머리카락 물리 적용) (0) | 2026.04.29 |