GameplayCue
GameplayCue를 이용하여 VFX를 적용하는 방법에 대해 알아보자.
GameplayCue(GC)는 시각 이펙트나 사운드 같은 게임 로직과는 관련이 없는 부가적인 기능을 담당한다.
GC에는 두 가지 종류가 있다.
- 스태틱 게임플레이 큐: 일시적으로 발생하는 특수효과에 사용, Excute 이벤트 발동
- 액터 게임플레이 큐: 일정 기간동안 발생하는 특수효과에 사용, Add/Remove 이벤트 발동
C++로도 구현할 수 있지만 BP로 구현하는 것이 생산적이다.
또한, GE에서 GC와 연동할 수 있도록 기능을 제공하고 있다.
다른 GAS객체들과는 다르게 GC는 GameplayCueManager가 관리한다.
부가적인 기능이라 그런 것 같다.
또한, GC는 GameplayTag를 통해 간단하게 실행할 수 있다.
GameplayCue C++
GameplayCue를 C++로 만들기 위해서는 GameplayCue를 상속받으면 되는데, 앞에서 말했듯이 두 가지 종류가 있다.
지금은 스태틱 GC만 사용하여 예제를 만들 것이기 때문에 GameplayNotify_Static이라는 클래스를 상속받아 만들면 된다.
캐릭터가 공격을 받았을 때, VFX가 실행되도록 만들어 보자.
//.h
UCLASS()
class PROJECT_API UGC_AttackHit : public UGameplayCueNotify_Static
{
GENERATED_BODY()
public:
UGC_AttackHit();
virtual bool OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const override;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=GameplayCue)
TObjectPtr<class UParticleSystem> ParticleSystem;
};
//.cpp
UGC_AttackHit::UGC_AttackHit()
{
ConstructorHelpers::FObjectFinder<UParticleSystem> ExplosionRef(TEXT("Assest Ref..."));
if (ExplosionRef.Succeeded())
{
ParticleSystem = ExplosionRef.Object;
}
}
bool UGC_AttackHit::OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const
{
const FHitResult* HitResult = Parameters.EffectContext.GetHitResult();
if (HitResult)
{
UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, HitResult->ImpactPoint, FRotator::ZeroRotator, true);
}
return false;
}
생성자에서는 어떠한 VFX를 실행할 것인지 설정해 주면 된다.
스태틱 GC는 Excute 이벤트가 실행되기 때문에 이를 override 하여 이미터를 생성하여 VFX가 실행되게 해 주면 된다.
GC에 대한 설정은 끝이다.
그러면 GC를 실행하기 위해서는 GamplayTag가 필요하기 때문에 GamepalyCue.Character.AttackHit이라는 태그를 하나 추가해야 한다.
GC를 식별하는 태그는 항상 GameplayCue가 있어야 한다.
다음은 GC를 실행을 요청하는 부분이다.
void UGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
...
FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
if (EffectSpecHandle.IsValid())
{
ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
FGameplayEffectContextHandle CueContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle);
CueContextHandle.AddHitResult(HitResult);
FGameplayCueParameters CueParam;
CueParam.EffectContext = CueContextHandle;
TargetASC->ExecuteGameplayCue(ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT, CueParam);
}
...
}
FGameplayEffectContextHandle을 UAbilitySystemBlueprintLibrary를 통해 GE에서 부터 받아온다.
이후, HitResult를 추가한 뒤 FGameplayCueParameters를 만들어 함수의 param으로 전달해주면 된다.
GC를 실행하는 함수는 ASC의 ExcuteGameplayCue이다.
첫 번째 매개변수는 GC를 식별하기위한 GameplayTag이고, 두 번째 매개변수는 GC에 전달할 Param이다.
HitResult를 넣은 이유는 GC에서 ImpactPoint에 이미터를 생성하기 때문이다.
또한, GC는 C++로 제작하여도 BP로 상속받아주는 것이 좋은데 태그를 식별하지 못할 수 있기 때문이다.
아이템 상자
오버랩하면 다양한 효과를 부여하는 상자를 만들어 보자.
상자가 열릴 때, GC를 통해 VFX를 실행하고, 다양한 효과를 오버랩한 액터에 부여하게 만들 예정이다.
우선, 상자 액터를 만들자.
//.h
UCLASS()
class PROJECT_API AGASItemBox : public AActor, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AGASItemBox();
virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
virtual void NotifyActorBeginOverlap(AActor* Other) override;
protected:
virtual void PostInitializeComponents() override;
void ApplyEffectToTarget(AActor* Target);
void InvokeGameplayCue(AActor* Target);
protected:
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> ASC;
UPROPERTY(VisibleAnywhere, Category=Box)
TObjectPtr<class UBoxComponent> Trigger;
UPROPERTY(VisibleAnywhere, Category = Box)
TObjectPtr<class UStaticMeshComponent> Mesh;
UPROPERTY(EditAnywhere, Category = GAS)
TSubclassOf<class UGameplayEffect> GameplayEffectClass;
UPROPERTY(EditAnywhere, Category = GAS, Meta=(Categories=GameplayCue))
FGameplayTag GameplayCueTag;
};
//.cpp
void AGASItemBox::NotifyActorBeginOverlap(AActor* Other)
{
Super::NotifyActorBeginOverlap(Other);
InvokeGameplayCue(Other);
ApplyEffectToTarget(Other);
Mesh->SetHiddenInGame(true);
SetActorEnableCollision(false);
SetLifeSpan(2.f);
}
void AGASItemBox::ApplyEffectToTarget(AActor* Target)
{
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target);
if (TargetASC)
{
FGameplayEffectContextHandle EffectContext = TargetASC->MakeEffectContext();
EffectContext.AddSourceObject(this);
FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass, 1, EffectContext);
if (EffectSpecHandle.IsValid())
{
TargetASC->BP_ApplyGameplayEffectSpecToSelf(EffectSpecHandle);
}
}
}
void AGASItemBox::InvokeGameplayCue(AActor* Target)
{
FGameplayCueParameters Param;
Param.SourceObject = this;
Param.Instigator = Target;
Param.Location = GetActorLocation();
ASC->ExecuteGameplayCue(GameplayCueTag, Param);
}
오버랩 이벤트가 발생하면 실행되는 NotifyActorBeginOverlap함수를 override 하여 GE와 GC를 실행하게 제작하였다.
이미 연 상자를 다시 여는 것은 어색하기 때문에 2초 뒤에 없어지게 하였다.
ApplyEffectToTarget은 오버랩한 액터에게 설정해 놓은 GE를 실행시키는 함수이다.
액터로부터 ASC를 받아온 뒤, FGameplayEffectSpectHandle을 만들어 GE를 실행시켜주면 된다.
BP로 간단하게 대미지를 주는 GE를 만들었다.
Gameplay Cues를 보면 GE가 실행될 때 실행되는 GC를 설정할 수 있다.
InvokeGameplayCue는 GC를 실행하는 함수이다.
Param을 설정한 뒤, ASC를 통해 미리 설정한 태그로 GC실행을 요청하면 된다.
GC는 BP를 통해 구현하였다.
간단하게 이미터를 생성하는 GC이다.
그렇다면 Heal을 주는 상자도 간단하게 만들 수 있다.
상자의 GE만 변경해주면 된다.
하지만, GE를 이렇게 설정하면 GE를 통해 CurrentValue만 변경되어 원하는 대로 동작하지 않는다.
따라서, BaseValue를 변경하게 설정해야 하는데 이는 Period를 설정해주면 된다.
이렇게 설정하면 2초 동안 0.25초 간격으로 Health를 2씩 더해준다.(BaseValue)
또한, 이를 통해 무적상태로 만들어주는 상자도 간단하게 만들 수 있다.
GE만 만들어서 상자에 설정해 주면 된다.
캐릭터를 무적상태로 만들기 위해서는 Character.State.Invinsible이라는 태그를 캐릭터에 부여해 주면 된다.
해당 태그가 부여되어 있으면 PreGameplayEffectExecute함수에서 대미지를 변경하여 피해를 입지 않게 만든다.
무적상태를 제거하기 위해서는 Target Tags옵션을 RemoveOtherGameplayEffectComponent로 변경한 뒤, OwningTagQuery에 해당 태그를 넣어주면 된다.
무기 상자
아이템 상자를 상속받아 플레이어가 무기를 획득할 수 있는 상자를 만들어 보자.
//.h
UCLASS()
class PROJECT_API AGASWeaponBox : public AGASItemBox
{
GENERATED_BODY()
protected:
virtual void NotifyActorBeginOverlap(AActor* Other) override;
protected:
UPROPERTY(EditAnywhere, Category = GAS, Meta=(Categories=Event))
FGameplayTag WeaponEventTag;
};
//.cpp
void AGASWeaponBox::NotifyActorBeginOverlap(AActor* Other)
{
Super::NotifyActorBeginOverlap(Other);
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Other, WeaponEventTag, FGameplayEventData());
}
무기상자에서는 태그를 통해 이벤트를 실행하는 SendGameplayEventToActor를 실행해 준다.
캐릭터에서는 이 이벤트를 수신하여 동작할 함수를 바인딩해 주어야 한다.
void AGASCharacterPlayer::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
AABGASPlayerState* GASPS = Cast<AABGASPlayerState>(GetPlayerState());
if (GASPS)
{
ASC = GASPS->GetAbilitySystemComponent();
ASC->InitAbilityActorInfo(GASPS, this);
ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONEQUIP).AddUObject(this, &AGASCharacterPlayer::EquipWeapon);
ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONUNEQUIP).AddUObject(this, &AGASCharacterPlayer::UnequipWeapon);
...
}
}
void AGASCharacterPlayer::EquipWeapon(const FGameplayEventData* EventData)
{
if (Weapon)
{
Weapon->SetSkeletalMesh(WeaponMesh);
const float CurrentAttackRange = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute());
const float CurrentAttackRate = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute());
ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange + WeaponRange);
ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate + WeaponAttackRate);
}
}
void AGASCharacterPlayer::UnequipWeapon(const FGameplayEventData* EventData)
{
if (Weapon)
{
Weapon->SetSkeletalMesh(nullptr);
const float CurrentAttackRange = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute());
const float CurrentAttackRate = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute());
ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange - WeaponRange);
ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate - WeaponAttackRate);
}
}
ASC를 통해 직접 Attribute의 값을 바꾸는 것은 바람직하지 않다.
GE를 통해 바꿔주는 것이 바람직 하지만, 간단한 예시이기 때문에 직접 값을 바꾸게 구현하였다.