5. Gameplay Effects

groot616·2024년 8월 12일

5. Gameplay Effects

목차

  1. Gameplay Effects의 특징
  2. AuraEffectActor 클래스 수정 및 GameplayEffect 적용
  3. CurveTable 생성 및 적용

5.1 Gameplay Effects의 특징

Gameplay EffectUGameplayEffect 타입의 오브젝트이다.
Gameplay EffectAttributeGameplay Tag 를 변경하기 위해 사용된다.
Gameplay Effect 는 데이터 중심 설계이다(로직을 추가하지 않음).
Gameplay Effect 에 기반한 블루프린트를 생성하고, 이를 하위 클래스로 세분화하지 않음.(BP_GameplayEffect의 자식 클래스로 세분화하지 않는다는 의미)
Gameplay EffectAttributeModifiersExecutions 로 변경함

  • Modifier의 operation 종류
    • Add
    • Multiply
    • Divide
    • Override(Attribute 값을 제공된 크기의 값으로 대체)

이러한 연산에서 사용되는 크기(Magnitude)는 Magnitude Calculation 을 통해 생성되는데, 이때 여러가지 타입이 존재한다.

  • Magnitude Calculation Type
    • Scalable Float
      간단한 수정 유형으로써, 크기를 직접 하드코딩된 값으로 지정할 수도 있고, Gameplay Effect 의 레벨(캐릭터 레벨에 따른 hp 증가 등등)에 따라 크기를 조정하는 테이블을 사용할 수도 있음
    • Attribute Based
      다른 Attribute 값을 사용함(체력에 힘과 동일한 값 추가 가능, 플레이어 체력에 힘의 10배와 같은 값을 추가)
    • Custom Calculation Class (MMC)
      Attribute 나 다른 값들과 같은 것들을 캡쳐하도록 디자인된 클래스를 생성하여 임의로 복잡한 계산에 사용할 수 있음
      이러한 클래스들을 보통 Modifier Magnitude Calculation ( MMC ) 라고 부르는데 이러한 custom calculation을 기반으로 단일 Attribute 를 변경하는 강력한 방법
    • Set by Caller
      key-value 쌍으로 이루어져 있고, Gameplay Tag 나 이름에 연관된 값을 할당함
      (name은 데이터베이스에서의 그 name이라고 이해하면 될 것 같음)
      Gameplay Effect 를 생성하거나 적용할 때, 코드 로직에 따라 ModifierMagnitude 를 설정하는데 유용함
  • Execution
    Gameplay Effect 에서 Attribute 값을 변경하기 위한 더욱 강력한 방법은 Gameplay Effect Execution Calculation 이라는 Custom Execution을 사용하는 것
    이를 통해 하나 이상의 Attribute 를 변경시킬 수 있고, 코드로 선택한 모든 다른 작업도 수행 가능함

Gameplay EffectDuration Policy 를 가진다.

  • Instance
    즉시 효과가 적용될 수 있고, Modifier 가 일회성으로 즉시 적용시킴
  • Duration
    일정 시간동안 Attribute 를 수정할 수 있는 지속시간을 가지고 있을 수 있음(장판기)
  • Infinite
    Attribute 를 수정할 수 있는 지속시간이 무한일 수도 있음(사라지지 않는 디버프)
    제거 가능하긴 하지만 수동으로 제거할 때 까지는 남아 있음
    Gameplay Effect 는 여러개 겹칠 수 있음.(여러개의 버프 혹은 디버프)
    Gameplay EffectGameplay Tag 를 추가할 수 있음
    Gameplay Effect 는 능력을 부여할 수도 있음
    Gameplay Effect 는 직접적으로 적용할 수도 있지만, 보통 좀 더 가벼운 버전인 Gameplay Effect Spec 를 생성하여 사용함
    • Spec 은 GAS에서 일반적이고 최적화의 한 형태
    • Spec 에는 수정을 수행하는데 필요한 최소한의 정보가 포함되어 있음
    • Spec 은 직접적으로 객체화(인스턴스화)되지 않고, 필요할 때마다 효과를 생성하고 적용할 수 있는 기본객체(CDO)가 사용됨

대략적인 요약
Gameplay Effect 클래스는 자식 클래스로 세분화할 필요가 없을 정도로 유연함
복잡한 Attribute 수정이 필요한 경우, Custom Execution 또는 Modify Calcuation 을 사용함
또한 Gameplay Tag 와 함께 정보를 전달하며, 적용되는 효과에 대한 추가적인 정보를 저장할 수 있음

5.2 AuraEffectActor 클래스 수정 및 GameplayEffect 적용

해당 클래스는 메시와 오버랩을 위한 구체를 생성하고, 오버랩시 실행될 기능들을 구현해 둔 클래스이다.
메시나 오버랩 스피어는 개발자가 아닌 디자이너에 의해 수정이 필요할 수 있으므로 해당 부분은 블루프린트로 옮긴다.

  • AuraEffectActor.h
// AuraEffectActor.h

// class USphereComponent; 필요없으므로 삭제
...

public:
	AAuraEffectActor();
    
    // OnOverlap, EndOverlap 함수 삭제
    
protected:
	...
    
private:
	// mesh, sphere 관련 삭제
    
  • AuraEffectActor.cpp
// AuraEffectActor.cpp

#include "Actor/AuraEffectActor.h"
// 나머지 필요없는 헤더파일 전부 삭제

AAuraEffectActor::AAuraEffectActor()
{
	PrimaryActorTick.bCanEverTick = false;
    
    // mesh, sphere 관련 삭제
    SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneRoot"));
}

AAuraEffectActor::BeginPlay()
{
	Super::BeginPlay();
    
    // Sphere관련 델리게이트 삭제
}

// OnOverlap, EndOverlap 함수 구현부 삭제

컴파일 후 에디터로 돌아와 월드에 배치해둔 포션을 삭제하고, BP_HealthPotion 파일을 연 다음 스태틱 메시를 다시 추가해주고, 스케일을 조절해준다.

또한 Collision PresetNoCollision 으로 변경하여 스태틱 메시와의 충돌이 발생하지 않도록 한다.

동일하게 오버랩을 위한 Sphere Collision 을 추가해준다.

Sphere Collision 의 경우, 기본적으로 OverlapAllDynamic 으로 설정되어 있으므로 그대로 냅둔다.

Event Graph 탭으로 넘어가서 On Component Begin Overlap(Sphere) 노드를 생성하고 오버랩 발생시 관련된 기능을 실행할 수 있도록 한다.

이제 오버랩 발생시 실행될 기능을 만들어야 한다.
다시 AuraEffectActor 클래스로 넘어와서 Gameplay Effect 추가를 위한 코드를 작성한다

  • AuraEffectActor.h
// AuraEffectActor.h

class UGameplayEffect;

protected:
	...
    
    // 파라미터로 타겟과 GameplayEffect를 받음으로써 해당 타겟에게 변화를 적용시킬 예정
    UFUNCTION(BlueprintCallable)
    void ApplyEffectToTarget(AActor* Target, TSubclassOf<UGameplayEffect> GameplayEffectClass);
    
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;
  • AuraEffectActor.cpp
// AuraEffectActor.cpp

...
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
...

void AAuraEffectActor::ApplyEffectToTarget(AActor* Target, TSubclassOf<UGameplayEffect>GameplayEffectClass)
{
	UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target);
    if(TargetASC == nullptr) return;
    
    check(GameplayEffectClass);
    // FGameplayEffectContextHandle 타입의 구조체를 생성
    FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
    // 어느 object가 이 effect를 발생시키는지 지정, this로 인해 AuraEffectActor가 effect 발생
    EffectContextHandle.AddSourceObject(this);
    // 생성한 구조체와 GameplayEffectClass를 토대로 GameplayEffectSpec 생성
    const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass, 1.f, EffectContextHandle);
    // TargetASC에게 effect 적용
    TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
}

5.2.1 즉시 적용되는 GameplayEffect 생성

ApplyEffectToTargetBlueprintCallable 로 설정해뒀으므로 에디터에서 함수 호출이 가능하다.
에디터로 돌아와서 BP_HealthPotion 을 열고 ApplyEffectToTarget 노드를 생성한다.

GameplayEffectClass 를 적용시키기 위해 GameplayEffect 기반의 블루프린트 GE_PotionHeal 을 생성한다.

생성 전에 정리를 위해 BP_HealthPotionPotion 폴더 새로 생성후 해당 폴더로 move

GE_PotionHeal 파일을 열어보면 Duration 탭이 있는데, effect의 기간을 설정해준다.

  • Instant
    즉시
  • Infinite
    계속
  • Has Duration
    기간 있음

힐은 즉시 이루어져야하므로 Instant 로 설정해준다.

Gameplay Effect 탭을 보면 Modifiers 가 있다.
해당 탭을 통해 Attribute 를 변경시키게 된다.
+ 키를 눌러 추가해준 뒤 AuraAttributeSet.Health 를 선택해주고

체력을 더해야 하므로 Add 를 선택해준다.

Modifier Magnitude 탭에서 float 타입으로 조정하도록 Scalable Float 를 선택하고 25만큼 회복하도록 추가한다.

다시 BP_HealthPotion 으로 돌아와 Instant Gameplay Effect Class 노드를 추가시켜 준뒤 만들어둔 GE_PotionHeal 을 적용시켜주고

오버랩시 effect가 적용되고, 액터가 파괴되도록 노드를 구상해준다.

컴파일 후 실행하면 정상적으로 GameEffect 가 적용되는 것을 확인할 수 있다.

플레이 버튼 우측에 있는 점 세개를 클릭하고, Number of Players : 2 , Net Mode : Play As Listen Server 로 설정해 준 다음 다른 플레이어로 포션 획득시에도 정상적으로 작동하는 것을 확인할 수 있다.

마나포션도 동일하게 구현할 수 있다. 먼저 BP_ManaPotion 을 생성한다.

GE_PotionMana 도 생성해주고

동일하게 값들을 적용시켜주고

BP_ManaPotion 의 메시와 Sphere Collision 을 추가해주고

스케일 조절과 메시를 적용시킨다.

메테리얼을 마나처럼 보이도록 다른 메테리얼을 적용시키고

Instant Gameplay Effect Class 탭에서 생성해둔 GE_PotionMana 를 적용시킨다.

Sphere Collision 의 스케일도 조절해준다.

동일하게 메시의 콜리전을 NoCollision 으로 수정해주고

BP_HealthPotion 과 동일하게 노드를 구성해준다.

제대로 적용되는지 확인하기 위해 마나의 초기값을 변경한 후

  • AuraAttributeSet.cpp
// AuraAttributeSet. cpp

...

UAuraAttributeSet::UAuraAttributeSet()
{
	...
	InitMana(10.f);
	InitMaxMana(50.f);
}

컴파일 후 실행하면 정상적으로 동작하는 것을 확인할 수 있다.

추가
혹시 클라이언트측에서의 마나 회복이 안될 경우
아래 코드 확인 필요

  • AuraAttributeSet.cpp
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // 변수가 복제되는 것을 허용하고, 특정 조건에 따라 복제되도록 지정함
    // Health가 네트워크에서 복제되고, OnRep_Health()가 Health 변수의 변경을 감지할 때 호출됨
    // COND_None에 의해 특정 조건 없이 복제되도록 설정하고, REPNOTIFY_Always를 통해 상시 REPNOTIFY 함수를 호출하도록 함
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
    /** 코드 작성 확인 필요 */
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Mana, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, MaxMana, COND_None, REPNOTIFY_Always);
    /** 코드 작성 확인 필요 */
}

추가
ApplyEffectToTarget()` 함수 블루프린트로도 구현 가능함
본 정리에서는 코드 그대로 사용

5.2.2 Duration이 존재하는 GameplayEffect 생성

먼저 GameplayEffect 기반 블루프린트를 하나 생성한다.

effect 적용에 기간을 가지도록 Has Duration 으로 변경하고 기간을 2초로 설정한다.

이번에는 MaxHealth 가 증가되도록 옵션을 선택하고, 증가치를 100으로 정한다.

해당 GameEffect를 적용하기 위해 AuraEffectActor 클래스에서 새로운 변수를 하나 생성한다.

  • AuraEffectActor.h
// AuraEffectActor.h

protected:
	...
    
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
    TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;

에디터로 돌아와서, 생성한 GameplayEffect 를 적용시킬 액터를 하나 생성한다.

BP_HealthCrystal 파일을 열고 메시와 Capsule Collision 을 추가해주고

메시의 할당과 스케일 조절을 한 다음

메시의 콜리전을 NoCollision 으로 변경하고

Capsule Collision 의 크기 변경과

메시의 위치를 캡슐에 맞게 조절해주고

GE_CrystalHealthEvent Graph 에서 사용할 수 있도록 적용시켜준다.

Event Graph 탭에서 노드를 구성한 뒤 컴파일하고

월드에 BP_CrystalHeath 를 배치한 후 실행시키면 정상적으로 동작하는 것을 확인할 수 있다.

Duration 으로 만들어둔 것이므로 2초뒤에 MaxHealth 는 다시 원래대로 100으로 돌아온다.

5.2.3 주기적인 GameplayEffect 생성(5.2.2에 포함)

Duration 밑에 있는 Period 값을 수정하여 주기적인 GameplayEffect를 적용시킬 수 있다.
Execute Periodic Effect on Application 을 활성화하면 주기적인 GameplayEffect 를 즉시 적용시킬 수 있다.
비활성화시 Period 에 입력된 수치(그림에서는 1)만큼 대기시간이 지난 후에 GameplayEffect 가 적용된다.

먼저 GE_CrystalHeal 파일을 열고 Period 를 1로 지정한 후

MaxHealth 가 아닌 Health 에 적용되도록 변경하고, 수치도 10으로 변경한다.

컴파일 후 실행하면 Execute Periodic Effect on Applicaiton 이 활성화 되어있으므로 즉시 10회복하고, 1초라는 주기에 따라 10씩, 총 2초동안 회복되기 때문에 30이 회복한 80이 되는 것을 확인할 수 있다.

주기를 0.1로 변경하고 회복량을 1로 변경한다면 좀더 촘촘하게 차오르는 체력을 확인할 수 있다.
마나도 동일하게 구현한다.
먼저 GE_CrystalMana 를 생성하고 값들을 수정한다.

BP_ManaCrystal 을 생성하고, 메시와 Sphere Collision 추가 및 콜리전 수정과 함께 Event Graph 에서 노드를 추가한다.

실행하면 정상적으로 동작하는 것을 확인할 수 있다.

5.2.4 Stacking을 이용한 GameplayEffect 순차적 실행(5.2.2에 포함)

스택을 통해 여러개의 GamepalyEffect 가 적용될 경우 순차적으로 실행되도록 할 수 있다.
예를 들어 위에서 만든 마나크리스탈을 3개 한번에 먹으면 마나가 급격하게 차오르는데 이런 상황을 방지할 수 있다.
Stacking Type 의 옵션은 3가지가 있는데

  • None
    스택 없음
  • Aggregate by Source
    스택 한도가 Source인 GameplayEffect 에 적용
    A라는 GameplayEffect 스택 한도가 2인 경우에, source에 스택이 생성되고, 캐릭터가 2개를 초과한 GameplayEffect 효과를 받으려고 하면 무시
    B라는 GameplayEffect 는 A와 관련없으므로 따로 스택 계산
  • Aggregate by Target
    스택 한도가 Target인 캐릭터에 적용
    캐릭터의 스택한도가 2인 경우, A 라는 GameplayEffect 2개의 효과를 받을 경우 B 라는 GameplayEffect 는 한도에 따라 캐릭터가 효과를 받지 못함

GE_CrystalMana 를 수정한다.
(변경점 : Duration 3->1, Stacking : Aggregate by Source(Stack:2), Execute Periodic Effect on Applicaiton 활성화->비활성화)

컴파일후 월드에 마나 크리스탈을 2개 더 배치후 실행하면 최대스택인 2만큼 회복되서 20만 회복되는 것을 확인할 수 있다.

추가

  • Stacking -> Stack Duration Refresh Policy 에서 기존의 Refresh on Successful ApplicationNever Refresh 로 변경하면 Duration의 갱신이 이뤄지지 않음
    ex) 첫번째 크리스탈을 먹고 0.5초뒤에 크리스탈을 먹으면 Duration이 갱신되지 않으므로 15만큼 회복됨
  • Stacking -> Stack Period Reset Policy 에서 기존의 Reset on Successful ApplicationNever Reset 으로 변경하면 Period의 갱신이 이뤄지지 않음
  • Stacking -> Stack Expiration Policy
    • Clear Entire Stack
      GameplayEffect 가 끝나면 모든 스택 초기화
      ex) 두개를 한번에 먹은 상태 + Duration이 끝나지 않은 상태에서 하나 더 먹으면 해당 효과는 무시되므로 마나는 20만 차게 됨
    • Remove Single Stack and Refresh Duration
      Duration이 끝나면 하나의 스택만 삭제하고 새로운 Duration 실행
      ex) 두개를 한번에 먹은 상태 + 하나의 duration이 끝난 상태에서 하나 더 먹을 경우 스택이 추가되므로 마나는 20보다 더 차게 됨(10 + 5 차는 도중 하나 더먹으면 스택 추가되서 +10 = 25)
    • Refresh Duration
      스택이 만료될 때 duration을 갱신하여 스택이 유지되도록 함
      ex) 하나를 먹으면 스택이 생성됨, 10회복후 스택이 만료되지만 선택한 옵션때문에 duration이 갱신되어 다시 회복을 하며 스택이 생성됨

게임에서 적용시킬 옵션은 스택을 1로 변경하고, GameplayEffect 가 끝나면 stack을 초기화 하도록 하는 옵션을 적용시킨다.

BP_CrystalHealth 도 동일하게 수정해준다.

5.2.5 무한히 적용되는 GameplayEffect 생성

위와 동일하게 GameEffect를 적용하기 위해 AuraEffectActor 클래스에 블루프린트에서 접근 가능한 변수 하나를 생성한다.

  • AuraEffectActor.h
// AuraEffectActor.h

protected:
	...
    
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
    TSubclassOf<UGameplayEffect> InfiniteGameplayEffectClass;

추가
블루프린트 정리

GameplayEffect 를 작동하게 하는 AuraEffectActor 기반 블루프린트를 생성한다.

BP_FireArea 파일을 열고 Niagara Particle System ComponentBox Collision 을 생성하고

에셋을 적용시킨다.

이제 적용시킬 GameEffect를 만들기 위해 GameplayEffect 기반 블루프린트 GE_FireArea 를 생성한다.

Duration PolicyInfinte 로 변경하고

주기를 1초로 지정해준 뒤

체력이 5씩 줄어들도록 Modifires 를 설정해준다.

다시 BP_FireArea 로 돌아와서 Infinite Gameplay Effect Class 를 방금 생성한 GE_FireArea 로 설정해주고

Event Graph 에서 노드를 추가해준다.

컴파일 후 실행하면
체력이 초당 5씩 줄어드는 것을 확인할 수 있다.
여기서 문제점이 하나 발생하는데, Box Collision 을 벗어나도 체력이 마이너스로 계속 줄어드는 문제가 생기는 것을 확인할 수 있다.

즉 해당 영역을 벗어나면 그 영역에서 적용되던 GameplayEffect 를 제거해야 한다.

  • AuraEffectActor.h
// AuraEffectActor.h

...
#include "GameplayEffectTypes.h"
...

class UAbilitySystemComponent;

// 적용 시점 지정
UENUM(BlueprintType)
enum class EEffectApplicationPolicy
{
	// 오버랩시 GameplayEffect 적용
	ApplyOnOverlap,
    
    // 오버랩 종료시 GameplayEffect 적용
    ApplyOnEndOverlap,
    
    // GameplayEffet 미적용
    DoNotApply
};

// GameplayEffect 종료 Policy
UENUM(BlueprintType)
enum class EEffectRemovalPolicy
{
	// 오버랩 종료시 GameplayEffect 종료
	RemoveOnEndOverlap,
    
    // 종료 미적용
    DoNotRemove
};

...

protected:
	...
    
    /** 코드 추가 */
    // 오버랩시 GameplayEffect Policy에 따라 GameplayEffectd 적용
    UFUNCTION(BlueprintCallable)
    void OnOverlap(AActor* TargetActor);
    
    // 오버랩 종료시 GameplayEffect Policy에 따라 GameplayEffect 종료
    UFUNCTION(BlueprintCallable)
    void OnEndOverlap(AActor* TargetActor);
    /** 코드 추가 */
    
    ...
    
    /** 코드 추가 */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
    bool bDestroyOnEffectRemoval = false;
    /** 코드 추가 */
    
    // 적용시킬 GameplayEffect 클래스, GameplayEffect 기반 블루프린트 생성후 적용예정
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;
	
    /** 코드 추가 */
    // InstantEffect 적용 policy, 기본값 : 미적용
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	EEffectApplicationPolicy InstantEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
	/** 코드 추가 */
    
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;
	
    /** 코드 추가 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	EEffectApplicationPolicy DurationEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
	/** 코드 추가 */

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	TSubclassOf<UGameplayEffect> InfiteGameplayEffectClass;
    
    /** 코드 추가 */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	EEffectApplicationPolicy InfiniteEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
    
    // InfiniteEffect Remove 적용 policy, 오버랩 끝날때로 적용
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
    EEffectRemovalPolicy InfiniteEffectRemovalPolicy = EEffectRemovalPolicy::RemoveOnEndOverlap;
    
    // 현재 적용중인 GameplayEffect와 적용대상의 ASC map
    TMap<FActiveGameplayEffectHandle, UAbilitySystemComponent*> ActiveEffectHandles;
    /** 코드 추가 */
  • AuraEffectActor.cpp
// AuraEffectActor.cpp

...

void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
	...
    // 기존 TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get()); 수정
    const FActiveGameplayEffectHandle ActiveEffectHandle = TargetASC->ApplyGameplayEffecctSpecToSelf(*EffectSpecHandle.Data.Get());
    /*
    * EffectSpecHandle.Data.Get() = GameplayEffectSpec
    * EffectSpecHandle.Data.Get()->Def.Get() = GameplayEffectSpec->GameplayEffect
    */ 
    // GameplayEffect 가 infinite인지 확인하는 변수
    const bool bIsInfinite = EffectSpecHandle.Data.Get()->Def.Get()->DurationPolicy == EGameplayEffectDurationType::Infinite;
    // GameplayEffect가 infinite이고, 무한으로 적용되는 GameplayEffect의 삭제 policy가 오버랩이 종료되는 시점일 경우
    if(bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
    {
    	// map에 적용중인 ActiveEffectHandle과 TargetASC 추가
    	ActiveEffectHandles.Add(ActiveEffectHandle, TargetASC);
    }
}
    
void AAuraEffectActor::OnOverlap(AActor* TargetActor)
{
	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)
{
	if(InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
    {
    	ApplyEffectToTarget(TargetActor, InstantGameplayEffectClass);
    }
    
    if(DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
    {
    	ApplyEffectToTarget(TargetActor, DurationGameplayEffectClass);
    }
    
    if(InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
    {
    	ApplyEffectToTarget(TargetActor, InfiniteGameplayEffectClass);
    }
    
    if(InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
    {
    	UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
        if(!IsValid(TargetASC)) return;
        
        // 반복문 도중에 remove 발생시 크래시를 유발할 수 있으므로 배열로 모아두는 작업을 하기위함
        TArray<FActiveGameplayEffectHandle> HandlesToRemove;
        // 모든 ActiveEffectHandles를 순회
        for(TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*> HandlePair : ActiveEffectHandles)
        {
        	
            // 동일하다면 제거하고, 제거할 항목을 목록에 HandlesToRemove에 추가
        	if(TargetASC == HandlePair.Value)
            {
            	TargetASC->RemoveActiveGameplayEffect(HandlePair.Key);
                HandlesToRemove.Add(HandlePair.Key);
            }
        }
        // HandlesToRemove를 순회
        for(FActiveGameplayEffectHandle& Handle: HandlesToRemove)
        {
        	// HandlesToRemove 내의 항목들 제거
        	ActiveEffectHandles.FindAndRemoveChecked(Handle);
        }
    }
}

에디터로 돌아와서 BP_FireArea 에서 Infinite Effect Application Policy : Apply on Overlap 으로 설정한다.

기존의 노드를 수정해서 Box Collision 에 오버랩시, 오버랩 종료시 코드에서 구현해둔 함수를 호출하도록 변경한다.
(기존의 Apply Effect to Target 노드 삭제)

컴파일 후 실행하면 정상적으로 hp가 줄어들고, 범위를 벗어나면 hp가 줄어들지 않는 것을 확인할 수 있다.

여기에도 동일하게 스택을 적용시킬 수 있다.
GE_FireArea 에서 아래와 같이 스택 설정을 해준다.

실행시 겹친 횟수에 따라 -5, -10, -15씩 체력이 줄어드는 것을 확인할 수 있다.
여기서도 문제가 생기는데 3스택 도트데미지를 받는 위치에서 2스택 또는 1스택의 도트데미지를 받는 위치로 이동하면 더이상 도트데미지가 발생하지 않는 문제가 생긴다.
원인은 코드상에서 for 반복문을 통해 모든 스택을 제거해버리기 때문이다.

아래의 코드 부분
for (TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*> HandlePair : ActiveEffectHandles)
{
	// 동일하다면 제거하고, 제거할 항목을 목록에 HandlesToRemove에 추가
	if (TargetASC == HandlePair.Value)
	{
		TargetASC->RemoveActiveGameplayEffect(HandlePair.Key);
		HandlesToRemove.Add(HandlePair.Key);
	}
}

해결방법은 간단한데
TargetASC->RemoveActiveGameplayEffect(HandlePair.Key , 1); 로 코드를 수정해주면된다.
두번째 파라미터는 스택을 제거하는 것과 관련있는데 -1일 경우 모든 스택을 제거하고, 1일 경우 스택을 하나씩 제거하는 것을 의미한다.
컴파일후 실행하면 범위를 벗어나도 도트데미지가 멈추지 않고 겹친만큼 계속 피해를 받도록 바뀐 것을 확인할 수 있다.

추가
BP_FireArea 수정
Niagara 추가 및 Box Collision 사이즈 확대

5.2.6 PreAtributeChange()를 이용한 도트뎀 마이너스체력 오류 해결

PreAttributechange() 에 대한 설명

  • 변경 전에 CurrentValue 에 대한 변경 사항을 반영함
  • Attributes 의 변경에 의해 트리거됨
    • Attribute Accessors 에 의한 변경
    • GameplayEffect 에 의한 변경
  • 이 함수는 modifier 를 영구적으로 변경하지 않으며, 수정자를 쿼리할 때 반환되는 값을 변경함
  • 이후 작업에서는 모든 Modifier 로부터 현재 값을 다시 계산함
    • 해당 과정에서 다시 clamp 작업이 요구됨

PreAttributeChange() 함수를 통해 체력의 새로운 값이 0보다 작을 경우 0으로 조정하여 음수가 되는 것을 방지할 수 있다.
아래는 간단한 예시이다.

  • AuraAttributeSet.h
// AuraAttributeSet.h
/** 예시 **/

public:
	...
    
    // Attribute 최대 증감치 설정
    // 참조를 파라미터로 받으므로 값을 변경시킬 수 있음
    // 이를 통해 체력이 -로 넘어가게 되면 0으로 조정
    virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
    
    ...
  • AuraAttributeSet.cpp
/** 예시 **/
// AuraAttributeSet.cpp

...

void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	Super::PreAttributeChange(Attribute, NewValue);
    
    if(Attribute == GetHealthAttribute())
    {
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
        UE_LOG(LogTemp, Warning, TEXT("Health: %f"), NewValue);
    }
    
    if(Attribute == GetMaxHealthAttribute())
    {
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
        UE_LOG(LogTemp, Warning, TEXT("MaxHealth: %f"), NewValue);
    }
    
    if(Attribute == GetManaAttribute())
    {
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
        UE_LOG(LogTemp, Warning, TEXT("Mana: %f"), NewValue);
    }
    
    if(Attribute == GetMaxManaAttribute())
    {
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
        UE_LOG(LogTemp, Warning, TEXT("MaxMana: %f"), NewValue);
    }
}

컴파일 후 실행하면 아래와 같이 hp가 0 미만으로 떨어지지 않게 된다.

위의 코드와 같은 방식을 통해 최대 체력과 마나가 최대치를 넘기거나, 0미만으로 감소하는 것을 방지할 수 있다.
이제 코드를 작성한다.
(바로 위의 AuraAttributeSet.hoverridePreAttributeChange(...) 필요)

  • AuraAttributeSet.cpp
// AuraAttributeSet.cpp
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	Super::PreAttributeChange(Attribute, NewValue);
    
    if(Attribute == GetHealthAttribute())
    {
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
    }
     
    if(Attribute == GetManaAttribute())
    {
     	NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
    }
}

5.2.7 PostGameplayEffectExecute() 를 이용한 도트뎀 마이너스체력 오류 해결

앞에서 사용한 PreAttributeChange() 함수의 경우 Modifier를 영구적으로 변경하는 것이 아닌, Modifier를 조회할 때 참조되는 값을 변경하는 형태이다.
나중에 수행되는 작업은 모든 Modifier를 다시 계산하여 현재 값을 산출하는데, 이러면 값에 대해 다시 Clamp를 통해 제한을 걸어주어야 한다.
그래서 사용되는 함수가 PostGameplayEffectExecute() 함수이다.
PostGameplayEffectExecute() 함수는 PreAttributeChange() 함수와 다르게 GameplayEffect 효과가 적용된 후에 후에 호출된다.
먼저 Source와 Target의 프로퍼티에 접근할 수 있도록 코드를 작성해야 한다.
(Source = GameplayEffect 효과 제공자, Target, GameplayEffect 효과를 받는 대상)

  • AuraAttributeSet.h
// AuraAttributeSet.h

public:
	...
	virtual void PreAttributeChange(...) const override;
	virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;

	...
  • AuraAttributeSet.cpp
// AuraAttributeSet.cpp

...
#include "GameplayEffectExtension.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "GameFramework/Character.h" // VS에서 밑줄오류표시 안생기는데 오류같음. 헤더파일 포함 필수!!
...

void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModeCallback& Data)
{
	Super::PostGameplayEffectExecute(Data);
    
    // 로그 확인용
    //// Data를 통해 어느 Attribute가 변경되었는지 확인
    //if(Data.EvaluatedData.Attribute == GetHealth())
    //{
    //	// 현재 체력 로그표시
    //	  UE_LOG(LogTemp, Warning, TEXT("Health from GetHealth(): %f"), GetHealth());
    //    // 감소되는 크기 로그표시
    //    UE_LOG(LogTemp, Warning, TEXT("Magnitude: %f"), Data.EvaluatedData.Magnitude);    
    //}
    
    // SourceASC 접근을 위한 코드
    const FGameplayEffectContextHandle EffectContextHandle = Data.EffectSpec.GetContext();
    const UAbilitySystemComponent* SourceASC = EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
    
    // AvatarActor 유효성 검사
    if(IsValid(SourceASC) && SourceASC->AbilityActorInfo.IsValid() && SourceASC->AbilityActorInfo->AvatarActor.IsValid())
    {
    	// AvatarActor와 Controller 변수화
    	AActor* SourceAvatarActor = SourceASC->AbilityActorInfo->AvatarActor.Get();
        const AController* SourceController = SourceASC->AbilityActorInfo->PlayerController.Get();
        
        // SourceController가 null일 경우 캐스팅을 통해 할당
        if(SourceController == nullptr && SourceAvatarActor != nullptr)
        {
        	if(const APawn* Pawn = Cast<APawn>(SourceAvatarActor))
        	{
            	SourceController = Pawn->GetController();
        	}
        }
        if(SourceController)
        {
        	ACharacter* SourceCharacter = Cast<ACharacter>(SourceController->GetPawn());
        }
    }
    
    // TargetActor 접근을 위한 코드
    if(Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
    {
    	AActor* TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
		AController* TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
        ACharacter* TargetCharacter = Cast<ACharacter>(TargetAvatarActor);
        UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetAvatarActor);
    }
}

프로퍼티 접근을 위한 변수들을 구조체를 통해 따로 관리할 수 있도록 코드를 수정한다.

  • AuraAttributeSet.h
// AuraAttributeSet.h

...

#define  ...

USTRUCT()
struct FEffectProperties
{
	GENERATED_BODY()
    
    FEffectProperties(){}
    
    FGameplayEffectContextHandle EffectContextHandle;
    
    UPROPERTY()
    UAbilitySystemComponent* SourceASC = nullptr;
    
    UPROPERTY()
    AActor* SourceAvatarActor = nullptr;
    
    UPROPERTY()
    AController* SourceController = nullptr;
    
    UPROPERTY()
    ACharacter* SourceCharacter = nullptr;
    
    UPROPERTY()
    UAbilitySystemComponent* TargetASC = nullptr;
    
    UPROPERTY()
    AActor* TargetAvatarActor = nullptr;
    
    UPROPERTY()
    AController* TargetController = nullptr;
    
    UPROPERTY()
    ACharacter* TargetCharacter = nullptr;
};

...

private:
	void SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const;
  • AuraAttributeSet.cpp
// AuraAttributeSet.cpp

...

void UAuraAttributeSet::SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const
{
	/** 코드 수정 */
	/** Source = GameplayEffect 효과 제공자, Target, GameplayEffect 효과를 받는 대상 */
    // SourceASC 프로퍼티 접근을 위한 코드
    Props.EffectContextHandle = Data.EffectSpec.GetContext();
    Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
    /** 코드 수정 */
    
    // SourceAvatarActor 유효성 검사 및 SourceAvatarActor, SourceController프로퍼티 접근을 위한 코드
    if(IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid() && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
    {
    	// AvatarActor와 Controller 변수화
    	Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
        Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
        
        // SourceConroller 유효성 검사를 통해 nullptr일 경우 캐스팅을 통한 재할당
        if(Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
        {
        	if(const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
        	{
            	Props.SourceController = Pawn->GetController();
        	}
        }
        if(Props.SourceController)
        {
        	Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
        }
    }
    
    // TargetActor 접근을 위한 코드
    if(Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
    {
    	Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
		Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
        Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
        Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
    }
}

void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModeCallback& Data)
{
	Super::PostGameplayEffectExecute(Data);
	
    FEffectProperties Props;
    SetEffectProperties(Data, Props);
    
    // 추후에 진도 나가면 해당 챕터에서 코드 이어서 작성
    // Props.
}

5.2.8 100 초과한 회복시 데미지를 입지 않는 오류 해결

아래와 같이 최대체력을 초과한 회복의 경우 데미지를 초과분을 잃을 때까지 체력이 감소하지 않는다.

원인은 PostGameplayEffectExecute() 함수가 GameplayEffect 효과가 적용된 후에 호출되므로 현재 체력량이 100을 넘겨버리기 때문에 발생한다.
해당 함수에서 현재 체력값을 가져와 값에 clamp를 걸어주면 해결된다.

  • AuraAttributeSet.cpp
// AuraAttributeset.cpp

void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffecctModCallbackData& Data)
{
	...
    
    if(Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
    	SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
    }
    
    if(Data.EvaluatedData.Attribute == GetManaAttribute())
    {
    	SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
    }
    
}

실생시 정상적으로 감소되는 것을 확인할 수 있다.

5.3 CurveTable, ActorLevel 생성 및 적용

5.3.1 CurveTable 생성

에디터에서 Miscellaneous -> CurveTable 을 통해 커브 테이블을 하나 생성한다.

우측 아래로 수직으로 꺾이는 화살표를 클릭하고 스프레드 시트에 값을 입력한다.

좌측 상단의 그래프를 클릭하면 스프레드 시트에 입력한 값을 토대로 커브가 리니어하게 생성된 것을 확인할 수 있다.

커브명을 HealingCurve 로 변경하고

GE_PotionHeal 로 돌아와서 커브를 추가해준다.

추가
Scalable Float Magnitude : 1 로 수정.
해당 수치는 커브 테이블값에 값을 곱하여 스케일을 수정하기 때문이다.
(Prieview At NUM 의 경우 값의 미리보기 역할)

5.3.2 ActorLevel 생성

각각의 수치(5, 7.5, 10, 15, 20, ...)들을 적용시키기 위해서 하드코딩한 ActorLevel을 따로 변수를 통해 관리해야할 필요가 있다.

  • AuraEffectActor.h
// AuraEffectActor.h

protected:
	...

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
	float ActorLevel = 1.f;
  • AuraEffectActor.cpp
// AuraEffectActor.cpp

void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
	...
    const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass, ActorLevel, EffectContextHandle);
    ...
}

컴파일 후, 체력 포션을 여러개 복사한다.
월드에 배치된 액터를 선택하고 ActorLevel 을 각각 다르게 수정한다.

실행시 커브테이블 값에 따라 체력이 회복되는 것을 확인할 수 있다.

  • +5
  • +7.5
  • +10
  • +18.75
    (x축 = 4.75일때 y축 18.75 증가)

5.3.3 마나 관련 커브 추가

마나도 동일하게 커브를 추가하여 회복시킬 수 있다.
먼저 하나의 테이블에 여러 커브를 추가할 수 있으므로, 다른 커브 테이블과 헷갈리지 않도록 CT_Potion 으로 수정한다.

ManaCurve 를 하나 추가하고, 스프레드시트에 수치를 입력한다.

필요에 따라 그래프탭에서 삼각형 모양의 포인트를 이용해 수치 조절또한 가능하다.

삼각형 포인트를 통해 그래프를 리니어하게 변경하여 수정된 값들

GE_PotionMana 로 돌아와서 스케일값을 1.0으로 변경하고, 커브 테이블을 적용시킨다.

월드에 배치시킨 마나 포션의 ActorLevel 을 수정한 후

실행하면 정상적으로 작동하는 것을 확인할 수 있다.

0개의 댓글