Creating own Gameplay Effect Context
GAS 공통 참고 강의 및 문서
https://www.udemy.com/course/unreal-engine-5-gas-top-down-rpg/?couponCode=ST15MT31224
https://github.com/tranek/GASDocumentation
GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer s
My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project. - tranek/GASDocumentation
github.com
https://docs.unrealengine.com/5.1/en-US/gameplay-ability-system-for-unreal-engine/
Gameplay Ability System
High-level view of the Gameplay Ability System
docs.unrealengine.com
주의 : 틀린 내용이 있을 수 있음. 5.1.1 버전으로 작성
GameplayEffectTypes.h에서 GameplayEffectContext에 대해 아래와 같이 설명한다.
USTRUCT()
struct GAMEPLAYABILITIES_API FGameplayEffectContext
/**
* Data structure that stores an instigator and related data, such as positions and targets
* Games can subclass this structure and add game-specific information
* It is passed throughout effect execution so it is a great place to track transient information about an execution
*/
GASDocumentation에서는 아래와 같이 설명한다.
GameplayEffectContext 구조체는 GameplayEffectSpec의 Instagator와TargetData에 대한 정보를 저장한다.
또한 ModifierMagnitudeCalculations/GameplayEffectExecutionCalculations, AttributeSets 및 GameplayCues와
같은 곳 사이에 임의의 데이터를 전달하는 데 사용할 수 있는 좋은 구조체이다.
즉 Gameplay Effect Context에서는 GAS 프레임워크 안에서 기본적인 Instigator, AbilityLevel등과 같은 멤버 변수, 함수들이 이미 잘 제공되어 있으며 데이터 (GameplayEffect) 전달 과정에서 유용하게 사용할 수 있다.
하지만 개발 과정에서 GAS 프레임워크안에서 사용할 멤버 변수와 함수들이 필요하다면(예를들어 EffectContext에서 같이 전달 할 변수를 추가하는 등..) 이를 위해 Gameplay Effect Context의 새로운 서브클래스를 만들 수 있다.
그 과정을 요약하자면 다음과 같다.
1. FGameplayEffectContext의 서브 클래스를 생성한다.
2. FGameplayEffectContext::GetScriptStruct()을 오버라이드한다.
3. FGameplayEffectContext::Duplicate()를 오버라이드한다.
4. 새 데이터가 복제되어야 할 경우 FGameplayEffectContext::NetSerialize()를 오버라이드한다.
5. 부모 구조체인 FGameplayEffectContext와 같이 서브클래스에 대한 TStructOpsTypeTraits를 구현한다.
6. AbilitySystemGlobals 클래스에서 AllocGameplayEffectContext()를 오버라이드하여 서브클래스의 새 객체를 반환한다.
1. FGameplayEffectContext의 서브 클래스를 생성.
우선 FGameplyEffectContext는 UCLASS가 아니라 USTRUCT기 때문에, 에디터에서 클래스를 생성하는 대신에, 구조체를 작성할 헤더와 cpp 파일이 필요하다.
예를 들어 ProjectAbilityTypes.h와 ProjectAbilityTypes.cpp (사진에선 AuraAbilityTypes)를 만든다. Visual Studio의 경우 솔루션 탐색기에서 직접 추가하여 만들 수 있다.
이제 FGameplayEffectContext에서 파생된 새로운 FGameplayEffectContext 구조체를 만들 수 있다. 아래는 새롭게 만든 FAuraGameplayEffectContext 헤더파일이다. 아래는 헤더 파일 전문이다.
#pragma once
#include "GameplayEffectTypes.h"
#include "AuraAbilityTypes.generated.h"
USTRUCT(BlueprintType)
struct FAuraGameplayEffectContext : public FGameplayEffectContext
{
GENERATED_BODY()
public:
bool IsCriticalHit() const { return bIsCriticalHit; }
bool IsBlockedHit() const { return bIsBlockedHit; }
void SetIsCriticalHit(bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }
void SetIsBlockedHit(bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }
/** Custom serialization, subclasses must override this */
virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
/** Creates a copy of this context, used to duplicate for later modifications */
virtual FAuraGameplayEffectContext* Duplicate() const override
{
FAuraGameplayEffectContext* NewContext = new FAuraGameplayEffectContext();
*NewContext = *this;
if (GetHitResult())
{
// Does a deep copy of the hit result
NewContext->AddHitResult(*GetHitResult(), true);
}
return NewContext;
}
UScriptStruct* FAuraGameplayEffectContext::GetScriptStruct() const
{
return FAuraGameplayEffectContext::StaticStruct();
}
protected:
UPROPERTY()
bool bIsBlockedHit = false;
UPROPERTY()
bool bIsCriticalHit = false;
};
template<>
struct TStructOpsTypeTraits<FAuraGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FAuraGameplayEffectContext>
{
enum
{
WithNetSerializer = true,
WithCopy = true
};
};
GameplayContext에서 IsCriticalHit과 IsBlockedHit이라는 두개의 bool 변수를 추가로 전달하기 위해 새로운 FGameplayContext인 FAuraGameplayContext 구조체를 만들었다.
2. FGameplayEffectContext::GetScriptStruct()을 오버라이드.
우선 GetScriptStruct함수를 오버라이드 해야한다.
/** Returns the actual struct used for serialization, subclasses must override this! */
virtual UScriptStruct* GetScriptStruct() const
{
return FGameplayEffectContext::StaticStruct();
}
3. FGameplayEffectContext::Duplicate()를 오버라이드.
또한 Duplicate 함수를 오버라이드 해야한다.
실수로 2, 3단계을 생략하고 NetSerialize 함수 오버라이드만 할 수 있는데, 올바른 GAS 프레임워크 내에서 동작하기 위해 반드시 필요하다.
/** Creates a copy of this context, used to duplicate for later modifications */
virtual FAuraGameplayEffectContext* Duplicate() const override
{
FAuraGameplayEffectContext* NewContext = new FAuraGameplayEffectContext();
*NewContext = *this;
if (GetHitResult())
{
// Does a deep copy of the hit result
NewContext->AddHitResult(*GetHitResult(), true);
}
return NewContext;
}
4. FGameplayEffectContext::NetSerialize()를 오버라이드.
GAS는 기본적으로 네트워크 기능도 포함하기 때문에, 이 프레임워크를 확장해서 사용할 때는 네트워크 관련 내부 코드도 확장해 작성할 수도 있다.
따라서 새로운 FGameplayEffectContext 구조체를 만들 때 가장 중요한 부분으로
NetSerialize 함수를 오버라이드 하고, Context내에서 추가한 변수들에 대한 복제(Replicate)를 처리 해야한다.
#include "AuraAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
if (Ar.IsSaving())
{
if (Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (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;
}
if (bIsBlockedHit)
{
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
RepBits |= 1 << 8;
}
}
Ar.SerializeBits(&RepBits, 9);
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;
}
if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
위는 cpp파일 전문이다.
FArchive : Base class for archives that can be used for loading, saving, and garbage collecting in a byte order neutral way.
바이트 순서 중립 방식으로 로드, 저장 및 가비지 수집에 사용할 수 있는 아카이브의 기본 클래스이다.
UPackageMap이란
bOutSuccess란
NetSerialize 함수 동작
FGameplayEffectContext::NetSerialize()함수를 살펴보면 이미 GameplayEffectContext에서 직렬화해야하여 전송해야할 멤버변수들에 대하여 비트 직렬화가 되어있다.
ㄴ복제를 위한 비트 연산 보충 설명
커스텀 변수 복제를 위해 필요한 것..
설명 추후 작성
5. TStructOpsTypeTraits를 구현한다.
template<>
struct TStructOpsTypeTraits<FAuraGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FAuraGameplayEffectContext>
{
enum
{
WithNetSerializer = true,
WithCopy = true
};
};
6. AllocGameplayEffectContext()를 오버라이드.
마지막으로 새로운 커스텀 FGameplayEffectContext를 사용하기 위해 프로젝트 별 AbilitySystemGlobal에 아래 함수를 오버라이드 한다.
// AbilitySystemGlobals.h
/** Should allocate a project specific GameplayEffectContext struct. Caller is responsible for deallocation */
virtual FGameplayEffectContext* AllocGameplayEffectContext() const;
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemGlobals.h"
#include "AuraAbilitySystemGlobals.generated.h"
/**
*
*/
UCLASS()
class AURA_API UAuraAbilitySystemGlobals : public UAbilitySystemGlobals
{
GENERATED_BODY()
virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;
};
#include "AbilitySystem/AuraAbilitySystemGlobals.h"
#include "AuraAbilityTypes.h"
FGameplayEffectContext* UAuraAbilitySystemGlobals::AllocGameplayEffectContext() const
{
return new FAuraGameplayEffectContext();
}
마지막으로 오버라이드 한 함수에서 이제 FGameplayEffectContext 대신 FAuraGameplayEffectContext (새롭게 만든 커스텀 클래스)를 반환하게 만든다.
빌드 후 에디터를 다시 시작하면 엔진에서 새로운 커스텀 FGameplayEffectContext를 사용한다.
예를들어 이제 FGameplayEffectContext에 bBlockingHit, bIsCriticalHit 변수를 담아서 사용가능하다. Effect를 적용할 때 bBlockingHit (공격을 막았는 지)를 기준으로 로직을 추가로 작성가능하다.
물론 새로운 EffectContext를 파생하지 않고서도 다른방법으로도 구현가능하다. 하지만 GAS 프레임워크는 강력하기에 잘 사용한다면 좋을 것 같다.