GameplayEffect
이전 포스팅에서는 Ability에서 AttributeSet의 값을 직접 바꿨다.
이번에는 GameplayEffect를 사용하여 Attribute를 변경해 보자.
이전과 동일하게 공격 판정을 통해 공격에 적중한 타깃에게 피해를 주어야 한다.
여기서 GameplayEffect를 적용하여 간편하게 피해를 주는 로직을 구현할 수 있다.
GameplayEffect
GameplayEffect(GE)란 게임에 영향을 주는 객체이다.
즉, 게임에 필요한 데이터에 영향을 주는 객체를 말한다.
예를 들어, 지금의 경우 공격에 적중한 타깃의 Health값을 깎아주어야 한다.
이때, Health값을 변경하는 역할을 하는 객체가 GE이다.
GE는 보통 AttributeSet과 함께 동작하도록 구성되어 있다.
GE는 다양한 기능을 제공하여 원하는 시스템을 구현하기에 용이하다.
GE에는 세 가지 타입 중 하나를 선택해야 한다.
- Instant: 어트리뷰트에 즉각적으로 영향을 줌, 한 프레임에 실행
- Duration: 지정한 시간 동안 영향을 줌
- Infinite: 명시적으로 종료하지 않으면 계속하여 영향을 줌
GE를 잘 활용하면 코딩없이 다양한 기능을 구현할 수 있다.
GE는 특수한 함수나 로직으로 동작하지 않고 보통 설정으로 적용하기 때문에 Blueprint를 활용하는 것이 효율적이다.
GameplayEffect C++
우선, GameplayEffect를 상속받아 체력을 깎는 GE를 만들어 보자.
앞에서 말했듯이 GE는 특수한 함수나 로직이 필요하지 않다.
생성자에서 설정만 해주면 된다.
이때, 중요한 구조체가 있다.
GE가 Attribute를 어떻게 변경할지에 대한 명세가 필요하다.
예를 들어, 공격이라면 Health를 깎아야 하지만 회복이라면 Health를 더해주어야 한다.
이에 대한 명세를 지정하는 구조체가 모디파이어(Modifier)이다.
모디파이어에는 어떠한 Attribute에 영향을 줄 것인지, 어떠한 연산을 적용할 것인지 지정해 주면 된다.
모디파이어 계산 방법에는 네 가지가 있다.
- ScalableFloat: 실수(데이터테이블 적용 가능)
- AttributeBased: 특정 Attribute 기반
- CustomCalculationClass: 계산을 담당하는 전용 클래스 활용
- SetByCaller: 데이터 태그를 활용한 데이터 전달
모디파이어 없이도 계산 로직을 만드는 것도 가능하다.(GameplayEffectExecutionCalculation)
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.f);
FGameplayEffectModifierMagnitude ModMagnitude(DamageAmount);
HealthModifier.ModifierMagnitude = ModMagnitude;
Modifiers.Add(HealthModifier);
}
이렇게 제작한 GE를 적용하는 방법은 다음과 같다.
...
FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect);
if (EffectSpecHandle.IsValid())
{
ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
}
...
GA에는 ApplyGameplayEffectSpecToTarget라는 함수가 이미 선언되어 있다.
이를 사용해서 GE를 적용시킬 수 있는데 적용할 GE를 FGameplayEffectSpecHandle이라는 구조체로 감싸주어 매개변수로 넘겨주어야 한다.
FGameplayEffectSpecHandle을 만들 때는 MakeOutgoingGameplayEffectSpec이라는 함수로 만들 수 있다.
GameplayEffect BP
그렇다면 이제 GE를 BP로 만드는 법을 알아보자.
GE는 BP로 만드는 것이 훨씬 효율적이다.
GameplayEffect를 상속받은 BP를 하나 만들고 몇 가지 설정만 해주면 C++로 만든 GE와 동일하게 적용할 수 있다.
Duration Policy를 지정하는 부분에서는 위에서 언급한 세 가지 옵션 중 하나를 고를 수 있다.
Modifiers에는 원하는 모디파이어를 추가할 수 있다.
모디파이어에는 어떤 Attribute에 영향을 줄 것인지, 어느 정도의 영향을 줄 것인지 등을 설정할 수 있다.
또한, GE의 Magnitude를 어떻게 결정할 것인지에 대한 옵션도 지정할 수 있다.
즉, C++로 구현한 모든 부분을 에디터로 간단하게 설정할 수 있다.
Modifier
GE는 결국 모디파이어를 어떻게 설정하느냐에 따라 다양한 기능을 구현할 수 있다.
예를 들어, 일정 기간 동안 지속되는 버프를 부여하거나 보호막이나 무적 같은 기능 또한 만들 수 있다.
모디파이어를 설정하는데 중요한 부분을 살펴보자.
우선, Modifier Magnitude이다.
Magnitude의 계산 방식을 지정할 수 있는데, 위에서 언급한 바와 같이 총 네 가지의 방식이 있다.
- ScalableFloat: 실수(데이터테이블 적용 가능)
- AttributeBased: 특정 Attribute 기반
- CustomCalculationClass: 계산을 담당하는 전용 클래스 활용
- SetByCaller: 데이터 태그를 활용한 데이터 전달
ScalableFloat은 실수를 더하거나 곱하는 등의 연산을 수행하는 옵션이다.
하지만, 데이터 테이블을 사용해 Level마다 다른 값을 적용할 수 있다.
Level을 설정하는 부분은 GA에서 GE를 적용할 때, FGameplayEffectSpecHandle를 만드는 부분에서 두 번째 매개변수로 넘겨주어 지정할 수 있다.
콤보 공격마다 대미지를 증가하게 한다고 가정하면 콤보 공격에 Idx를 증가하게 한 뒤 Idx를 넘겨주어 생성하면 된다.
...
//CurrentLevel = 콤보 공격 Idx
FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
if (EffectSpecHandle.IsValid())
{
ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
}
AttributeBased는 Attribute의 값을 기반으로 Magnitude를 결정하는 옵션이다.
어떤 값을 기반으로 어떻게 적용할 것인지 결정할 수 있다.
ScalableFloat과 마찬가지로 데이터테이블을 활용할 수 있다.
CustomCalculationClass는 Magnitude값을 계산하는 전용 클래스에게 계산을 맡기고 결괏값을 받아와 적용하는 것이다.
CustomCalculationClass는 GameplayModMagnitudeCalculation을 상속받아 생성할 수 있다.
복잡한 계산 로직을 실행하여 결괏값을 구할 수 있다는 장점이 있다.
SetByCaller는 Caller가 값을 지정하여 넘겨주고 넘겨받은 값을 무조건 적용하는 옵션이다.
영향을 받는 타깃은 넘어온 값이 어떻게 계산되었는지는 알 수 없지만 넘겨받은 값을 그대로 적용해야 한다.
이는 Tag를 이용하여 구현하는데, 지정된 태그에 있는 Magnitude의 Data값을 보고 그 값을 적용시킨다.
...
FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
if (EffectSpecHandle.IsValid())
{
EffectSpecHandle.Data->SetSetByCallerMagnitude(ABTAG_DATA_DAMAGE, -SourceAttirbute->GetAttackRate());
ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
}
SetSetByCallerMagnitude()라는 함수를 이용하여 지정할 수 있는데, 첫 번째 매개변수는 Data를 식별할 태그이고 두 번째 매개변수는 값이다.
Meta Attribute
여태까지 구현한 GAS를 통해 공격에 적중하면 체력이 깎이는 시스템을 구현할 수 있다.
하지만, 현재는 Health의 값을 직접 접근해 깎아주게끔 구현되어 있다.
이렇게 되면 추가적인 버프나 무적 같은 기능을 구현하기 어려워진다.
따라서, 허상의 데이터만 다루는 Attribute를 만들어 이를 통해 Health값을 변경한다면 추가적인 기능을 구현할 때 더욱 간편하게 구현할 수 있다.
이런 Meta Attribute는 사용한 뒤 0으로 초기화해주어야 한다.
또한, 리플리케이션에서도 제외하는 것이 일반적이다.
이런 MetaAttribute는 AttributeSet에서 관리해 주는 것이 좋다.
//.h
...
UPROPERTY(BlueprintReadOnly, Category = Health, meta = (AllowPrivateAccess = true))
FGameplayAttributeData Damage;
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
//.cpp
void UMyAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
float MinimumHealth = 0.f;
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), MinimumHealth, GetMaxHealth()));
}
else if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
SetHealth(FMath::Clamp(GetHealth() - GetDamage(), MinimumHealth, GetMaxHealth()));
SetDamage(0.f);
}
}
PostGameplayEffectExecute는 GE가 적용된 후 실행되는 함수이다.
만약 기존처럼 Health값을 직접 변경했다면 버프를 적용하려면 PostGameplayEffectExecute에서 값을 다시 조정해주어야 한다.
그렇기 때문에 Damage라는 Meta Attribute를 만들어 PostGameplayEffectExecute에서 계산하여 버프를 적용하면 훨씬 간단하게 구현할 수 있다.
GE를 통한 캐릭터 스탯 초기화
GE는 꼭 GA에서 적용할 필요는 없다.
캐릭터가 생성되거나 레벨업하는 경우 스탯을 초기화하고 싶은 경우가 있다.
특정한 시점에 스탯을 초기화해 주는 GE를 실행하면 간단하게 구현할 수 있다.
지금은 NPC에 Controller가 빙의했을 때 레벨에 따라 스탯이 초기화되게 구현해 보겠다.
void ANPC::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);
}
}
GE를 적용하기 위해서는 조금 복잡한 구조체가 필요하다.
우선 Context에 대한 Handle을 만들어 데이터를 설정한 뒤, GameplayEffectSpecHandle을 만들어 ASC를 통해 실행하면 된다.
GE는 위에서 설정한 것과 같이 ScalarbleFloat으로 설정하여 데이터테이블을 통해 Level로 값을 받아와 설정하였다.