이전 글에서는 GameplayAbility와 GameplayEvent 흐름을 정리했다.
GameplayAbility는 캐릭터가 수행하는 행동 단위이고,GameplayEvent는 Ability끼리 직접 함수를 호출하지 않고 이벤트를 전달하는 방식으로 사용할 수 있었다.
이번 글에서는 GAS에서 수치 변화가 어떻게 처리되는지 정리해보려고 한다.
강의 실습을 진행하면서 처음에는 체력을 깎는다면
그냥 Health -= Damage처럼 값을 직접 수정하면 되지 않을까 생각했다.
하지만 GAS에서는 보통 Attribute 값을 직접 수정하기보다,GameplayEffect를 통해 Attribute를 변경하는 흐름을 사용한다.
그리고 변경된 Attribute는 RepNotify나 Attribute Change Delegate를 통해
UI 갱신 흐름으로 연결할 수 있다.
이번 글에서는 AttributeSet, GameplayEffect, Attribute Replication,
Attribute Change Delegate, Curve Table, SetByCaller 흐름을 간단히 정리해보려고 한다.
AttributeSet이란?
AttributeSet은 캐릭터가 가진 수치 값을 모아두는 클래스이다.
예를 들어 다음과 같은 값들을 Attribute로 관리할 수 있다.
- Health
- MaxHealth
- Mana
- Stamina
- AttackPower
- Defense
GAS에서 Attribute는 단순한 float 값이라기보다,
게임플레이 시스템 안에서 계산되고 수정될 수 있는 수치라고 볼 수 있다.
예를 들어 체력은 단순히 현재 값만 있는 것이 아니라,
GameplayEffect에 의해 감소하거나 회복될 수 있고 복제나 UI 갱신 흐름과도 연결될 수 있다.
실습에서는 Health, MaxHealth 같은 값을 AttributeSet에 두고,
GameplayEffect를 통해 값을 변경하는 구조를 사용했다.
AttributeSet의 역할
AttributeSet은 단순히 수치를 저장하는 역할만 하는 것은 아니다.
GameplayEffect에 의해 Attribute 값이 변경되었을 때
값을 보정하거나, 특정 조건을 확인하거나, 변경 사실을 다른 시스템에 알리는 흐름과 연결될 수 있다.
예를 들어 Health가 변경되었을 때는 다음과 같은 처리를 생각할 수 있다.
Health 변경
-> Health가 0 이하인지 확인
-> 사망 처리 Ability 실행
-> UI 갱신
또 MaxHealth보다 Health가 커지지 않도록 제한하거나,
Health가 0보다 작아지지 않도록 보정하는 처리도 AttributeSet 쪽에서 다룰 수 있다.
즉, AttributeSet은 GAS에서 수치 값을 저장하고,
그 값이 변경될 때 필요한 후처리와 연결되는 지점이라고 볼 수 있다.
Attribute Accessor 매크로
GAS에서 Attribute를 정의할 때는 Attribute 접근 함수를 만들어야 하는 경우가 많다.
언리얼에서는 이를 위해 GAMEPLAYATTRIBUTE_PROPERTY_GETTER,GAMEPLAYATTRIBUTE_VALUE_GETTER, GAMEPLAYATTRIBUTE_VALUE_SETTER,GAMEPLAYATTRIBUTE_VALUE_INITTER 같은 매크로를 제공한다.
이 매크로들을 Attribute마다 반복해서 작성하면 코드가 길어지기 때문에,
보통 다음과 같이 ATTRIBUTE_ACCESSORS 매크로로 묶어서 사용한다.
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
이 매크로를 사용하면 Attribute마다 필요한 접근 함수들을 한 번에 만들 수 있다.
예를 들어 Health Attribute에 대해 다음과 같은 함수들이 만들어진다.
GetHealthAttribute()
GetHealth()
SetHealth()
InitHealth()
각 함수의 역할은 대략 다음과 같다.
| 함수 | 역할 |
|---|---|
GetHealthAttribute() |
이 Attribute가 어떤 클래스의 어떤 프로퍼티인지 나타내는 FGameplayAttribute 반환 |
GetHealth() |
현재 Health 값 반환 |
SetHealth() |
Health 값 설정 |
InitHealth() |
초기 Health 값 설정 |
처음에는 단순히 getter/setter를 만드는 매크로처럼 보였지만,
GAS에서는 Attribute를 식별하기 위한 FGameplayAttribute도 필요하기 때문에
이런 매크로 패턴을 자주 사용하는 것 같다.
Attribute Replication
멀티플레이 환경에서는 Attribute 값도 클라이언트에 동기화되어야 한다.
예를 들어 서버에서 데미지를 적용해 Health가 감소했다면,
클라이언트에서도 그 값이 반영되어야 UI를 갱신할 수 있다.
이를 위해 Attribute에 Replication 설정을 추가할 수 있다.
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
그리고 GetLifetimeReplicatedProps에서 해당 Attribute를 등록한다.
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
여기서 COND_None은 특별한 조건 없이 관련 클라이언트에게 복제한다는 의미이고,REPNOTIFY_Always는 값이 복제될 때 RepNotify를 항상 호출하도록 하는 옵션이다.
GAS Attribute 복제에서는 예측이나 내부 값 갱신 흐름을 맞추기 위해REPNOTIFY_Always와 GAMEPLAYATTRIBUTE_REPNOTIFY를 함께 사용하는 패턴을 자주 볼 수 있다.
GAMEPLAYATTRIBUTE_REPNOTIFY
Attribute의 OnRep 함수에서는 보통 GAMEPLAYATTRIBUTE_REPNOTIFY를 호출한다.
void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth);
}
이 매크로는 단순히 OnRep 함수가 호출되었다는 것에서 끝나는 것이 아니라,
ASC가 Attribute 변경을 인식할 수 있도록 연결해주는 역할을 한다.
즉, 서버에서 Attribute 값이 변경되고 클라이언트로 복제되면 OnRep 함수가 호출되고,
그 안에서 GAMEPLAYATTRIBUTE_REPNOTIFY를 통해 GAS 쪽에 값 변경을 알려준다.
이 흐름을 통해 Attribute Change Delegate 같은 시스템도 정상적으로 동작할 수 있다.
정리하면 Attribute Replication 흐름은 다음과 같다.
서버에서 Attribute 변경
-> 클라이언트로 Attribute 복제
-> OnRep_Attribute 호출
-> GAMEPLAYATTRIBUTE_REPNOTIFY 호출
-> ASC가 Attribute 변경 인식
-> Attribute Change Delegate 흐름으로 연결
GameplayEffect로 Attribute 변경하기
GAS에서는 보통 Attribute 값을 직접 수정하지 않고 GameplayEffect를 통해 변경한다.
데미지를 적용한다면 Health 값을 직접 깎는 대신 데미지 GameplayEffect를 대상 ASC에 적용한다.
공격 Ability 실행
-> 대상 Actor 탐색
-> 대상 ASC 가져오기
-> 데미지 GameplayEffect Spec 생성
-> 대상 ASC에 GameplayEffect 적용
-> Health Attribute 감소
이렇게 하면 Attribute 변경이 GAS 흐름 안에서 처리된다.
즉, 단순히 Health 값을 직접 수정하는 것보다
복제, OnRep, Attribute Change Delegate, UI 갱신 흐름과 자연스럽게 연결할 수 있다.
실습에서도 기본 공격 Ability에서 히트박스나 Trace로 맞은 Actor를 찾고,
대상 Actor의 ASC에 GameplayEffect를 적용해 데미지를 처리했다.
GameplayEffect 적용 흐름
GameplayEffect를 적용할 때는 보통 GameplayEffectSpec을 만든 뒤 대상에게 적용한다.
개념적으로 보면 다음과 같은 흐름이다.
공격자 ASC
-> Make Outgoing Spec으로 GameplayEffectSpec 생성
피격자 ASC
-> ApplyGameplayEffectSpecToTarget으로 Spec 적용
GameplayEffect
-> Health Attribute에 Add 연산으로 데미지 적용
예를 들어 데미지 GameplayEffect가 Health에 Add -10을 적용하도록 설정되어 있다면,
해당 GameplayEffect가 적용될 때 대상의 Health가 10 감소한다.
이 구조의 핵심은 Attribute 값을 바꾸는 책임을 GameplayEffect가 가진다는 점이다.
그래서 데미지, 회복, 버프, 디버프 같은 수치 변화는
가능하면 Attribute를 직접 수정하기보다 GameplayEffect로 처리하는 흐름이 자연스럽다.
Attribute 값 변경 전후 처리
AttributeSet은 Attribute 값을 저장하는 역할뿐만 아니라,
값이 변경되기 전후에 필요한 처리를 할 수 있는 함수들도 제공한다.
이번 글에서는 대표적으로 다음과 같은 함수들만 간단히 정리했다.
| 함수 | 호출 시점 | 사용 예시 |
|---|---|---|
PreAttributeChange |
Attribute 값이 변경되기 전 | 변경될 값을 미리 보정 |
PostAttributeChange |
Attribute 값이 변경된 후 | 변경 이후 필요한 처리 |
PreGameplayEffectExecute |
GameplayEffect가 Attribute 값을 수정하기 전 | Effect 적용 여부 판단 또는 사전 처리 |
PostGameplayEffectExecute |
GameplayEffect가 Attribute의 Base Value를 수정한 후 | 최종 값 보정, 사망 조건 확인 |
예를 들어 Health가 MaxHealth를 넘지 않도록 제한하거나,
Health가 0 아래로 내려가지 않게 보정하는 처리를 넣을 수 있다.
또 GameplayEffect로 데미지가 적용된 뒤
Health가 0 이하인지 확인하고 사망 흐름으로 연결할 수도 있다.
이번 실습에서는 주로 PostGameplayEffectExecute를 사용했다.
데미지 GameplayEffect가 Health를 변경한 뒤,
변경된 Health 값을 확인하고 필요한 후처리를 연결하기에 적절했기 때문이다.
즉, AttributeSet은 단순히 수치를 보관하는 클래스가 아니라,
GameplayEffect로 인해 값이 바뀌는 순간에 필요한 규칙을 적용할 수 있는 지점이라고 볼 수 있다.
PostGameplayEffectExecute
이번 실습에서는 여러 Attribute 변경 전후 함수 중에서 주로 PostGameplayEffectExecute를 사용했다.
데미지 GameplayEffect가 Health를 변경한 뒤,
변경된 Health 값을 확인하고 후처리를 연결하기에 적합했기 때문이다.
GameplayEffect 적용
-> Attribute 값 변경
-> PostGameplayEffectExecute 호출
-> 값 보정 또는 사망 조건 확인
예를 들어 Health가 0 이하가 되었는지 확인하거나,
값이 최소/최대 범위를 벗어나지 않도록 보정하는 처리를 넣을 수 있다.
다만 하나의 GameplayEffect에서 여러 Attribute를 함께 수정하는 경우에는 주의가 필요하다.
각 Modifier가 적용되는 과정에서 PostGameplayEffectExecute가 여러 번 호출될 수 있기 때문이다.
그래서 MaxHealth처럼 기준이 되는 값을 먼저 초기화하고,
Health 같은 현재값을 나중에 초기화하는 식으로 순서를 신경 쓰는 것이 안전하다.
Curve Table을 이용한 GameplayEffect Magnitude
GameplayEffect의 Modifier Magnitude는 고정값만 사용할 수 있는 것이 아니다.Scalable Float을 사용하면 Curve Table을 연결해 레벨에 따라 다른 값을 적용할 수 있다.
데미지 값이 레벨에 따라 달라져야 한다면 Curve Table에 레벨별 데미지를 저장할 수 있다.

그리고 GameplayEffect에서는 이 Curve Table 값을 Magnitude로 사용할 수 있다.
실습에서는 데미지 값을 Curve Table에서 양수로 관리하고,
GameplayEffect에서 Health에 음수로 더해 체력을 감소시키는 방식을 사용했다.
개념적으로 보면 다음과 같다.
Make Outgoing Spec의 Level
-> Curve Table의 X 값
-> 레벨별 Damage 값 가져오기
-> GameplayEffect Modifier Magnitude 계산
-> Health Attribute 변경
즉, Curve Table은 레벨별 수치를 제공하고 GameplayEffect는 그 수치를 이용해 Attribute를 변경한다.
Ability Level과 GameplayEffect Magnitude
AbilitySpec에는 Level 값이 있다.
이 값은 Ability 클래스 자체의 레벨이라기보다,
ASC에 등록된 해당 AbilitySpec의 레벨이라고 볼 수 있다.
Ability 실행 중 현재 Ability Level을 가져와 GameplayEffectSpec을 만들 때 Level로 전달하면,
GameplayEffect의 Scalable Float이 해당 Level 기준으로 값을 계산할 수 있다.
흐름을 정리하면 다음과 같다.
AbilitySpec.Level 설정
-> Ability 실행
-> Get Ability Level
-> Make Outgoing Spec의 Level에 전달
-> Curve Table에서 해당 Level 값 사용
-> GameplayEffect Magnitude 계산
-> Attribute 변경
예를 들어 공격 Ability의 레벨이 2라면 Make Outgoing Spec에 Level 2를 넘기고,
Curve Table에서 2레벨 데미지 값을 가져와 Health를 감소시킬 수 있다.
만약 서버에서 AbilitySpec의 Level을 직접 변경했다면
변경 사항을 반영하기 위해 MarkAbilitySpecDirty 같은 처리가 필요할 수 있다.
즉, Ability 레벨과 GameplayEffect Magnitude를 연결하면
Ability 성장에 따라 데미지나 회복량이 달라지는 구조를 만들 수 있다.
SetByCaller
Curve Table은 미리 정해진 레벨별 수치를 가져올 때 유용하다.
하지만 실행 시점에 결정되는 값을 GameplayEffect에 전달해야 할 때도 있다.
예를 들어 Projectile마다 다른 Damage 값을 가지고 있다면,
GameplayEffect 안에 데미지를 고정해두기 어렵다.
이럴 때 사용할 수 있는 방식이 SetByCaller이다.
SetByCaller는 GameplayEffectSpec을 만들 때 실행 시점의 값을 태그와 함께 넣어두고,
GameplayEffect가 그 값을 Magnitude로 사용하도록 하는 방식이다.
단, 이 값을 사용하려면 GameplayEffect의 Modifier Magnitude가SetByCaller 값을 사용하도록 설정되어 있어야 한다.
흐름은 다음과 같다.
Projectile이 Damage 값을 가짐
-> Overlap 발생
-> GameplayEffectSpec 생성
-> SetByCaller 태그로 Damage 값 전달
-> GameplayEffect가 SetByCaller 값을 Magnitude로 사용
-> Health Attribute 감소
예를 들어 투사체마다 데미지가 다르다면,
투사체의 Damage 값을 GameplayEffectSpec에 넣어 대상에게 적용할 수 있다.
이렇게 하면 같은 GameplayEffect를 사용하더라도
실행 시점에 전달한 값에 따라 다른 데미지를 적용할 수 있다.
Attribute Change Delegate
Attribute 값이 변경되었을 때 UI를 갱신해야 한다면 매 Tick마다 값을 확인하는 방식은 비효율적이다.
GAS에서는 ASC의 Attribute Change Delegate에 바인딩해서 특정 Attribute가 변경될 때만 UI를 갱신할 수 있다.
예를 들어 Health 변경을 감시한다면 다음과 같은 흐름이다.
ASC 초기화
-> AttributeSet 가져오기
-> Health Attribute Change Delegate에 바인딩
-> GameplayEffect로 Health 변경
-> Delegate 호출
-> UI 갱신
실습에서는 GetGameplayAttributeValueChangeDelegate를 사용해 Health Attribute 변경 시점을 감지했다.
이 방식은 UI가 Attribute 값을 계속 감시하지 않아도 되고,
변경이 발생한 시점에만 반응할 수 있다는 장점이 있다.
UI 갱신 흐름
실습에서는 UI가 ASC와 AttributeSet을 바로 사용할 수 없는 경우도 고려했다.
예를 들어 Player의 ASC와 AttributeSet이 PlayerState에 있다면,
클라이언트에서 BeginPlay 시점에 아직 PlayerState나 ASC가 준비되지 않았을 수 있다.
그래서 UI에서는 다음과 같은 흐름을 사용했다.
위젯 BeginPlay
-> ASC / AttributeSet 캐싱 시도
-> 아직 준비되지 않았다면 초기화 완료 Delegate에 바인딩
-> ASC와 AttributeSet 준비 완료
-> Attribute Change Delegate에 바인딩
-> 초기값 한 번 UI에 반영
-> 이후 Attribute 변경 시 UI 갱신
중요한 점은 UI가 Attribute 초기화가 끝났는지 확인해야 한다는 것이다.
ASC와 AttributeSet을 캐싱했다고 해서 Attribute 값까지 모두 초기화되었다고 보장되는 것은 아니다.
따라서 Attribute 초기화가 아직 끝나지 않았다면 초기화 완료 Delegate에 바인딩해두고,
초기화가 끝난 뒤 Attribute Change Delegate를 연결하는 방식이 안전했다.
반대로 이미 Attribute 초기화가 끝난 상태라면
Delegate에 기다릴 필요 없이 바로 Attribute Change Delegate에 바인딩하고 초기값을 UI에 반영하면 된다.
Attribute Change Task
실습에서는 Attribute Change Delegate를 블루프린트에서 사용하기 쉽게 감싸기 위해UBlueprintAsyncActionBase를 상속한 Attribute Change Task도 만들었다.
이 Task는 ASC와 감시할 Attribute를 받아,
해당 Attribute 값이 변경될 때 블루프린트 이벤트를 Broadcast하는 역할을 한다.
흐름은 다음과 같다.
ListenForAttributeChange 호출
-> ASC와 Attribute 지정
-> ASC의 Attribute Change Delegate에 바인딩
-> Attribute 변경
-> OnAttributeChanged Broadcast
-> 블루프린트에서 NewValue / OldValue 사용
-> EndTask에서 바인딩 해제
이 구조를 사용하면 C++에서 직접 Attribute Change Delegate를 바인딩하지 않고도,
블루프린트 Ability나 UI에서 Attribute 변경에 반응할 수 있다.
다만 Task가 더 이상 필요 없어졌다면 EndTask에서 바인딩을 해제해주는 것이 중요하다.
데미지, Attribute, UI 흐름 정리
이번 실습에서 클라이언트 UI 기준으로 가장 중요하게 느껴진 흐름은 다음과 같다.
공격 Ability 실행
-> 대상 Actor 탐색
-> 대상 ASC 가져오기
-> GameplayEffectSpec 생성
-> 대상에게 GameplayEffect 적용
-> Health Attribute 변경
-> Attribute Replication / RepNotify
-> Attribute Change Delegate 호출
-> UI 갱신
즉, Health 값은 AttributeSet에 저장되지만,
그 값을 변경하는 주된 흐름은 GameplayEffect를 통해 이루어진다.
그리고 GameplayEffect로 Attribute가 변경되면
GAS의 RepNotify와 Attribute Change Delegate 흐름을 통해 UI 갱신까지 연결할 수 있다.
그래서 GAS에서는 UI가 Attribute를 직접 Tick으로 감시하기보다,
Attribute Change Delegate에 바인딩해서 변경 시점에만 갱신하는 방식이 더 자연스럽다.
정리
이번 글에서는 AttributeSet과 GameplayEffect 흐름을 정리했다.
핵심은 Attribute 값을 어디에 저장하고, 그 값이 어떤 방식으로 변경되고,
변경 결과가 UI까지 어떻게 전달되는지를 이해하는 것이다.
간단히 정리하면 다음과 같다.
| 개념 | 역할 |
|---|---|
AttributeSet |
Health, Mana, Stamina 같은 수치 저장 |
GameplayEffect |
Attribute 값을 변경하는 효과 |
| Attribute 변경 전후 함수 | Attribute 값이 바뀌기 전후에 보정이나 후처리를 할 수 있는 지점 |
GAMEPLAYATTRIBUTE_REPNOTIFY |
Attribute 복제 후 GAS에 변경 사실 알림 |
Attribute Change Delegate |
Attribute 변경 시점에 반응하기 위한 Delegate |
Curve Table |
레벨별 수치 제공 |
SetByCaller |
실행 시점에 Magnitude 값 전달 |
Attribute 변경 전후 함수에는 대표적으로 PreAttributeChange, PostAttributeChange,PreGameplayEffectExecute, PostGameplayEffectExecute 같은 함수들이 있다.
이번 실습에서는 그중에서도 GameplayEffect가 적용된 뒤
Health 값을 확인하고 후처리하기 위해 PostGameplayEffectExecute를 주로 사용했다.
마무리
이전에는 데미지를 처리할 때 주로 TakeDamage를 이용했다.
TakeDamage 안에서 데미지 타입을 나누거나 Health 값을 직접 감소시키고,
그 결과 Health가 0 이하가 되면 사망 조건을 확인하는 식으로 구현했다.
이 방식도 단순한 구조에서는 충분히 사용할 수 있지만,
버프, 디버프, 레벨별 데미지, 상태 태그, UI 갱신, 네트워크 복제까지 함께 고려하면
처리해야 할 흐름이 점점 많아질 수 있다고 느꼈다.
GAS를 처음 봤을 때는 AttributeSet, GameplayEffect, GameplayAbility,GameplayTag 같은 개념이 나뉘어 있어서 오히려 복잡하게 느껴졌다.
하지만 정리해보니 GAS는 수치 저장, 수치 변경, 상태 표현, 이벤트 반응, UI 갱신 흐름을
각각의 역할로 분리해서 관리할 수 있게 해주는 구조였다.
특히 Attribute 값은 AttributeSet에 저장하고, 값의 변경은 GameplayEffect를 통해 처리하며,
변경 결과는 RepNotify나 Attribute Change Delegate를 통해 UI와 연결할 수 있다는 점이 인상적이었다.
처음에는 구조가 복잡해 보였지만,
다양한 상황을 확장성 있게 처리하기에는 훨씬 강력한 도구라는 것을 알 수 있었다.
아직은 강의 실습을 통해 익힌 단계지만 앞으로 전투 시스템이나 스탯 시스템을 설계할 때
GAS를 적용하여 구현할 수 있도록 더 공부해야겠다.
'언리얼 엔진' 카테고리의 다른 글
| [언리얼 엔진] GAS Cost, Cooldown, GameplayCue 정리 (1) | 2026.06.23 |
|---|---|
| [언리얼 엔진] GAS GameplayAbility와 GameplayEvent 정리 (0) | 2026.06.18 |
| [언리얼 엔진] GAS 기본 구성 요소 정리 (0) | 2026.06.17 |
| [언리얼 엔진] Delegate 정리 (1) | 2026.06.16 |
| [언리얼 엔진] 스마트 포인터 정리 (0) | 2026.06.15 |