[Unreal Engine 5] GAS_멀티플레이에서 GameplayCue가 작동하는 방식

2026. 4. 17. 19:23·프로그래밍/Unreal Engine 5

장판 스킬 이펙트가 시전자 본인에게만 안 보이는 버그를 파고들면서 GAS의
GameplayCue 복제 메커니즘을 깊게 이해하게 됐다. 정리해둔다.


1. GameplayCue란?

GAS에서 시각 및 청각 효과(파티클, 사운드, 카메라 쉐이크 등)를 담당하는 레이어. 게임플레이 로직(데미지, 버프 등)과 연출을 완벽하게 분리하기 위해 존재한다.


2. ASC(AbilitySystemComponent)의 Replication Mode 3가지

ASC는 SetReplicationMode()를 통해 복제 전략을 결정한다.

💡 Mixed 모드의 함정
Mixed 모드에서는 Owner(시전자 본인)가 GameplayCue 복제 데이터를 받지 못한다. 하지만 일반적으로 Prediction(클라이언트 예측)을 통해 로컬에서 즉시 실행되기 때문에 시각적으로는 문제가 생기지 않는다. (Full 모드는 Owner도 서버로부터 복제 데이터를 받음)


3. GameplayCue는 어떻게 복제되는가?

저장소: FActiveGameplayCueContainer (FastArray)

GAS는 활성화된 Cue 목록을 FActiveGameplayCueContainer에 저장한다. 이 컨테이너는 언리얼의 FFastArraySerializer를 상속하여 구현되어 있다.

[FastArray의 특징]

  • 일반 TArray 복제는 배열 전체를 직렬화해서 전송하므로 비효율적이다.
  • FastArray는 변경된 항목(추가/제거/수정)만 분리해서 전송한다.
  • 각 항목마다 고유한 ReplicationID와 ReplicationKey를 보유한다.

Cue 항목 구조

struct FActiveGameplayCue
{
    FGameplayTag GameplayCueTag;
    FPredictionKey PredictionKey; // ← 복제 여부를 결정하는 핵심 필드
    FGameplayCueParameters Parameters;
};

4. PredictionKey와 복제 스킵 현상

LocalPredicted 어빌리티가 클라이언트에서 활성화될 때의 흐름:

  1. 클라이언트: PredictionKey(예: K1) 생성.
  2. 클라이언트: FScopedPredictionWindow 오픈 → ASC->ScopedPredictionKey = K1 할당.
  3. 클라이언트: ServerActivateAbility(K1) RPC를 서버로 전송.
  4. 클라이언트: 로컬에서 어빌리티 즉시 실행 (예측 성공).

서버가 어빌리티를 처리할 때의 흐름:

  1. 서버: ScopedPredictionKey = K1 (클라이언트가 보낸 Key 사용).
  2. 서버: AddGameplayCue() 호출 → FActiveGameplayCue.PredictionKey = K1 기록.
  3. 서버 (FastArray 복제 시):
    • Owner 클라이언트에게: "너는 이미 K1으로 예측해서 재생했겠지?" → 전송 스킵
    • 다른 클라이언트들에게: "너희는 K1을 모르지?" → 정상 전송

🚨 문제가 발생하는 경우 (본인에게만 이펙트가 안 보이는 버그)

클라이언트가 AddGameplayCue를 로컬에서 예측(호출)하지 않았는데, 서버가 LocalPredicted 스코프 안에서 이를 호출해 버리면 다음과 같은 엇갈림이 발생한다.

  • 서버: "Owner는 K1으로 예측했을 거야" (전송 스킵)
  • Owner 클라이언트: "난 예측한 적 없는데?" (수신도 없고, 실행도 안 됨 → 이펙트 영구 미표시)
  • 다른 클라이언트들: K1을 모르므로 서버로부터 정상 수신 (이펙트 정상 표시)

5. 해결 방법: ScopedPredictionKey 초기화

서버에서 AddGameplayCue를 호출하기 직전에 Key를 강제로 비워주면, FastArray가 해당 항목을 "비예측(Unpredicted) 서버 권위 항목"으로 인식하여 Owner를 포함한 모든 클라이언트에게 정상적으로 전송한다.

// 서버 측 처리 로직
FPredictionKey SavedKey = OwnerASC->ScopedPredictionKey;
OwnerASC->ScopedPredictionKey = FPredictionKey(); // Key 강제 초기화 (비우기)

// 이제 Owner를 포함한 모든 클라이언트에게 복제됨
OwnerASC->AddGameplayCue(Tag, Params); 

OwnerASC->ScopedPredictionKey = SavedKey; // Key 원상 복구

6. AddGameplayCue 내부 분기 동작 원리

AddGameplayCue_Internal()이 호출될 때 내부적으로 다음과 같이 분기된다.

├─ IsOwnerActorAuthoritative() == true (서버)
│ └─ FastArray에 항목 추가 (PredictionKey 붙임)
│ └─ NetMulticast RPC로 이벤트 전파
│
└─ ScopedPredictionKey.IsLocalClientKey() == true (클라 예측)
└─ FastArray에 예측 항목 추가
└─ 로컬에서 즉시 InvokeGameplayCueEvent(OnActive) 호출


7. GameplayCue 생명주기 (AGameplayCueNotify_Actor 기준)

⚠️ 주의사항 (오브젝트 풀링)
AGameplayCueNotify_Actor는 기본적으로 풀링(Pooling) 처리된다. bAutoDestroyOnRemove = false로 설정된 경우, 파괴되지 않고 풀로 반환되어 다음 호출 시 재사용된다.
따라서, 이전 인스턴스의 상태(스케일, 컬러, 타이머 등)가 남아있을 수 있으므로 반드시 OnRemove에서 상태를 초기화(Reset)해야 한다.


8. AGameplayCueNotify_Actor 생성자 작성 시 주의사항

SetupAttachment(RootComponent)를 사용할 때, AGameplayCueNotify_Actor의 생성자 시점에는 RootComponent가 초기화되어 있지 않아 null 참조 경고가 발생할 수 있다.

이 상태로 오브젝트 풀에서 재사용되면, SetActorLocation이 Niagara 컴포넌트에 제대로 반영되지 않아 이펙트가 월드 원점(0,0,0) 부근 등 엉뚱한 위치에 나타나는 버그가 발생한다.

// ❌ 잘못된 방법 (RootComponent가 null이라 경고 발생)
NiagaraComp = CreateDefaultSubobject<UNiagaraComponent>(TEXT("Niagara"));
NiagaraComp->SetupAttachment(RootComponent); 

// ✅ 올바른 방법 (Niagara 컴포넌트 자체를 Root로 지정)
NiagaraComp = CreateDefaultSubobject<UNiagaraComponent>(TEXT("Niagara"));
SetRootComponent(NiagaraComp);

9. Cue 전용 GE 설계 시 주의사항

GameplayCue(GC)를 단독으로 트리거하기 위해 전용 GameplayEffect(GE)를 만들 때는 다음 두 가지 설정을 반드시 확인해야 한다.

① Require Modifier Success to Trigger Cues 체크 해제

  • 체크 시 (True): Modifier(능력치 변경 등)가 성공적으로 적용됐을 때만 Cue가 발동됨.
  • 해제 시 (False): Modifier가 없어도 Cue가 무조건 발동됨.

⚠️ 주의사항 Cue 전용 GE는 내부적으로 적용할 Modifier가 존재하지 않는다. 따라서 이 옵션을 체크된 상태로 두면 "Modifier 성공 조건"을 영구히 충족하지 못해 Cue가 아예 발동되지 않는 버그가 발생한다. 반드시 체크를 해제해야 한다.

② GE EffectContext로 Cue 파라미터 전달하기

C++에서 AddGameplayCue를 직접 호출할 때는 FGameplayCueParameters에 데이터를 자유롭게 담을 수 있지만, GE 방식을 통해 발동시킬 때는 파라미터 전달 경로가 다르다. EffectContext를 활용해야 한다.

[서버/시전자 측: 파라미터 주입]

FGameplayEffectContextHandle Context = OwnerASC->MakeEffectContext();

// 1. 발생 위치 전달
Context.AddOrigin(GetActorLocation()); 
// 2. 원본 액터(EffectCauser) 전달
Context.AddInstigator(this, this);     

// 이후 GE 적용 (이 Context를 타고 Cue까지 데이터가 넘어감)
OwnerASC->ApplyGameplayEffectToSelf(CueGE, 1.0f, Context);

[GCN 측: 파라미터 추출]

// 1. 위치 꺼내기
const FVector Origin = Parameters.EffectContext.GetOrigin();

// 2. EffectCauser로 원본 액터에 직접 접근해 추가 데이터 읽기
ASMASkillField* SourceField = Cast<ASMASkillField>(Parameters.EffectContext.GetEffectCauser());

10. GCN 풀링 재사용 시 WhileActive 오버라이드 필수

AGameplayCueNotify_Actor는 성능 최적화를 위해 오브젝트 풀링(Pooling)으로 인스턴스를 재사용한다. 이로 인해 생명주기(Lifecycle) 함수 호출 순서에 함정이 존재한다.

  • 첫 번째 사용: OnActive 호출 → 나이아가라(Niagara) Activate (정상 표시) → OnRemove 호출 → 나이아가라 Deactivate → 풀(Pool)로 반환
  • 두 번째 사용: OnActive가 아닌 WhileActive가 호출됨!

🚨 문제점: WhileActive를 오버라이드하지 않으면 기본 구현(아무것도 안 함)이 실행된다. 즉, 나이아가라 컴포넌트가 다시 활성화되지 않아 두 번째부터는 이펙트가 화면에 보이지 않는다.

[해결 방법: WhileActive에서 OnActive 재호출]
동일한 초기화 로직이 필요하다면, 아래처럼 WhileActive에서 OnActive를 다시 불러주는(위임하는) 패턴이 가장 간결하고 안전하다.

bool AGCN_SkillField::WhileActive_Implementation(
    AActor* MyTarget, const FGameplayCueParameters& Parameters)
{
    // 풀에서 꺼내져 재사용될 때도, 처음 생성됐을 때와 동일하게 초기화하도록 위임
    return OnActive_Implementation(MyTarget, Parameters);
}

💡 최종 요약

GameplayCue 관련 트러블슈팅 및 버그 원인 총정리

'프로그래밍 > Unreal Engine 5' 카테고리의 다른 글

[Unreal Engine5] GA_SkillBase 리팩토링 회고 — 애니메이션 추가와 GameplayCue owner 복제 이슈  (1) 2026.04.21
[Unreal Engine5] GAS의 GameplayEffectContextHandle과 GameplayEffectSpecHandle  (0) 2026.04.20
[Unreal Engine 5] 멀티플레이 아키텍처: GAS 퀵슬롯과 인벤토리 설계  (0) 2026.04.09
[Unreal Engine 5] 멀티플레이어 디버깅 유틸리티 & WorldContext  (0) 2026.04.06
[Unreal Engine 5] Module과 Plugin의 차이점  (0) 2026.04.02
'프로그래밍/Unreal Engine 5' 카테고리의 다른 글
  • [Unreal Engine5] GA_SkillBase 리팩토링 회고 — 애니메이션 추가와 GameplayCue owner 복제 이슈
  • [Unreal Engine5] GAS의 GameplayEffectContextHandle과 GameplayEffectSpecHandle
  • [Unreal Engine 5] 멀티플레이 아키텍처: GAS 퀵슬롯과 인벤토리 설계
  • [Unreal Engine 5] 멀티플레이어 디버깅 유틸리티 & WorldContext
hanong8
hanong8
hanong8 님의 블로그 입니다.
  • hanong8
    HaNong
    hanong8
  • 전체
    오늘
    어제
    • 분류 전체보기 (101) N
      • 프로그래밍 (98) N
        • Unreal Engine 5 (45)
        • C++ (22)
        • UML (2)
        • 자료구조 (2)
        • 알고리즘 (9)
        • 개발일지 (4)
        • DirectX11 (5)
        • Git (2)
        • 코드카타 (7) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hanong8
[Unreal Engine 5] GAS_멀티플레이에서 GameplayCue가 작동하는 방식
상단으로

티스토리툴바