개발 이야기 안하는 개발자

언리얼 5_4_2 어트리뷰트와 게임플레이 이펙트의 이해 본문

Unreal/이득우의 언리얼 프로그래밍

언리얼 5_4_2 어트리뷰트와 게임플레이 이펙트의 이해

07e 2024. 3. 27. 11:59
반응형

캐릭터 어트리뷰트를 제작한다.

 

어트리뷰트 세트

GAS에서 제공하는 언리얼 오브젝트 이다.

단일 어트리뷰트 데이터인 GameplayAttributeData의 묶음이다.

GameplayAttributeData는 하나의 값이 아닌 두 가지 값을 구성되어 있다.

- BaseValue (기본 값), CurrentValue (변동 값)

- PreAttributeChange(), PostAttributeChange(), PreGameplayEffectExecute(), PostGameplayEffectExecute()

어트리뷰트 세트 접근자 매크로를 만들어서 제공함.

ASC는 초기화될 때 같은 액터(Owner)에 있는 AttributeSet 타입 객체를 찾아서 등록하기 때문에 따로 초기화를 제작하지 않아도 된다. 

 

상호작용할 NPC에게 ASC를 제작한다.

PossessedBy에서 초기화 해준다 (InitAbilityActorInfo(this, thsi))

 

사용할 AttributeSet을 UAttributeSet로 상속받아 새로운 클래스를 만든다.

#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
    
    ...
    
class ARENABATTLEGAS_API UABCharacterAttributeSet : public UAttributeSet
{
	GENERATED_BODY()
	
public:
	UABCharacterAttributeSet();

	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);

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

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 = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxHealth;

};

FGameplayAttributeData로 어트리뷰트 데이터를 만든다.

만든 데이터 셋을 Get,Set,Init 등을 제작하면 너무 많은 메소드를 제작해야한다.

언리얼에선 이를 위해서 매크로인 ATTRIBUTE_ACCESSORS를 제공해준다.

그럼 Get,Set,Init 말고 Property Getter도 지원하는데, 이는 가져오는 데이터가 맞는 데이터인지 비교하기 위해서 사용된다.

PreAttributeChange는 값이 변경되기 전에 호출되는 메소드이다.

PostAttributeChange는 값이 변경된 이후에 호출되는 메소드이다.

 

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

InitHealth는 제작했던 메크로가 지원해주는 메소드 이다.

이렇게 하면 헬스의 값이 최대값으로 초기화 된다.

 

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())
	{
		ABGAS_LOG(LogABGAS, Log, TEXT("Health : %f -> %f"), OldValue, NewValue);
	}
}

요구하는 어트리뷰트가 맞는지 비교하기 위해서 GetAttributeProperty를 사용한다. (GetHealthAttribute())

체력값이 변경되면 값이 변하도록 제작해준다.

 

이후 GASPlayerState에 Attributeset를 만든다. (저번에 만들었던 Player의 ASC를 가지고 있던 객체.Owner)

class ARENABATTLEGAS_API AABGASPlayerState : public APlayerState, public IAbilitySystemInterface
{
	...
	UPROPERTY()
	TObjectPtr<class UABCharacterAttributeSet> AttributeSet;
};


AABGASPlayerState::AABGASPlayerState()
{
	...
	AttributeSet = CreateDefaultSubobject<UABCharacterAttributeSet>(TEXT("AttributeSet"));
}

ASC와 Attribute를 묶거나 초기화같은걸 하지 않아도 되는 이유가 ASC의 Init에서 Attribute가 있는지 체크를 한다.

그렇기 때문에 같은 객체에 AttributeSet이 있다면 따로 초기화 하지 않아도 가져와서 설정한다.

 

과거 스크립트에서 TA(TargetActor)에서 공격 범위를 상수로 정의 했었다.

해당 내용을 AttributeSet에서 가져오도록 스크립트를 수정한다.

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

	UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
	if (!ASC)
	{
		ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!"));
		return FGameplayAbilityTargetDataHandle();
	}

	const UABCharacterAttributeSet* AttributeSet = ASC->GetSet<UABCharacterAttributeSet>();
	if (!AttributeSet)
	{
		ABGAS_LOG(LogABGAS, Error, TEXT("ABCharacterAttributeSet not found!"));
		return FGameplayAbilityTargetDataHandle();
	}

	FHitResult OutHitResult;
	const float AttackRange = AttributeSet->GetAttackRange();
	const float AttackRadius = AttributeSet->GetAttackRadius();

	...
    
}

 

해당 AT를 사용했던 GA(GameplayAbility)도 수정해주어야 한다.

void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
		ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));

		UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();
		UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
		if (!SourceASC || !TargetASC)
		{
			ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!"));
			return;
		}

		const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>();
		UABCharacterAttributeSet* TargetAttribute = const_cast<UABCharacterAttributeSet*>(TargetASC->GetSet<UABCharacterAttributeSet>());
		if (!SourceAttribute || !TargetAttribute)
		{
			ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!"));
			return;
		}

		const float AttackDamage = SourceAttribute->GetAttackRate();
		TargetAttribute->SetHealth(TargetAttribute->GetHealth() - AttackDamage);

	}

	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

적용되는 데미지 연산은 AttributeSet의 데이터를 가져와서 변경하도록 수정한다.

이제 NPC를 공격하면 NPC의 체력이 깍이는 것을 볼수 있다.

이때 Clamp를 걸어놓았기 때문에 0이하로는 떨어지지 않는것을 볼 수 있다.

 

 

게임플레이 이펙트를 제작해본다.

게임플레이 이펙트 GE(Gameplay Effect)

GAS는 게임에 영향을 주는 객체를 별도로 분리해서 관리한다.

게임에 영향을 준다는 것은 대부분 게임 데이터를 변경한다는 것을 뜻한다.

대부분 게임플레이 이펙트와 어트리뷰트는 함께 동작하도록 구성되어 있다.

- Instant(한프레임에 즉각 적용) , Duration(지정한 시간동안 적용) , Infinite(종료하지않으면 계속 적용)

 

게임플레이 이펙트 모디파이어

GE에서 어트리뷰트의 변경 방법을 지정한 설정을 모디파이어라 한다.

적용할 어트리뷰트의 지정 , 설정(더하기,곱하기, 덮어쓰기 등)으로 활용한다.

- ScalableFloat (실수) , AttritubeBased (특정 어트리뷰트기반) , CustomCalculationClass (계산을 담당하는 전용 클래스)

- SetByCaller (데이터 태그를 활용한 데이터 전달)

모디파이어 없이 자체 계산 로직을 만드는 것도 가능하다 (GameplayEffectExecutionCalculation을 상속받아 구현하면됨)

C++로 설정하기엔 문법이 복잡해서 블루프린트로 하는게 더 빠르고 효율적이다.

 

게임플레이 이펙트 생성 과정

게임플레이 이펙트 컨텍스트와 게임플레이 이펙트 스펙을 통해 생성 가능.

컨텍스트 : GE에서 계산에 필요한 데이터를 담은 객체

- 가해자(Instigator), 가해수단(Causer), 판단정보(HitResult) 등등

스펙 : GE에 관련된 정보를 담고 있는 객체

- 레벨, 모디파이어 및 각종 태그에 대한 정보, 게임플레이 이펙트 컨텍스트 핸들

ASC는 각 데이터를 핸들 객체를 통해 간접적으로 관리한다.

이펙트 컨텍스트 핸들을 만든 후, 이펙트 스펙 핸들을 생성하는 순서로 진행되어야 한다.

 

 

상대방의 체력을 깍는 이펙트를 제작한다.

GameplayEffect를 상속받아 제작한다.

UABGE_AttackDamage::UABGE_AttackDamage()
{
	DurationPolicy = EGameplayEffectDurationType::Instant;

	FGameplayModifierInfo HealthModifier;
	HealthModifier.Attribute = FGameplayAttribute(FindFieldChecked<FProperty>(UABCharacterAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(UABCharacterAttributeSet, Health)));
	HealthModifier.ModifierOp = EGameplayModOp::Additive;

	FScalableFloat DamageAmount(-30.0f);
	FGameplayEffectModifierMagnitude ModMagnitude(DamageAmount);

	HealthModifier.ModifierMagnitude = ModMagnitude;
	Modifiers.Add(HealthModifier);
}

Instant로 정의해서 한프레임만에 바로 즉각적용되도록 정의한다.

Modifier에서 Attribute는 사용하는 Attribute가 맞는지 확인하는 내용이다.

이때, Property를 가져오려고 하면(Health) Protected로 제작했기 때문에 에러가 뜬다.

public으로 바꾸던가, 아니면 AttributeSet에서 friend 설정을 해서 데이터를 가져갈 수 있도록 수정해야 한다.

	friend class UABGE_AttackDamage;

Modifier에서 ModifierOp를 더하는 기능으로 설정한다.

FScalableFloat이란 값으로 숫자를 정의한다.

이후, Modifier에 정의한 숫자를 넣어주고, 해당 이펙트의 기능을 추가하면 된다. (Modifiers.add)

 

이렇게 되면 원래 데미지 연산을 하던 GA에서 원래 로직을 제거하고 이펙트를 추가하면 된다.

.h
	UPROPERTY(EditAnywhere, Category = "GAS")
	TSubclassOf<class UGameplayEffect> AttackDamageEffect;

...


.cpp
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
		ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));

		UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();

		const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>();

		FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect);
		if (EffectSpecHandle.IsValid())
		{
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
		}
	}

	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

기존에 데이터를 받아 체력이 닳던 부분을 제거하고 이펙트의 기능을 호출하도록 변경했다.

EffectHandle를 제작하고, MakeOutgoingGameplayEffectSpec을 통해 데미지 이펙트를 초기화햇다.

ApplyGameplayEffectSpectToTarget을 호출해서 대상에게 이펙트가 적용되도록 호출한다.

 

블루프린트에서 AttackDamageEffect를 지정한다.

 

이렇게 하면 정상작동하는 것을 볼 수 있다.

이번엔 블루프린트를 제작해본다.

Cpp로 제작한것처럼 Gameplay Effect를 상속받고, Modifier를 추가해준다.

Attritube를 Health로 하고 Add한다. 값은 AttributeBased의 -30.0으로 하면 Cpp와 동일한 기능을 한다.

이렇게 하고, GA의 타겟 Attack Damage Effect를 위 블루프린트로 수정한다.

 

이번에 이펙트를 호출한 사람이 지정한 값을 바로 적용할 수 있도록 변경해 본다.

그렇게 하기 위해선 Scalable Float이 아니고 Set by Caller로 Caller가 지정한 값을 활용하도록 변경해야 한다.

 

이러한 방법을 위해 태그를 추가한다.

그리고 해당 태그를 지정한다.

 

#define ABTAG_DATA_DAMAGE FGameplayTag::RequestGameplayTag(FName("Data.Damage"))

코드도 EffectSpectHandle을 제작하는 부분을 수정한다.

		...
        
        FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect);
		if (EffectSpecHandle.IsValid())
		{
			EffectSpecHandle.Data->SetSetByCallerMagnitude(ABTAG_DATA_DAMAGE, -SourceAttribute->GetAttackRate());			
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
		}
        
        ...

가져온 데이터를 태그가 위치한 곳에 배치한다. 즉, AttackRate였던 30의 값을 마이너스를 붙여서 블루프린트로 지정해놓은 곳에 Data를 배치한다고 보면 된다.

 

Custom Calculation Class는 계산을 직접 구현하는 방식이다.

블루프린트로 제작한다면 Gameplay Mod Magnitude Calculation을 상속받은 블루프린트에서 다음과 같이 만든다.

-30의 값만 전달하면 되기 때문에 다음과 같이 만들고 Effect에서 블루프린트를 지정해 주면 된다.

 

 

Attribute Based 방식이다.

사용할 Attribute 의 값을 직접 AttackRate로 지정한다.

Attribute Source는 해당 값이 내 데이터인지 타겟의 데이터인지를 지정하는 것이다. (source가 나)

Attribute Calcution Type을 Base Value로 기본값으로 지정한다.

Coefficient 를 -1 로 지정해서 원래 값인 30을 -30으로 변경해서 연산하도록 한다.

 

메타 어트리뷰트

어트리뷰트의 설정을 위해 사전에 미리 설정하는 임시 어트리뷰트

예)체력을 바로 깍지 않고, 대미지를 통해 체력을 감소하도록 설정 (체력- 일반어트리뷰트, 대미지- 메타 어트리뷰트)

대미지를 사용하는 경우 기획 추가에 유연한 대처가 가능 (무적, 실드, 콤보 공격력 보정 등)

메타 어트리뷰트는 적용후 바로 0으로 값을 초기화 하도록 설정.

메타 어트리뷰트는 리플리케이션에서 제외시키는 것이 일반적

 

메타 어트리뷰트도 동일하게 AttributeSet에서 FGameplayAttributeData로 정의한다.

	ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, Damage);
    
	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData Damage;

AttributeSet에서 Health를 수정하게 하지말고 받은 Damage를 적용받아 연산하도록 수정한다.

void UABCharacterAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	if (Attribute == GetDamageAttribute())
	{
		NewValue = NewValue < 0.0f ? 0.0f : NewValue;
	}
}

void UABCharacterAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	Super::PostGameplayEffectExecute(Data);

	float MinimumHealth = 0.0f;

	if (Data.EvaluatedData.Attribute == GetHealthAttribute())
	{
		ABGAS_LOG(LogABGAS, Warning, TEXT("Direct Health Access : %f"), GetHealth());
		SetHealth(FMath::Clamp(GetHealth(), MinimumHealth, GetMaxHealth()));
	}
	else if (Data.EvaluatedData.Attribute == GetDamageAttribute())
	{
		ABGAS_LOG(LogABGAS, Log, TEXT("Damage : %f"), GetDamage());
		SetHealth(FMath::Clamp(GetHealth() - GetDamage(), MinimumHealth, GetMaxHealth()));
		SetDamage(0.0f);
	}
}

받은 Modifier 데이터를 가져와서 어트리뷰트에 맞춰 연산하도록 한다.

블루프린트에서도 사용하는 Attribute가 Damage가 되도록 수정한다.

이렇게 하면 데미지를 전달해줘서 체력이 깍이도록 로직을 구현한다.

 

레벨과 커브 테이블

게임플레이 이펙트에는 추가적으로 레벨 정보를 지정할 수 있음.

게임플레이 이펙트에 저장된 레벨 정보를 사용해 데이터 테이블에서 특정 값을 가져올 수 있음.

ScalabeFloat모디파이어 타입에서 이를 사용하는 것이 가능하다.

이를 활용해 다양한 기능을 구현하는 것이 가능하다.

- 콤보 추가시 증가하는 최종 대미지 계산, 현재 캐릭터 레벨에 따른 초기 스탯

- 언리얼 에디터에서 쉽게 제작이 가능하다.

콤보에 맞춰 데미지가 증가하도록 변경한다.

콤보는 총 4번이기 때문에 4번에 맞춰 데미지가 증가하도록 구현한다.

몽타주가 각 몇단계인지 알아야 하기 때문에 여기에 데이터를 추가하도록 한다.

몽타주에는 노티파이가 걸릴때마다 태그를 통해 콤보데이터를 보내고 있다.

여기에 지금 공격 번째를 포함해서 데이터를 쏠것이다.

.h
	UPROPERTY(EditAnywhere)
	float ComboAttackLevel;
    
    
.cpp notify()
...
			FGameplayEventData PayloadData;
			PayloadData.EventMagnitude = ComboAttackLevel;
			UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(OwnerActor, TriggetGameplayTag, PayloadData);
...

PayloadData에는 데이터를 심어서 보낼 수 있다.

이를 활용해서 태그에 데이터를 심어서 보낸다.

EventMagnitude를 사용해서 넘긴다.

 

받은 AttackhitCheck에서 현재 레벨을 받아와 체크한다.

.cpp attackHitcheck _ ActivateAbility()

...
	CurrentLevel = TriggerEventData->EventMagnitude; //currentLevel은 flaot
...

.cpp attackHitCheck _ OnTraceResultCallBack()

...
		FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
		if (EffectSpecHandle.IsValid())
		{
			EffectSpecHandle.Data->SetSetByCallerMagnitude(ABTAG_DATA_DAMAGE, -SourceAttribute->GetAttackRate());			
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
		}
...

이제 레벨을 넘겨주는 행위는 완료가 되었다.

제작한 커브데이터를 활용하면 된다.

곱해주는 대상은 제작한 ComboDamageTable이고 이때 각 레벨은 ComboDamage벨류이다.

넘겨받은 Magnitude값은 바로 적용된다.

 

같은 방식으로 레벨을 구현하고 레벨에 맞는 데미지를 구현해본다.

데이터 테이블을 제작한다.

 

이번 이펙트는 캐릭터를 초기화할때 사용해본다.

GameplayEffect를 상속받는 BPGE_CHaracterStat을 제작한다.

이후 MaxHealth를 Table에 맞춰 초기화 하고, 체력도 동일하게 제작한다.

이를 NPC가 활용할 것이다.

	UPROPERTY()
	TObjectPtr<class UABCharacterAttributeSet> AttributeSet;

	UPROPERTY(EditAnywhere, Category = GAS)
	TSubclassOf<class UGameplayEffect> InitStatEffect;

	UPROPERTY(EditAnywhere, Category = GAS)
	float Level;
void AABGASCharacterNonPlayer::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	ASC->InitAbilityActorInfo(this, this);

	FGameplayEffectContextHandle EffectContextHandle = ASC->MakeEffectContext();
	EffectContextHandle.AddSourceObject(this);
	FGameplayEffectSpecHandle EffectSpecHandle = ASC->MakeOutgoingSpec(InitStatEffect, Level, EffectContextHandle);
	if (EffectSpecHandle.IsValid())
	{
		ASC->BP_ApplyGameplayEffectSpecToSelf(EffectSpecHandle);
	}
}

컨텍스트핸들을 제작하고, 스펙핸들을 제작한다.

이후, 핸들을 ASC에 적용한다.

이렇게 하면 ASC가 GA를 거치지 않고 바로 GE를 사용해서 스스로의 데이터를 수정할 수 있다.

 

샌드백 NPC는 제작한 GasCharacterNonPlayer를 상속받아 블루프린트로 제작한다.

제작한 Stat Effect에 제작했던 레벨에 맞게 체력이 오르도록 CharacterStat Effect를 넣어준다.

이렇게 해서 Effect로 콤보데미지에 추가 공격을,

최대체력도 Effect로 비율로 변경하도록 제작했다.

 

UI시스템을 어트리뷰트와 연동되도록 제작한다.

상태에 맞는 체력바를 제작한다.

 

UserWidget과 WidgetComponent를 제작한다.

UserWidget은 UswerWidget을 상속받고 어빌시스템을 상속받는다.

 

UserWidget이 gas를 사용하기 때문에 하나 정의한다.

class ARENABATTLEGAS_API UABGASUserWidget : public UUserWidget, public IAbilitySystemInterface
{
	GENERATED_BODY()
	
public:
	virtual void SetAbilitySystemComponent(AActor* InOwner);
	virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;

protected:
	UPROPERTY(EditAnywhere, Category = GAS)
	TObjectPtr<class UAbilitySystemComponent> ASC;
	
};
void UABGASUserWidget::SetAbilitySystemComponent(AActor* InOwner)
{
	if (IsValid(InOwner))
	{
		ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(InOwner);
	}
}

UAbilitySystemComponent* UABGASUserWidget::GetAbilitySystemComponent() const
{
	return ASC;
}

SetAbilitySystemComponent를 활용해서 오너의 ASC를 가져오도록 구현한다.

 

제작한 UABGASUserWidget를 상속받는 HpBarWidget을 제작한다.

class ARENABATTLEGAS_API UABGASHpBarUserWidget : public UABGASUserWidget
{
	GENERATED_BODY()
	
protected:
	virtual void SetAbilitySystemComponent(AActor* InOwner) override;

	virtual void OnHealthChanged(const FOnAttributeChangeData& ChangeData);
	virtual void OnMaxHealthChanged(const FOnAttributeChangeData& ChangeData);

	void UpdateHpBar();

protected:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<class UProgressBar> PbHpBar;

	UPROPERTY(meta = (BindWidget))
	TObjectPtr<class UTextBlock> TxtHpStat;

	float CurrentHealth = 0.0f;
	float CurrentMaxHealth = 0.1f;
	
	FLinearColor HealthColor = FLinearColor::Red;
	FLinearColor InvinsibleColor = FLinearColor::Blue;
};

HpBar와 Text를 받아와서 여기에 정보를 넣을 예정이다.

만약, 이름이 똑같다면 특별히 넣어주지 않아도 스스로 매칭이 된다.

 

void UABGASHpBarUserWidget::SetAbilitySystemComponent(AActor* InOwner)
{
	Super::SetAbilitySystemComponent(InOwner);

	if (ASC)
	{
		ASC->GetGameplayAttributeValueChangeDelegate(UABCharacterAttributeSet::GetHealthAttribute()).AddUObject(this, &UABGASHpBarUserWidget::OnHealthChanged);
		ASC->GetGameplayAttributeValueChangeDelegate(UABCharacterAttributeSet::GetMaxHealthAttribute()).AddUObject(this, &UABGASHpBarUserWidget::OnMaxHealthChanged);

		const UABCharacterAttributeSet* CurrentAttributeSet = ASC->GetSet<UABCharacterAttributeSet>();
		if (CurrentAttributeSet)
		{
			CurrentHealth = CurrentAttributeSet->GetHealth();
			CurrentMaxHealth = CurrentAttributeSet->GetMaxHealth();

			if (CurrentMaxHealth > 0.0f)
			{
				UpdateHpBar();
			}
		}
	}
}

AttributeSet에 있는 값들의 변경사항에 따라 델리게이트를 등록할 수 있다.

그래서 값이 변경될때마다 특정 메소드가 호출되도록 바인딩을 진행한다.

void UABGASHpBarUserWidget::OnHealthChanged(const FOnAttributeChangeData& ChangeData)
{
	CurrentHealth = ChangeData.NewValue;
	UpdateHpBar();
}

void UABGASHpBarUserWidget::OnMaxHealthChanged(const FOnAttributeChangeData& ChangeData)
{
	CurrentMaxHealth = ChangeData.NewValue;
	UpdateHpBar();
}

바인딩 된 데이터들 이다.

void UABGASHpBarUserWidget::UpdateHpBar()
{
	if (PbHpBar)
	{
		PbHpBar->SetPercent(CurrentHealth / CurrentMaxHealth);
	}

	if (TxtHpStat)
	{
		TxtHpStat->SetText(FText::FromString(FString::Printf(TEXT("%.0f/%0.f"), CurrentHealth, CurrentMaxHealth)));
	}
}

변경된 체력에 맞춰 체력바의 모습을 변경시킨다.

 

GasHpBarUserWidget으로 위젯을 제작하고 위와같이 만들어둔다.

 

캐릭터 머리에 띄울 HP widget 컴포넌트를 제작한다.

void UABGASWidgetComponent::InitWidget()
{
	Super::InitWidget();

	UABGASUserWidget* GASUserWidget = Cast<UABGASUserWidget>(GetWidget());
	if (GASUserWidget)
	{
		GASUserWidget->SetAbilitySystemComponent(GetOwner());
	}
}

컴포넌트가 사용할 위젯을 받아 해당 위젯의 GAS를 넣어준다.

 

 

	UPROPERTY(VisibleAnywhere)
	TObjectPtr<class UABGASWidgetComponent> HpBar;
//생성자
	HpBar = CreateDefaultSubobject<UABGASWidgetComponent>(TEXT("Widget"));
	HpBar->SetupAttachment(GetMesh());
	HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f));
	static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_HpBar.WBP_HpBar_C"));
	if (HpBarWidgetRef.Class)
	{
		HpBar->SetWidgetClass(HpBarWidgetRef.Class);
		HpBar->SetWidgetSpace(EWidgetSpace::Screen);
		HpBar->SetDrawSize(FVector2D(200.0f, 20.f));
		HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}

캐릭터에게 WidgetComponent를 제작한다.

widgetComponent가 가질 Widget은 제작했던 hpbarwidget으로 지정한다.

 

이렇게 해서 받는 데미지에 맞춰 체력이 깍이는게 위젯으로 보일 것이다.

 

캐릭터가 체력이 다 달경우 죽도록 하는 기능을 제작한다.

이것도 기능이기 때문에 AttributeSet에 제작한다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOutOfHealthDelegate);

...

	mutable FOutOfHealthDelegate OnOutOfHealth;
    
    ...
    	bool bOutOfHealth = false;

플레이어가 죽을경우 죽는다고 이벤트를 보내야 한다.

 

죽을 경우 브로드캐스트를 진행한다.

죽을 경우 확실하게 태그로 남겨야 한다.

Dead일 경우의 태그를 추가했다.

#define ABTAG_CHARACTER_INVINSIBLE FGameplayTag::RequestGameplayTag(FName("Character.State.Invinsible"))

 

캐릭터가 사용하기 때문에 이를 정의하고, 이를 상속받는 Player와 Non Player 모두에게 추가한다.

	UFUNCTION()
	virtual void OnOutOfHealth();

 

OnOutOfHealth는 예전에 만들었던 SetDead를 호출한다.

void AABGASCharacterPlayer::OnOutOfHealth()
{
	SetDead();
}
void AABGASCharacterNonPlayer::PossessedBy(AController* NewController)
{
	...
	AttributeSet->OnOutOfHealth.AddDynamic(this, &ThisClass::OnOutOfHealth);
	...
}


...


void AABGASCharacterPlayer::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	AABGASPlayerState* GASPS = GetPlayerState<AABGASPlayerState>();
	if (GASPS)
	{
		...
		const UABCharacterAttributeSet* CurrentAttributeSet = ASC->GetSet<UABCharacterAttributeSet>();
		if (CurrentAttributeSet)
		{
			CurrentAttributeSet->OnOutOfHealth.AddDynamic(this, &ThisClass::OnOutOfHealth);
		}
		...
	}
}

델리게이트도 올바르게 연결한다.

void UABCharacterAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	...

	if ((GetHealth() <= 0.0f) && !bOutOfHealth)
	{
		Data.Target.AddLooseGameplayTag(ABTAG_CHARACTER_ISDEAD);
		OnOutOfHealth.Broadcast();
	}

	bOutOfHealth = (GetHealth() <= 0.0f);
}

UI쪽에서 체력이 다 떨어짐을 받으면 브로드캐스트로 델리게이트를 호출한다.

 

그럼 순서를 보면 UI에서 체력 검사를 진행하고

죽은 상태일 경우 Delegate를 쏜다.

등록되어있던 캐릭터가 OnOutOfHealth를 호출하고

이는 setdead()를 호출하게 된다.

 

그럼 기존에 있던 Dead애니메이션이 호출되고 태그도 Dead로 변경되게 된다.

 

기간형 게임플레이 이펙트

유요기간동안 태그와 같은 상태를 가질 수 있다.

중첩(여러개)를 가지도록 할 수 있다.

인스턴트는 Base값을 변경하지만, 기간형은 Current값을 변경하고 원래대로 돌려놓는다.

기간형 게임 이펙트는 다양한 종류의 스킬, 버프 효과를 코딩없이 구현할 수 있다.

 

짧은 시간동안 무적이 되는 기능을 제작해 본다.

Gameplay Effect를 블루프린트로 제작한다.

3초동안 해당 태그가 Add된다.

 

GA를 상속받은 블루프린트를 제작해본다.

어빌리티가 활성화되면 스스로에게 해당 GE를 발동시킨다.

NPC블루프린트이다.

GA를 바로 부여한다. 방금 제작한 GA부여하고 3초뒤에 활성화 한다.

그렇다면3초뒤에 3초동안 무적상태가 될 것이다.

 

무적상태가 되면 체력바의 색을 변경시킬 예정이다.

HpBarUserWidget에 메소드를 추가한다.

.h
    virtual void OnInvinsibleTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
    
    ...
  
  
.cpp
  	...
		ASC->RegisterGameplayTagEvent(ABTAG_CHARACTER_INVINSIBLE, EGameplayTagEventType::NewOrRemoved).AddUObject(this, &UABGASHpBarUserWidget::OnInvinsibleTagChanged);
		PbHpBar->SetFillColorAndOpacity(HealthColor);
	...
    
void UABGASHpBarUserWidget::OnInvinsibleTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
	if (NewCount > 0)
	{
		PbHpBar->SetFillColorAndOpacity(InvinsibleColor);
		PbHpBar->SetPercent(1.0f);
	}
	else
	{
		PbHpBar->SetFillColorAndOpacity(HealthColor);
		UpdateHpBar();
	}
}

제작했던 태그를 그대로 적용하면 된다.

 

bool UABCharacterAttributeSet::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data)
{
	if (!Super::PreGameplayEffectExecute(Data))
	{
		return false;
	}

	if (Data.EvaluatedData.Attribute == GetDamageAttribute())
	{
		if (Data.EvaluatedData.Magnitude > 0.0f)
		{
			if (Data.Target.HasMatchingGameplayTag(ABTAG_CHARACTER_INVINSIBLE))
			{
				Data.EvaluatedData.Magnitude = 0.0f;
				return false;
			}
		}
	}

	return true;
}

어트리뷰트셋에서 만약 지금 Invinsible 태그를 가지고 있다면 받는 데미지를 0으로 무효화하는 로직을 추가한다.

이렇게 무적상태동안 데미지를 받지 않도록 제작한다.

 

캐릭터의 공격 범위가 점차 커지는 버프를 제작해본다.

Gameplay Effect를 블루프린트로 제작한다.

공격의 스택이 4회동안 공격 범위가 커진다.

15의 크기만큼 공격 범위가 커지도록 제작해 본다.

지속시간은 2초이다.

 

.h

...
	UPROPERTY(EditAnywhere, Category = "GAS")
	TSubclassOf<class UGameplayEffect> AttackBuffEffect;
    
    
    ...
    
.cpp
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
		ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));

		UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();

		const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>();

		//const float AttackDamage = SourceAttribute->GetAttackRate();
		//TargetAttribute->SetHealth(TargetAttribute->GetHealth() - AttackDamage);

		FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
		if (EffectSpecHandle.IsValid())
		{
			//EffectSpecHandle.Data->SetSetByCallerMagnitude(ABTAG_DATA_DAMAGE, -SourceAttribute->GetAttackRate());			
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
		}

		FGameplayEffectSpecHandle BuffEffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackBuffEffect);
		if (BuffEffectSpecHandle.IsValid())
		{
			ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, BuffEffectSpecHandle);
		}
	}

	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

공격을 진행할때 버프를 체크하고, 버프효과를 적용한다.

 

제작한 내용은 BPGA_AttackHitCheck에 넣어서 적용한다.

 

3초뒤에 무적이 활성화 된다.

3초뒤에 무적이 종료가 되고, 유저가 공격이 진행되는 동안 공격 범위가 커지는 것을 볼 수 있다.

왼쪽에 AttackRate의 크기가 변경되지만 Base의 값은 바뀌지 않고, 시간이 끝나면 다시 원래대로 돌아오는 것을 볼 수 있다.

 

반응형