언리얼/게임 프로젝트

GameplayAbilitySystem을 이용한 RPG 프로젝트 - (20) Gameplay Effect의 적용 / 삭제 정책을 본격적으로 사용하기

monstro 2024. 12. 14. 14:00
728x90
반응형

지난 포스트에 이어서 설정을 구성하는 작업을 다시 시작하겠습니다.

그 전에 먼저 Gameplay Effect가 작동하는 명세를 간단한 이미지로 표현하였습니다.

 

 

1) Instant GE

한 프레임동안 동작하는 Instant GE의 경우

BeginOverlap과 EndOverlap 이벤트에서 모두 적용합니다.

EndOverlap에서 해당 GE를 적용하는 이유는 Instant GE의 경우 적용되어도

한 프레임이 지난 후에 삭제되기 때문입니다.

 

2) Duration GE

지정한 Duration 동안 동작하는 Duration GE의 경우도 Instant GE와 비슷하게 동작합니다.

BeginOverlap과 EndOverlap 이벤트에서 모두 적용합니다.

마찬가지로 EndOverlap에서 해당 GE를 적용하는 이유는 Duration 이후에는 GE를 삭제하기 때문입니다.

 

3) Infinite GE

그런데 영원히 적용되는 Infinite GE의 경우는 조금 다르게 동작합니다.

BeginOverlap과 EndOverlap 이벤트에서 모두 적용하지만,

Overlap이 종료되는 EndOverlap 이벤트에서는 해당 GE를 삭제해야 합니다.

그래야 GE가 영구히 플레이어에게 적용되지 않기 때문입니다.

 

이제 기존의 AureEffectActor에서 위의 명세를 위한 로직을 설정하겠습니다.

 

1) Overlap 이벤트 발생시 GE를 Target에게 적용하는 AuraEffectActor

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameplayEffectTypes.h"
#include "AuraEffectActor.generated.h"

/*
* Forward Declaration
*/
class UGameplayEffect; 
class UAbilitySystemComponent;

// GE 적용을 위한 정책
UENUM(BlueprintType)
enum class EEffectApplicationPolicy : uint8
{
	ApplyOnOverlap,
	ApplyOnEndOverlap,
	DoNotApply
};

// GE 삭제를 위한 정책
UENUM(BlueprintType)
enum class EEffectRemovalPolicy : uint8
{
	RemoveOnEndOverlap,
	DoNotRemove
};

UCLASS()
class AURA_API AAuraEffectActor : public AActor
{
	GENERATED_BODY()
	
public:	
	AAuraEffectActor();


protected:
	virtual void BeginPlay() override;

	/*
	* Property Section
	*/
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	bool bDestroyOnEffectApplication = false;

	/*
	* Function Section
	*/
	UFUNCTION(BlueprintCallable)
	void OnOverlap(AActor* TargetActor);

	UFUNCTION(BlueprintCallable)
	void OnEndOverlap(AActor* TargetActor);

	/*
	* Instant GE Section
	*/
	UFUNCTION(BlueprintCallable)
	void ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass);

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	EEffectApplicationPolicy InstantEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;

	/*
	* Duration GE Section
	*/
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	EEffectApplicationPolicy DurationEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;

	/*
	* Infinite GE Section
	*/
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	TSubclassOf<UGameplayEffect> InfiniteGameplayEffectClass;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	EEffectApplicationPolicy InfiniteEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	EEffectRemovalPolicy InfiniteEffectRemovalPolicy = EEffectRemovalPolicy::RemoveOnEndOverlap;

	// 현재 동작중인 GameplayEffectHandle과 ASC를 매핑하는 Map
	TMap<FActiveGameplayEffectHandle, UAbilitySystemComponent*> ActiveEffectHandles;
};

 

구성은 위와 같습니다.

추가적으로 Instant GE / Duration GE / Infinite GE를 위한 섹션을 구분짓도록 하겠습니다.

다른 GE들과 다르게 Infinite GE는 GE 삭제를 위한 정책이 필요하므로 위와 같이 구성하였습니다.

 

프로퍼티에서 볼 수 있는 TMap은 Infinite GE를 삭제하는데 사용합니다.

Key값으로 현재 동작중인 GE의 Wrapper 클래스FActiveGameplayEffectHandle을 사용하고

Value값으로 AbilitySystemComponent를 사용합니다.

 

.
.
.

void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
	UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
	if (TargetASC)
	{
		check(GameplayEffectClass);
		FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
		EffectContextHandle.AddSourceObject(this);
		const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass, 1.0f, EffectContextHandle);
		// Target의 ASC를 가져오고 GE를 적용한다 
		// 이때 적용된 GE를 현재 동작하는 GE를 나타내는 Wrapper 클래스인 FActiveGameplayEffectHandle의 변수로 저장한다
		const FActiveGameplayEffectHandle ActiveEffectHandle = TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());

		// GameplayEffectSpecHandle로부터 GameplayEffectSpec을 가져오고 GameplayEffectSpec으로부터 GameplayEffect를 가져옴
		// 가져온 GameplayEffect의 Duration이 Inifinite인지 확인
		const bool bIsInfinite = EffectSpecHandle.Data.Get()->Def.Get()->DurationPolicy == EGameplayEffectDurationType::Infinite;
		if (bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
		{	
			// Duration이 Inifinite이고, GE의 종료 정책이 EndOverlap으로 설정되었다면
			// 해당 GE를 맵에 저장한다
			ActiveEffectHandles.Add(ActiveEffectHandle, TargetASC);
		}
	}
}

void AAuraEffectActor::OnOverlap(AActor* TargetActor)
{
	// Instant, Duration, Infinite GE의 적용 정책이 ApplyOnOverlap이라면 GE를 적용한다
	if (InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
	{
		ApplyEffectToTarget(TargetActor, InstantGameplayEffectClass);
	}
	if (DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
	{
		ApplyEffectToTarget(TargetActor, DurationGameplayEffectClass);
	}
	if (InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
	{
		ApplyEffectToTarget(TargetActor, InfiniteGameplayEffectClass);
	}
}

void AAuraEffectActor::OnEndOverlap(AActor* TargetActor)
{
	// Instant, Duration, Infinite GE에 대한 적용 정책이 ApplyEndOverlap이라면 GE를 적용한다
	if (InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
	{
		ApplyEffectToTarget(TargetActor, InstantGameplayEffectClass);
	}
	if (DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
	{
		ApplyEffectToTarget(TargetActor, DurationGameplayEffectClass);
	}
	if (InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
	{
		ApplyEffectToTarget(TargetActor, InfiniteGameplayEffectClass);
	}
	// 이때 Infinite GE의 종료 정책이 RemoveOnEndOverlap이라면 다음의 로직을 수행한다
	if (InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
	{
		// 우선 GE의 적용 대상의 ASC를 가져온다
		UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
		if (TargetASC)
		{
			// 대상의 ASC에서 삭제할 GE 핸들을 저장할 리스트를 만든다 
			TArray<FActiveGameplayEffectHandle> HandlesToRemove;
			// GameplayEffectHandle과 ASC를 매핑하는 Map을 튜플로 순회하면서
			for (TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*> HandlePair : ActiveEffectHandles)
			{
				// 대상의 ASC가 튜플의 값 즉, GE의 현재 적용대상의 ASC와 동일하다면
				if (TargetASC == HandlePair.Value)
				{
					// 대상의 ASC로부터 현재 적용되는 GE를 삭제하고 Stack의 Count를 1 감소시킨다
					TargetASC->RemoveActiveGameplayEffect(HandlePair.Key, 1);
					// 삭제할 GE 핸들을 저장할 리스트안에 삭제된 GE를 저장한다
					HandlesToRemove.Add(HandlePair.Key);
				}
			}
			// 순회와 삭제가 동시에 일어나면 null 크래시가 발생하므로
			// 순회 로직과 삭제 로직을 분리하였다
			for (FActiveGameplayEffectHandle& Handle : HandlesToRemove)
			{
				// 삭제할 GE 핸들을 저장한 리스트를 순화하면서 해당 핸들을 삭제한다
				ActiveEffectHandles.FindAndRemoveChecked(Handle);
			}
		}
	}
}

 

기존의 GE를 적용하는 함수인 ApplyEffectToTarget 함수에서는 

Instant GE나 Duration GE그대로 적용하지만,

Infinite GE의 경우 적용하지만, 이전에 정의한 TMap에 보관하도록 합니다.

 

Overlap 이벤트시에 발생할 OnOverlap 함수에서는

GE의 적용 정책에 따라 ApplyEffectToTarget 함수를 호출해 GE를 적용합니다.

 

Overlap 이벤트가 종료되면 발생항 OnEndOverlap 함수에서는

GE의 종료 정책에 따라 ApplyEffectToTarget 함수를 호출해 GE를 적용합니다.

그리고 Infinite GE의 경우 다른 GE들과는 다르게

종료하기 위한 로직을 따로 수행하도록 합니다.

 

기존에 ApplyEffectToTarget 함수에서 채워넣은 TMap을 순회하면서

GE 적용 대상의 ASC에서 TMap에 저장된 적용중인 GE를 삭제하고 Stack의 Count를 1감소시킵니다.

 

TMap을 순회하면서 TMap에 저장된 GE를 Target의 ASC로부터 삭제한 후,

TMap에서 TMap에 저장된 GE를 삭제해야 합니다.

순회와 삭제가 동시에 일어나면 null 크래시가 발생하므로

순회 로직과 삭제 로직을 분리하여 수행합니다.

 

이제 언리얼 에디터에서 작업하도록 하겠습니다.

2) 언리얼 에디터

적용하고자 하는 Infinite GE를 만들겠습니다.

 

한번에 GE를 최대 3개씩 적용하게끔 설정하였습니다.

1초 간격으로 Health를 1씩 감소시키도록 설정하였습니다.

 

 

블루 프린트의 Event Graph에서는 위와 같이 동작하도록 하였습니다.

 

 

Applied Effects의 설정은 위와 같이 구성하였습니다.

이제 동작이 되는지 확인을 해보겠습니다.

 

 

위와 같이 잘 적용되는 것을 볼 수 있습니다.

728x90
반응형