11. Advanced Damage Techniques

목차

  1. Gameplay Effect Context
  2. Ability System Globals
  3. Custom Effect Context 사용
  4. Floating Text 기타 기능 추가(마지막에 HitReact 관련 오류 있음)
  5. Damage Types
  6. 클라이언트측 오류 수정

11.1 Gameplay Effect Context

11.1.1 EffectContextHandle

언리얼에서의 Gameplay Effect ContextGameplay Effect (버프, 디버프, 상태변화 등)을 적용할 때 그 효과가 어떤 상황에서 발생했는지를 설명하는 데이터를 제공한다.

  • 주요 구성 요소
    1. SourceActor : 효과를 발생시킨 원본 액터
    2. TargetActor : 효과가 적용되는 대상 액터
    3. Effect Causer : 특정 효과를 발생시킨 원인(효과의 발생 원인을 더 구체적으로 식별하는데 유용)
    4. Contextual Information : 효과가 발생한 상황에 대한 추가 정보
      Ex) 공격이 주어진 위치, 상태, 또는 특정 조건에서 발생했는지를 포함 가능
  • 용도
    1. GameplayEffect의 정의 : 특정 Effect가 어떤 상황에서 발생했는지 정의 가능
    2. 디버깅 : 문제 해결 및 디버깅 과정에서 Effect가 예상되로 작동하는지 확인하기 위해 사용
    3. 맞춤형 응답 : 다양한 상황에 맞게 Effect를 조정하거나, 맞춤형 반응을 구현할 수 있음
      Ex) 특정 지역에서만 발생하는 효과나 조건부로 발동하는 효과 설정

AuraAttributeSet 클래스의 SetEffectProperties() 함수에 EffectContextHandle을 초기화하는 코드가 있다.

void UAuraAttributeSet::SetEffectProperties(...)
{
	Props.EffectContextHandle = Data.EffectSpec.GetContext();
    Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
    
    ...
}

디버그 모드로 두번째 지점에 BreakPoint를 걸어두고 파이어볼트를 사출하면 EffectContextHandle 이 가지고 있는 정보를 확인해볼 수 있다.
( EffectContextHnadle -> Data 선택)

살펴보면 EffectContextHandleFGameplayEffectContext 타입의 Data 를 가지고 있고, 그 아래로 다양한 데이터를 가지고 있다.
해당 데이터를 AuraProjectileSpell 클래스에서 코드를 추가하여 할당하도록 한다.

  • AuraProjectileSpell.cpp
#inclue "..."

...

// TODO: 함수명찾아서 입력
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
	...
    const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibaray::GetAbilitySystemComponent(GetAvaatarActorFromActorInfo());
    /** 코드 추가 */
    FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
    EffectContextHandle.SetAbility(this);
    EffectContextHandle.AddSourceObject(Projectile);
    TArray<TWeakObjectPtr<AActor>> Actors;
    Actors.Add(Projectile);
    EffectContextHandle.AddActors(Actors);
    FHitResult HitResult;
    HitResult.Location = ProjectileTargetLocation;
    EffectContextHandle.AddHitResult(HitResult);
    /** 코드 추가 */
    
    /** 코드 수정 : SourceASC->MakeEffectContext() => EffectContextHandle */
    const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), EffectContextHandle);
    /** 코드 수정 : SourceASC->MakeEffectContext() => EffectContextHandle */
    ...
}

디버그모드로 실행하여 적을 공격할 경우, AttributeSet 클래스의 EffectContextHandle 에서 다양한 데이터들이 할당된 것을 확인할 수 있다.

11.1.2 Custom Gameplay Effect Context

11.1.1에서 Gameplay Effect Context 에는 다양한 데이터가 있고, 해당 데이터를 토대로 GameplayEffect 가 발생된다는 것을 알았다.
이제 크리티컬 발생 여부 관련한 bool 타입 값을 이용하기 위해 Custom Gameplay Effect Context 가 필요하다.
Visual Studio 에서 public 우클릭 -> 추가 -> 새항목 -> AuraAbilityTypes.h 헤더파일을 생성하고

private 에도 동일하게 AuraAbilityTypes.cpp 를 생성한다.

  • AuraAbilityTypes.h
#pragma once

#include "GameplayEffectTypes.h
#include "AuraAbilityTypes.generated.h"

// FGameplayEffectContext 구조체로부터 파생된 구조체 생성
USTRUCT(BlueprintType)
struct FAuraGameplayEffectContext : public FGameplayEffectContext
{
	GENERATED_BODY()
    
public:
	bool IsBlockedHit() const { return bIsBlockedHit; }
	bool IsCriticalHit() const { return bIsCriticalHit; }
    
    void SetIsBlockedHit(bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }
    void SetIsCriticalHit(bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }
    
	/** Returns the actual struct used for serialization, subclass must override this! */
    virtual UScriptStruct* GetScriptStruct() const
    {
    	return FGameplayEffectContext::StaticStruct();
    }
    
    /** Custom serialization, subclasses must override this */
    virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
    
protected:
	UPROPERTY()
    bool bIsBlockedHit = false;
    
    UPROPERTY()
    bool bIsCriticalHit = false;
};
  • AuraAbilityTypes.cpp
#include "AuraAbilityTypes.h"

bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
	return true;
}

11.1.3 Net Serialize 설명

NetSerialize() 는 언리얼에서 네트워크를 통한 객체의 직렬화와 역직렬화를 관리하는 함수이다.
언리얼에서는 멀티플레이어 환경을 지원하기 위해 네트워크 통신을 사용하여 게임의 상태를 동기화하는데, 이때 데이터의 전송 및 수신을 효율적으로 처리하기 위해 객체의 상태를 직렬화(Serialize)하여 네트워크를 통해 전송하고, 수신 측에서는 이 데이터를 역직렬화(Deserialize)하여 객체의 상태를 복원한다.

간단요약
직렬화를 통해 데이터의 바이트화(전송 및 수신을 효율적으로 하기 위함)하여 전송
역직렬화를 통해 바이트화된 객체를 조립하여 데이터화
  • NetSerialize 함수 파라미터 설명
/* 
 * FArchive& Ar
 * 바이트 순서와 무관하게 로딩, 저장 및 가비지 수집에 사용할 수 있는 아카이브의 기본 클래스
 * 직렬화된 데이터를 저장하거나, 데이터를 직렬화할 수 있음
 *
 * FArchive에 100101과 같은 비트데이터가 있다고 가정(Saving/Loading/Storing Data같은 것들)
 * << 연산자를 여러 다른 타입에 대해 오버로드하며
 * 이 연산자는 저장하거나 로딩하는 컨텍스트에 따라 양방향으로 작동함
 * Saving/Loading을 예시로 A->B로 Saving할때도 << 연산자 사용, B->A로 Loading할때도 << 연산자 사용
 */
/*
 * class UPackageMap* Map
 * 네트워크 통신을 위해 객체와 이름을 인덱스와 매핑
 * 객체를 직렬화할 때 0과 1의 문자로 컨버트됨
 * 이때 각 객체를 고유한 인덱스로 구별하기 위함
 */
/*
 * bool& bOutSuccess : 직렬화(Serialize) 성공시 true return
 */
bool FGameplyaEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
	...
}
  • <<|= 계산 간단 정리예제

    1. A 라는 uint32 타입 RepBits0000 0000 0000 0000 0000 0000 0000 0011 이라고 가정
    2. 1 << 3 에 의해 0000 0000 0000 0000 0000 0000 0000 0001 에서 1 은 왼쪽으로 3번 이동한 결과인 0000 0000 0000 0000 0000 0000 0000 1000 이 됨.
    3. |= 연산에 의해 최종적으로 0000 0000 0000 0000 0000 0000 0000 1011 이 됨.
  • NetSerialize() 함수 정리

    1. NetSerialize() 함수는 uint8 타입의 RepBits 를 반환한다.
    2. RepBits0000 0000 으로 초기화되어 있다.
    3. 함수 내부에서 조건문에 따라 연산을 진행한다.
      처음은 |= 연산을 통해 특정 상태나 옵션에 대한 플래그 설정을 진행한다.

      두번째는 & 연산을 통해 특정 비트가 체크되어 있는지 확인한다.

11.1.4 AuraAbilityTypes 클래스 코드 작성

설명을 참조하여 NetSerialize() 함수를 코드에 작성한다

  • AuraAbilityTypes.cpp
#include "..."

bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
	// GameplayEffectTypes.cpp에 있는 NetSerialize() 함수에 있는 코드 복사 + 붙여넣기
	uint32 RepBits = 0;
    if(Ar.IsSaving())
    {
    	if (bReplicateInstigator && Instigator.IsValid())
		{
			RepBits |= 1 << 0;
		}
		if (bReplicateEffectCauser && EffectCauser.IsValid() )
		{
			RepBits |= 1 << 1;
		}
		if (AbilityCDO.IsValid())
		{
			RepBits |= 1 << 2;
		}
		if (bReplicateSourceObject && SourceObject.IsValid())
		{
			RepBits |= 1 << 3;
		}
		if (Actors.Num() > 0)
		{
			RepBits |= 1 << 4;
		}
		if (HitResult.IsValid())
		{
			RepBits |= 1 << 5;
		}
		if (bHasWorldOrigin)
		{
			RepBits |= 1 << 6;
		}
        // Custom으로 필요한 조건 추가
        if(bIsBlockedHit)
        {
        	RepBits |= 1 << 7;
        }
        if(bIsCriticalHit)
        {
        	RepBits |= 1 << 8;
        }
	}
    
    // Custom으로 조건문 2개 추가했으므로 9 
    Ar.SerializeBits(&RepBits, 9);
    
    // GameplayEffectTypes.cpp에 있는 NetSerialize() 함수에 있는 코드 복사 + 붙여넣기
    if (RepBits & (1 << 0))
	{
		Ar << Instigator;
	}
	if (RepBits & (1 << 1))
	{
		Ar << EffectCauser;
	}
	if (RepBits & (1 << 2))
	{
		Ar << AbilityCDO;
	}
	if (RepBits & (1 << 3))
	{
		Ar << SourceObject;
	}
	if (RepBits & (1 << 4))
	{
		SafeNetSerializeTArray_Default<31>(Ar, Actors);
	}
	if (RepBits & (1 << 5))
	{
		if (Ar.IsLoading())
		{
			if (!HitResult.IsValid())
			{
				HitResult = TSharedPtr<FHitResult>(new FHitResult());
			}
		}
		HitResult->NetSerialize(Ar, Map, bOutSuccess);
	}
	if (RepBits & (1 << 6))
	{
		Ar << WorldOrigin;
		bHasWorldOrigin = true;
	}
	else
	{
		bHasWorldOrigin = false;
	}
    // Custom으로 추가한 조건문 체크를 위함 
    if (RepBits & (1 << 7))
	{
		Ar << bIsBlockedHit;
	}
    if (RepBits & (1 << 8))
	{
		Ar << bIsCriticalHit;
	}
    
    // GameplayEffectTypes.cpp에 있는 NetSerialize() 함수에 있는 코드 복사 + 붙여넣기
    if (Ar.IsLoading())
	{
		AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
	}	

	bOutSuccess = true;
	return true;
}

마지막으로 FGameplayEffectContext 객체의 복사본을 생성하는 함수를 GameplayEffetTypes.h 에서 복사하여 헤더파일에 추가해준다.

  • AuraAbilityTypes.h
#include "..."

USTRUCT(BlueprintType)
struct FAuraGameplayEffectContext : public FGameplayEffectContext
{
    GENERATED_BODY()
    
public:
	...
    
    virtual UScriptStruct* GetScriptStruct() const
    {
    	return ...;
    }
    
    /** 코드 추가 */
    /** Creates a copy of this context, used to duplicate for later modifications */
    virtual FGameplayEffectContext* Duplicate() const
    {
    	FGameplayEffectContext* NewContext = new FGameplayEffectContext();
        *NewContext = *this;
        if(GetHitResult())
        {
        	// Does a deep copy of the hit result
            NewContext->AddHitResult(*GetHitResult(), true);
        }
        return NewContext;
    }
    /** 코드 추가 */
    
    ...
    
protected:
	...
};

/*
 * FAuraGameplayEffectContext라는 구조체에 대해 Unreal Engine의 특성 시스템을 활용하여
 * 네트워크 직렬화와 복사 기능을 지원하도록 설정함
 * 이를 통해 FAuraGameplayEffectContext는 네트워크를 통해 데이터를 송수신할 수 있으며
 * 복사 연산을 안전하게 수행할 수 있는 구조체로 정의됨
 * Custom GameplayEffectContext를 생성하는데 필요함
 */
template<>
struct TStructOpsTypeTraits<FAuraGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FAuraGameplayEffectContext>
{
	enum
    {
    	WithNetSerializer = true,
        withCopy = true
    };
};

추가
UE 5.3 버전에서 Duplicate() 함수 선언시 FAuraGameplayEffectContext 타입으로 수정 필요

	/** Creates a copy of this context, used to duplicate for later modifications */
    virtual FAuraGameplayEffectContext* Duplicate() const
    {
    	FAuraGameplayEffectContext* NewContext = new FAuraGameplayEffectContext();
        *NewContext = *this;
        if(GetHitResult())
        {
        	// Does a deep copy of the hit result
            NewContext->AddHitResult(*GetHitResult(), true);
        }
        return NewContext;
    }

그에 따라 GetScriptStruct() 함수 또한 바로 StaticStruct() 를 리턴하도록 수정

	/** Returns the actual struct used for serialization, subclass must override this! */
	virtual UScriptStruct* GetScriptStruct() const
	{
    	return StaticStruct();
	}

11.2 Ability System Globals 서브클래스 생성

커스텀으로 생성한 GameplayEffectContext 를 사용하기 위해서는 따로 정의가 있는 Ability System Globals 가 필요하다.
즉, AbilitySystemGlobals 서브클래스로 만들면, 원하는 커스텀 클래스와 구조체를 지정할 수 있다.
프로젝트에서 해당 클래스를 통해 관리를 하게 되면, 해당 클래스의 AllocGAmeplayEffectContext() 함수를 통해 FAuraGameplayEffectContext 를 리턴하게 되어, 커스텀 FGameplayEffectContext 를 프로젝트에 적용시킬 수 있게 된다.

생성경료: c++/Aura/public/AbilitySystem

  • AuraAbilitySystemGlobals.h
#include "..."

UCLASS()
class AURA_API UAuraAbilitySystemGlobals : public UAbilitySystemGlobals
{
	GENERATED_BODY()
    
    // AuraProjectileSpell.cpp에 있는 코드와 동일
    /*
     * MakeEffectContext() 함수에서 FGameplayEffectContextHandle 타입의 구조체 생성시 파라미터로 해당 함수 사용
     * FGameplayEffectContextHandle Context = FGameplayEffectContextHandle(UAbilitySystemGlobals::Get().AllocGameplayEffectContext());
     */
    virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;
};
  • AuraAbilitySystemGlobals.cpp
#include "..."
#include "AuraAbilityTypes.h"

FGameplayEffectContext* UAuraAbilitySystemGlobals::AllocGameplayEffectContext() const
{
	// 11.1장에서 생성한 커스텀 GameplayEffectContext를 리턴
	return new FAuraGameplayEffectContext();
}

마지막으로 AuraAbilitySystemGlobals 를 지정해주어야 한다.

파일경로: 프로젝트 경로/Config


DefaultGame.ini 파일을 열고 아래 문장을 추가해준다.

[/Script/GameplayAbilities.AbilitySystemGlobals]
+AbilitySystemGlobalsClassName="/Script/Aura.AuraAbilitySystemGlobals"


정상적으로 적용되었는지 확인하기 위해 AuraProjectileSpell.cpp 에서 EffectContextHandle 을 생성한 지점에 breakpoint를 걸어두고 디버그 모드로 실행하여 공격시 확인 가능하다.

  • AuraProjectileSpell.cpp
#include "..."

...

void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
	...
    
    if (CombatInterface)
    {
    	...
        
		const UAbilitySystemComponent* SourceASC = 	UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
		FGameplayEffectContextHandle EffectContextHandle = SourceASC->MakeEffectContext();
		EffectContextHandle.SetAbility(this);
		EffectContextHandle.AddSourceObject(Projectile);
		TArray<TWeakObjectPtr<AActor>> Actors;
		Actors.Add(Projectile);
		EffectContextHandle.AddActors(Actors);
		FHitResult HitResult;
		HitResult.Location = ProjectileTargetLocation;
		/* breakpoint */EffectContextHandle.AddHitResult(HitResult);
    
    	...
    }
}

...

확인해보면 커스텀으로 생성한 FAuraGameplayEffectContext 가 적용되는 것을 확인할 수 있다.

AuraAttributeSet.cpp 에서 FEffectProperties 타입의 Prop 에도 breakpoint를 걸어두고 디버그 모드로 실행시 정상적으로 FAuraGameplayEffectContext 가 적용되는 것을 확인할 수 있다.

  • AuraAttributeSet.cpp
#include "..."

...

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

 	FEffectProperties Props;
 	SetEffectProperties(Data, Props);

 	/* breakpoint */if (Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
    	...
    }
    
    ...
}

...

11.3 Custom Effect Context 사용

커스텀 FGameplayEffectContextFAuraGameplayEffectContext 와 이를 사용하기 위한 서브 클캐스인 AuraAbilitySystemGlobals 까지 생성을 마쳤으니 사용하는 일만 남았다.
커스텀 GameplayEffectExecutionCalculation 클래스인 ExecCalc_Damage 클래스에는 BlockChanceCriticalHitChance 관련 코드가 있고, 이를 이용해 FAuraGameplayEffectContext 에 해당 bool값을 적용시킬 수 있다.

  • ExecCalc_Damage.cpp
#include "..."
#include "AuraAbilityTypes.h"

...

void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	...
    
    const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
    
    /** 코드 추가 */
    // Context 생성을 위한 코드
    FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
    FGameplayEffectContext* Context = EffectContextHandle.Get();
    // AuraContext 생성을 위한 캐스팅
    FAuraGameplayEffectContext* AuraContext = static_cast<FAuraGameplayEffectContext*>(Context);
    AuraContext->SetIsBlockedHit(bBlocked);
    /** 코드 추가 */
    
    ...
}

블루프린트에서 Auracontext->IsBlockedHit() 에 대한 GetSet 이 가능하게 하도록 AuraAbilitySystemLibrary.h 에 함수를 추가해준다.
( bCriticalHit 관련해서는 AuraAbilitySystemLibrary 클래스에 코드 추가 후, 추가 코드를 통해 추가 작성 예정)

  • AuraAbilitySystemLibrary.h
#include "..."

class ...;

UCLASS()
class AURA_API UAuraAbilitySystemLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
    
public:
	...
    
    // Getter bIsBlockedHit()
    UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
    static bool IsBlockedHit(const FGameplayEffectContextHandle& EffectContextHandle);
    
    // Getter bIsCriticalHit()
    UFUNCTION(BlueprintPure, Category = "AuraAbilitySystemLibrary|GameplayEffects")
    static bool IsCriticalHit(const FGameplayEffectContextHandle& EffectContextHandle);
};
  • AuraAbilitySystemLibrary.cpp
#include "..."
#include "AuraAbilityTypes.h"

...

bool UAuraAbilitySystemLibrary::IsBlockedHit(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
    {
    	return AuraEffectContext->IsBlockedHit();
    }
    return false;
}

bool UAuraAbilitySystemLibrary::IsCriticalHit(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FAuraGameplayEffectContext* AuraEffectContext = static_cast<const FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
    {
    	return AuraEffectContext->IsCriticalHit();
    }
    return false;
}

컴파일 후 디버그 모드로 실행하여 GA_FireBolt 에 추가 가능한 것과 Get Effect Context 노드를 통해 return 되는 FameplayEffectContextHandle 핀과 연결되는 것을 확인할 수 있다.

동일하게 블루프린트에서 Setter 에 접근가능하도록 코드를 추가작성해준다.

  • AuraAbilitySystemLibrary.h
#include "..."

class ...;

UCLASS()
class AURA_API UAuraAbilitySystemLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
    
public:
	...
    
	// Setter bIsBlockedHit()
    // Setter 이므로 const 를 통한 상수화 x
    UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
    static void SetIsBlockedHit(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit);
};
  • AuraAbilitySystemLibrary.cpp
#include "..."

...

void UAuraAbilitySystemLibrary::SetIsBlockedHit(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit)
{
	if(FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
    {
    	AuraEffectContext->SetIsBlockedHit(bInIsBlockedHit);
    }
}

컴파일 후 GA_FireBolt 에서 SetIsBlockedHit 노드를 생성하면 한가지 문제가 발생한다.
언리얼 엔진에서 코드상의 함수의 inputnon_const reference 일 경우 Output 핀으로 설정한다.
(SetIsBlockedHit() 함수에서 FGameplayEffectContextHandleInput 이지만 블루프린트에서 노드 생성시 Output 으로 나타나는 것)

이를 해결하기 위한 코드 수정이 필요한데 UPARAM(ref) 라는 문장만 추가해주면 된다.

// AuraAbilitySystemLibrary.h
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetItBlockedHit(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit);

컴파일 후 실행하면 정상적으로 Input 핀에 EffectContextHandle 이 존재하는 것을 확인할 수 있다.

동일하게 SetIsCriticalHit() 함수에 대해서 코드를 작성한다.

  • AuraAbilitySystemLibrary.h
#include "..."

class ...;

UCLASS()
class AURA_API UAuraAbilitySystemLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
    
public:
	...
    
	// Setter bIsBlockedHit()
    // Setter 이므로 const 를 통한 상수화 x
    UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
    static void SetIsCriticalHit(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsCriticalHit);
};
  • AuraAbilitySystemLibrary.cpp
#include "..."

...

void UAuraAbilitySystemLibrary::SetIsCriticalHit(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsCriticalHit)
{
	if(FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
    {
    	AuraEffectContext->SetIsCriticalHit(bInIsCriticalHit);
    }
}

컴파일 후 GA_FireBolt 에서 SetIsCriticalHit 노드 생성이 가능한지 확인한다.

블루프린트에서 접근 가능하게 만든 함수이지만 ExecCalc_Damage 클래스에서 AbilitySystemLibrary 클래스에 접근하여 함수 호출을 통한 코드 작성도 가능해졌으므로 리팩토링을 진행한다.
또한 bCriticalHit 에 대해서도 코드를 작성한다.

  • ExecCalc_Damage.cpp
#include "..."

...

void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	...
    
    FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
    /** 코드 추가 */
    UAuraAbilitySystemLibrary::SetIsBlockedHit(EffectContextHandle, bBlocked);
    /** 코드 추가 */
    
    ...
    
    ...
    const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
    
    /** 코드 추가 */
    UAuraAbilitySystemLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
    /** 코드 추가 */
    
    ...
}

지금까지의 과정을 통해 BlockHit 여부와 CriticalHit 여부를 확인할 수 있게 되었다.
이제 해당 값을 통해 텍스트를 다르게 출력할 수 있다.
그러기 위해서 해당 과정을 처리하는 함수에 블락 여부와 크리티컬 여부를 파라미터로 전달해야 한다.

  • AuraAttributeSet.h
#include "..."

UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
public:
	GENERATED_BODY()
    
public:
	...
    
private:
	void SetEffectProperties(...) const;
    /** 코드 수정 : bool bBlockedHit, bool bCriticalHit 추가 */
    void ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const;
    /** 코드 수정 */
  • AuraAttirbuteSet.cpp
#include "..."
#include "AbilitySystem/AuraAbilitySystemLibrary.h"

...

/** 코드 수정 : bool bBlockedHit, bool bCriticalHit 추가 */
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
	...
}
/** 코드 수정 */

void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	...
    
    if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
	{
    	...
		if (LocalIncomingDamage > 0.f)
		{
    		...
            /** 코드 추가 */
            const bool bBlockedHit = UAuraAbilitySystemLibrary::IsBlockedHit(Props.EffectContextHandle);
            const bool bCriticalHit = UAuraAbilitySystemLibrary::IsCriticalHit(Props.EffectContextHandle);
            /** 코드 추가 */
    		/** 코드 수정 : bool bBlockedHit, bool bCriticalHit 추가 */
    		ShowFloatingText(Props, LocalIncomingDamage, bBlockedHit, bCriticalHit);
            /** 코드 수정 */
        }
    }
}

정상적으로 추가됬는지 확인하기 위해 ShowFloatingText() 함수PC->ShowDamageNumber(Damage, Props.TargetCharactder); 라인에 breakpoint를 두고 디버그모드로 실행한다.

bBlock 여부를 확실하게 확인하기 위해 GE_SecondaryAttributes_TEST 에서 BlockChance 의 확률을 100으로 설정해주고

실행하여 공격하면 bBlockedHit = true 가 되는 것을 확인할 수 있다.

11.4 Floating Text 기타 기능 추가

11.4.1 블루프린트에서 bBlockedHit/bCriticalHit 값 호출

11.3장의 마지막에 AttributeSet 클래스의 헤더파일과 c++의 ShowFloatingText() 함수에 bool 타입의 bBlockedHitbCriticalHit 을 파라미터로 받도록 수정했다.
ShowFloatingText() 함수는 해당 파라미터를 AuraPlayerController 클래스의 ShowDamageNumber() 에 전달하여 데미지를 출력하도록 하는 함수이므로 AuraPlayerController 클래스에서 bBlockedHitbCriticalHit 을 받도록 코드 수정이 필요하다.

  • AuraPlayerController.h
#include "..."

...

UCLASS()
class AURA_API AAuraPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	...
    
    /** 코드 수정 : bool bBlockedHit, bool bCriticalHit 추가 */
	UFUNCTION(Client, Reliable)
	void ShowDamageNumber(float DamageAmount, ACharacter* TargetCharacter, bool bBlockedHit, bool bCriticalHit);
    /** 코드 수정 */
    
protected:
	...
    
private:
	...
    
};

AuraPlayercontroller.cpp 클래스에서 동일하게 파라미터를 추가해주고, 미리 SetDamageText() 함수에 두 bool 타입의 변수를 전달하도록 코드를 작성한다.

  • AuraPlayerController.cpp
#include "..."

...

/** 코드 수정 : bool bBlockedHit, bool bCriticalHit 추가 */
void AAuraPlayerController::ShowDamageNumber_Implementation(float Damage, ACharacter* TargetCharacter, bool bBlockedHit, bool bCriticalHit)
{
	if(IsValid(TasrgetCharacter) && DamageTextComponentClass)
    {
    	...
        /** 코드 수정 : bBlockedHit, bCriticalHit 추가 */
        DamageText->SetDamageText(DamageAmount, bBlockedHit, bCriticalHit);
    	/** 코드 수정 */
    }
}
/** 코드 수정 */

...

AuraPlayercontrollerSetDamageText() 에서 bool 타입 변수를 받을 수 있도록 미리 코드를 작성하였기 때문에, DamageTextComponent 클래스에서 두 bool 타입 변수를 받을 수 있도록 코드를 수정한다.
( BlueprintImplementation 이므로 c++ 코드 수정 필요x)

  • DamageTextComponent.h
#include "..."

UCLASS()
class API_AURA UDamageTextComponent : public UWidgetComponent
{
	GENERATED_BODY()
    
public:
	/** 코드 수정 : bool bBlockedHit, bool bCriticalHit 추가 */
	UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
    void SetDamageText(float Damage, bool bBlockedHit, bool bCriticalHit);
    /** 코드 수정 */
};

11.3장에서 AuraAttributeSet 클래스의 ShowFloatingText() 함수 파라미터만 추가하였고, PC->ShowDamageNumber() 부분에는 bool 타입 변수 2개를 전달하지 않은 채 디버그 모드로 적용 여부만 확인했다.
여기에도 bool 타입 변수 2개를 받도록 코드를 수정한다.

  • AuraAttributeSet.cpp
#include "..."

...

void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
    if (Props.SourceCharacter != Props.TargetCharacter)
    {
        if (AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
        {
        	/** 코드 수정 : bBlockedHit, bCriticalHit 추가 */
            PC->ShowDamageNumber(Damage, Props.TargetCharacter, bBlockedHit, bCriticalHit);
            /** 코드 수정 */
        }
    }
}

...

컴파일 후 BP_DamageTextComponentEvent Set Damage Text 노드를 확인하면 두 개의 bool 타입 변수가 추가된 것을 확인할 수 있다.

BP_DamageTextComponent 에서는 Damage 값을 WBP_DamageText 에 전달하도록 노드가 구성되어 있다.

bBlockedHitbCriticalHit 여부에 따라 텍스트 외형을 변경하기 위해 bool 값을 전달시켜야 하므로
WBP_DamageTextUpdate Damage Text 함수에 Input 을 2개 추가한다.

마지막으로 BP_DamageTextComponent 로 돌아와 노드를 연결시켜 값을 전달할 수 있도록 한다.

이제 WBP_DamageText 에서 bool값에 따라 텍스트의 외형을 다르게 출력하도록 할 수 있다.

11.4.2 Text Color 추가

WBP_DamamgeText 에서 bool 타입 값에 따라 텍스트의 책을 변경하기 위한 함수를 하나 생성하고, bool값 2개를 파마리터로 받도록 input 을 추가한다.

그리고 Color 를 리턴하도록 Slate Color 타입 output 을 추가한다.

Color 핀을 우클릭하여 지역변수(Local Variable)로 승격시키고

IsBlockIsCrit 또한 스파게티화를 방지하기 위해 BlockCrit 지역변수로 승격시킨다.

  • Block = true and Crit = false 인 경우
    Branch 를 통해 조건문 설정

    조건에 따른 색 설정
  • Block = false and Crit = true 인 경우
    Branch 의 false에 Branch 를 통한 조건문 설정

    조건에 따른 색 설정
  • Block = true and Crit = true 인 경우
    두번째 Branch 의 false에 Branch 를 통한 조건문 생성

    조건에 따른 색 설정
  • Block = false and Crit = false 인 경우
    세번째 Branch 의 false에 Branch 를 통한 조건문 생성

    조건에 따른 색 설정( Saturation 을 최대로 낮추면 흰색)

    추가
    마지막 Branchtrue , false 둘다 연결

최종적으로 모든 OutColorSetter 에 대해 Retune Node 와 연결시킨다.

WBP_DamageTextUpdateDamageText 함수로 돌아와 GetColorBasedOnBlockAndCrit 함수를 호출하고 핀을 연결시킨 다음 TextColor 지역변수로 승격시키고 나머지 노드와 연결한다.

그리고 TextDamage 를 통해 Set Color 를 호출하여 연결시켜준다.

컴파일 후 실행하면 Block되어 파란색으로 색이 변경된 것을 확인할 수 있다.
( GE_SecondaryAttributes_TEST 에서 BlockChance 의 값이 100이므로 파란색으로만 표시되는게 정상)

GE_SecondaryAtributes_TESTBlockChance 를 50으로 변경하여 테스트를 진행해보면 조건에 따라 알맞은 색이 표시되는 것을 확인할 수 있다.

추가
!!!주의!!! : GE_SecondaryAttributes_TEST 는 적에게 적용되는 GameplayEffect 이므로 해당 파일의 BlockChance 수치를 조절하였음.
캐릭터의 경우 GE_SecondaryAttributes 를 사용하므로 크리티컬 관련 조정을 위해 GE_SecondaryAttributes 의 값을 수정해야함

크리티컬 확률이 많이 낮으므로 약간의 수정이 필요

GE_SecondaryAttributes 를 살펴보면 CriticalHitChanceArmorPenetration 과 관련 있음을 확인할 수 있음.

계수를 수정하여 확률을 올린다
Pre ... : .5 , Post ... : 10

ArmorPenetration 또한 계수를 수정해준다.
Post ... : 25

확률 증가가 확인되었다면 테스트 진행

  • No Block, No Crit
  • Block, Crit
  • No Block, Crit

11.4.3 Hit Message 출력

텍스트 출력을 위해 WBP_DamageText 에서 Text 를 추가한다.

IsVariable 을 활성화시켜주고

중앙정렬시킨 다음

Justification 도 중앙정렬해주고

Font : NanumBrushScript-Regular_Font , Size : 52 , Outline Size : 2 로 설정해준다.

텍스트에 애니메이션을 추가해주기 위해 HitMessageAnim 을 추가해주고

텍스트에 대한 트랙 추가 및 Transform 을 추가해준다.

아래 테이블에 따라 Transform 값을 변경시킨다.

secxy
00-20
0.05030
0.150-20
10-35
  • 0 sec
  • 0.05 sec
  • 0.15 sec
  • 1 sec

아래 테이블에 따라 Scale 값을 변경시킨다.

secxy
000
0.0511
0.150.60.6

추가
52의 1.5배인 78로 Font Size 변경

  • 0 sec
  • 0.05 sec
  • 0.15 sec

애니메이션 재생이 끝날 때 쯤 텍스트가 사라지도록 Render Opacity 를 추가해준다.

  • 0 sec
  • 1 sec

Event Graph 탭에서 HitMessageAnimPlay Animation 노드를 연결하여 피격시 텍스트 애니메이션이 출력되도록 한다.

컴파일 후 실행하면 피격시 텍스트가 출력되는 것을 확인할 수 있다.

이제 Block 이나 Crit 여부에 따라 텍스트를 다르게 출력해야 한다.
WBP_DamageTextGetHitMessageBasedOnBlockAndCrit 함수를 생성하고 IsBlockIsCritInput 으로 받도록 설정한다.

inputBlockCrit 지역변수로 승격시키고

Text 타입 Message 를 리턴하도록 Output 을 추가해준 뒤 동일하게 지역변수로 승격시킨다.

GetColorBasedOnBlockAndCrit 함수의 Branch 와 동일하게 노드를 구성할 것이므로 해당 노드를 복사해서 붙여넣고 다른 노드와 연결시킨다.

각 조건에 따라 출력할 텍스츠를 설정해준다.

  • Block, No Crit
  • No Block, Crit
  • Block, Crit
  • No Block, No Crit
    (공백 맞음)

    최종적으로 모든 Setter 에 대해 Return Node 와 연결시킨다.

    UpdateDamageText 함수로 돌아와 깔끔한 정리를 위해 정리를 위해 BlockCrit 을 지역변수로 승격시킨다.

    GetHitMessageBasedOnBlockAndHit 노드를 생성하고 MessageHitMessage 지역변수로 승격시켜준 다음

    나머지 노드를 연결시켜준다.
    이때 마지막에 SetText(Text) 노드를 통해 TextHitMessageHitMessage 로 설정시키도록 노드를 추가한다.

    추가
    DamageHitDamage 지역변수 승격 후 연결

컴파일 후 실행시 각 조건에 따른 텍스트가 정상적으로 출력되는 것을 확인할 수 있다.

  • No Block, No Crit
  • Block, No Crit
  • Block, Crit
  • No Block, Crit

마지막으로 SetColorAndOpacity 를 통해 텍스트에도 동일하게 색을 추가할 수 있다.

  • No Block, No Crit
  • Block, No Crit
  • Block, Crit
  • No Block, Crit

    추가
    GA_HitReact 에서 Instancing Policy : Instanced Per Execution 으로 변경

추가TODO
HitReact 발생하지 않는 오류 수정필요 chap.157쯤 발생

11.5 Damage Types

11.5.1 Damage Types 추가

먼저 데미지 타입을 지정하기 위한 태그를 추가한다.

  • AuraGameplayTag.h
#include "..."

struct FAuraGameplayTags
{
public:
	...
    
    /*
 	 *InputTags
 	 */
    ...
    
    FGameplayTag Damage;
    /** 코드 추가 */
    FGameplayTag Damage_Fire;
    /** 코드 추가 */
    
    ...
    
protected:
	
private:
	...
};
  • AuraGameplayTag.cpp
#include "..."

FAuraGameplayTags FAuraGameplayTags::GameplayTags;

void FAuraGameplayTags::InitializeNativeGameplayTags()
{
	...
    
    /*
 	 * InputTags
 	 */
    ...
    
    GameplayTags.Damage = UGameplayTagsManager::Get().AddNativeGameplayTag(
    	FName("Damage"), FString("Damage"));
     
    /** 코드 추가 */
    GameplayTags.Damage_Fire = UGameplayTagsManager::Get().AddNativeGameplayTag(
    	FName("Damage.Fire"), FString("Fire Damage Type"));
    /** 코드 추가 */
     
    ...
}

그냥 AuraGameplayAbility 에 데미지 관련 값들을 추가하는 것보다, Damage 관련 관리를 위한 AuraGameplayAbility 기반 AuraDamageGameplayAbility 클래스를 생성하는 것이 더 효율적이므로 클래스를 새로 생성한다.

생성경로: public/AbilitySystem/Abilities


AuraProjectileSpell 클래스는 AuraGameplayAbility 클래스를 상속받는데 AuraDadmageGameplayAbility 를 상속받도록 코드를 수정한다.
또한 AuraDamageGameplayAbility 클래스에서 관리할만한 부분은 코드를 이동시킨다.

  • AuraProjectileSpell.h
#include "..."
/** 코드 수정 : AuraGameplayAbility -> AuraDamageGameplayAbility */
#include "AbilitySystem/Abilities/AuraDamageGameplayAbility.h"
/** 코드 수정 */
#include "..."

...

/** 코드 수정 : UAuraGameplayAbility -> UAuraDamageGameplayAbility */
CLASS()
class AURA_API UAuraProjectileSpell : public UAuraDamageGameplayAbility
{
	...
    
    /** 코드 이동 : AuraDamageGameplayAbility.h로 이동 */
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TSubclassOf<UGameplayEffect> DamageEffectClass;
    /** 코드 이동 */
};
/** 코드 수정 */

AuraGameplayAbility.h 에 있는 Damage 또한 AuraDamageGameplayAbility 로 이동시킨다.

  • AuraGameplayAbility.h
#include "..."

UCLASS
class AURA_API UAuraGameplayAbility : public UGameplayAbility
{
	GENERATED_BODY()

public:
	...
    
    /** 코드 이동 : AuraDamageGameplayAbility.h로 이동 */
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
    FScalableFloat Damage;
    /** 코드 이동 */
};

이동시킬 코드를 AuraDamageGameplayAbility 로 옮기고, 데미지타입과 데미지를 묶을 Map을 생성한다.

  • AuraDamageGameplayAbility.h
#include "..."

UCLASS()
class AURA_API UAuraDamageGameplayAbility : public UAuraGAmeplayAbility
{
	GENERATED_BODY()

protected:
	/** AuraProjectileSpell.h에서 이동된 코드 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TSubclassOf<UGameplayEffect> DamageEffectClass;
    /** 이동된 코드 */
    
    /** AuraGameplayAbility.h에서 이동된 코드 */
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
    FScalableFloat Damage;
    /** 이동된 코드 */
    
    /** 코드 추가 */
    // Damage와 DamageType을 묶은 맵
    UPROPERTY(EditDefaultsOnly, Category = "Damage")
    TMap<FGameplayTag, FScalableFloat> DamageTypes;
    /** 코드 추가 */
};

이제 Damage 라는 변수를 사용하는 모든 곳에 타입과 데미지를 같이 받도록 코드 수정이 필요하다.
AuraProjectileSpell 클래스에서 공격 관련 코드에, 데미지와 데미지 타입을 같이 넘기기 위한 코드를 추가하고 수정한다.

  • AuraProjectileSpell.cpp
#include "..."

...

void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
	...
    
    if (CombatInterface)
	{
    	...
        
    	const FAuraGameplayTags GameplayTags = FAuraGamelayTags::Get()
    
    	/** 코드 추가 */
    	for(auto& Pair : DamageTypes)
    	{
    		// DamageTypes의 밸류값을 통해 값 전달
    		const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());
        	// DamageTypes의 키값을 통해 태그 전달
        	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, ScaledDamage);
    	}
    	/** 코드 추가 */
    
    	/** 코드 삭제 */
     	const float ScaledDamage = Damage.GetValueAtLevel(10);
 	 	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, ScaledDamage);
    	/** 코드 삭제 */
    
    	...
    }
}

ExecCalc_Damage 클래스도 수정이 필요하다.

float Damage = Spec.GetSetByCallerMagnitude(FAuraGameplayTags::Get().Damage);

에서 태그에 따른 데미지를 가져오는 부분을 수정해야 한다.
먼저 AuraGameplayTags 클래스에 Tarray 배열을 하나 추가한다.

  • AuraGameplayTags.h
#include "..."

struct FAuraGameplayTags
{
public:
	...
    
    /*
 	 *InputTags
 	 */
    ...
    
    FGameplayTag Damage_Fire;
    
    /** 코드 추가 */
    TArray<FGameplayTag> DamageTypes;
    /** 코드 추가 */
    
    ...
    
protected:
	
private:
	...
};
  • AuraGameplayTags.cpp
#include "..."

FAuraGameplayTags FAuraGameplayTags::GameplayTags;

void FAuraGameplayTags::InitializeNativeGameplayTags()
{
	...
    
    /*
 	 * InputTags
 	 */
    ...
     
    GameplayTags.Damage_Fire = UGameplayTagsManager::Get().AddNativeGameplayTag(
    	FName("Damage_Fire"), FString("Fire Damage Type"));
    /** 코드 추가 */
    GameplayTags.DamageTypes.Add(GameplayTags.Damage_Fire);
    /** 코드 추가 */
    ...
}
  • ExecCalc_Damage.cpp
#include "..."

...

void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	...
    EvaluationParameters.TargetTags = TargetTags;
    
    /** 코드 수정 : spec.GetSetByCallerMagnitude(...) -> 0.f 로 수정 */
    float Damage = 0.f;
    /** 코드 수정 */
    /** 코드 추가 */
    for(FGameplayTag DamageTypeTag : FAuraGameplayTags::Get().DamageTypes)
    {
    	const float DamageTypeValue = Spec.GetSetByCallerMagnitude(DamageTypeTag);
        Damage += DamageTypeValue;
    }
    /** 코드 추가 */
    
    ...
}

이제 AuraDamageGameplayAbility 클래스에 있는 FScalableFloat Damage 는 사용하지 않으므로 삭제한다.

  • AuraDamageGameplayAbility.h
#include "..."

UCLASS()
class AURA_API UAuraDamageGameplayAbility : public UAuraGAmeplayAbility
{
	GENERATED_BODY()

protected:
	...
    
    /** 코드 삭제 */
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
    FScalableFloat Damage;
    /** 코드 삭제 */
    
    ...
};

컴파일하여 다른 곳에서 Damage 에 대해 참조 혹은 사용을 하는지 확인하고 없다면 디버그모드로 실행한다.
GA_FireBolt 에서 이제 DamageTypes 를 추가할 수 있다.

코드상으로 문제가 없다면 실행시 데미지가 정상적으로 들어간다.

  • 추가1
    레벨에 비해 크리티컬 데미지가 너무 높으므로 수치 조절
    GE_SecondaryAttributes 에서 CriticalHitDamageArmorPenetration 과 관련있음을 확인

    지난번에 Floating Text 관련해서 색 추가와 텍스트 추가때 테스트를 위해 Post 계수를 25로 지정하였으므로 25 -> 16 로 수정이 필요

    마지막으로 ArmorPenetration 수치에 따른 과한 CriticalHitDamage 증가를 방지하기 위한 Pre 계수 1.5 -> 0.25 로 수정

    컴파일 후 실행시 적당한 수치를 가지는 것 확인
  • 추가2
    일반 공격시의 데미지 수치가 너무 낮으므로 수치 조절 필요
    CT_Damage 에서 그래프를 적당히 조절

    컴파일 후 실행하면 적절한 수치를 가짐
  • 추가3
    현재 적은 GE_SecondaryAttributes_TEST 에서 BlockChance 가 50이라는 높은 수치이므로 15로 변경

11.5.2 Damage Types 및 속성타입저항 태그 추가

여러 데미지 타입 관련 태그를 추가로 작성한다.

  • AuraGameplayTags.h
#include "..."

struct FAuraGameplayTags
{
public:
	...
    
    /*
     * Secondary Attributes
     */
    ...
    
    /** 코드 추가 */
    /*
     * Resistances
     */
    FGameplayTag Attributes_Resistance_Fire;
	FGameplayTag Attributes_Resistance_Lightning;
    FGameplayTag Attributes_Resistance_Arcane;
    FGameplayTag Attributes_Resistance_Physical;
    /** 코드 추가 */
    
    ...
    
    FGameplayTag Damage;
    /*
     * Damage Types
     */
    FGameplayTag Damage_Fire;
    /** 코드 추가 */
    FGameplayTag Damage_Lightning;
    FGameplayTag Damage_Arcane;
    FGameplayTag Damage_Physical;
    /** 코드 추가 */
    
    /** 코드 수정 : TArray -> TMap */
    TMap<FGameplayTag, FGameplayTag> DamageTypesToResistances;
    /** 코드 수정 */
    
    /*
 	 * Effects
 	 */
    ...
    
protected:

private:
	...
};
  • AuraGameplayTags.cpp
#include "..."

FAuraGameplayTags FAuraGameplayTags::GameplayTags;

void FAuraGameplayTags::InitializeNativeGameplayTags()
{
	...
    
    GameplayTags.Damage = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Damage"), FString("Damage"));
    
    /*
     * Damage Types
     */
    GameplayTags.Damage_Fire = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Damage.Fire"), FString("Fire Damage Types"));
    /** 코드 삭제 */
    GameplayTags.DamageTypes.Add(GameplayTags.Damage_Fire);
    /** 코드 삭제 */
    
    /** 코드 추가 */
    GameplayTags.Damage_Lightning = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Damage.Lighting"), FString("Lightning Damage Types"));
    
    GameplayTags.Damage_Arcane = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Damage.Arcane"), FString("Arcane Damage Types"));
    
    GameplayTags.Damage_Physical = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Damage.Physical"), FString("Physical Damage Types"));

	/*
     * Resistances
     */
    GameplayTags.Attributes_Resistance_Fire = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Attributes.Resistance.Fire"), FString("Resistance to Fire damage"));
    
    GameplayTags.Attributes_Resistance_Lightning = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Attributes.Resistance.Lightning"), FString("Resistance to Lightning damage"));
    
    GameplayTags.Attributes_Resistance_Arcane = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Attributes.Resistance.Arcane"), FString("Resistance to Arcane damage"));
    
    GameplayTags.Attributes_Resistance_Physical = UGameplayTagsManager::Get().AddNativeGameplayTag(
    FName("Attributes.Resistance.Physical"), FString("Resistance to Physical damage"));
    
    /*
     * Map of Damage Types to Resistance
     */
    GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Fire, GameplayTags.Attributes_Resistance_Fire);
    GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Lightning, GameplayTags.Attributes_Resistance_Lightning);
    GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Arcane, GameplayTags.Attributes_Resistance_Arcane);
    GameplayTags.DamageTypesToResistances.Add(GameplayTags.Damage_Physical, GameplayTags.Attributes_Resistance_Physical);
    /** 코드 추가 */
    
    /*
     * Effects
     */
    ...
}

TArray 타입 변수를 TMap 으로 변경하였으므로 ExecCalc_Damage 클래스에서 사용하던 코드 수정이 필요하다.

  • ExecCalc_Damage
#include "..."

...

void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	...
    
    float Damage = 0.f;
    /** 코드 수정 : for 조건문 및 내부 수정 */
    for(const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
    {
    	// GetSetByCallerMagnitude()에서 () 내부 Pair.Key로 수정
    	const float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
    	...
    }
    
    ...
}

11.5.3 속성타입저항 Attribute 추가

속성타입저항 Attribute 를 추가한다.

  • AuraAttributeSet.h
#include "..."

...

UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
public:
	...
    
    /*
     * Secondary Attributes
     */
	...
    
    /*
     * Resistance Attributes
     */
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_FireResistance, Category = "Resistance Attributes")
 	FGameplayAttributeData FireResistance;
 	ATTRIBUTE_ACCESSORS(UAuraAttributeSet, FireResistance);
    
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_LightningResistance, Category = "Resistance Attributes")
 	FGameplayAttributeData LightningResistance;
 	ATTRIBUTE_ACCESSORS(UAuraAttributeSet, LightningResistance);
    
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_ArcaneResistance, Category = "Resistance Attributes")
 	FGameplayAttributeData ArcaneResistance;
 	ATTRIBUTE_ACCESSORS(UAuraAttributeSet, ArcaneResistance);
    
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_PhysicalResistance, Category = "Resistance Attributes")
 	FGameplayAttributeData PhysicalResistance;
 	ATTRIBUTE_ACCESSORS(UAuraAttributeSet, PhysicalResistance);
    
    ...
    
    /*
 	 * Secondary Attributes OnRep
 	 */
 	...
    
    /*
     * Resistance Attributes OnRep
     */
    UFUNCTION()
	void OnRep_FireResistance(const FGameplayAttributeData& OldFireResistance) const;
	
    UFUNCTION()
	void OnRep_LightningResistance(const FGameplayAttributeData& OldLightningResistance) const;

	UFUNCTION()
	void OnRep_ArcaneResistance(const FGameplayAttributeData& OldArcaneResistance) const;

	UFUNCTION()
	void OnRep_PhysicalResistance(const FGameplayAttributeData& OldPhysicalResistance) const;

    ...
    
private:
	...
};
  • AuraAttributeSet.cpp
#include "..."

UAuraAttributeSet::UAuraAttributeSet()
{
	...
    
	/* Primary Attributes */
    ...
    
    /* Secondary Attributes */
    ...
    
    /* Resistance Attributes */
    TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Fire, GetFireResistanceAttribute);
    TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Lightning, GetLightningResistanceAttribute);
    TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Arcane, GetArcaneResistanceAttribute);
    TagsToAttributes.Add(GameplayTags.Attributes_Resistance_Physical, GetPhysicalResistanceAttribute);
}

void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	...
    
	/** Primary Attributes */
    ...
    
    /** Secondary Attributes */
    ...
    
    /** Resistance Attributes */
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, FireResistance, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, LightningResistance, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, ArcaneResistance, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, PhysicalResistance, COND_None, REPNOTIFY_Always);
     
     ...
}

...

void UAuraAttributeSet::OnRep_FireResistance(const FGameplayAttributeData& OldFireResistance) const
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, FireResistance, OldFireResistance);
}

void UAuraAttributeSet::OnRep_LightningResistance(const FGameplayAttributeData& OldLightningResistance) const
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, LightningResistance, OldLightningResistance);
}

void UAuraAttributeSet::OnRep_ArcaneResistance(const FGameplayAttributeData& OldArcaneResistance) const
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, ArcaneResistance, OldArcaneResistance);
}

void UAuraAttributeSet::OnRep_PhysicalResistance(const FGameplayAttributeData& OldPhysicalResistance) const
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, PhysicalResistance, OldPhysicalResistance);
}

컴파일 후 GE_SecondaryAttributes 에 각 저항에 대한 값들을 추가해준다.

또한 WBP_AttributeMenuSECONDARY ATTRIBUTES 에도 추가된 Attribute 를 추가한다.

SetAttributeTags 함수에서 각 Row 에 대한 태그를 연결시켜준다.

추가
노드가 너무 많아 보기 복잡하므로 함수화하여 정리

마지막으로 DA_AttributeInfo 에서 태그와 이름, 설명을 추가해준다.

컴파일 후 실행하면 Attribute Menu 에 새로 추가한 Attributes 들이 표시되는 것을 확인할 수 있다.

이제 각 속성저항값을 캡쳐하여 사용하도록 해야 한다.

  • ExecCalc_Damage.cpp
#include "..."

struct AuraDamageStatics
{
	...
    
    DECLARE_ATTRIBUTE_CAPTUREDEF(FireResistance);
    DECLARE_ATTRIBUTE_CAPTUREDEF(LightningResistance);
    DECLARE_ATTRIBUTE_CAPTUREDEF(ArcaneResistance);
    DECLARE_ATTRIBUTE_CAPTUREDEF(PhysicalResistance);
    
    // CapDef가 어떤 태그와 관련있는지를 확인하기 위한 TMap
    TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition> TagsToCaptureDefs;
    
    AuraDamageStatics()
 	{
    	...
        
        DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, FireResistance, Target, false);
        DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, LightningResistance, Target, false);
        DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArcaneResistance, Target, false);
        DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, PhysicalResistance, Target, false);
        
        const FAuraGameplayTags& Tags = FAuraGameplayTags::Get();
        TagsToCaptureDefs.Add(Tags.Attributes_Secondary_Armor, ArmorDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Secondary_ArmorPenetration, ArmorPenetrationDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Secondary_BlockChance, BlockChanceDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitChance, CriticalHitChanceDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitDamage, CriticalHitDamageDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Secondary_CriticalHitResistance, CriticalHitResistanceDef);
       
        TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Fire, FireResistanceDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Lightning, LightningResistanceDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Arcane, ArcaneResistanceDef);
        TagsToCaptureDefs.Add(Tags.Attributes_Resistance_Physical, PhysicalResistanceDef);
    }
};

static const AuraDamageStatics& DamageStatics()
{
	...
}

UExecCalc_Damage::UExecCalc_Damage()
{
	...
    
    RelevantAttributesToCapture.Add(DamageStatics().FireResistanceDef);
    RelevantAttributesToCapture.Add(DamageStatics().LightningResistanceDef);
    RelevantAttributesToCapture.Add(DamageStatics().ArcaneResistanceDef);
    RelevantAttributesToCapture.Add(DamageStatics().PhysicalResistanceDef);
}

void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	...
    
    float Damage = 0.f;
    for (const TTuple<FGameplayTag, FGameplayTag>& Pair : FAuraGameplayTags::Get().DamageTypesToResistances)
    {
    	/** 코드 추가 */
        const FGameplayTag DamageTypeTag = Pair.Key;
        const FGameplayTag ResistanceTag = Pair.Value;
        
        checkf(AuraDamageStatics().TagsToCaptureDefs.Contains(ResistanceTag), TEXT("TagsToCaptureDefs doesn't contain Tag: [%s] in ExecCalc_Damage"), * ResistanceTag.ToString());
        const FGameplayEffectAttributeCaptureDefinition CaptureDef = AuraDamageStatics().TagsToCaptureDefs[ResistanceTag];
        /** 코드 추가 */
        
        /** 코드 수정 : const 삭제 */
        float DamageTypeValue = Spec.GetSetByCallerMagnitude(Pair.Key);
        /** 코드 수정 */
        
        /** 코드 추가 */
        float Resistance = 0.f;
        ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, Resistance);
        Resistance = FMath::Clamp(Resistance, 0.f, 100.f);
        
        DamageTypeValue *= ( 100.f - Resistance ) / 100.f;
        /** 코드 추가 */

		Damage += DamageTypesValue;
    }
    
    ...
}

컴파일 후 breakpoint를 해당 지점에 걸고

공격을 시도하면 속성과 속성저항이 정상적으로 적용되는 것을 확인할 수 있다.

(속성저항 값을 아직 지정해주지 않았으므로 0이 나오는게 정상)

속성저항값을 추가해주기 위해 GE_SecondaryAttributes_TEST 에서 FireResistance 에 대한 Modifire 를 추가해준다.

다시 동일한 위치에 breakpoint를 걸고 실행하면 정상적으로 수치가 적용되어 데미지가 반으로 줄어드는 것을 확인할 수 있다.

11.6 클라이언트측 오류 수정

11.6.1 캐릭터 관련 오류

간헐적으로 캐릭터 클래스 관련 Error 가 표시됨.

TODO: 추후 오류 확인되면 오류문장 작성

디버그모드를 통해 확인해보면 멀티로 진행시 CharacterClassInfo 클래스의 GetCharacterClassInfo() 함수를 호출시 null 값을 참조하여 오류가 발생했던 것으로 기억

  • 원인: GameMode 는 서버에서만 Valid 하고, 클라이언트측에서 null이기 때문

  • AuraEnemy.cpp

#include "..."

...

void AAuraEnemy::BeginPlay()
{
	...
    
    /** 코드 수정 : if(HasAuthority()) 추가 */
    if(HasAuthority())
    {
		// Ability를 적이 사용할 수 있도록 하는 함수(Ability 할당)
		UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComponent);
    }
    /** 코드 추가 */
    
    ...
}

...

void AAuraEnemy::InitAbilityActorInfo()
{
	...
	
    /** 코드 수정 : if(HasAuthority()) 추가 */
	if (HasAuthority())
	{
		InitializeDefaultAttributes();
	}
    /** 코드 수정 */
}

...

11.6.2 Play As Client (2player) 옵션에서 투사체 진행시 LoopingSoundComponent 가 nullptr를 참조하는 문제

  • 원인: LoopingSoundComponent 에 접근하기 전에 Projectile 이 파괴될 수도 있어서 생기는 문제
  • AuraProjectile.cpp
/** 코드 수정 */
if(LoopingSoundComponent) LoopingSoundComponent->Stop();
/** 코드 수정 */

11.6.3 Client측에서 데미지 위젯이 표시되지 않는 문제

  • 원인: AttributeSet.cpp 를 확인하면 ShowFloatingText(..) 함수가 해당 기능을 수행하는 것을 확인할 수 있음.

    해당 함수가 호출되는 곳은
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	...
    
    if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
	{
    	...
        if (LocalIncomingDamage > 0.f)
		{
        	...
            if (bFatal)
			{
            	...
            }
            else
            {
            	...
            }
            ...
            ShowFloatingText(Props, LocalIncomingDamage, bBlockedHit, bCriticalHit);
        }
    }
}

부분인데
여기서 GetIncomingDamageAttribute()meta attribute 이므로 리플리케이트되지 않기 때문에 클라이언트측에서 표시되지 않는다.
ShowFloatingText() 함수를 살펴보면

void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
    // 자기 자신에게 데미지를 가하는게 아닌 경우에만 출력하도록 함
    if (Props.SourceCharacter != Props.TargetCharacter)
    {
        if (AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
        {
            PC->ShowDamageNumber(Damage, Props.TargetCharacter, bBlockedHit, bCriticalHit);
        }
    }
}

UGameplayStaics::GetPlayerController() 부분은 서버에서 실행되는 경우, 서버측의 PlayerController 를 반환하게 되므로 클라이언트의 PlayerController 를 반환받지 못한다.

if(Props.SourceCharacter != Props.TargetCharacter)

를 통해 Props.SourceCharactger 즉, 공격하는 쪽에 대한 정보가 있으므로
Props.SourceCharacter->Controller 로 대체해야 한다.

  • 수정된 코드
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage, bool bBlockedHit, bool bCriticalHit) const
{
    if (Props.SourceCharacter != Props.TargetCharacter)
    {
    	// 모든 클라이언트 컨트롤러 정보는 서버측에 있지만, 올바른 클라이언트 컨트롤러를 전달하기 위해 코드 수정 필요
        if (AAuraPlayerController* PC = Cast<AAuraPlayerController>(Props.SourceCharacter->Controller))
        {
            PC->ShowDamageNumber(Damage, Props.TargetCharacter, bBlockedHit, bCriticalHit);
        }
    }
}

추가 5.3 기준으로 위의 수정만 하면 데미지가 다른쪽에 표시되지 않지만 혹시 몰라서 추가 작성
마지막으로 하나 더 수정해야 할 것이 있다.
ShowDamageNumber() 가 서버에서도 실행되기 때문에 서버에도 데미지가 출력된다.
ShowDamageNumber() 정의부분에서 코드를 수정한다.

  • AuraPlayerController.cpp
void AAuraPlayerController::ShowDamageNumber_Implementation(float DamageAmount, ACharacter* TargetCharacter, bool bBlockedHit, bool bCriticalHit)
{
    /** 코드 수정 : && IsLocalController() 추가 */
    if (IsValid(TargetCharacter) && DamageTextComponentClass && IsLocalController())
    {
        ...
    }
    /** 코드 수정 */
}

컴파일 후 실행시 클라이언트측에서도 정상적으로 데미지가 출력되는 것을 확인할 수 있다.

11.6.4 클라이언트측에서 투사체가 적을 무시하고 뚫고 지나가는 문제

5.3버전으로는 오류 발생x
Chap.162 12:57부분부터 해결 과정

11.6.3 오류 해결 이후 발생하는 문제같은데 5.3 기준 문제없이 투사체에 의해 데미지가 적용됨
혹시몰라 추가작성해둠

  • 원인
    AuraProjectile.cppOnSphereOverlap() 함수에서 문제 발생

    두 지점에 breakpoint를 걸고 체크해보면 Destroy() 에서 Projectile 이 자기 스스로를 파괴하는 문제가 발생하기 때문이다.
  • 코드 수정
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{   
    if (OtherActor == GetInstigator()) return;

	/** 코드 추가 */
    // Projectile의 자신과의 충돌을 막기 위함
    if (DamageEffectSpecHandle.Data.IsValid() && DamageEffectSpecHandle.Data.Get()->GetContext().GetEffectCauser() == OtherActor) return;
    /** 코드 추가 */
    
    ...
}

11.6.5 Projectile이 적 머리 위로 지나가 충돌하지 않는 문제

  • AuraProjectileSpell.cpp
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
	...
    if(CombatInterface)
    {
    	...
        /** 코드 삭제 */
        Rotaion.Pitch = 0.f;
        /** 코드 삭제 */
    }
}

11.6.6 클라이언트측에서 ImpactSound가 두개로 적용되어 들리는 문제

  • AuraProjectileSpell.cpp
  • 원인: bHit 이 참일 경우 이미 클라이언트측에서 재생을 하지만 이를 서버측에서 한번 더 재생하여서 생기는 문제
void AAuraProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	...
    
    /** 코드 수정 : if조건문 추가 */
    if (!bHit)
	{
    	UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
    	UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
    	if (LoopingSoundComponent) LoopingSoundComponent->Stop();
    }
    /** 코드 수정 */
    
    ...
}

0개의 댓글