UE5 GAS 어트리뷰트 & Gameplay Effect
GAS에서 Attribute란 무엇인가
섹션 제목: “GAS에서 Attribute란 무엇인가”Gameplay Ability System(GAS)의 핵심 데이터 단위는 FGameplayAttributeData입니다. HP, MP, 공격력, 이동 속도처럼 캐릭터의 수치를 표현하며, 단순 float 변수와 달리 BaseValue와 CurrentValue를 분리해 버프/디버프를 안전하게 관리합니다.
- BaseValue: 영구적인 기본 수치 (레벨업, 장비 장착 등으로 변경)
- CurrentValue: 일시적 효과가 적용된 최종 수치 (버프, 독 등으로 변동)
AttributeSet 정의
섹션 제목: “AttributeSet 정의”헤더 파일
섹션 제목: “헤더 파일”#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);};소스 파일
섹션 제목: “소스 파일”#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 구조 이해
섹션 제목: “GameplayEffect 구조 이해”GameplayEffect(GE)는 어트리뷰트를 변경하는 유일한 공식 채널입니다. 직접 SetHealth()를 호출하는 것보다 GE를 통한 변경이 예측(Prediction), 복제(Replication), 스택(Stack) 기능을 모두 활용할 수 있어 권장됩니다.
Duration 타입
섹션 제목: “Duration 타입”| 타입 | 설명 | 활용 예 |
|---|---|---|
Instant | 즉시 적용, BaseValue 변경 | 레벨업 스탯 증가, 즉시 데미지 |
Duration | 지정 시간 동안 유지 후 제거 | 5초 방어력 버프 |
Infinite | 수동 제거 전까지 유지 | 장비 스탯, 패시브 효과 |
Modifier 설정
섹션 제목: “Modifier 설정”GE의 Modifier 하나는 다음을 정의합니다.
- Attribute: 영향을 줄 어트리뷰트 (예:
UMyAttributeSet.Health) - Modifier Op:
Add,Multiply,Divide,Override중 선택 - Magnitude: 수치 계산 방식 (
ScalableFloat,AttributeBased,CustomCalculationClass등)
C++에서 GameplayEffect 적용
섹션 제목: “C++에서 GameplayEffect 적용”// 데미지 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()); }}SetByCaller 활용 이유
섹션 제목: “SetByCaller 활용 이유”하드코딩된 Magnitude 대신 SetByCaller를 사용하면 동일한 GE 에셋으로 다양한 수치를 런타임에 주입할 수 있습니다. 어빌리티마다 별도 GE를 만들 필요가 없어 에셋 관리가 단순해집니다.
GameplayCue로 시각 효과 연동
섹션 제목: “GameplayCue로 시각 효과 연동”GAS는 어트리뷰트 변경에 맞춰 파티클, 사운드 등 시각 효과를 트리거하는 GameplayCue 시스템을 내장합니다.
// 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 클래스를 활용한 복잡한 데미지 공식 구현을 학습하는 것을 권장합니다.