언리얼에서의 Gameplay Effect Context 는 Gameplay Effect (버프, 디버프, 상태변화 등)을 적용할 때 그 효과가 어떤 상황에서 발생했는지를 설명하는 데이터를 제공한다.
SourceActor : 효과를 발생시킨 원본 액터TargetActor : 효과가 적용되는 대상 액터Effect Causer : 특정 효과를 발생시킨 원인(효과의 발생 원인을 더 구체적으로 식별하는데 유용)Contextual Information : 효과가 발생한 상황에 대한 추가 정보GameplayEffect의 정의 : 특정 Effect가 어떤 상황에서 발생했는지 정의 가능AuraAttributeSet 클래스의 SetEffectProperties() 함수에 EffectContextHandle을 초기화하는 코드가 있다.
void UAuraAttributeSet::SetEffectProperties(...)
{
Props.EffectContextHandle = Data.EffectSpec.GetContext();
Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
...
}
디버그 모드로 두번째 지점에 BreakPoint를 걸어두고 파이어볼트를 사출하면 EffectContextHandle 이 가지고 있는 정보를 확인해볼 수 있다.
( EffectContextHnadle -> Data 선택)

살펴보면 EffectContextHandle 은 FGameplayEffectContext 타입의 Data 를 가지고 있고, 그 아래로 다양한 데이터를 가지고 있다.
해당 데이터를 AuraProjectileSpell 클래스에서 코드를 추가하여 할당하도록 한다.
#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.1에서 Gameplay Effect Context 에는 다양한 데이터가 있고, 해당 데이터를 토대로 GameplayEffect 가 발생된다는 것을 알았다.
이제 크리티컬 발생 여부 관련한 bool 타입 값을 이용하기 위해 Custom Gameplay Effect Context 가 필요하다.
Visual Studio 에서 public 우클릭 -> 추가 -> 새항목 -> AuraAbilityTypes.h 헤더파일을 생성하고

private 에도 동일하게 AuraAbilityTypes.cpp 를 생성한다.
#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;
};
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
return true;
}
NetSerialize() 는 언리얼에서 네트워크를 통한 객체의 직렬화와 역직렬화를 관리하는 함수이다.
언리얼에서는 멀티플레이어 환경을 지원하기 위해 네트워크 통신을 사용하여 게임의 상태를 동기화하는데, 이때 데이터의 전송 및 수신을 효율적으로 처리하기 위해 객체의 상태를 직렬화(Serialize)하여 네트워크를 통해 전송하고, 수신 측에서는 이 데이터를 역직렬화(Deserialize)하여 객체의 상태를 복원한다.
간단요약
직렬화를 통해 데이터의 바이트화(전송 및 수신을 효율적으로 하기 위함)하여 전송
역직렬화를 통해 바이트화된 객체를 조립하여 데이터화
/*
* 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)
{
...
}
<< 와 |= 계산 간단 정리예제
A 라는 uint32 타입 RepBits 가 0000 0000 0000 0000 0000 0000 0000 0011 이라고 가정1 << 3 에 의해 0000 0000 0000 0000 0000 0000 0000 0001 에서 1 은 왼쪽으로 3번 이동한 결과인 0000 0000 0000 0000 0000 0000 0000 1000 이 됨.|= 연산에 의해 최종적으로 0000 0000 0000 0000 0000 0000 0000 1011 이 됨.NetSerialize() 함수 정리
NetSerialize() 함수는 uint8 타입의 RepBits 를 반환한다.RepBits 는 0000 0000 으로 초기화되어 있다.|= 연산을 통해 특정 상태나 옵션에 대한 플래그 설정을 진행한다.
& 연산을 통해 특정 비트가 체크되어 있는지 확인한다.
설명을 참조하여 NetSerialize() 함수를 코드에 작성한다
#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 에서 복사하여 헤더파일에 추가해준다.
#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(); }
커스텀으로 생성한 GameplayEffectContext 를 사용하기 위해서는 따로 정의가 있는 Ability System Globals 가 필요하다.
즉, AbilitySystemGlobals 서브클래스로 만들면, 원하는 커스텀 클래스와 구조체를 지정할 수 있다.
프로젝트에서 해당 클래스를 통해 관리를 하게 되면, 해당 클래스의 AllocGAmeplayEffectContext() 함수를 통해 FAuraGameplayEffectContext 를 리턴하게 되어, 커스텀 FGameplayEffectContext 를 프로젝트에 적용시킬 수 있게 된다.
생성경료: c++/Aura/public/AbilitySystem



#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;
};
#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를 걸어두고 디버그 모드로 실행하여 공격시 확인 가능하다.
#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 가 적용되는 것을 확인할 수 있다.
#include "..."
...
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
FEffectProperties Props;
SetEffectProperties(Data, Props);
/* breakpoint */if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
...
}
...
}
...

커스텀 FGameplayEffectContext 인 FAuraGameplayEffectContext 와 이를 사용하기 위한 서브 클캐스인 AuraAbilitySystemGlobals 까지 생성을 마쳤으니 사용하는 일만 남았다.
커스텀 GameplayEffectExecutionCalculation 클래스인 ExecCalc_Damage 클래스에는 BlockChance 와 CriticalHitChance 관련 코드가 있고, 이를 이용해 FAuraGameplayEffectContext 에 해당 bool값을 적용시킬 수 있다.
#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() 에 대한 Get 과 Set 이 가능하게 하도록 AuraAbilitySystemLibrary.h 에 함수를 추가해준다.
( bCriticalHit 관련해서는 AuraAbilitySystemLibrary 클래스에 코드 추가 후, 추가 코드를 통해 추가 작성 예정)
#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);
};
#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 에 접근가능하도록 코드를 추가작성해준다.
#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);
};
#include "..."
...
void UAuraAbilitySystemLibrary::SetIsBlockedHit(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit)
{
if(FAuraGameplayEffectContext* AuraEffectContext = static_cast<FAuraGameplayEffectContext*>(EffectContextHandle.Get()))
{
AuraEffectContext->SetIsBlockedHit(bInIsBlockedHit);
}
}
컴파일 후 GA_FireBolt 에서 SetIsBlockedHit 노드를 생성하면 한가지 문제가 발생한다.
언리얼 엔진에서 코드상의 함수의 input 이 non_const reference 일 경우 Output 핀으로 설정한다.
(SetIsBlockedHit() 함수에서 FGameplayEffectContextHandle 은 Input 이지만 블루프린트에서 노드 생성시 Output 으로 나타나는 것)


이를 해결하기 위한 코드 수정이 필요한데 UPARAM(ref) 라는 문장만 추가해주면 된다.
// AuraAbilitySystemLibrary.h
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|GameplayEffects")
static void SetItBlockedHit(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit);
컴파일 후 실행하면 정상적으로 Input 핀에 EffectContextHandle 이 존재하는 것을 확인할 수 있다.

동일하게 SetIsCriticalHit() 함수에 대해서 코드를 작성한다.
#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);
};
#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 에 대해서도 코드를 작성한다.
#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 여부를 확인할 수 있게 되었다.
이제 해당 값을 통해 텍스트를 다르게 출력할 수 있다.
그러기 위해서 해당 과정을 처리하는 함수에 블락 여부와 크리티컬 여부를 파라미터로 전달해야 한다.
#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;
/** 코드 수정 */
#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.3장의 마지막에 AttributeSet 클래스의 헤더파일과 c++의 ShowFloatingText() 함수에 bool 타입의 bBlockedHit 과 bCriticalHit 을 파라미터로 받도록 수정했다.
ShowFloatingText() 함수는 해당 파라미터를 AuraPlayerController 클래스의 ShowDamageNumber() 에 전달하여 데미지를 출력하도록 하는 함수이므로 AuraPlayerController 클래스에서 bBlockedHit 과 bCriticalHit 을 받도록 코드 수정이 필요하다.
#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 타입의 변수를 전달하도록 코드를 작성한다.
#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);
/** 코드 수정 */
}
}
/** 코드 수정 */
...
AuraPlayercontroller 의 SetDamageText() 에서 bool 타입 변수를 받을 수 있도록 미리 코드를 작성하였기 때문에, DamageTextComponent 클래스에서 두 bool 타입 변수를 받을 수 있도록 코드를 수정한다.
( BlueprintImplementation 이므로 c++ 코드 수정 필요x)
#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개를 받도록 코드를 수정한다.
#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_DamageTextComponent 의 Event Set Damage Text 노드를 확인하면 두 개의 bool 타입 변수가 추가된 것을 확인할 수 있다.

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

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


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

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



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


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


IsBlock 과 IsCrit 또한 스파게티화를 방지하기 위해 Block 과 Crit 지역변수로 승격시킨다.


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 을 최대로 낮추면 흰색)
추가
마지막Branch는true,false둘다 연결
최종적으로 모든 OutColor 의 Setter 에 대해 Retune Node 와 연결시킨다.

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

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

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

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

추가
!!!주의!!! :GE_SecondaryAttributes_TEST는 적에게 적용되는GameplayEffect이므로 해당 파일의BlockChance수치를 조절하였음.
캐릭터의 경우GE_SecondaryAttributes를 사용하므로 크리티컬 관련 조정을 위해GE_SecondaryAttributes의 값을 수정해야함
크리티컬 확률이 많이 낮으므로 약간의 수정이 필요
GE_SecondaryAttributes를 살펴보면CriticalHitChance는ArmorPenetration과 관련 있음을 확인할 수 있음.
계수를 수정하여 확률을 올린다
Pre ... : .5,Post ... : 10
ArmorPenetration또한 계수를 수정해준다.
Post ... : 25
확률 증가가 확인되었다면 테스트 진행



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

IsVariable 을 활성화시켜주고

중앙정렬시킨 다음

Justification 도 중앙정렬해주고

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

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

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

아래 테이블에 따라 Transform 값을 변경시킨다.
| sec | x | y |
|---|---|---|
| 0 | 0 | -20 |
| 0.05 | 0 | 30 |
| 0.15 | 0 | -20 |
| 1 | 0 | -35 |




아래 테이블에 따라 Scale 값을 변경시킨다.
| sec | x | y |
|---|---|---|
| 0 | 0 | 0 |
| 0.05 | 1 | 1 |
| 0.15 | 0.6 | 0.6 |
추가
52의 1.5배인 78로Font Size변경



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


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

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

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


두 input 을 Block 과 Crit 지역변수로 승격시키고

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



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

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




Setter 에 대해 Return Node 와 연결시킨다.

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

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

SetText(Text) 노드를 통해 TextHitMessage 를 HitMessage 로 설정시키도록 노드를 추가한다.


추가
Damage도HitDamage지역변수 승격 후 연결
컴파일 후 실행시 각 조건에 따른 텍스트가 정상적으로 출력되는 것을 확인할 수 있다.




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





추가
GA_HitReact에서Instancing Policy : Instanced Per Execution으로 변경
추가TODO
HitReact 발생하지 않는 오류 수정필요 chap.157쯤 발생
먼저 데미지 타입을 지정하기 위한 태그를 추가한다.
#include "..."
struct FAuraGameplayTags
{
public:
...
/*
*InputTags
*/
...
FGameplayTag Damage;
/** 코드 추가 */
FGameplayTag Damage_Fire;
/** 코드 추가 */
...
protected:
private:
...
};
#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 클래스에서 관리할만한 부분은 코드를 이동시킨다.
#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 로 이동시킨다.
#include "..."
UCLASS
class AURA_API UAuraGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
...
/** 코드 이동 : AuraDamageGameplayAbility.h로 이동 */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
FScalableFloat Damage;
/** 코드 이동 */
};
이동시킬 코드를 AuraDamageGameplayAbility 로 옮기고, 데미지타입과 데미지를 묶을 Map을 생성한다.
#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 클래스에서 공격 관련 코드에, 데미지와 데미지 타입을 같이 넘기기 위한 코드를 추가하고 수정한다.
#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 배열을 하나 추가한다.
#include "..."
struct FAuraGameplayTags
{
public:
...
/*
*InputTags
*/
...
FGameplayTag Damage_Fire;
/** 코드 추가 */
TArray<FGameplayTag> DamageTypes;
/** 코드 추가 */
...
protected:
private:
...
};
#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);
/** 코드 추가 */
...
}
#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 는 사용하지 않으므로 삭제한다.
#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에서CriticalHitDamage는ArmorPenetration과 관련있음을 확인
지난번에Floating Text관련해서 색 추가와 텍스트 추가때 테스트를 위해 Post 계수를 25로 지정하였으므로25 -> 16로 수정이 필요
마지막으로ArmorPenetration수치에 따른 과한CriticalHitDamage증가를 방지하기 위한Pre계수1.5 -> 0.25로 수정
컴파일 후 실행시 적당한 수치를 가지는 것 확인
- 추가2
일반 공격시의 데미지 수치가 너무 낮으므로 수치 조절 필요
CT_Damage에서 그래프를 적당히 조절
컴파일 후 실행하면 적절한 수치를 가짐
- 추가3
현재 적은GE_SecondaryAttributes_TEST에서BlockChance가 50이라는 높은 수치이므로 15로 변경
여러 데미지 타입 관련 태그를 추가로 작성한다.
#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:
...
};
#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 클래스에서 사용하던 코드 수정이 필요하다.
#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);
...
}
...
}
속성타입저항 Attribute 를 추가한다.
#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:
...
};
#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_AttributeMenu 의 SECONDARY ATTRIBUTES 에도 추가된 Attribute 를 추가한다.

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

추가
노드가 너무 많아 보기 복잡하므로 함수화하여 정리
마지막으로 DA_AttributeInfo 에서 태그와 이름, 설명을 추가해준다.

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

이제 각 속성저항값을 캡쳐하여 사용하도록 해야 한다.
#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를 걸고 실행하면 정상적으로 수치가 적용되어 데미지가 반으로 줄어드는 것을 확인할 수 있다.


간헐적으로 캐릭터 클래스 관련 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();
}
/** 코드 수정 */
}
...
LoopingSoundComponent 가 nullptr를 참조하는 문제

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

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()) { ... } /** 코드 수정 */ }컴파일 후 실행시 클라이언트측에서도 정상적으로 데미지가 출력되는 것을 확인할 수 있다.
5.3버전으로는 오류 발생x
Chap.162 12:57부분부터 해결 과정
11.6.3 오류 해결 이후 발생하는 문제같은데 5.3 기준 문제없이 투사체에 의해 데미지가 적용됨
혹시몰라 추가작성해둠
AuraProjectile.cpp 의 OnSphereOverlap() 함수에서 문제 발생
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;
/** 코드 추가 */
...
}
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
...
if(CombatInterface)
{
...
/** 코드 삭제 */
Rotaion.Pitch = 0.f;
/** 코드 삭제 */
}
}
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();
}
/** 코드 수정 */
...
}