콘텐츠로 이동

UE5 GAS 어트리뷰트 & Gameplay Effect

Gameplay Ability System(GAS)의 핵심 데이터 단위는 FGameplayAttributeData입니다. HP, MP, 공격력, 이동 속도처럼 캐릭터의 수치를 표현하며, 단순 float 변수와 달리 BaseValueCurrentValue를 분리해 버프/디버프를 안전하게 관리합니다.

  • BaseValue: 영구적인 기본 수치 (레벨업, 장비 장착 등으로 변경)
  • CurrentValue: 일시적 효과가 적용된 최종 수치 (버프, 독 등으로 변동)

MyAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.generated.h"
// GAS 매크로: Getter / Setter / InitMetaData 함수를 자동 생성
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class MYGAME_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UMyAttributeSet();
// 어트리뷰트 변경 전 클램핑 처리
virtual void PreAttributeChange(
const FGameplayAttribute& Attribute,
float& NewValue) override;
// GameplayEffect 적용 후 콜백
virtual void PostGameplayEffectExecute(
const FGameplayEffectModCallbackData& Data) override;
// --- Health ---
UPROPERTY(BlueprintReadOnly, Category = "Attributes", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)
UPROPERTY(BlueprintReadOnly, Category = "Attributes", ReplicatedUsing = OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)
// --- Mana ---
UPROPERTY(BlueprintReadOnly, Category = "Attributes", ReplicatedUsing = OnRep_Mana)
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Mana)
// --- 메타 어트리뷰트 (임시 데미지 계산용, 복제 불필요) ---
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Damage;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Damage)
protected:
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);
UFUNCTION()
virtual void OnRep_Mana(const FGameplayAttributeData& OldMana);
};
MyAttributeSet.cpp
#include "MyAttributeSet.h"
#include "Net/UnrealNetwork.h"
#include "GameplayEffectExtension.h"
UMyAttributeSet::UMyAttributeSet()
{
// 초기값은 GameplayEffect의 Instant 타입으로 설정하는 것이 권장됨
InitHealth(100.f);
InitMaxHealth(100.f);
InitMana(50.f);
}
void UMyAttributeSet::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Mana, COND_None, REPNOTIFY_Always);
}
void UMyAttributeSet::PreAttributeChange(
const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
// MaxHealth 변경 시 현재 HP 비율 유지
if (Attribute == GetMaxHealthAttribute())
{
NewValue = FMath::Max(NewValue, 1.f);
}
// HP / MP는 0 이하로 내려가지 않도록 클램핑
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
}
void UMyAttributeSet::PostGameplayEffectExecute(
const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 메타 어트리뷰트 'Damage'를 실제 Health 감소에 적용
if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
const float DamageAmount = GetDamage();
const float NewHealth = FMath::Max(GetHealth() - DamageAmount, 0.f);
SetHealth(NewHealth);
SetDamage(0.f); // 메타 어트리뷰트 초기화
// HP가 0이 되면 사망 처리 이벤트 브로드캐스트 가능
if (NewHealth <= 0.f)
{
// 게임플레이 이벤트 전송 예시
// FGameplayEventData EventData;
// UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(...);
}
}
}
void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth);
}
void UMyAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxHealth, OldMaxHealth);
}
void UMyAttributeSet::OnRep_Mana(const FGameplayAttributeData& OldMana)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Mana, OldMana);
}

GameplayEffect(GE)는 어트리뷰트를 변경하는 유일한 공식 채널입니다. 직접 SetHealth()를 호출하는 것보다 GE를 통한 변경이 예측(Prediction), 복제(Replication), 스택(Stack) 기능을 모두 활용할 수 있어 권장됩니다.

타입설명활용 예
Instant즉시 적용, BaseValue 변경레벨업 스탯 증가, 즉시 데미지
Duration지정 시간 동안 유지 후 제거5초 방어력 버프
Infinite수동 제거 전까지 유지장비 스탯, 패시브 효과

GE의 Modifier 하나는 다음을 정의합니다.

  • Attribute: 영향을 줄 어트리뷰트 (예: UMyAttributeSet.Health)
  • Modifier Op: Add, Multiply, Divide, Override 중 선택
  • Magnitude: 수치 계산 방식 (ScalableFloat, AttributeBased, CustomCalculationClass 등)

// 데미지 GameplayEffect를 코드로 생성하고 적용하는 예시
void AMyCharacter::ApplyDamageEffect(
UAbilitySystemComponent* TargetASC,
TSubclassOf<UGameplayEffect> DamageEffectClass,
float DamageAmount)
{
if (!TargetASC || !DamageEffectClass) return;
// EffectContext 생성: 이펙트의 출처(Source) 정보를 담음
FGameplayEffectContextHandle ContextHandle =
GetAbilitySystemComponent()->MakeEffectContext();
ContextHandle.AddSourceObject(this);
// EffectSpec 생성: 실제 이펙트 인스턴스, 레벨 1.0 적용
FGameplayEffectSpecHandle SpecHandle =
GetAbilitySystemComponent()->MakeOutgoingSpec(
DamageEffectClass, 1.0f, ContextHandle);
if (SpecHandle.IsValid())
{
// Set By Caller 방식으로 데미지 수치를 동적으로 주입
// GE의 Magnitude를 SetByCaller로 설정했을 때 사용
SpecHandle.Data->SetSetByCallerMagnitude(
FGameplayTag::RequestGameplayTag(FName("Data.Damage")),
DamageAmount);
// 대상 ASC에 이펙트 적용
TargetASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}

하드코딩된 Magnitude 대신 SetByCaller를 사용하면 동일한 GE 에셋으로 다양한 수치를 런타임에 주입할 수 있습니다. 어빌리티마다 별도 GE를 만들 필요가 없어 에셋 관리가 단순해집니다.


GAS는 어트리뷰트 변경에 맞춰 파티클, 사운드 등 시각 효과를 트리거하는 GameplayCue 시스템을 내장합니다.

GameplayCue.Character.Hit
// GameplayEffect의 GameplayCues 섹션에 태그 추가:
// 코드에서 직접 발동:
FGameplayCueParameters CueParams;
CueParams.NormalizedMagnitude = DamageAmount / GetMaxHealth();
AbilitySystemComponent->ExecuteGameplayCue(
FGameplayTag::RequestGameplayTag(FName("GameplayCue.Character.Hit")),
CueParams);

GameplayCueNotify_Static(일회성)과 GameplayCueNotify_Actor(지속성) 두 가지 베이스 클래스를 상황에 맞게 사용합니다.


AttributeSet을 직접 NewObject로 생성하는 경우 UAbilitySystemComponent::AddAttributeSetSubobject() 또는 GetOrCreateAttributeSubobject()를 사용해야 ASC가 해당 어트리뷰트 셋을 관리합니다. 직접 NewObject하면 복제가 동작하지 않습니다.

PreAttributeChange에서 실제 데이터를 변경하는 경우 PreAttributeChange는 클램핑 목적으로만 사용해야 합니다. 실제 어트리뷰트 간 연동(예: 데미지 -> HP 감소)은 반드시 PostGameplayEffectExecute에서 처리해야 합니다.

클라이언트에서 직접 어트리뷰트를 수정하는 경우 모든 어트리뷰트 변경은 서버에서 발생해야 합니다. 클라이언트 예측(Prediction Key)을 사용하면 UI가 즉시 반응하면서도 서버 결과를 최종 기준으로 맞출 수 있습니다.


GAS의 AttributeSet과 GameplayEffect는 처음에는 설정 과정이 복잡하게 느껴질 수 있지만, 이 구조 덕분에 복제, 예측, 스택, 태그 기반 조건 처리를 모두 일관된 방식으로 처리할 수 있습니다.

다음 단계로는 GameplayAbility 내에서 어빌리티 코스트(Mana 소모)와 쿨다운을 GE로 처리하는 방법, 그리고 ModifierMagnitudeCalculation 클래스를 활용한 복잡한 데미지 공식 구현을 학습하는 것을 권장합니다.