본문 바로가기
Unreal 게임 개발/Unreal 강의 개인 정리

캐릭터 어트리뷰트 & 데미지 입히기(Attribute 사용) - GAS

by daisy0461 2025. 8. 23.

어트리뷰트 세트(Attribute Set)

  • 단일 어트리뷰트 데이터인 GameplayAttributeData의 묶음
  • GameplayAttributeData는 두 가지 값으로 구성되어 있음
    BaseValue : 기본 값. 영구히 적용되는 고정 스탯 값을 관리하는데 사용
    CurrentValue : 변동 값, 버프 등으로 임시적으로 변동된 값을 관리하는데 사용
  • ASC는 초기화될 때 ASC를 가지고 있는 Actor에 있는 AttributeSet 타입 객체를 찾아서 등록함.

어트리뷰트 세트의 주요 함수

  • PreAttributeChange : 어트리뷰트 변경 전에 호출
  • PostAttributeChange : 어트리뷰트 변경 후에 호출
  • PreGameplayEffectExecute : 게임플레이 이펙트 적용 전에 호출
  • PostGameplayEffectExecute : 게임플리에 이펙트 적용 후에 호출

Attribute 생성 방법

AttributeSet을 상속받아서 구현하면 된다.

 

ASC가 AttributeSet을 관리 및 등록하는 방법

AbilitySystemComponent의 함수 중 InitializeComponet()를 보자.

void UAbilitySystemComponent::InitializeComponent()
{
	Super::InitializeComponent();

	// Look for DSO AttributeSets (note we are currently requiring all attribute sets to be subobjects of the same owner. This doesn't *have* to be the case forever.
	AActor *Owner = GetOwner();
	InitAbilityActorInfo(Owner, Owner);	// Default init to our outer owner

	// cleanup any bad data that may have gotten into SpawnedAttributes
	for (int32 Idx = SpawnedAttributes.Num()-1; Idx >= 0; --Idx)
	{
		if (SpawnedAttributes[Idx] == nullptr)
		{
			SpawnedAttributes.RemoveAt(Idx);
		}
	}

	TArray<UObject*> ChildObjects;
	GetObjectsWithOuter(Owner, ChildObjects, false, RF_NoFlags, EInternalObjectFlags::Garbage);

	for (UObject* Obj : ChildObjects)
	{
		UAttributeSet* Set = Cast<UAttributeSet>(Obj);
		if (Set)  
		{
			SpawnedAttributes.AddUnique(Set);
		}
	}

	SetSpawnedAttributesListDirty();
}

자기 Onwer가 가지고  있는 자식 Object들을 ChildeObjects로 가져온다.

이 Array를 돌려서 AttributeSet인 형이라 SpawnedAttributes에 추가한다.

해당 Array에 추가해서 관리하게 된다.

AddUnique이기에 중복된 Attribute를 두개 이상 선언할 수는 없다.

 

 

AttributeSet 예시

해당 코드는 헤더 코드이다.

#pragma once

#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "ABCharacterAttributeSet.generated.h"

//여기 ATTRIBUTE_ACCESSORS라는 매크로는 AttributeSet.h에 있어서 복사해서 사용하면 된다.
//해당 매크로는 한줄의 매크로로 4개의 함수를 지정해주는 역할이다.
/*
* GAMEPLAYATTRIBUTE_PROPERTY_GETTER : 어트리뷰트를 처리할 때 해당 어트리뷰트인지 확인할 때 사용한다.
* VALUE_GETTER : Current 값을 가져오는 매크로이다.
* VALUE_SETTER : Base값을 버꿔주는 함수이다. 해당 함수로 Base 값을 확정한다.
* VALUE_INITTER : BaseValue와 CurrentValue값을 같은 값으로 지정하는 역할이다.
*/
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

UCLASS()
class ARENABATTLEGAS_API UABCharacterAttributeSet : public UAttributeSet
{
	GENERATED_BODY()
public:
	UABCharacterAttributeSet();

	virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
	virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;

	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, AttackRange);
	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxAttackRange);
	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, AttackRadius);
	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxAttackRadius);
	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, AttackRate);
	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxAttackRate);
	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, Health);
	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxHealth);


protected:
	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData AttackRange;
	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxAttackRange;

	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData AttackRadius;
	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxAttackRadius;

	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData AttackRate;
	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxAttackRate;

	UPROPERTY(BlueprintReadOnly, Category = "Health", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData Health;
	UPROPERTY(BlueprintReadOnly, Category = "Health", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxHealth;
};

다양한 FGameplayAttributeData를 선언하고 해당 선언에 Max값을 정해준다.

그리고 각 FGameplayAttributeData당 ATTRIBUTE_ACCESSORS매크로를 활용하여 다양한 함수를 만들어준다.

UAttributeSet에서 간단하게 복사해서 사용할 수 있도록 제공하고 있다.

 

UABCharacterAttributeSet::UABCharacterAttributeSet() :
	AttackRange(100.f),
	MaxAttackRange(300.f),
	AttackRadius(50.f),
	AttackRate(30.0f),
	MaxAttackRadius(150.0f),
	MaxAttackRate(100.0f),
	MaxHealth(100.0f)
{
	InitHealth(GetMaxHealth());
}

생성자에서 다음과 같이 넣어 초기값을 설정해줄 수 있다.

 

AABGASCharacterNonPlayer::AABGASCharacterNonPlayer()
{
	ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
	AttributeSet = CreateDefaultSubobject<UABCharacterAttributeSet>(TEXT("AttributeSet"));
}
---------------------------------------------------------------------------------------------
AABGASPlayerState::AABGASPlayerState()
{
	//PlayerState에서 ASC생성
	ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
	//네트워크 사용 시 컴포넌트가 리플리케이션되도록 함.
	ASC->SetIsReplicated(true);

	AttributeSet = CreateDefaultSubobject<UABCharacterAttributeSet>(TEXT("Attribute"));
}

Player가 아닌 캐릭터에겐 생성자에 직접 넣어주고

Player는 PlayerCharacter에서 ASC를 생성하는 것이 아닌 PlayerState에서 생성하기에 AttributeSet도 PlayerState에서 생성한다.

 

그럼 AttributeSet의 생성과 Setting은 끝났으니 직접 해당 값을 사용해보자.

FGameplayAbilityTargetDataHandle AABTA_Trace::MakeTargetData() const
{
	ACharacter* Character = CastChecked<ACharacter>(SourceActor);

	UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
	if (!ASC)
	{
		GAS_LOG(LogABGAS, Error, TEXT("ASC Not Found"));
		return FGameplayAbilityTargetDataHandle();
	}

	//ASC에서 AttributeSet을 가져온다. GetSet이 GetAtrributeSet의 줄임이다.
	//주의할 점은 GetSet은 const로 불러온다는 점이다.
	const UABCharacterAttributeSet* AttributeSet = ASC->GetSet< UABCharacterAttributeSet>();
	if (!AttributeSet)
	{
		GAS_LOG(LogABGAS, Error, TEXT("CharacterAttributeSet Not Found"));
		return FGameplayAbilityTargetDataHandle();
	}

	FHitResult OutHitResult;
	//임시 하드코딩
	//const float AttackRange = 100.0f;
	//const float AttackRadius = 50.0f;
	//Attribute로 가져오기
	const float AttackRange = AttributeSet->GetAttackRange();
	const float AttackRadius = AttributeSet->GetAttackRadius();
    ...
    }

매크로에서 Get을 사용할 수 있게 했기 때문에
GetAttackRange, GetAttackRadius를 사용하면 간단하게 들고올 수 있다.

 

이제 데미지를 입혀보자.

void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	//FGameplayAbilityTargetDataHandle는 내부적으로 TArray<TSharedPtr<FGameplayAbilityTargetData>> 컨테이너를 들고 있다.
	//TagetDataHasHitResult는 TargetDataHandle에서 0번째 요소를 뽑아 해당 요소에 HitResult가 존재하는지 확인하는 함수이다.
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		//TargetDataHandle의 0번째 요소에서 HitResult를 Get하여 저장.
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
		//어떤 Target이 맞았는지 출력
		GAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *HitResult.GetActor()->GetName());

		UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();
		//맞은 Target의 GAS를 들고온다.
		UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
		
		if (!SourceASC || !TargetASC)
		{
			GAS_LOG(LogABGAS, Error, TEXT("ASC not found!"));
			return;
		}

		//Source와 Target의 AttributeSet을 들고온다.
		const UABCharacterAttributeSet* SourceAttribute =  SourceASC->GetSet<UABCharacterAttributeSet>();
		//Target은 Health 값을 변경해야하기에 const_cast를 하여 const를 없앤다.
		UABCharacterAttributeSet* TargetAttribute = const_cast<UABCharacterAttributeSet*>(TargetASC->GetSet<UABCharacterAttributeSet>());
		if (!SourceAttribute || !TargetAttribute)
		{
			GAS_LOG(LogABGAS, Error, TEXT("Attribute not found!"));
			return;
		}
		
		const float AttackDamge = SourceAttribute->GetAttackRate();
		TargetAttribute->SetHealth(TargetAttribute->GetHealth() - AttackDamge);
	
	}

	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;

	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

GA에서 TargetDataHandle에서 뽑은 HitResult를 사용해 맞은 Actor를 들고온 후

SourceActor와 TargetActor의 어트리뷰트를 들고와서

SourceActor의 AttackRate만큼 TargetActor의 Health를 뺍니다.

 

void UABCharacterAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	if (Attribute == GetHealthAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
	}
}

void UABCharacterAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
	if (Attribute == GetHealthAttribute())
	{
		GAS_LOG(LogABGAS, Log, TEXT("Helath : %f -> %f"), OldValue, NewValue);
	}
}

이때 변경하는 Health값이 0이하로 떨어지지 않게 PreAttributeChange에서 Clamp를 걸어서 0.0을 최소로 합니다.