UnrealEngine5/공부

[UE5/GAS] Character Attribute로 Damage 전달하기

왹져박사 2025. 6. 1. 15:36

인프런 [이득우의 언리얼 프로그래밍 Part4 - 게임플레이 어빌리티 시스템] 강의 공부 노트

강의를 들으며 개념 정리 후, 다른 프로젝트에 실습하고 있습니다. 


 

AttributeSet

단일 Attribute Data (속성)인 GameplayAttributeData의 묶음.

기본 Attribute 값을 설정하고, 오류를 피하기 위한 최대 Attribute 값도 설정이 필요하다. 

 

GameplayAttributeData의 구성

  • BaseValue : 기본 값으로 영구적으로 고정되는 고정 스탯 값 관리에 사용됨.
  • CurrentValue : 변동 값으로 버프 등으로 임시적으로 변동되는 값 관리에 사용됨.

 

주요 함수

  • PreAttributeChange : Attribute 변경 전에 호출하여 검사
  • PostAttributeChange : Attribute 변경 후에 호출하여 변경 전과 후 값 전달
  • PreGameplayEffectExcute : GameplayEffect 적용 전에 호출
  • PostGameplayEffectExcute : GameplayEffect 적용 후에 호출

AttributeSet 접근자 매크로 → 많이 수행되는 기능에 대해 매크로 제공

ASC는 초기화될 때 같은 액터에 있는 UAttributeSet 타입 객체를 찾아 등록함 (수동으로 등록 필요X)

 


 

1. Monster에 GAS 적용하기

강의와 다른 점은, 진행하는 프로젝트는 PvE로 진행되는 시스템이다.

그래서 단순한 기능만을 가진 잡몹을 Character나 Pawn이 아닌 Actor로 상속받아 생성하였고,

따라서 캐릭터에서 ASC 초기화하던 PossesedBy 호출 시점이 아닌

Actor의 PostInitializeComponents 호출 시점에 ASC를 초기화하도록 설정하였다.

→ Actor가 스폰된 후, 필수 Component들이 초기화 된 후 위험 요소가 없을 때기 때문에 선정.

void AVSEnemyTrashMob::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	// Actor spawn 이후, Component들이 준비된 후에 ASC 초기화.
	ASC->InitAbilityActorInfo(this, this);
}

 


 

2. 기본 캐릭터 AttributeSet 생성

상속받은 AttributeSet의 소스코드를 살펴보면

 

Attribute에서 반복적으로 자주 사용되어야 할 함수들을 매크로로 지정하여 제공하고 있다.

  1. Property Getter
  2. Value Getter (Current Value)
  3. Value Setter (Base Value)
  4. Value Initter (Current Value와 Base Value를 같은 값으로 초기화)

위 4개의 함수가 한 프로퍼티마다 반복적으로 사용됨에, 이를 다음 매크로 사용으로 한번에 선언이 가능하게 한다.

ATTRIBUTE_ACCESSORS(ClassName, PropertyName)

/*
 * To use this in your game you can define something like this, and then add game-specific functions as necessary:
 * 
 *	#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
 *	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
 *	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
 *	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
 *	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
 * 
 *	ATTRIBUTE_ACCESSORS(UMyHealthSet, Health)
 */

 

 

PreAttributeChange에서 두번째 인자, NewValue의 값이 레퍼런스이기 때문에,

기획 의도에서 벗어난 값일 경우 보정이 가능한 함수이다.

PostAttributeChange는 값 변경 이후, 확인하는 함수이다.

virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }

/** Called just after any modification happens to an attribute. */
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) { }

 

 

위 매크로와 함수들을 사용하여 AttributeSet의 Property 설정과 값을 초기화한다.

이후에는 이를 데이터 테이블과 연결할 수 있을 듯 하다.

 

이제 생성한 AttributeSet를 생성했던 캐릭터에 추가해준다.

CharaterPlayer는 현재 PlayerState가 관리하고 있어 PlayerState에 AttributeSet을 추가해야 ASC가 자동으로 관리하게 된다.

이는 UAbilitySystemComponentInitializeComponent함수에서 알 수 있다.

void UAbilitySystemComponent::InitializeComponent()
{
	// ...
	// 자식 오브젝트들을 검색하여 AttibuteSet라면 목록에 추가한다. 
	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);
		}
	}
}

 


3. 플레이어가 아닌 다른 객체도 타겟으로 디버깅하는 설정 변경하기

기존에 Tag를 디버깅하던 것을 PageUp, PageDown 키를 통하여 캐릭터를 변경할 수 있다.

DefaultGame.ini에 다음을 추가한다.

[/Script/GameplayAbilities.AbilitySystemGrobals]
bUseDebugTargetFromHud=True

4. 생성한 Attribute 기반으로 공격 판정 변경하기

1) 기존 직접 값을 입력했던 부분을 Attribute 기반으로 수정하기

 

TA의 변경된 부분.

FGameplayAbilityTargetDataHandle AVSTA_Trace::MakeTargetData() const
{
  //...
	// SourceActor의 ASC 가져오기. 
	UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
	if (!ASC)
	{
		LOL_LOG(LogProjectLOL, Error, TEXT("ASC NOT FOUND"));
		return FGameplayAbilityTargetDataHandle();
	}
	// ASC의 AttributeSet 가져오기.
	const UVSCharacterAttributeSet* AttributeSet = ASC->GetSet<UVSCharacterAttributeSet>();
	if (!AttributeSet)
	{
		LOL_LOG(LogProjectLOL, Error, TEXT("ASC NOT FOUND"));
		return FGameplayAbilityTargetDataHandle();
	}

  //...

	// 공격 판정 범위. 
	const float AttackRange = AttributeSet->GetAttackRange();
	const float AttackRadius = AttributeSet->GetAttackRadius();
}

 

2) 감지한 TargetActor에 데미지 전달하기

 

VSGA_SkillAttackHitCheck의 변경된 부분.

void UVSGA_SkillAttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	// 타겟 데이터 핸들의 첫번째 데이터가 있는지 확인.
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
		LOL_LOG(LogProjectLOL, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));

		// SourceActor와 TargetActor의 ASC 가져오기. 
		UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();
		UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
		if (!SourceASC || !TargetASC)
		{
			LOL_LOG(LogProjectLOL, Error, TEXT("ASC NOT FOUND"));
			return;
		}

		const UVSCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UVSCharacterAttributeSet>();
		// TargetActor에 데미지를 주기 위해 값 변경 필요 -> 임시적으로 const_cast로 속성을 변경하여 값 변경 중. 
		// 이후 GameplayEffect를 사용하여 변경할 것. 
		UVSCharacterAttributeSet* TargetAttribute = const_cast<UVSCharacterAttributeSet*>(TargetASC->GetSet<UVSCharacterAttributeSet>());
		if (!SourceAttribute || !TargetAttribute)
		{
			LOL_LOG(LogProjectLOL, Error, TEXT("Attribute NOT FOUND"));
			return;
		}
		
		// 데미지 주기.
		const float AttackDamage = SourceAttribute->GetAttackPower();
		TargetAttribute->SetHP(TargetAttribute->GetHP() - AttackDamage);
	}

 

 

3) HP보정과 변경 디버깅을 위해 AttributeSet의 PreAttributeChange, PostAttributeChange 변경

void UVSCharacterAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	// HP가 0 이하로 내려가면 보정. 
	if (Attribute == GetHPAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0, GetMaxHP());
	}
}

void UVSCharacterAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
	// HP 변경 값 디버깅.
	if (Attribute == GetHPAttribute())
	{
		LOL_LOG(LogProjectLOL, Log, TEXT("Changed HP %d -> %d"), OldValue, NewValue);
	}
}

 

결과 화면

 

다음 강의를 통해 GameplayEffect까지 배우면, 현재 프로젝트에 맞게 설계할 수 있을 듯 하다!