GAS : Gameplay Tag & Gameplay Cue

김혁·2025년 10월 9일

챌린지

목록 보기
13/14

Gameplay Tag에 대하여...

Gameplay Tag의 핵심 개념

  • GameplayTag : 문자열 기반의 계층적 라벨 시스템

핵심 특징

  1. 계층 구조 : 점(.)으로 구분된 트리 형태
  2. 상속 검색 : 상위 태그로 하위 모든 태그 검색 가능
  3. 컨테이너 기반 : 여러 태그를 동시에 보유 가능 (FGameplayTagContainer)
  4. 에디터 통합 : 자동완성, 검증, 관리 도구 제공

계층 구조의 장점

태그 계층 구조 예시

Status
├── Status.Buff
│   ├── Status.Buff.Temporary.Haste
│   └── Status.Buff.Permanent.Blessing
└── Status.Debuff
    ├── Status.Debuff.MovementImpaired
    │   ├── Status.Debuff.MovementImpaired.Slow
    │   └── Status.Debuff.MovementImpaired.Stun
    └── Status.Debuff.DamageOverTime
        ├── Status.Debuff.DamageOverTime.Poison
        └── Status.Debuff.DamageOverTime.Burning

가능한 검색

// 모든 디버프 체크
if (ASC->HasMatchingGameplayTag("Status.Debuff"))
{
    // 새로운 디버프가 추가되어도 코드 수정 불필요
}

// 이동 방해 디버프만 체크
if (ASC->HasMatchingGameplayTag("Status.Debuff.MovementImpaired"))
{
    // 감속, 속박, 스턴 중 하나라도 있으면 실행
}

// 정확히 스턴만 체크
if (ASC->HasMatchingGameplayTag("Status.Debuff.MovementImpaired.Stun"))
{
    // 오직 스턴 상태일 때만
}

// 다수의 태그 체크
FGameplayTagContainer RequiredTags;
RequiredTags.AddTag("Status.Buff.Temporary.Haste");
RequiredTags.AddTag("Status.Buff.Permanent.Blessing");

if (ASC->HasAnyMatchingGameplayTags(RequiredTags))
{
    // 하나라도 있으면
}

if (ASC->HasAllMatchingGameplayTags(RequiredTags))
{
    // 모두 있어야 함
}

태그 저장과 검색 메커니즘

내부 구조

struct FGameplayTag
{
    FName TagName;  // 해시 기반 빠른 비교
};

struct FGameplayTagContainer
{
    TArray<FGameplayTag> GameplayTags;
    TArray<FGameplayTag> ParentTags;  // 미리 계산된 부모 태그들
};

최적화 포인트

  • FName 기반 O(1) 비교 속도
  • 부모 태그 미리 계산하여 캐시
  • 네트워크에서 인덱스로 전송

Gameplay Ability와 태그 연결

class UMyAbility : public UGameplayAbility
{
    // 이 Ability가 가진 태그들
    FGameplayTagContainer AbilityTags;

    // 이 태그들이 있으면 실행 불가
    FGameplayTagContainer BlockAbilitiesWithTag;

    // 이 태그들이 모두 있어야 실행 가능
    FGameplayTagContainer ActivationRequiredTags;

    // 실행 중에 추가되는 태그들
    FGameplayTagContainer ActivationOwnedTags;
};

파이어볼트 스킬 설정 예시

UFireBoltAbility::UFireBoltAbility()
{
    AbilityTags.AddTag("Ability.Fire.Bolt");
    AbilityTags.AddTag("Ability.Offensive");

    BlockAbilitiesWithTag.AddTag("Status.Debuff.Silenced");
    BlockAbilitiesWithTag.AddTag("Status.Debuff.Frozen");

    ActivationOwnedTags.AddTag("Status.Casting");
    ActivationOwnedTags.AddTag("Cooldown.FireBolt");
}

태그의 종류별 정리

GameplayAbility의 태그들

// 1. AbilityTags - 이 스킬을 식별하는 태그 (다른 시스템이 이 스킬을 찾을 때 사용)
AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Ability.Fireball")));

// 2. ActivationRequiredTags - 캐릭터가 반드시 가져야 스킬 사용 가능한 태그들
ActivationRequiredTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("State.Alive")));

// 3. ActivationBlockedTags - 캐릭터가 하나라도 가지면 스킬 사용 불가한 태그들
ActivationBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Status.Silenced")));

// 4. SourceRequiredTags - 시전자가 반드시 가져야 하는 태그들
SourceRequiredTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Class.Mage")));

// 5. SourceBlockedTags - 시전자가 가지면 스킬 사용 불가한 태그들
SourceBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Debuff.Disarmed")));

// 6. TargetRequiredTags - 대상이 반드시 가져야 스킬 적용 가능한 태그들
TargetRequiredTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Type.Enemy")));

// 7. TargetBlockedTags - 대상이 가지면 스킬 적용 불가한 태그들
TargetBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Status.Invulnerable")));

// 8. CancelAbilitiesWithTag - 이 스킬 발동 시 해당 태그의 다른 스킬들을 취소
CancelAbilitiesWithTag.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Ability.Channeling")));

// 9. BlockAbilitiesWithTag - 이 스킬 활성화 중 해당 태그의 스킬 사용 차단
BlockAbilitiesWithTag.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Ability.Movement")));

GameplayEffect의 태그들

// 1. AssetTags - 이 Effect를 식별하는 태그 (디버깅/로그용)
AssetTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Effect.Damage.Fire")));

// 2. InheritableOwnedTagsContainer - Effect 지속 중 대상에게 부여되는 태그들
InheritableOwnedTagsContainer.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Status.Burning")));

// 3. RemoveGameplayEffectsWithTags - 이 Effect 적용 시 제거할 다른 Effect들의 태그
RemoveGameplayEffectsWithTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Status.Debuff")));

// 4. ApplicationTagRequirements.RequireTags - 대상이 가져야 Effect 적용 가능한 태그들
ApplicationTagRequirements.RequireTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("State.Alive")));

// 5. ApplicationTagRequirements.IgnoreTags - 대상이 가지면 Effect 적용 불가한 태그들
ApplicationTagRequirements.IgnoreTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Status.Immune")));

// 6. OngoingTagRequirements.RequireTags - Duration/Infinite Effect가 계속 유지되려면 필요한 태그들
OngoingTagRequirements.RequireTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("State.InCombat")));

// 7. RemovalTagRequirements.RequireTags - 이 태그가 생기면 Effect 자동 제거
RemovalTagRequirements.RequireTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Action.Dispel")));

// 8. GrantedApplicationImmunityTags - 이 Effect가 부여하는 면역 태그들
GrantedApplicationImmunityTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Type.Poison")));

ASC의 태그 관리

// 1. Loose Gameplay Tags - 수동으로 추가/제거하는 태그 (Effect/Ability와 무관)
ASC->AddLooseGameplayTag(FGameplayTag::RequestGameplayTag(TEXT("Debug.Invincible")));
ASC->RemoveLooseGameplayTag(FGameplayTag::RequestGameplayTag(TEXT("Debug.Invincible")));

// 2. Granted Tags - GameplayEffect가 부여하는 태그 (Effect 지속 동안만 유지)
// Effect 설정 (생성자에서):
InheritableOwnedTagsContainer.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Status.Stunned")));
// Effect 적용:
ASC->ApplyGameplayEffectToSelf(StunEffect, 1.0f, ASC->MakeEffectContext());

// 3. Activation Owned Tags - Ability 실행 중에만 부여되는 태그
// Ability 설정 (생성자에서):
ActivationOwnedTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("State.Dashing")));
// Ability 실행:
ASC->TryActivateAbilityByClass(DashAbilityClass);  // 자동으로 태그 추가/제거됨

// 4. Aggregated/Owned Tags - 위 3가지의 합집합 (읽기 전용)
FGameplayTagContainer CurrentTags;
ASC->GetOwnedGameplayTags(CurrentTags);  // Loose + Granted + ActivationOwned 전부

// 5. Replicated Tags - 네트워크로 동기화되는 태그들
ASC->SetReplicatedLooseGameplayTags(TagContainer);  // 서버에서 설정
ASC->GetReplicatedLooseGameplayTags();  // 클라이언트에서 읽기

// 6. Blocked/Required/Cancel 관계 태그 - Ability 발동 조건 제어
// GameplayAbility 클래스에서 설정:
ActivationRequiredTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("State.Alive")));
ActivationBlockedTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Status.Stunned")));
BlockAbilitiesWithTag.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Ability.Movement")));
CancelAbilitiesWithTag.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Ability.Channel")));


Gameplay Cue에 대하여...

Gameplay Cue의 개념

  • 특정 게임플레이 이벤트나 상태 변화가 발생했다는 것을 알려주는 네트워크 알림 역할
  • 게임 로직의 복잡한 네트워크 동기화 과정에서 순수한 시각/청각적 효과만을 분리하여 처리함
  • 일반적으로 해당 이벤트를 경험하는 클라이언트에서만 실행됨
  • Gameplay Tag를 사용하여 그 태그에 바인딩된 Gameplay Cue 클래스를 찾아서 특정 효과를 발생시켜줌

Gameplay Cue의 세 가지 타입

1. Execute Cue

  • 즉시 실행 후 자동 처리
  • 폭발, 타격, 힐링 이펙트
  • 생명 주기 : ExecuteGameplayCue 호출 -> Cue 클래스 찾기 -> OnExecute 실행 -> 자동 정리
ASC->ExecuteGameplayCue(FGameplayTag::RequestGameplayTag("GameplayCue.Explosion.Fireball"));

2. Add Cue

  • 수동 제거까지 지속
  • 버프/디버프 상태 표시
  • 화상, 독 이펙트
  • 생명 주기 : AddGameplayCue 호출 -> Cue 인스턴스 생성 -> OnActive 실행 -> WhileActive 반복 -> RemoveGameplayCue 호출 -> OnRemove 실행 -> 정리
ASC->AddGameplayCue(FGameplayTag::RequestGameplayTag("GameplayCue.Status.Burning"));

3. Remove Cue

  • Add된 Cue 정리
  • 종료 연출 추가 가능
ASC->RemoveGameplayCue(FGameplayTag::RequestGameplayTag("GameplayCue.Status.Burning"));

Parameters를 통한 데이터 전달

  • 이펙트를 동적으로 연출하기 위해서 파라미터를 통해 데이터 전달 가능
FGameplayCueParameters CueParams;

// 기본 정보
CueParams.RawMagnitude = DamageAmount;
CueParams.Location = HitLocation;
CueParams.Normal = SurfaceNormal;
CueParams.PhysicalMaterial = HitMaterial;

// 게임플레이 정보
CueParams.GameplayEffectLevel = Level;
CueParams.AbilityLevel = AbilityLevel;
CueParams.EffectCauser = CastingActor;
CueParams.SourceObject = WeaponObject;

ASC->ExecuteGameplayCue("GameplayCue.Impact.Weapon", CueParams);

연출에서 활용 예시

void OnExecute(AActor* Target, const FGameplayCueParameters& Parameters) override
{
    // 데미지 크기에 따른 이펙트 조절
    float EffectScale = FMath::Clamp(Parameters.RawMagnitude / 100.0f, 0.5f, 3.0f);

    // 표면 재질에 따른 사운드 선택
    if (Parameters.PhysicalMaterial)
    {
        USoundBase* HitSound = GetSoundForMaterial(Parameters.PhysicalMaterial);
    }

    // 스킬 레벨에 따른 색상 변화
    FLinearColor EffectColor = FMath::Lerp(
        FLinearColor::White,
        FLinearColor::Gold,
        Parameters.AbilityLevel / 10.0f
    );
}

Gameplay Cue 클래스 계층

GameplayCueNotify_Static

  • 가장 흔하게 사용되며, 주로 일회성 효과를 구현할 때 사용
  • 이 클래스는 인스턴스를 생성하지 않고, CDO 상에서 작동함
  • 메모리에 단 하나의 객체만 존재하며, 모든 호출에 재사용됨
  • 단발성, 순간적인 시간 및 청각 효과에 적합
class UMyGameplayCue_Simple : public UGameplayCueNotify_Static
{
public:
    virtual void OnExecute_Implementation(
        AActor* Target,
        const FGameplayCueParameters& Parameters,
        const FGameplayEffectContextHandle& EffectContext
    ) override
    {
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), SimpleParticle, Parameters.Location);
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), SimpleSound, Parameters.Location);
    }
};

GameplayCueNotify_Actor

  • 지속적인 효과특정 위치에 존재하며 틱이 필요한 복잡한 효과를 구현할 때 사용
  • Gameplay Effect가 활성화될 때 액터 인스턴스를 생성
  • 캐릭터에게 적용되는 버프 효과, 지속적인 장판형 데미지 이펙트 등에 사용
class AMyGameplayCue_Complex : public AGameplayCueNotify_Actor
{
public:
    virtual void OnActive(AActor* Target, const FGameplayCueParameters& Parameters) override;
    virtual bool WhileActive(AActor* Target, const FGameplayCueParameters& Parameters) override;
    virtual void OnRemove(AActor* Target, const FGameplayCueParameters& Parameters) override;

private:
    UPROPERTY()
    UParticleSystemComponent* ComplexParticle;

    UPROPERTY()
    UAudioComponent* LoopingAudio;
};

독 스택 시스템 예시

PoisonEffect

  • 스킬 설계
    • 20초 동안 유지되며 4초마다 발동
    • 체력 -15의 데미지
    • 스택 시스템을 통해 최대 5까지 중첩될 수 있게
    • 스택마다 Gameplay Cue를 통해 다른 이펙트 발동

PoisonEffect.h

// PoisonEffect.h

#pragma once

#include "CoreMinimal.h"
#include "GameplayEffect.h"
#include "PoisonEffect.generated.h"

UCLASS()
class GASSAMPLE_API UPoisonEffect : public UGameplayEffect
{
	GENERATED_BODY()

public:
	UPoisonEffect();
};

PoisonEffect.cpp

// PoisonEffect.cpp

#include "PoisonEffect.h"
#include "NativeGameplayTags.h"
#include "GameplayEffectComponents/TargetTagsGameplayEffectComponent.h"
#include "GASSample/GAS/AttributeSet/MyAttributeSet.h"

UPoisonEffect::UPoisonEffect()
{
	// 20초 동안 4초마다 발동
	DurationPolicy = EGameplayEffectDurationType::HasDuration;
	DurationMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(20.f));
	Period = 4.f;
	bExecutePeriodicEffectOnApplication = true;

	// 체력 -15 데미지
	FGameplayModifierInfo DamageModifier;
	DamageModifier.ModifierOp = EGameplayModOp::Additive;
	DamageModifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(-15.f));
	DamageModifier.Attribute = UMyAttributeSet::GetHealthAttribute();

	Modifiers.Add(DamageModifier);

	// 태그 부여
	UTargetTagsGameplayEffectComponent* TargetTags = CreateDefaultSubobject<UTargetTagsGameplayEffectComponent>(TEXT("TargetTagsComp"));
	FInheritedTagContainer TagChanges = TargetTags->GetConfiguredTargetTagChanges();
	TagChanges.Added.AddTag(FGameplayTag::RequestGameplayTag("Status.Poisoned"));
	TargetTags->SetAndApplyTargetTagChanges(TagChanges);

	// 스택 시스템을 통해 최대 5까지 중첩될 수 있게 설정
	StackingType = EGameplayEffectStackingType::AggregateByTarget;
	StackLimitCount = 5;
	StackDurationRefreshPolicy = EGameplayEffectStackingDurationPolicy::RefreshOnSuccessfulApplication;
	StackPeriodResetPolicy = EGameplayEffectStackingPeriodPolicy::ResetOnSuccessfulApplication;
	StackExpirationPolicy = EGameplayEffectStackingExpirationPolicy::ClearEntireStack;

	// Gameplay Cue 발동되게 연결
	FGameplayEffectCue PoisonCue;
	PoisonCue.GameplayCueTags.AddTag(FGameplayTag::RequestGameplayTag("GameplayCue.Status.Poisoned"));
	GameplayCues.Add(PoisonCue);
}

GameplayCue_Status_Poisoned.h

GameplayCue_Status_Poisoned.h

// GameplayCue_Status_Poisoned.h

#pragma once

#include "CoreMinimal.h"
#include "GameplayCueNotify_Actor.h"
#include "GameplayCue_Status_Poisoned.generated.h"

UCLASS()
class GASSAMPLE_API AGameplayCue_Status_Poisoned : public AGameplayCueNotify_Actor
{
	GENERATED_BODY()

public:
	virtual bool OnActive_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) override;
	virtual bool OnRemove_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) override;
    
protected:
	// Effect
	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Effects")
	UParticleSystem* PoisonEffect;

	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Effects")
	UParticleSystem* PoisonAuraEffect;

	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Effects")
	UParticleSystem* PoisonStackIncreaseEffect;

	// Sound
	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Sounds")
	USoundBase* LightPoisonSound;

	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Sounds")
	USoundBase* ModeratePoisonSound;
	
	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Sounds")
	USoundBase* HeavyPoisonSound;
	
	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Sounds")
	USoundBase* LethalPoisonSound;
	
	UPROPERTY(EditDefaultsOnly, Category = "GameplayCue|Sounds")
	USoundBase* PoisonStackIncreaseSound;

private:
	UPROPERTY()
	UParticleSystemComponent* PoisonParticleComponent;

	UPROPERTY()
	UParticleSystemComponent* AuraParticleComponent;

	UPROPERTY()
	UAudioComponent* PoisonSoundComponent;
};

GameplayCue_Status_Poisoned.cpp

// GameplayCue_Status_Poisoned.cpp

#include "GameplayCue_Status_Poisoned.h"

#include "AbilitySystemComponent.h"
#include "Components/AudioComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"

bool AGameplayCue_Status_Poisoned::OnActive_Implementation(AActor* Target, const FGameplayCueParameters& Parameters)
{
	Super::OnActive_Implementation(Target, Parameters);

	int32 StackCount = 1;
	
	UAbilitySystemComponent* ASC = Parameters.EffectContext.GetOriginalInstigatorAbilitySystemComponent();
	if (ASC)
	{
		FGameplayEffectQuery Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(
			FGameplayTagContainer(FGameplayTag::RequestGameplayTag("Status.Poisoned")));
		TArray<FActiveGameplayEffectHandle> ActiveEffects = ASC->GetActiveEffects(Query);

		if (ActiveEffects.Num() > 0)
		{
			const FActiveGameplayEffect* ActiveGE = ASC->GetActiveGameplayEffect(ActiveEffects[0]);
			if (ActiveGE)
			{
				StackCount = ActiveGE->Spec.GetStackCount();
			}
		}
	}

	// Spawn Particle Effect
	if (USkeletalMeshComponent* Mesh = Target->FindComponentByClass<USkeletalMeshComponent>())
	{
		PoisonParticleComponent = UGameplayStatics::SpawnEmitterAttached(
			PoisonEffect,
			Mesh,
			NAME_None,
			FVector::ZeroVector,
			FRotator::ZeroRotator,
			EAttachLocation::KeepRelativeOffset);

		if (PoisonParticleComponent)
		{
			float Intensity = FMath::Clamp(StackCount / 5.f, 0.2f, 1.f);
			PoisonParticleComponent->SetFloatParameter("Intensity", Intensity);

			FLinearColor PoisonColor = FMath::Lerp(FLinearColor::Green, FLinearColor(0.5f, 0.f, 1.f, 1.f), (StackCount - 1) / 4.f);
			PoisonParticleComponent->SetColorParameter("PoisonColor", PoisonColor);
		}
	}

	// Sound Effect
	USoundBase* PoisonSound = nullptr;
	float SoundVolume = 1.0f;

	switch (StackCount)
	{
	case 1:
		PoisonSound = LightPoisonSound;
		SoundVolume = 0.3f;
		break;
	case 2:
		PoisonSound = ModeratePoisonSound;
		SoundVolume = 0.5f;
		break;
	case 3:
	case 4:
		PoisonSound = HeavyPoisonSound;
		SoundVolume = 0.7f;
		break;
	case 5:
	default:
		PoisonSound = LethalPoisonSound;
		SoundVolume = 1.0f;
		break;
	}

	if (PoisonSound)
	{
		PoisonSoundComponent = UGameplayStatics::SpawnSoundAttached(PoisonSound, Target->GetRootComponent());

		if (PoisonSoundComponent)
		{
			PoisonSoundComponent->SetVolumeMultiplier(SoundVolume);
		}
	}
	
	return true;
}

bool AGameplayCue_Status_Poisoned::OnRemove_Implementation(AActor* Target, const FGameplayCueParameters& Parameters)
{
	Super::OnRemove_Implementation(Target, Parameters);

	if (PoisonParticleComponent)
	{
		PoisonParticleComponent->bAutoDestroy = true;
		PoisonParticleComponent->Deactivate();
		PoisonParticleComponent = nullptr;
	}
	
	if (AuraParticleComponent)
	{
		AuraParticleComponent->bAutoDestroy = true;
		AuraParticleComponent->Deactivate();
		AuraParticleComponent = nullptr;
	}

	if (PoisonSoundComponent)
	{
		PoisonSoundComponent->FadeOut(1.f, 0.f);
		PoisonSoundComponent = nullptr;
	}

	return true;
}
profile
게임 개발자를 향해..

0개의 댓글