Unreal GAS (33) - Execution Calculation, Custom FGameplayEffectContext

wnsduf0000·2025년 12월 1일

Unreal_GAS

목록 보기
34/34
  • Execution Calculation - Implementing Attributes into Calculation
    • GameplayEffect 블루프린트 에디터에서 직접 Modifier의 Magnitude Calculation Type을 SetByCaller로 설정하여 사용하는 것이 아닌, UGameplayEffectExecutionCalculation에서 사용하도록 수정하는 작업
      /* Get Set By Caller Magnitude from FGameplayEffectSpec */
      // In Case of ProjectileSpell, SetByCallerMagnitude was set by FGameplayAbility(AuraProjectileSpell - AssignTagSetByCallerMagnitude())
      float Damage = EffectSpec.GetSetByCallerMagnitude(AuraGameplayTags.Damage);
      • 기존에는 GameplayAbility에서 영향을 줄 Attribute와 SetByCaller Magnitude 및 DataTag가 설정된 GameplayEffect의 Magnitude 값을 UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude()을 통해 설정하고, 이를 AttributeSet에서 반응하게 함으로서 데미지 계산을 수행하는 로직을 만들었었음.
        (2025 / 11 / 21 - Meta Attribute)
        - 하지만 이제 UGameplayEffectExecutionCalculation 내에서 SetByCaller에 대한 처리를 직접 수행할 것이므로, GameplayEffect 블루프린트의 Modifier 설정을 제거함.
    • UGameplayEffectExeculationCalculation 서브 클래스 작성’ 에서 처리했던 것을 바탕으로,
      다른 Attribute들 또한 UGameplayEffectExecutionCalculation에서 접근하여 복잡한 데미지 계산을 수행함
      - 기존 작성한 AuraDamageStatics 구조체에 참조할 변수들을 위한 매크로를 작성함.
      Source와 Target, 그리고 Snapshot 여부를 고려하여 추가하면 됨.
            struct AuraDamageStatics
            {
            	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
            	DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
            	DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
            	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitChance);
            	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitDamage);
            	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitResistance);
            
            	AuraDamageStatics()
            	{
            		DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
            		DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArmorPenetration, Source, false);
            		DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false);
            		DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitChance, Source, false);
            		DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitDamage, Source, false);
            		DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitResistance, Target, false);
            	}
            };
            
            UExecCalc_Damage::UExecCalc_Damage()
            {
            	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
            	RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
            	RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
            	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitChanceDef);
            	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitDamageDef);
            	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitResistanceDef);
            }
      • 데미지 계산에 있어 특정 Attribute에 대해서는 상황에 따라 다른 계수(Coefficient)를 적용하기 위하여 CharacterClassInfo에 관련 CurveTable을 생성하고,
        CharacterClassInfo에 쉽게 접근하기 위한 함수를 UAuraAbilitySystemLibrary에 생성함.
             // AuraAbilitySystemLibrary.cpp
             UCharacterClassInfo* UAuraAbilitySystemLibrary::GetCharacterClassInfo(const UObject* WorldContextObject)
             {
             	AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
             	if (AuraGameMode == nullptr) return nullptr;
             	
             	return AuraGameMode->CharacterClassInfo;
             }
           /* Get Coefficients from CharacterClassInfo */
           UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
           
           // Armor Penetration
           const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("ArmorPenetration"), FString());
           const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
           
           // Effective Armor (Coefficient for Armor - ArmorPenetration)
           const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("EffectiveArmor"), FString());
           const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(SourceCombatInterface->GetPlayerLevel());
           
           // Effective CriticalHitChance (Coefficient for CriticalHitChance - CriticalHitResistance)
           const FRealCurve* EffectiveCriticalHitChanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("EffectiveCriticalHitChance"), FString());
           const float EffectiveCriticalHitChanceCoefficient = EffectiveCriticalHitChanceCurve->Eval(SourceCombatInterface->GetPlayerLevel());
           
        • 참조한 모든 Attribute들과 CurveTable등을 이용하여 실제 데미지 계산을 수행하면 됨.
             void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
             {
             	// 필요한 레퍼런스 획득
             	const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
             	const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
             	
             	AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
             	AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
             	ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
             	ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
             
             	const FGameplayEffectSpec& EffectSpec = ExecutionParams.GetOwningSpec();
             	FAuraGameplayTags AuraGameplayTags = FAuraGameplayTags::Get();
             
             	FAggregatorEvaluateParameters EvaluationParameters;
             	EvaluationParameters.SourceTags = EffectSpec.CapturedSourceTags.GetAggregatedTags();
             	EvaluationParameters.TargetTags = EffectSpec.CapturedTargetTags.GetAggregatedTags();
             
             	/* Get Set By Caller Magnitude from FGameplayEffectSpec */
             	// In Case of ProjectileSpell, SetByCallerMagnitude was set by FGameplayAbility(AuraProjectileSpell - AssignTagSetByCallerMagnitude())
             	float Damage = EffectSpec.GetSetByCallerMagnitude(AuraGameplayTags.Damage);
             
             	// Attribute 캡쳐 및 보정 (PreAttributeChange()에서 처리하는 것과 동일하게)
             	/* Capture Attribute From Target and Source */
             	float TargetBlockChance = 0.f;
             	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters, TargetBlockChance);
             	
             	float TargetArmor = 0.f;
             	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, TargetArmor);
             	TargetArmor = FMath::Max<float>(0.f, TargetArmor);
             
             	float SourceArmorPenetration = 0.f;
             	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef, EvaluationParameters, SourceArmorPenetration);
             	SourceArmorPenetration = FMath::Max<float>(0.f, SourceArmorPenetration);
             
             	float SourceCriticalHitChance = 0.f;
             	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef, EvaluationParameters, SourceCriticalHitChance);
             	SourceArmorPenetration = FMath::Max<float>(0.f, SourceCriticalHitChance);
             
             	float TargetCriticalHitResistance = 0.f;
             	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef, EvaluationParameters, TargetCriticalHitResistance);
             	SourceArmorPenetration = FMath::Max<float>(0.f, TargetCriticalHitResistance);
             
             	float SourceCriticalHitDamage = 0.f;
             	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef, EvaluationParameters, SourceCriticalHitDamage);
             	SourceArmorPenetration = FMath::Max<float>(0.f, SourceCriticalHitDamage);
             
             	// 커브 데이터 ...
             
             	// 데미지 계산 수행
             	/* Calculate Damage */
             	// 1. Calculate Block Chance, if Blocked, reduce Damage by 50%
             	const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
             	Damage = bBlocked ? Damage * 0.5f : Damage;
             
             	// 2. Calculate Effective Armor by reducing Target's Armor by Source's Armor Penetration
             	// Reduce Damage by Effective Armor * Coefficient
             	const float EffectiveArmor = TargetArmor *= (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
             	Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
             
             	// 3. Calculate Critical Hit Chance and Damage
             	// Critical Hit Chance = Source's Critical Hit Chance - Target's Critical Hit Resistance
             	// If Critical Hit occurs, Double Damage + Critical Hit Damage
             	const float EffectiveCriticalHitChance = FMath::Max<float>(0.f, SourceCriticalHitChance - TargetCriticalHitResistance * EffectiveCriticalHitChanceCoefficient);
             	const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
             	Damage = bCriticalHit ? (Damage * 2) + SourceCriticalHitDamage : Damage;
             
             	// 계산된 데미지 전송
             	// Use as much FGameplayModifierEvaluatedData as we want
             	// FGameplayModifierEvaluatedData(FGameplayAttibute AttributeToModify, EGameplayModOp ModificationType, float Magnitude)
             	FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(), EGameplayModOp::Additive, Damage);
             	OutExecutionOutput.AddOutputModifier(EvaluatedData);
             }
  • Gameplay Effect Context - Make Custom Effect Context
    • FGameplayEffectContext에 대해서 더 자세히 알아보고, 이를 상속하여 확장하는 방법을 알아본다.
    • FGameplayEffectContext 및 FGameplayEffectContextHandle이 제공하는 기능 및 한계
      - SourceObject, Actors, HitResults 등 여러 타입의 변수를 내장하고 자유롭게 사용할 수 있는 함수까지 제공한다.
      - 사용자는 이 변수들을 활용하여 담고자 하는 여러 정보를 자유롭게 담을 수 있다.
      ```cpp
      // FGameplayEffectContextHandle
      // Handle that wraps a FGameplayEffectContext or subclass, to allow it to be polymorphic and replicate properly
      
      USTRUCT(BlueprintType)
      struct GAMEPLAYABILITIES_API FGameplayEffectContextHandle
      {
      	GENERATED_USTRUCT_BODY()
      
      	FGameplayEffectContextHandle()
      	{
      	}
      
      	virtual ~FGameplayEffectContextHandle()
      	{
      	}
      
      	/** Constructs from an existing context, should be allocated by new */
      	explicit FGameplayEffectContextHandle(FGameplayEffectContext* DataPtr)
      	{
      		Data = TSharedPtr<FGameplayEffectContext>(DataPtr);
      	}
      
      	/** Sets from an existing context, should be allocated by new */
      	void operator=(FGameplayEffectContext* DataPtr)
      	{
      		Data = TSharedPtr<FGameplayEffectContext>(DataPtr);
      	}
      
      	void Clear()
      	{
      		Data.Reset();
      	}
      
      	bool IsValid() const
      	{
      		return Data.IsValid();
      	}
      
      	/** Returns Raw effet context, may be null */
      	FGameplayEffectContext* Get()
      	{
      		return IsValid() ? Data.Get() : nullptr;
      	}
      	const FGameplayEffectContext* Get() const
      	{
      		return IsValid() ? Data.Get() : nullptr;
      	}
      
      	/** Returns the list of gameplay tags applicable to this effect, defaults to the owner's tags */
      	void GetOwnedGameplayTags(OUT FGameplayTagContainer& ActorTagContainer, OUT FGameplayTagContainer& SpecTagContainer) const
      	{
      		if (IsValid())
      		{
      			Data->GetOwnedGameplayTags(ActorTagContainer, SpecTagContainer);
      		}
      	}
      
      	/** Sets the instigator and effect causer. Instigator is who owns the ability that spawned this, EffectCauser is the actor that is the physical source of the effect, such as a weapon. They can be the same. */
      	void AddInstigator(class AActor *InInstigator, class AActor *InEffectCauser)
      	{
      		if (IsValid())
      		{
      			Data->AddInstigator(InInstigator, InEffectCauser);
      		}
      	}
      
      	/** Sets Ability instance and CDO parameters on context */
      	void SetAbility(const UGameplayAbility* InGameplayAbility)
      	{
      		if (IsValid())
      		{
      			Data->SetAbility(InGameplayAbility);
      		}
      	}
      
      	/** Returns the immediate instigator that applied this effect */
      	virtual AActor* GetInstigator() const
      	{
      		if (IsValid())
      		{
      			return Data->GetInstigator();
      		}
      		return nullptr;
      	}
      
      	/** Returns the Ability CDO */
      	const UGameplayAbility* GetAbility() const
      	{
      		if (IsValid())
      		{
      			return Data->GetAbility();
      		}
      		return nullptr;
      	}
      
      	/** Returns the Ability Instance (never replicated) */
      	const UGameplayAbility* GetAbilityInstance_NotReplicated() const
      	{
      		if (IsValid())
      		{
      			return Data->GetAbilityInstance_NotReplicated();
      		}
      		return nullptr;
      	}
      
      	/** Returns level this was executed at */
      	int32 GetAbilityLevel() const
      	{
      		if (IsValid())
      		{
      			return Data->GetAbilityLevel();
      		}
      		return 1;
      	}
      
      	/** Returns the ability system component of the instigator of this effect */
      	virtual UAbilitySystemComponent* GetInstigatorAbilitySystemComponent() const
      	{
      		if (IsValid())
      		{
      			return Data->GetInstigatorAbilitySystemComponent();
      		}
      		return nullptr;
      	}
      
      	/** Returns the physical actor tied to the application of this effect */
      	virtual AActor* GetEffectCauser() const
      	{
      		if (IsValid())
      		{
      			return Data->GetEffectCauser();
      		}
      		return nullptr;
      	}
      
      	/** Should always return the original instigator that started the whole chain. Subclasses can override what this does */
      	AActor* GetOriginalInstigator() const
      	{
      		if (IsValid())
      		{
      			return Data->GetOriginalInstigator();
      		}
      		return nullptr;
      	}
      
      	/** Returns the ability system component of the instigator that started the whole chain */
      	UAbilitySystemComponent* GetOriginalInstigatorAbilitySystemComponent() const
      	{
      		if (IsValid())
      		{
      			return Data->GetOriginalInstigatorAbilitySystemComponent();
      		}
      		return nullptr;
      	}
      
      	/** Sets the object this effect was created from. */
      	void AddSourceObject(const UObject* NewSourceObject)
      	{
      		if (IsValid())
      		{
      			Data->AddSourceObject(NewSourceObject);
      		}
      	}
      
      	/** Returns the object this effect was created from. */
      	UObject* GetSourceObject() const
      	{
      		if (IsValid())
      		{
      			return Data->GetSourceObject();
      		}
      		return nullptr;
      	}
      
      	/** Returns if the instigator is locally controlled */
      	bool IsLocallyControlled() const
      	{
      		if (IsValid())
      		{
      			return Data->IsLocallyControlled();
      		}
      		return false;
      	}
      
      	/** Returns if the instigator is locally controlled and a player */
      	bool IsLocallyControlledPlayer() const
      	{
      		if (IsValid())
      		{
      			return Data->IsLocallyControlledPlayer();
      		}
      		return false;
      	}
      
      	/** Add actors to the stored actor list */
      	void AddActors(const TArray<TWeakObjectPtr<AActor>>& InActors, bool bReset = false)
      	{
      		if (IsValid())
      		{
      			Data->AddActors(InActors, bReset);
      		}
      	}
      
      	/** Add a hit result for targeting */
      	void AddHitResult(const FHitResult& InHitResult, bool bReset = false)
      	{
      		if (IsValid())
      		{
      			Data->AddHitResult(InHitResult, bReset);
      		}
      	}
      
      	/** Returns actor list, may be empty */
      	const TArray<TWeakObjectPtr<AActor>> GetActors()
      	{
      		return Data->GetActors();
      	}
      
      	/** Returns hit result, this can be null */
      	const FHitResult* GetHitResult() const
      	{
      		if (IsValid())
      		{
      			return Data->GetHitResult();
      		}
      		return nullptr;
      	}
      
      	/** Adds an origin point */
      	void AddOrigin(FVector InOrigin)
      	{
      		if (IsValid())
      		{
      			Data->AddOrigin(InOrigin);
      		}
      	}
      
      	/** Returns origin point, may be invalid if HasOrigin is false */
      	virtual const FVector& GetOrigin() const
      	{
      		if (IsValid())
      		{
      			return Data->GetOrigin();
      		}
      		return FVector::ZeroVector;
      	}
      
      	/** Returns true if GetOrigin will give valid information */
      	virtual bool HasOrigin() const
      	{
      		if (IsValid())
      		{
      			return Data->HasOrigin();
      		}
      		return false;
      	}
      
      	/** Returns debug string */
      	FString ToString() const
      	{
      		return IsValid() ? Data->ToString() : FString(TEXT("NONE"));
      	}
      
      	/** Creates a deep copy of this handle, used before modifying */
      	FGameplayEffectContextHandle Duplicate() const
      	{
      		if (IsValid())
      		{
      			FGameplayEffectContext* NewContext = Data->Duplicate();
      			return FGameplayEffectContextHandle(NewContext);
      		}
      		else
      		{
      			return FGameplayEffectContextHandle();
      		}
      	}
      
      	/** Comparison operator */
      	bool operator==(FGameplayEffectContextHandle const& Other) const
      	{
      		if (Data.IsValid() != Other.Data.IsValid())
      		{
      			return false;
      		}
      		if (Data.Get() != Other.Data.Get())
      		{
      			return false;
      		}
      		return true;
      	}
      
      	/** Comparison operator */
      	bool operator!=(FGameplayEffectContextHandle const& Other) const
      	{
      		return !(FGameplayEffectContextHandle::operator==(Other));
      	}
      
      	/** Custom serializer, handles polymorphism of context */
      	bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
      
      private:
      	friend UE::Net::FGameplayEffectContextHandleAccessorForNetSerializer;
      
      	TSharedPtr<FGameplayEffectContext> Data;
      };
      
      ```
      
      - FGameplayEffectContextHandle은 FGameplayEffectContext의 멤버에 쉽게 접근할 수 있게 해주는 일종의 래퍼 구조체이다.
          // 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
           */
          USTRUCT()
          struct GAMEPLAYABILITIES_API FGameplayEffectContext
          {
          	GENERATED_USTRUCT_BODY()
          
          	FGameplayEffectContext()
          	: AbilityLevel(1)
          	, WorldOrigin(ForceInitToZero)
          	, bHasWorldOrigin(false)
          	, bReplicateSourceObject(false)
          	, bReplicateInstigator(false)
          	, bReplicateEffectCauser(false)
          	{
          	}
          
          	FGameplayEffectContext(AActor* InInstigator, AActor* InEffectCauser)
          	: AbilityLevel(1)
          	, WorldOrigin(ForceInitToZero)
          	, bHasWorldOrigin(false)
          	, bReplicateSourceObject(false)
          	, bReplicateInstigator(false)
          	, bReplicateEffectCauser(false)
          	{
          		FGameplayEffectContext::AddInstigator(InInstigator, InEffectCauser);
          	}
          
          	virtual ~FGameplayEffectContext()
          	{
          	}
          
          	/** Returns the list of gameplay tags applicable to this effect, defaults to the owner's tags */
          	virtual void GetOwnedGameplayTags(OUT FGameplayTagContainer& ActorTagContainer, OUT FGameplayTagContainer& SpecTagContainer) const;
          
          	/** Sets the instigator and effect causer. Instigator is who owns the ability that spawned this, EffectCauser is the actor that is the physical source of the effect, such as a weapon. They can be the same. */
          	virtual void AddInstigator(class AActor *InInstigator, class AActor *InEffectCauser);
          
          	/** Sets the ability that was used to spawn this */
          	virtual void SetAbility(const UGameplayAbility* InGameplayAbility);
          
          	/** Returns the immediate instigator that applied this effect */
          	virtual AActor* GetInstigator() const
          	{
          		return Instigator.Get();
          	}
          
          	/** Returns the CDO of the ability used to instigate this context */
          	const UGameplayAbility* GetAbility() const;
          
          	/** Returns the specific instance that instigated this, may not always be set */
          	const UGameplayAbility* GetAbilityInstance_NotReplicated() const;
          
          	/** Gets the ability level this was evaluated at */
          	int32 GetAbilityLevel() const
          	{
          		return AbilityLevel;
          	}
          
          	/** Returns the ability system component of the instigator of this effect */
          	virtual UAbilitySystemComponent* GetInstigatorAbilitySystemComponent() const
          	{
          		return InstigatorAbilitySystemComponent.Get();
          	}
          
          	/** Returns the physical actor tied to the application of this effect */
          	virtual AActor* GetEffectCauser() const
          	{
          		return EffectCauser.Get();
          	}
          
          	/** Modify the effect causer actor, useful when that information is added after creation */
          	void SetEffectCauser(AActor* InEffectCauser)
          	{
          		EffectCauser = InEffectCauser;
          		bReplicateEffectCauser = CanActorReferenceBeReplicated(InEffectCauser);
          	}
          
          	/** Should always return the original instigator that started the whole chain. Subclasses can override what this does */
          	virtual AActor* GetOriginalInstigator() const
          	{
          		return Instigator.Get();
          	}
          
          	/** Returns the ability system component of the instigator that started the whole chain */
          	virtual UAbilitySystemComponent* GetOriginalInstigatorAbilitySystemComponent() const
          	{
          		return InstigatorAbilitySystemComponent.Get();
          	}
          
          	/** Sets the object this effect was created from. */
          	virtual void AddSourceObject(const UObject* NewSourceObject)
          	{
          		SourceObject = MakeWeakObjectPtr(const_cast<UObject*>(NewSourceObject));
          		bReplicateSourceObject = NewSourceObject && NewSourceObject->IsSupportedForNetworking();
          	}
          
          	/** Returns the object this effect was created from. */
          	virtual UObject* GetSourceObject() const
          	{
          		return SourceObject.Get();
          	}
          
          	/** Add actors to the stored actor list */
          	virtual void AddActors(const TArray<TWeakObjectPtr<AActor>>& IActor, bool bReset = false);
          
          	/** Add a hit result for targeting */
          	virtual void AddHitResult(const FHitResult& InHitResult, bool bReset = false);
          
          	/** Returns actor list, may be empty */
          	virtual const TArray<TWeakObjectPtr<AActor>>& GetActors() const
          	{
          		return Actors;
          	}
          
          	/** Returns hit result, this can be null */
          	virtual const FHitResult* GetHitResult() const
          	{
          		return const_cast<FGameplayEffectContext*>(this)->GetHitResult();
          	}
          
          	/** Returns hit result, this can be null */
          	virtual FHitResult* GetHitResult()
          	{
          		return HitResult.Get();
          	}
          
          	/** Adds an origin point */
          	virtual void AddOrigin(FVector InOrigin);
          
          	/** Returns origin point, may be invalid if HasOrigin is false */
          	virtual const FVector& GetOrigin() const
          	{
          		return WorldOrigin;
          	}
          
          	/** Returns true if GetOrigin will give valid information */
          	virtual bool HasOrigin() const
          	{
          		return bHasWorldOrigin;
          	}
          
          	/** Returns debug string */
          	virtual FString ToString() const;
          
          	/** Returns the actual struct used for serialization, subclasses must override this! */
          	virtual UScriptStruct* GetScriptStruct() const
          	{
          		return FGameplayEffectContext::StaticStruct();
          	}
          
          	/** 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;
          	}
          
          	/** True if this was instigated by a locally controlled actor */
          	virtual bool IsLocallyControlled() const;
          
          	/** True if this was instigated by a locally controlled player */
          	virtual bool IsLocallyControlledPlayer() const;
          
          	/** Custom serialization, subclasses must override this */
          	virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
          
          protected:
          	static bool CanActorReferenceBeReplicated(const AActor* Actor);
          
          	// The object pointers here have to be weak because contexts aren't necessarily tracked by GC in all cases
          
          	/** Instigator actor, the actor that owns the ability system component */
          	UPROPERTY()
          	TWeakObjectPtr<AActor> Instigator;
          
          	/** The physical actor that actually did the damage, can be a weapon or projectile */
          	UPROPERTY()
          	TWeakObjectPtr<AActor> EffectCauser;
          
          	/** The ability CDO that is responsible for this effect context (replicated) */
          	UPROPERTY()
          	TWeakObjectPtr<UGameplayAbility> AbilityCDO;
          
          	/** The ability instance that is responsible for this effect context (NOT replicated) */
          	UPROPERTY(NotReplicated)
          	TWeakObjectPtr<UGameplayAbility> AbilityInstanceNotReplicated;
          
          	/** The level this was executed at */
          	UPROPERTY()
          	int32 AbilityLevel;
          
          	/** Object this effect was created from, can be an actor or static object. Useful to bind an effect to a gameplay object */
          	UPROPERTY()
          	TWeakObjectPtr<UObject> SourceObject;
          
          	/** The ability system component that's bound to instigator */
          	UPROPERTY(NotReplicated)
          	TWeakObjectPtr<UAbilitySystemComponent> InstigatorAbilitySystemComponent;
          
          	/** Actors referenced by this context */
          	UPROPERTY()
          	TArray<TWeakObjectPtr<AActor>> Actors;
          
          	/** Trace information - may be nullptr in many cases */
          	TSharedPtr<FHitResult>	HitResult;
          
          	/** Stored origin, may be invalid if bHasWorldOrigin is false */
          	UPROPERTY()
          	FVector	WorldOrigin;
          
          	UPROPERTY()
          	uint8 bHasWorldOrigin:1;
          
          	/** True if the SourceObject can be replicated. This bool is not replicated itself. */
          	UPROPERTY(NotReplicated)
          	uint8 bReplicateSourceObject:1;
          	
          	/** True if the Instigator can be replicated. This bool is not replicated itself. */
          	UPROPERTY(NotReplicated)	
          	uint8 bReplicateInstigator:1;
          
          	/** True if the Instigator can be replicated. This bool is not replicated itself. */
          	UPROPERTY(NotReplicated)	
          	uint8 bReplicateEffectCauser:1;
          };
      • Instigator, EffectCauser, AbilityCDO, AbilityInstanceNotReplicated, Actors, HitResult, WorldOrigin와 같은 변수들에 정보를 담을 수 있다.
        AbilitySystemComponent의 MakeEffectContext()를 통해 AddInstigator()가 호출되므로 Instigator, EffectCauser, InstigatorAbilitySystemComponent는 자동적으로 초기화되고,
        이 외 변수들은 제공되는 함수를 통해 생성된 FGameplayEffectContext에서 자유롭게 설정할 수 있다.
      • 단, 자체적으로 FGameplayEffectContext에서 제공하지 않는 타입이라면?
        • 데미지 판정에 의해서 생성되는 위젯 WBP_DamageText에 공격 판정에 대한 추가 정보를 나타내고 싶다.
          예를 들어, Block 판정인 경우 Block 텍스트를 같이 표시하며 데미지 숫자 텍스트의 색상을 바꾸는 등의 연출을 하고 싶다.
        • 문제는 데미지의 Block, Critical 등의 판정은 UGameplayEffectExecutionCalculation에서 진행하는데,
          데미지 텍스트 위젯 출력을 담당하는 AttributeSet의 PostGameplayEffectExecute()에서는 UGameplayEffectExecutionCalculation의 정보에 접근할 수 없다.
          이러한 정보에 접근하기 가장 좋은 경로는 FGameplayEffectContext로 보인다.
          • 왜냐하면, PostGameplayEffectExecute()의 const FGameplayEffectModCallbackData& 매개변수를 통해
            FGameplayEffectModCallbackData의 멤버변수 EffectSpec(FGameplayEffectSpec)에 접근할 수 있는데,
            EffectSpec은 EffectContext를 멤버 변수로 갖기 때문이다.
        • 그런데 AttributeSet의 PostGameplayEffectExecute() 호출 시점에 데미지 판정에 대한 정보를 제공하려면 판정 여부를 저장하기 위한 bool 값 등이 필요한데,
          FGameplayEffectContext는 그러한 타입을 제공하지는 않는다.
          ```cpp
          struct FGameplayEffectModCallbackData
          {
          	FGameplayEffectModCallbackData(const FGameplayEffectSpec& InEffectSpec, FGameplayModifierEvaluatedData& InEvaluatedData, UAbilitySystemComponent& InTarget)
          		: EffectSpec(InEffectSpec)
          		, EvaluatedData(InEvaluatedData)
          		, Target(InTarget)
          	{
          	}
          
          	const struct FGameplayEffectSpec&		EffectSpec;		// The spec that the mod came from
          	struct FGameplayModifierEvaluatedData&	EvaluatedData;	// The 'flat'/computed data to be applied to the target
          
          	class UAbilitySystemComponent &Target;		// Target we intend to apply to
          };
          ```
          
          - 따라서 FGameplayEffectContext를 활용하면서도 원하는 정보를 전달하기 위해서 FGameplayEffectContext를 상속하는 구조체를 생성한다.
    • Custom FGameplayEffectContext (Substruct of FGameplayEffectContext)
      • FGameplayEffectContext를 상속한 커스텀 GameplayEffectContext 생성한다.
            #pragma once
            
            #include "GameplayEffectTypes.h"
            #include "AuraAbilityTypes.generated.h"
            
            USTRUCT(BlueprintType)
            struct FAuraGameplayEffectContext : public FGameplayEffectContext
            {
            	GENERATED_BODY()
            
            public:
            
            	bool IsBlockedHit() const { return bIsBlockedHit; }
            	void SetIsBlockedHit(const bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }
            
            	bool IsCriticalHit() const { return bIsCriticalHit; }
            	void SetIsCriticalHit(const bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }
            
            	/** Returns the actual struct used for serialization, subclasses must override this! */
            	virtual UScriptStruct* GetScriptStruct() const override
            	{
            		return FAuraGameplayEffectContext::StaticStruct();
            	}
            
            	/** Custom serialization, subclasses must override this */
            	virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) override;
            
            protected:
            
            	UPROPERTY()
            	bool bIsBlockedHit = false;
            
            	UPROPERTY()
            	bool bIsCriticalHit = false;
            };
        • FAuraGameplayEffectContext는 기존 FGameplayEffectContext에 더해, 데미지 계산에 사용되는 변수들을 추가했다.
          FAuraGameplayEffectContext를 기존 FGameplayEffectContext 대신 사용하고, 여기에 Block, Critical 등 데미지 판정 정보들을 함께 담아 넘기면
          AttributeSet의 PostGameplayEffectExecute()에서 이를 활용할 수 있을 것이다.
        • FGameplayEffectContext 상속 구조체를 정의하기 위해선 반드시 오버라이드 해야 하는 함수 GetScriptStruct()와 NetSerialize()가 존재한다.
          아래는 FGameplayEffectContext에 정의된 GetScriptStruct()와 NetSerialize()이다.
          • GetScriptStruct()
             /** Returns the actual struct used for serialization, subclasses must override this! */
             virtual UScriptStruct* GetScriptStruct() const
             {
             	return FGameplayEffectContext::StaticStruct();
             }
            • 언리얼의 Reflection System에서의 동작을 위해 UObject 파생 클래스는 UClass가 생성된다.
              클래스가 아닌 구조체인 UStruct의 경우는 ScriptStruct가 생성된다.
            • StaticStruct()는 StaticClass()와 비슷한 역할을 하는 함수이다.
            • 오버라이드 된 GetScriptStruct()의 경우는 단순하게 return을 FGameplayEffectContext::StaticStruct() 대신 FAuraGameplayEffectContext::StaticStruct()로 변경해주면 된다.
          • NetSerialize()
             bool FGameplayEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
             {
             	uint8 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;
             		}
             	}
             
             	Ar.SerializeBits(&RepBits, 7);
             
             	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 (Ar.IsLoading())
             	{
             		AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
             	}	
             	
             	bOutSuccess = true;
             	return true;
             }
            • 데이터 저장, 네트워크 전송 등의 처리를 위해 직렬화를 하는 함수이다.
              비트 연산을 통해 기존 FGameplayEffectContext에 존재하던 멤버 변수들을 처리하는 것을 볼 수 있다.
              단, FAuraGameplayEffectContext는 멤버 변수가 더 추가되었기 때문에, 이 함수를 오버라이드 하여 추가된 멤버변수에 대한 처리까지 해 주어야 한다.
               bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
               {
               	// 저장해야 할 변수의 종류가 늘어 uint8(8비트)만으로는 크기가 부족하므로, uint32를 사용
               	uint32 RepBits = 0;
               
               	// FArchive가 저장인지 확인
               	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;
               		}
               
               		// 추가한 변수들에 대응하도록 비트 연산 추가
               		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;
               }
    • Struct Ops Type Traits
       template<>
       struct TStructOpsTypeTraits< FGameplayEffectContext > : public TStructOpsTypeTraitsBase2< FGameplayEffectContext >
       {
       	enum
       	{
       		WithNetSerializer = true,
       		WithCopy = true		// Necessary so that TSharedPtr<FHitResult> Data is copied around
       	};
       };
      • 커스텀 FGameplayEffectContext를 만들어 Serialize 가능하게 하기 위해서는 하나 더 거쳐야 할 단계가 있는데, 그것이 바로 TStructOpsTypeTraits이다.
      • 템플릿에 명시된 구조체가 어떤 것을 할 수 있는지를 명시하는 구조체라고 보면 된다.
        부모 구조체인 TStructOpsTypeTraitsBase2를 살펴보면 아래와 같다.
        다만 FGameplayEffectContext가 오직 WithNetSerializer와 WithCopy만을 사용하고 있기 때문에, 커스텀 FGameplayEffectContext에서도 마찬가지로 하면 된다.
        • WithNetSerialize: NetSerialize()를 사용하는 구조체

        • WithCopy: 복사하여 할당 가능한 연산을 지원하는 구조체 (Duplicate() 함수를 FAuraGameplayEffectContext에 추가하면 됨)

          /** type traits to cover the custom aspects of a script struct **/
          template <class CPPSTRUCT>
          struct TStructOpsTypeTraitsBase2
          {
          	enum
          	{
          		WithZeroConstructor            = false,                         // struct can be constructed as a valid object by filling its memory footprint with zeroes.
          		WithNoInitConstructor          = false,                         // struct has a constructor which takes an EForceInit parameter which will force the constructor to perform initialization, where the default constructor performs 'uninitialization'.
          		WithNoDestructor               = false,                         // struct will not have its destructor called when it is destroyed.
          		WithCopy                       = !TIsPODType<CPPSTRUCT>::Value, // struct can be copied via its copy assignment operator.
          		WithIdenticalViaEquality       = false,                         // struct can be compared via its operator==.  This should be mutually exclusive with WithIdentical.
          		WithIdentical                  = false,                         // struct can be compared via an Identical(const T* Other, uint32 PortFlags) function.  This should be mutually exclusive with WithIdenticalViaEquality.
          		WithExportTextItem             = false,                         // struct has an ExportTextItem function used to serialize its state into a string.
          		WithImportTextItem             = false,                         // struct has an ImportTextItem function used to deserialize a string into an object of that class.
          		WithAddStructReferencedObjects = false,                         // struct has an AddStructReferencedObjects function which allows it to add references to the garbage collector.
          		WithSerializer                 = false,                         // struct has a Serialize function for serializing its state to an FArchive.
          		WithStructuredSerializer       = false,                         // struct has a Serialize function for serializing its state to an FStructuredArchive.
          		WithPostSerialize              = false,                         // struct has a PostSerialize function which is called after it is serialized
          		WithNetSerializer              = false,                         // struct has a NetSerialize function for serializing its state to an FArchive used for network replication.
          		WithNetDeltaSerializer         = false,                         // struct has a NetDeltaSerialize function for serializing differences in state from a previous NetSerialize operation.
          		WithSerializeFromMismatchedTag = false,                         // struct has a SerializeFromMismatchedTag function for converting from other property tags.
          		WithStructuredSerializeFromMismatchedTag = false,               // struct has an FStructuredArchive-based SerializeFromMismatchedTag function for converting from other property tags.
          		WithPostScriptConstruct        = false,                         // struct has a PostScriptConstruct function which is called after it is constructed in blueprints
          		WithNetSharedSerialization     = false,                         // struct has a NetSerialize function that does not require the package map to serialize its state.
          		WithGetPreloadDependencies     = false,                         // struct has a GetPreloadDependencies function to return all objects that will be Preload()ed when the struct is serialized at load time.
          		WithPureVirtual                = false,                         // struct has PURE_VIRTUAL functions and cannot be constructed when CHECK_PUREVIRTUALS is true
          		WithCanEditChange			   = false,							// struct has an editor-only CanEditChange function that can conditionally make child properties read-only in the details panel (same idea as UObject::CanEditChange)
          	};
          };
          /** 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;
          }
      • 따라서, FAuraGameplayEffectContext 구조체 아래에 새로운 TStructOpsTypeTraits 구조체를 생성한다.
        템플릿으로는 FGameplayEffectContext가 아닌 FAuraGameplayEffectContext를 넘겨주면 된다.
        ```cpp
        template<>
        struct TStructOpsTypeTraits<FAuraGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FAuraGameplayEffectContext>
        {
        	enum
        	{
        		WithCopy = true,
        		WithNetSerializer = true
        	};;
        };
        ```
      • 언리얼 엔진 5.3 이후 기준 변경점
        /** Returns the actual struct used for serialization, subclasses must override this! */
        virtual UScriptStruct* GetScriptStruct() const override
        {
        	// 5.3 버전 이후, 구조체이름::StaticStruct()가 아닌 그냥 StaticStruct()를 반환.
        	return StaticStruct();
        }
        
        /** Creates a copy of this context, used to duplicate for later modifications */
        // 5.3 버전 이후, 원래 FGameplayEffectContext* 였던 반환형을 직접 작성한 구조체와 일치하도록 변경.
        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;
        }
    • Ability System Globals
      • FAuraGameplayEffectContext를 실제로 사용하기 위해선 AbilitySystem에서
        FGameplayEffectContext 대신 FAuraGameplayEffectContext를 사용하도록 명시하는 과정이 필요하다.
        이것을 처리할 수 있는 클래스가 바로 AbilitySystemGlobals이다.
      • AbilitySystemGlobals의 상속 클래스인 AuraAbilitySystemGlobals 클래스를 생성하고,
        여기서 AllocGameplayEffectContext() 함수를 오버라이드 한다.
        ```cpp
        #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;
        };
        
        // AuraAbilitySystemGlobals.cpp
        #include "AbilitySystem/AuraAbilitySystemGlobals.h"
        #include "AuraAbilityTypes.h"
        
        FGameplayEffectContext* UAuraAbilitySystemGlobals::AllocGameplayEffectContext() const
        {
        	return new FAuraGameplayEffectContext();
        }
        ```
        
        - 오버라이드한 AllocGameplayEffectContext()는 기존에는 FGameplayEffectContext()를 생성하여 return하는 함수이지만,
        대신 FAuraGameplayEffectContext()를 생성하여 return하도록 해주면 된다.
      • 생성한 AuraAbilitySystemGlobals를 기존 AbilitySystemGlobals 대신 사용하도록 하려면 프로젝트 폴더 내 Config/DefaultGame.ini 파일의 수정이 필요하다.
        해당 설정 파일에서 아래와 같이 설정을 추가하면 된다.

        [/Script/GameplayAbilities.AbilitySystemGlobals]
        +AbilitySystemGlobalsClassName="/Script/Aura.AuraAbilitySystemGlobals"
        /Script/[모듈명(프로젝트명)].[AbilitySystemGlobals 클래스명]

profile
저는 게임 개발자로 일하고 싶어요

0개의 댓글