AttributeSet만 쓰는 구조
변수의 Set
1. ASC 사용
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = GAS, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
2. ASC 초기화, Attribute 변경 델리게이트 바인딩
// Actor 설정, Owner Actor와 Avatar Actor
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// Delegate 설정
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPXHealthSet::GetHealthAttribute()).AddUObject(this, &ACombatCharacter::HandleHealthAttributeChanged);
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPXHealthSet::GetMaxHealthAttribute()).AddUObject(this, &ACombatCharacter::HandleHealthAttributeChanged);
3. AttributeSet 설정
void ACombatCharacter::HandleHealthAttributeChanged(const FOnAttributeChangeData& ChangeData)
{
if (AbilitySystemComponent && HealthSet && HealthSet->GetHealth() > HealthSet->GetMaxHealth())
{
AbilitySystemComponent->SetNumericAttributeBase(UPXHealthSet::GetHealthAttribute(), HealthSet->GetMaxHealth());
}
TXCombatComponent->SetMaxHP(HealthSet->GetMaxHealth());
TXCombatComponent->SetCurrentHP(HealthSet->GetHealth());
TXCombatComponent->UpdateHPBar();
if (ChangeData.OldValue > 0.0f && ChangeData.NewValue <= 0.0f)
{
HandleDeath();
}
}
4. 기존 CombatComponent에서 계산하던 체력 로직을 ASC가 하게 변경
//////////////////////////////////////
////////////// 기존 로직 //////////////
//////////////////////////////////////
float UPXCombatComponent::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// only process damage if the character is still alive
if (CurrentHP <= 0.0f)
{
return 0.0f;
}
if (nullptr == OwnerCharacter)
return 0.0f;
// reduce the current HP
CurrentHP -= Damage;
//have we run out of HP?
if (CurrentHP <= 0.0f)
{
// die
OwnerCharacter->HandleDeath();
}
else
{
// update the life bar
UpdateHPBar();
PlayHitReaction();
}
return Damage;
}
//////////////////////////////////////
////////////// 바뀐 로직 //////////////
//////////////////////////////////////
float ACombatCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
if (Damage <= 0.0f || !HealthSet || HealthSet->GetHealth() <= 0.0f)
{
return 0.0f;
}
const float PreviousHealth = HealthSet->GetHealth();
AbilitySystemComponent->ApplyModToAttribute(UPXHealthSet::GetHealthAttribute(), EGameplayModOp::Additive, -Damage);
const float ActualDamage = FMath::Max(0.0f, PreviousHealth - HealthSet->GetHealth());
if (ActualDamage > 0.0f && HealthSet->GetHealth() > 0.0f && TXCombatComponent)
{
TXCombatComponent->PlayHitReaction();
}
return ActualDamage;
}
흐름 ApplyDamage → TakeDamage → GAS Health 감소 → Attribute 변경 → 기존 CombatComponent HP/UI 동기화 → 0 이하이면 HandleDeath
Attribute + GameplayAbility + GameplayTag 구조
AttributeSet
= Health, MaxHealth 같은 수치 데이터
GameplayAbility
= 공격, 피격, 회복, 대시, 사망 같은 행동
GameplayTag
= Dead, Stunned, Attacking, HitReact, Cooldown 같은 상태/조건
AbilitySystemComponent
= Ability 부여/실행, Attribute 변경, Tag 관리
1. ASC + AttributeSet 사용
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = GAS, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = GAS, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UPXHealthSet> HealthSet;
Character가 ASC를 가진다.
ACombatCharacter::ACombatCharacter()
{
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
HealthSet = CreateDefaultSubobject<UPXHealthSet>(TEXT("HealthSet"));
}
2. GameplayTag 정의
상태와 Ability 분류를 GameplayTag로 만든다.
// PXGameplayTags.h
#pragma once
#include "NativeGameplayTags.h"
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_State_Dead);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_State_HitReact);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_State_Attacking);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_State_Stunned);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Ability_Attack);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Ability_HitReact);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Ability_Death);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_InputTag_Attack);
// PXGameplayTags.cpp
#include "PXGameplayTags.h"
UE_DEFINE_GAMEPLAY_TAG(TAG_State_Dead, "State.Dead"); // 죽은 상태
UE_DEFINE_GAMEPLAY_TAG(TAG_State_HitReact, "State.HitReact"); // 피격 반응 중
UE_DEFINE_GAMEPLAY_TAG(TAG_State_Attacking, "State.Attacking"); // 공격 중
UE_DEFINE_GAMEPLAY_TAG(TAG_State_Stunned, "State.Stunned"); //
UE_DEFINE_GAMEPLAY_TAG(TAG_Ability_Attack, "Ability.Attack"); // 공격 Ability 분류
UE_DEFINE_GAMEPLAY_TAG(TAG_Ability_HitReact, "Ability.HitReact"); //
UE_DEFINE_GAMEPLAY_TAG(TAG_Ability_Death, "Ability.Death"); //
UE_DEFINE_GAMEPLAY_TAG(TAG_InputTag_Attack, "InputTag.Ability.Attack"); // 입력과 Ability를 연결하기 위한 태그
3. ASC 초기화, Attribute Delegate 바인딩, Tag Delegate 바인딩
void ACombatCharacter::BeginPlay()
{
Super::BeginPlay();
// Owner Actor와 Avatar Actor 설정
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// Attribute 변경 Delegate
AbilitySystemComponent
->GetGameplayAttributeValueChangeDelegate(UPXHealthSet::GetHealthAttribute())
.AddUObject(this, &ACombatCharacter::HandleHealthAttributeChanged);
AbilitySystemComponent
->GetGameplayAttributeValueChangeDelegate(UPXHealthSet::GetMaxHealthAttribute())
.AddUObject(this, &ACombatCharacter::HandleHealthAttributeChanged);
// GameplayTag 변경 Delegate
AbilitySystemComponent
->RegisterGameplayTagEvent(TAG_State_Dead, EGameplayTagEventType::NewOrRemoved)
.AddUObject(this, &ACombatCharacter::HandleDeadTagChanged);
AbilitySystemComponent
->RegisterGameplayTagEvent(TAG_State_HitReact, EGameplayTagEventType::NewOrRemoved)
.AddUObject(this, &ACombatCharacter::HandleHitReactTagChanged);
// Ability 부여
GiveDefaultAbilities();
}
4. AttributeSet 설정
체력 값은 AttributeSet에서 관리한다.
UCLASS()
class UPXHealthSet : public UAttributeSet
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, Category = Health)
FGameplayAttributeData Health;
UPROPERTY(BlueprintReadOnly, Category = Health)
FGameplayAttributeData MaxHealth;
static FGameplayAttribute GetHealthAttribute();
static FGameplayAttribute GetMaxHealthAttribute();
float GetHealth() const { return Health.GetCurrentValue(); }
float GetMaxHealth() const { return MaxHealth.GetCurrentValue(); }
void SetHealth(float NewValue) { Health.SetCurrentValue(NewValue); }
void SetMaxHealth(float NewValue) { MaxHealth.SetCurrentValue(NewValue); }
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override
{
Super::PreAttributeChange(Attribute, NewValue);
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
if (Attribute == GetMaxHealthAttribute())
{
NewValue = FMath::Max(0.0f, NewValue);
}
}
};
기존에는 Character의 Delegate에서 Health를 Clamp
→
AttributeSet 내부에서 Clamp하는 구조
5. Attribute 변경 처리
체력이 바뀌면 Character가 반응한다.
void ACombatCharacter::HandleHealthAttributeChanged(const FOnAttributeChangeData& ChangeData)
{
if (!AbilitySystemComponent || !HealthSet)
{
return;
}
const float CurrentHealth = HealthSet->GetHealth();
const float MaxHealth = HealthSet->GetMaxHealth();
TXCombatComponent->SetMaxHP(MaxHealth);
TXCombatComponent->SetCurrentHP(CurrentHealth);
TXCombatComponent->UpdateHPBar();
// 살아있다가 0 이하가 됐을 때만 사망 처리
if (ChangeData.OldValue > 0.0f && ChangeData.NewValue <= 0.0f)
{
// 죽음 상태 태그 부여
AbilitySystemComponent->AddLooseGameplayTag(TAG_State_Dead);
// Death Ability 실행
AbilitySystemComponent->TryActivateAbilitiesByTag(
FGameplayTagContainer(TAG_Ability_Death)
);
}
}
이제 HandleDeath()를 직접 호출하는 대신,State.Dead 태그를 부여하고 GA_Death를 실행한다.
Health <= 0
→ State.Dead 태그 부여
→ GA_Death 실행
→ 사망 애니메이션 / 충돌 제거 / 입력 차단 처리
6. Ability 부여
캐릭터가 시작될 때 기본 Ability를 ASC에 부여한다.
UPROPERTY(EditDefaultsOnly, Category = GAS)
TSubclassOf<UGameplayAbility> AttackAbilityClass;
UPROPERTY(EditDefaultsOnly, Category = GAS)
TSubclassOf<UGameplayAbility> HitReactAbilityClass;
UPROPERTY(EditDefaultsOnly, Category = GAS)
TSubclassOf<UGameplayAbility> DeathAbilityClass;
void ACombatCharacter::GiveDefaultAbilities()
{
if (!AbilitySystemComponent || !HasAuthority())
{
return;
}
if (AttackAbilityClass)
{
FGameplayAbilitySpec AttackSpec(AttackAbilityClass, 1);
AttackSpec.GetDynamicSpecSourceTags().AddTag(TAG_InputTag_Attack);
AttackSpec.GetDynamicSpecSourceTags().AddTag(TAG_Ability_Attack);
AbilitySystemComponent->GiveAbility(AttackSpec);
}
if (HitReactAbilityClass)
{
FGameplayAbilitySpec HitReactSpec(HitReactAbilityClass, 1);
HitReactSpec.GetDynamicSpecSourceTags().AddTag(TAG_Ability_HitReact);
AbilitySystemComponent->GiveAbility(HitReactSpec);
}
if (DeathAbilityClass)
{
FGameplayAbilitySpec DeathSpec(DeathAbilityClass, 1);
DeathSpec.GetDynamicSpecSourceTags().AddTag(TAG_Ability_Death);
AbilitySystemComponent->GiveAbility(DeathSpec);
}
}
ASC
└─ ActivatableAbilities
├─ GA_Attack
│ ├─ Ability.Attack
│ └─ InputTag.Ability.Attack
│
├─ GA_HitReact
│ └─ Ability.HitReact
│
└─ GA_Death
└─ Ability.Death
7. 입력으로 GameplayAbility 실행
기존에는 입력에서 Character나 CombatComponent 함수를 직접 호출했다.
TXCombatComponent->Attack();
GAS 구조에서는 입력 태그를 기준으로 Ability를 찾고 실행한다.
void ACombatCharacter::Input_AttackPressed()
{
if (!AbilitySystemComponent)
{
return;
}
// 죽었거나 기절 상태면 공격 불가
if (AbilitySystemComponent->HasMatchingGameplayTag(TAG_State_Dead) ||
AbilitySystemComponent->HasMatchingGameplayTag(TAG_State_Stunned))
{
return;
}
for (const FGameplayAbilitySpec& AbilitySpec : AbilitySystemComponent->GetActivatableAbilities())
{
if (AbilitySpec.Ability &&
AbilitySpec.GetDynamicSpecSourceTags().HasTagExact(TAG_InputTag_Attack))
{
AbilitySystemComponent->TryActivateAbility(AbilitySpec.Handle);
break;
}
}
}
Attack 입력
→ InputTag.Ability.Attack을 가진 AbilitySpec 검색
→ TryActivateAbility()
→ GA_Attack::CanActivateAbility()
→ GA_Attack::ActivateAbility()
8. GameplayAbility에서 GameplayTag 조건 검사
공격 Ability는 죽음/기절/피격 중이면 실행되지 않게 한다.
UCLASS()
class UGA_Attack : public UGameplayAbility
{
GENERATED_BODY()
public:
UGA_Attack()
{
AbilityTags.AddTag(TAG_Ability_Attack);
ActivationBlockedTags.AddTag(TAG_State_Dead);
ActivationBlockedTags.AddTag(TAG_State_Stunned);
ActivationBlockedTags.AddTag(TAG_State_HitReact);
ActivationBlockedTags.AddTag(TAG_State_Attacking);
}
virtual bool CanActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags,
const FGameplayTagContainer* TargetTags,
FGameplayTagContainer* OptionalRelevantTags) const override
{
if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags))
{
return false;
}
return ActorInfo && ActorInfo->AvatarActor.IsValid();
}
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get();
ACombatCharacter* Character = Cast<ACombatCharacter>(ActorInfo->AvatarActor.Get());
if (!ASC || !Character)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 공격 상태 태그 부여
ASC->AddLooseGameplayTag(TAG_State_Attacking);
// 기존 CombatComponent 공격 로직 호출 가능
Character->GetTXCombatComponent()->PlayAttackMontage();
// Montage 끝나는 시점이나 AnimNotify 시점에서 EndAbility 호출
// 예시에서는 바로 종료 처리
ASC->RemoveLooseGameplayTag(TAG_State_Attacking);
EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}
};
이제 공격 중 상태는 bool이 아니라 State.Attacking 태그로 표현한다.
GA_Attack 시작
→ State.Attacking 태그 부여
GA_Attack 종료
→ State.Attacking 태그 제거
9. 데미지 처리
//////////////////////////////////////
////////////// 기존 로직 //////////////
//////////////////////////////////////
float UPXCombatComponent::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
if (CurrentHP <= 0.0f)
{
return 0.0f;
}
if (nullptr == OwnerCharacter)
{
return 0.0f;
}
CurrentHP -= Damage;
if (CurrentHP <= 0.0f)
{
OwnerCharacter->HandleDeath();
}
else
{
UpdateHPBar();
PlayHitReaction();
}
return Damage;
}
데미지 수치 변경은 ASC를 통해 Attribute를 수정한다.
피격 반응은 직접 호출하지 않고 GA_HitReact로 넘긴다.
//////////////////////////////////////
////////////// 바뀐 로직 //////////////
//////////////////////////////////////
float ACombatCharacter::TakeDamage(
float Damage,
FDamageEvent const& DamageEvent,
AController* EventInstigator,
AActor* DamageCauser)
{
if (Damage <= 0.0f || !AbilitySystemComponent || !HealthSet)
{
return 0.0f;
}
if (AbilitySystemComponent->HasMatchingGameplayTag(TAG_State_Dead))
{
return 0.0f;
}
const float PreviousHealth = HealthSet->GetHealth();
// AttributeSet의 Health 변경
AbilitySystemComponent->ApplyModToAttribute(
UPXHealthSet::GetHealthAttribute(),
EGameplayModOp::Additive,
-Damage
);
const float ActualDamage = FMath::Max(0.0f, PreviousHealth - HealthSet->GetHealth());
if (ActualDamage > 0.0f && HealthSet->GetHealth() > 0.0f)
{
// 피격 반응도 Ability로 실행
AbilitySystemComponent->TryActivateAbilitiesByTag(
FGameplayTagContainer(TAG_Ability_HitReact)
);
}
return ActualDamage;
}
TakeDamage()
→ ASC.ApplyModToAttribute(Health, -Damage)
→ Health Attribute 변경 Delegate 호출
→ HPBar 갱신
→ Health <= 0이면 State.Dead 부여 + GA_Death 실행
→ 살아있으면 GA_HitReact 실행
10. HitReact Ability
피격 반응도 CombatComponent에서 직접 재생하지 않고 Ability로 만든다.
UCLASS()
class UGA_HitReact : public UGameplayAbility
{
GENERATED_BODY()
public:
UGA_HitReact()
{
AbilityTags.AddTag(TAG_Ability_HitReact);
ActivationBlockedTags.AddTag(TAG_State_Dead);
}
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override
{
UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get();
ACombatCharacter* Character = Cast<ACombatCharacter>(ActorInfo->AvatarActor.Get());
if (!ASC || !Character)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
ASC->AddLooseGameplayTag(TAG_State_HitReact);
Character->GetTXCombatComponent()->PlayHitReaction();
// 실제로는 Montage 종료 Delegate에서 제거하는 게 좋음
ASC->RemoveLooseGameplayTag(TAG_State_HitReact);
EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}
};
이제 피격 중인지 여부는 State.HitReact 태그로 확인한다.
if (AbilitySystemComponent->HasMatchingGameplayTag(TAG_State_HitReact))
{
// 피격 중
}
11. Death Ability
죽음 처리도 Character의 일반 함수 하나로 끝내지 않고 Ability로 만든다.
UCLASS()
class UGA_Death : public UGameplayAbility
{
GENERATED_BODY()
public:
UGA_Death()
{
AbilityTags.AddTag(TAG_Ability_Death);
// 죽음 Ability는 Dead 상태에서 실행될 수 있어야 한다.
// 그래서 State.Dead를 BlockedTags에 넣지 않는다.
}
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override
{
UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get();
ACombatCharacter* Character = Cast<ACombatCharacter>(ActorInfo->AvatarActor.Get());
if (!ASC || !Character)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
ASC->AddLooseGameplayTag(TAG_State_Dead);
Character->DisableInput(nullptr);
Character->GetTXCombatComponent()->UpdateHPBar();
Character->GetTXCombatComponent()->PlayDeathMontage();
// 충돌 제거, AI 정지, Ragdoll 등 처리
// Character->GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}
};
죽음 상태는 이제 bool이 아니라 태그로 판단한다.
if (AbilitySystemComponent->HasMatchingGameplayTag(TAG_State_Dead))
{
return;
}
12. GameplayTag 변경에 따른 반응
태그 변경 Delegate로 상태 변화를 감지할 수 있다.
void ACombatCharacter::HandleDeadTagChanged(const FGameplayTag Tag, int32 NewCount)
{
const bool bIsDead = NewCount > 0;
if (bIsDead)
{
// 입력 차단, UI 변경, AI 정지 등
}
else
{
// 부활 처리
}
}
void ACombatCharacter::HandleHitReactTagChanged(const FGameplayTag Tag, int32 NewCount)
{
const bool bIsHitReacting = NewCount > 0;
if (TXCombatComponent)
{
TXCombatComponent->SetIsHitReacting(bIsHitReacting);
}
}
흐름
공격
Input Attack
→ InputTag.Ability.Attack
→ ASC에서 해당 InputTag를 가진 FGameplayAbilitySpec 검색
→ TryActivateAbility()
→ GA_Attack 실행
→ State.Attacking 태그 부여
→ 공격 Montage 재생
→ AnimNotify에서 상대 TakeDamage 호출
→ 공격 종료 시 State.Attacking 제거
데미지
TakeDamage(Damage)
→ ASC.ApplyModToAttribute(Health, -Damage)
→ Health Attribute 변경
→ HandleHealthAttributeChanged()
→ HPBar 갱신
→ Health > 0이면 GA_HitReact 실행
→ Health <= 0이면 State.Dead 부여 + GA_Death 실행
피격
GA_HitReact 실행
→ State.HitReact 태그 부여
→ HitReaction Montage 재생
→ Montage 종료
→ State.HitReact 태그 제거
→ Ability 종료
사망
Health <= 0
→ State.Dead 태그 부여
→ GA_Death 실행
→ Death Montage 재생
→ 입력 차단
→ Collision 제거
→ Ability 종료
최종 구조
ACombatCharacter
├─ AbilitySystemComponent
├─ HealthSet
└─ TXCombatComponent
UPXHealthSet
├─ Health
└─ MaxHealth
GameplayTags
├─ State.Dead
├─ State.HitReact
├─ State.Attacking
├─ State.Stunned
├─ Ability.Attack
├─ Ability.HitReact
├─ Ability.Death
└─ InputTag.Ability.Attack
GameplayAbilities
├─ GA_Attack
├─ GA_HitReact
└─ GA_Death
CombatComponent
├─ HPBar 갱신
├─ Montage 재생
└─ 표현 처리
ASC
├─ Attribute 변경
├─ Ability 실행
├─ Tag 부여/제거
└─ AbilitySpec 관리
구조 요약
AttributeSet
= 체력 같은 수치 보관
GameplayAbility
= 공격, 피격, 사망 같은 행동 실행
GameplayTag
= 죽음, 피격 중, 공격 중 같은 상태 표현
ASC
= 수치 변경, Ability 실행, Tag 상태 관리
CombatComponent
= UI와 연출 처리'Unreal Engine' 카테고리의 다른 글
| UE Game play Framework 2 (0) | 2026.06.18 |
|---|---|
| UE Game play Framework 1 (0) | 2026.06.17 |
| GAS 1 (0) | 2026.06.15 |
| SmartObject 1 (0) | 2026.06.12 |
| GameplayMessageRouter Plugin 2 (1) | 2026.06.11 |