BlueRoseNote/03-UnrealEngine/Gameplay/GAS/(4)UGameplayEffect.md
2023-06-29 11:55:02 +08:00

16 KiB
Raw Permalink Blame History

UGameplayEffect

UGameplayEffect在框架中主要负责各种数值上的效果如果技能cd、类似黑魂中的异常效果堆叠与buff甚至连角色升级时的属性点添加都可以使用它来实现。

因为大多数逻辑都是设置数据子类的操作,所以对于这个类,本人推荐使用蓝图来进行操作。

简单使用教程

通过继承UGameplayEffect来创建一个新的GameplayEffect类并在构造函数中对相应的属性进行设置。之后在Ability类中调用ApplyGameplayEffectToOwner函数让GameplayEffect生效。

if (CommitAbility(Handle, ActorInfo, ActivationInfo))		// ..then commit the ability...
{			
	//	Then do more stuff...
	const UGameplayEffect* GameplayEffect = NewObject<UGE_DamageBase>();
	ApplyGameplayEffectToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, GameplayEffect, 5, 1);

	K2_EndAbility();
}

具体操作可以参考ActionRPG模板或者是我的项目代码。

Modifiers

本质是一个FGameplayModifierInfo结构体数组用于存储所有数值修改信息。FGameplayModifierInfo包含以下属性

  • Attribute 修改的目标属性集中的属性。
  • ModifierOp 修改方式。例如Override、Add、Multiply。
  • Magnitude已被废弃
  • ModifierMagnitude 修改的数值与类型,可以配置数据表。
  • EvaluationChannelSettings (不知道为什么没在编辑器中显示,而且代码中只有一处调用,所以直接跳过)
  • SourceTags 本身标签行为Effect生效所需或者忽略的标签
  • TargetTags 目标标签行为Effect生效所需或者忽略的标签

可以看得出ModifierMagnitude才是Modifiers的关键而它的本质是FGameplayEffectModifierMagnitude结构体。但是我们只需要学会初始化它即可。它具有以下四种类型

  • ScalableFloat 较为简单的使用方式使用ScalableFloat进行计算
  • AttributeBased 基于属性执行计算。
  • CustomCalculationClass 能够捕获多个属性进行自定义计算
  • SetByCaller 被蓝图或者代码显式设置

ScalableFloat的调用示例

ScalableFloat类型是用于设置固定值的简单方式同时它也支持通过CurveTable配合技能等级设置倍率。最后结果=固定值*倍率当然如果你向完全通过CurveTable来控制参数那就把固定值设置为1即可。

FGameplayModifierInfo info;

info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
info.ModifierOp = EGameplayModOp::Additive;

//固定值
//info.ModifierMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(100.0));

//CurveTable控制倍率
FScalableFloat damageValue = {1.0};
FCurveTableRowHandle damageCurve;
static ConstructorHelpers::FObjectFinder<UCurveTable> curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage"));
damageCurve.CurveTable = curveAsset.Object;
damageCurve.RowName = FName("Damage");
damageValue.Curve = damageCurve;

info.ModifierMagnitude = FGameplayEffectModifierMagnitude(damageValue);
Modifiers.Add(info);

PS.技能等级在ApplyGameplayEffectToOwner函数中设置。

AttributeBased的调用示例

最终计算过程可以在CalculateMagnitude函数中找到。

  1. 如果尝试捕获到数值不为None则将赋值给AttribValue。
  2. 判断AttributeCalculationType来计算对应的AttribValue。我不太了解代码中channel的概念如果channel不存在AttribValue为原本值
  3. 如果AttributeCurve存在则将AttribValue作为x轴值来查找y轴值并进行插值计算最后将结果赋值给AttribValue。
  4. 最终计算公式:$((Coefficient * (AttribValue + PreMultiplyAdditiveValue)) + PostMultiplyAdditiveValue)$

BackingAttribute

为GameplayEffect捕获GameplayAttribute的选项。你可以理解为Lambda表达式的捕获

  • AttributeToCapture捕获属性
  • AttributeSource捕获的目标自身还是目标对象
  • bSnapshot属性是否需要被快照没仔细看如果为false每次都会重新获取吧

AttributeCalculationType

默认值为AttributeMagnitude。

  • AttributeMagnitude使用最后通过属性计算出来的级数
  • AttributeBaseValue使用属性基础值
  • AttributeBonusMagnitude使用最后计算值-基础值)
  • AttributeMagnitudeEvaluatedUpToChannel不清楚使用方法关键是在编辑器中这个选项默认是不显示的
FGameplayModifierInfo info;

info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
info.ModifierOp = EGameplayModOp::Additive;
FAttributeBasedFloat damageValue;
damageValue.Coefficient = { 1.2f };
damageValue.PreMultiplyAdditiveValue = { 1.0f };
damageValue.PostMultiplyAdditiveValue = { 2.0f };
damageValue.BackingAttribute = FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
	, EGameplayEffectAttributeCaptureSource::Source, false);
FCurveTableRowHandle damageCurve;
static ConstructorHelpers::FObjectFinder<UCurveTable> curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage"));
damageCurve.CurveTable = curveAsset.Object;
damageCurve.RowName = FName("Damage");
damageValue.AttributeCurve = damageCurve;
damageValue.AttributeCalculationType = EAttributeBasedFloatCalculationType::AttributeMagnitude;

info.ModifierMagnitude = damageValue;
Modifiers.Add(info);

CustomCalculationClass的调用示例

与AttributeCalculationType相比少了属性捕获多了CalculationClassMagnitudeUGameplayModMagnitudeCalculation类

FGameplayModifierInfo info;

info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
info.ModifierOp = EGameplayModOp::Additive;
FCustomCalculationBasedFloat damageValue;
damageValue.CalculationClassMagnitude = UDamageMagnitudeCalculation::StaticClass();
damageValue.Coefficient = { 1.2f };
damageValue.PreMultiplyAdditiveValue = { 1.0f };
damageValue.PostMultiplyAdditiveValue = { 2.0f };

info.ModifierMagnitude = damageValue;
Modifiers.Add(info);

PS.如果这个计算过程还取决于外部非GameplayAbility框架的条件那么你可能需要重写GetExternalModifierDependencyMulticast()函数以获得FOnExternalGameplayModifierDependencyChange委托。从而实现当外部条件发生改变时及时更新计算结果。

UGameplayModMagnitudeCalculation

你可以通过继承UGameplayModMagnitudeCalculation来创建自定义的Calculation类。所需实现步骤如下

  1. 在构造函数中向RelevantAttributesToCapture数组添加需要捕获的属性。
  2. 实现CalculateBaseMagnitude事件。因为BlueprintNativeEvent类型所以既可以在c++里实现也可以在蓝图中实现关于两者结合可以参考UGameplayAbility类中ActivateAbility()的写法。

案例代码如下:

UCLASS(BlueprintType, Blueprintable, Abstract)
class ACTIONRPG_API UDamageMagnitudeCalculation : public UGameplayModMagnitudeCalculation
{
	GENERATED_BODY()
public:
	UDamageMagnitudeCalculation();

	float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
};
UDamageMagnitudeCalculation::UDamageMagnitudeCalculation()
{
	RelevantAttributesToCapture.Add(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
		, EGameplayEffectAttributeCaptureSource::Source, false));
}

float UDamageMagnitudeCalculation::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
	float damage{ 0.0f};

	FAggregatorEvaluateParameters InEvalParams;

	//捕获失败的容错语句
	if (!GetCapturedAttributeMagnitude(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
		, EGameplayEffectAttributeCaptureSource::Source, false), Spec, InEvalParams, damage)) {
		
		//如果这个变量会作为除数的话不能为0
		damage = 1.0f;
	}

	return damage;
}

Executions

Executions更为简单而且更加自由。只需要编写Calculation Class即可。它与Modifiers的不同之处在于一个Modifiers只能修改一个属性而Executions可以同时改动多个属性。

UGameplayEffectExecutionCalculation

这里我就直接复制actionRPG模板的代码了。 开头的RPGDamageStatics结构体与DamageStatics函数可以减少后面的代码量。可以算是FGameplayEffectAttributeCaptureDefinition的语法糖吧。

UCLASS()
class ACTIONRPG_API UDamageExecutionCalculation : public UGameplayEffectExecutionCalculation
{
	GENERATED_BODY()
public:
	UDamageExecutionCalculation();
	virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
struct RPGDamageStatics
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower);
	DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);

	RPGDamageStatics()
	{
		// Capture the Target's DefensePower attribute. Do not snapshot it, because we want to use the health value at the moment we apply the execution.
		DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false);

		// Capture the Source's AttackPower. We do want to snapshot this at the moment we create the GameplayEffectSpec that will execute the damage.
		// (imagine we fire a projectile: we create the GE Spec when the projectile is fired. When it hits the target, we want to use the AttackPower at the moment
		// the projectile was launched, not when it hits).
		DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true);

		// Also capture the source's raw Damage, which is normally passed in directly via the execution
		DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true);
	}
};

static const RPGDamageStatics& DamageStatics()
{
	static RPGDamageStatics DmgStatics;
	return DmgStatics;
}

UDamageExecutionCalculation::UDamageExecutionCalculation()
{
	RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef);
	RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef);
	RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
}

void UDamageExecutionCalculation::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
	UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();

	AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->AvatarActor : nullptr;
	AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->AvatarActor : nullptr;

	const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

	// Gather the tags from the source and target as that can affect which buffs should be used
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;

	// --------------------------------------
	//	Damage Done = Damage * AttackPower / DefensePower
	//	If DefensePower is 0, it is treated as 1.0
	// --------------------------------------

	//计算捕获属性的数值。
	float DefensePower = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DefensePowerDef, EvaluationParameters, DefensePower);
	//因为要做除数所以需要加入容错语句
	if (DefensePower == 0.0f)
	{
		DefensePower = 1.0f;
	}

	float AttackPower = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackPowerDef, EvaluationParameters, AttackPower);

	float Damage = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);

	//伤害计算公式
	float DamageDone = Damage * AttackPower / DefensePower;
	if (DamageDone > 0.f)
	{
		//这里可以修改多个属性
		OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, DamageDone));
	}
}

Period

Period指的是周期一般用于制作周期性技能。

//持续类型只有设置为HasDuration技能才能变成周期性的
DurationPolicy = EGameplayEffectDurationType::HasDuration;
//持续时间
DurationMagnitude = FGameplayEffectModifierMagnitude(1.0);
//周期,技能生效次数=持续时间/周期
Period = 2.0;

FGameplayEffectContainer与Spec

EPIC实现了这个结构体 调用URPGGameplayAbility::MakeEffectContainerSpecFromContainer 使用FGameplayEffectContainer生成FGameplayEffectContainerSpec结构体。

Spec是实例版本存储TargetDataHandle与EffectSpecHandle。通过MakeEffectContainerSpecFromContainer进行实例化但本质是通过Spec的AddTarget进行数据填充

之后再通过

ReturnSpec.TargetGameplayEffectSpecs.Add(MakeOutgoingGameplayEffectSpec(EffectClass, OverrideGameplayLevel));

填充EffectSpec数据。

MakeEffectContainerSpec则是个快捷函数 通过FGameplayTag寻找对应的Effect与Target数据。EventData则用于调用TargetType类的GetTarget函数用于获取符合要求的目标Actor

在ActionRPG中URPGTargetType_UseEventData的GetTarget用到了EventData。大致逻辑为首先寻找EventData里是否带有EventData.HitResult信息可以在Send Event To Actor中设置如果没有则返回EventData.Target信息。

void URPGTargetType_UseEventData::GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const
{
	const FHitResult* FoundHitResult = EventData.ContextHandle.GetHitResult();
	if (FoundHitResult)
	{
		OutHitResults.Add(*FoundHitResult);
	}
	else if (EventData.Target)
	{
		OutActors.Add(const_cast<AActor*>(EventData.Target));
	}
}

ApplyEffectContainer则是个方便函数。

实现在AnimNotify中向指定目标引用GameplayEffect

从GameplayAbilityComponent或者从GameplayAbility中设置. MakeOutgoingGameplayEffectSpec=> ApplyGameplayEffectSpecToTarget 位于UGameplayAbility ApplyGameplayEffectToTarget GameplayEffectSpec.GetContext().AddTarget()

RemoveGrantedByEffect()函数可以移除Ability中Instance类型的Effect。非常适合来清除翻滚免伤、技能硬直效果。

FRPGGameplayEffectContainerSpec URPGBlueprintLibrary::AddTargetsToEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec, const TArray<FHitResult>& HitResults, const TArray<AActor*>& TargetActors)
{
	FRPGGameplayEffectContainerSpec NewSpec = ContainerSpec;
	NewSpec.AddTargets(HitResults, TargetActors);
	return NewSpec;
}

TArray<FActiveGameplayEffectHandle> URPGBlueprintLibrary::ApplyExternalEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec)
{
	TArray<FActiveGameplayEffectHandle> AllEffects;

	// Iterate list of gameplay effects
	for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs)
	{
		if (SpecHandle.IsValid())
		{
			// If effect is valid, iterate list of targets and apply to all
			for (TSharedPtr<FGameplayAbilityTargetData> Data : ContainerSpec.TargetData.Data)
			{
				AllEffects.Append(Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get()));
			}
		}
	}
	return AllEffects;
}
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, GameplayEffectClass, GameplayEffectLevel);
if (SpecHandle.Data.IsValid())
{
	SpecHandle.Data->StackCount = Stacks;

	SCOPE_CYCLE_UOBJECT(Source, SpecHandle.Data->GetContext().GetSourceObject());
	EffectHandles.Append(ApplyGameplayEffectSpecToTarget(Handle, ActorInfo, ActivationInfo, SpecHandle, Target));
}

实用函数

Wait Input Release