Gameplay Tag에 대하여...
Gameplay Tag의 핵심 개념
- GameplayTag : 문자열 기반의 계층적 라벨 시스템
핵심 특징
- 계층 구조 : 점(.)으로 구분된 트리 형태
- 상속 검색 : 상위 태그로 하위 모든 태그 검색 가능
- 컨테이너 기반 : 여러 태그를 동시에 보유 가능 (
FGameplayTagContainer)
- 에디터 통합 : 자동완성, 검증, 관리 도구 제공
계층 구조의 장점
태그 계층 구조 예시
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
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;
}