Published on

GameplayAbilitySystem入门与实战(六):GameplayEffect(一)

Authors
  • avatar
    Name
    东哥
    Twitter

前言

GameplayEffectGAS框架中的重要性不言而喻, 内容非常多,,但是本身基本上只作为一个数据载体而存在,蓝图中也无法重写和执行任何事件.

本篇梳理一下GE的常用属性和基本概念, 实战部分我们在下一篇展开

基础定义

GE是属性修改的容器, 分为如下几个类型

Duration TypeGameplayCue EventWhen to use
InstantExecuteBaseValue的立即生效的处理,GameplayTags 不会被应用到, 即使在一帧之内
DurationAdd & RemoveCurrentValue持续修改.可以应用GameplayTags.持续事件在GE类中指定
InfiniteAdd & Remove类似Duration,但是是永久的直到手动移除(如通过ASC)

DurationInfinite模式会出现Period选项

image-20201201143832164

如果ExecutePeriodicEffectOnApplication=true,那么每隔Period秒就执行

periodic inhibition policy刷新策略,如是否覆盖

如果Tag匹配或者不匹配, GE可以临时性的关闭或者开启,这移除操作不会移除GE,只是临时的移除GE的修改效果

如果要手动的直接应用GE的修改, 可以调用

UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel)

GE的属性设置一般建议在编辑器用蓝图编辑, 理论上可以用cpp设定参数, 但是不直观也比较麻烦


一些重要的数据结构

FGameplayEffectContext

保存了GE相关的所有数据, 用于在GE执行的过程中传递重要信息

可以继承他用来扩展参数

//发起者,即拥有ASC的actor, 本项目是ASRCharacterBase类
UPROPERTY()
	`TWeakObjectPtr<AActor>` Instigator;

	/** 应用GE的类, 可以是武器或者子弹 */
	UPROPERTY()
	`TWeakObjectPtr<AActor>` EffectCauser;

	/** 响应GE的默认GA对象 */
	UPROPERTY()
	`TWeakObjectPtr<UGameplayAbility>` AbilityCDO;

	/** 响应GE的GA实例化对象 */
	UPROPERTY(NotReplicated)
	`TWeakObjectPtr<UGameplayAbility>` AbilityInstanceNotReplicated;

	/** 技能等级 */
	UPROPERTY()
	int32 AbilityLevel;

	/** 创建这个GE的对象, 可以是Actor或者其他对象 */
	UPROPERTY()
	`TWeakObjectPtr<UObject>` SourceObject;

	/**与发起者绑定的ASC */
	UPROPERTY(NotReplicated)
	`TWeakObjectPtr<UAbilitySystemComponent>` InstigatorAbilitySystemComponent;

	/** 引用的Actor */
	UPROPERTY()
	TArray<`TWeakObjectPtr<AActor>`> Actors;

	/** 射线数据 */
	`TSharedPtr<FHitResult>`	HitResult;

	/**原始坐标 ,如果bHasWorldOrigin为否则为空*/
	UPROPERTY()
	FVector	WorldOrigin;

	UPROPERTY()
	uint8 bHasWorldOrigin:1;

	/** SourceObject是否能同步, 这个布尔本身不同步 */
	UPROPERTY()
	uint8 bReplicateSourceObject:1;

FGameplayEffectContextHandle

用来处理FGameplayEffectContext的数据,只保存了一个变量TSharedPtr<FGameplayEffectContext> Data;

可以直接用FGameplayEffectContext构造以及赋值, 以及可以通过此数据结构获取和修改FGameplayEffectContext中的大多数数据

GameplayEffectSpec

  • GameplayEffectSpecGameplayEffect 创建的,可以通过方法MakeOutgoingSpec()创建
  • GameplayEffectSpecs 被成功创建后返回一个结构体FActiveGameplayEffect.
  • GameplayEffectSpec的级别 通常与创建“GameplayEffectSpec”的GA级别相同,但可以有所不同
  • GameplayEffectSpec的持续时间一般与GE相同,但是可以不同
  • 同样的 GameplayEffectSpec 的周期一般与GE相同但是也可以不同
  • GameplayEffectSpec的堆栈数量限制来自于GE
  • GameplayEffectContextHandle 告诉我们谁创建了 GameplayEffectSpec.
  • AttributesGameplayEffectSpec创建时就已经捕获了
  • 除了GE授予的GameplayTags之外,GameplayEffectSpec授予目标的DynamicGrantedTags
  • GE拥有的AssetTags会被添加到 GameplayEffectSpecDynamicAssetTags
  • SetByCaller TMaps.

FGameplayEffectAttributeCaptureDefinition

这是一个定义捕获数据的结构体

也就是我们蓝图中在GEEC中可以看到的几个属性, 其中在类UGameplayEffectCalculation中申明了TArray<FGameplayEffectAttributeCaptureDefinition> RelevantAttributesToCapture; 在派生类UGameplayEffectExecutionCalculation中申明了TArray<FGameplayEffectAttributeCaptureDefinition> InvalidScopedModifierAttributes;

FGameplayEffectCustomExecutionParameters

GEEC的输入参数

成员变量基本都是私有的, 了解几个函数

const FGameplayEffectSpec& GetOwningSpec() const;
UAbilitySystemComponent* GetTargetAbilitySystemComponent() const;
UAbilitySystemComponent* GetSourceAbilitySystemComponent() const;

上面几个比较简单, 字面意思

	bool AttemptCalculateCapturedAttributeMagnitude(const FGameplayEffectAttributeCaptureDefinition& InCaptureDef, const FAggregatorEvaluateParameters& InEvalParams, OUT float& OutMagnitude) const;

这个方法比较有用, 函数翻译过来是尝试计算捕获属性量级, 其实简单使用的话可以理解为把Def转换成浮点值

这里也用到了FAggregatorEvaluateParameters

FAggregatorEvaluateParameters

寄存器计算时用于传递参数的结构体, 参数不多

struct GAMEPLAYABILITIES_API FAggregatorEvaluateParameters
{
	FAggregatorEvaluateParameters()
		: SourceTags(nullptr)
		, TargetTags(nullptr)
		, IncludePredictiveMods(false) 
	{}

	const FGameplayTagContainer* SourceTags;
	const FGameplayTagContainer* TargetTags;

	/** Any mods with one of these handles will be ignored during evaluation */
	`TArray<FActiveGameplayEffectHandle>` IgnoreHandles;

	/** If any tags are specified in the filter, a mod's owning active gameplay effect's source tags must match ALL of them in order for the mod to count during evaluation */
	FGameplayTagContainer AppliedSourceTagFilter;

	/** If any tags are specified in the filter, a mod's owning active gameplay effect's target tags must match ALL of them in order for the mod to count during evaluation */
	FGameplayTagContainer AppliedTargetTagFilter;

	bool IncludePredictiveMods;
};

应用

image-20201201150412944可以简单的用上述方法在GAS中应用GE

如果想监听来自ASC的持续的GE效果,可以用如下方式

AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &AVGCharacterBase::OnActiveGameplayEffectAddedCallback);

virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);

同样的可以通过各种方法移除,如

image-20201201151513828

通过如下方式监听移除事件

AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

  • cpp的方式应用GE
//初始效果,用来初始化属性
	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "VGAS | Abilities")
		`TSubclassOf<class UGameplayEffect>` DefaultAttributeEffect;
	FGameplayEffectContextHandle EffectContextHandle = AbilitySystemComponent->MakeEffectContext();
	EffectContextHandle.AddSourceObject(this);

	FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(DefaultAttributeEffect, GetCharacterLevel(), EffectContextHandle);
	if (SpecHandle.IsValid())
	{
		FActiveGameplayEffectHandle ActiveGEHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), AbilitySystemComponent.Get());
	}

修改

image-20201201151641620

如上图分为 加/乘/除/覆盖 4个方式

  • 修改模式
Modifier TypeDescription
Scalable FloatFScalableFloats 一般可以用一个常量来定义,当然也可以用CurveTable来定义ScalableFloat
Attribute Based基于一个属性的修改
Custom Calculation Class通过一个类自定义修改,一般需要在cpp中操作,蓝图无法展开结构体
Set By CallerSetByCaller 一般在外部需要实时修改值的时候用,比如玩家按键时间决定此GE参数大小的情况下

乘除方式会用如下方式计算,可以理解为都基于1计算,而非比较容易理解的叠加在一起

1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...

比如如果有2个乘法计算,参数都是1.5,那么得到的结果并不是value*1.5*1.5,而是value*1*(0.5+0.5)

这里会有几个问题存在(每个系统都有各自的计算方法)

  • Multipliers:0.5
    • 1+(0.5-1)=0.5; 正确
  • Multipliers:0.5,0.5
    • 1 + (0.5 - 1) + (0.5 - 1) = 0; 错误

这里的问题在Paragon中是通过设计方面解决的,即在设计的时候就使用最多只有一个小于1的乘数

源码计算的代码如下

float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
//******************************
float FAggregatorModChannel::SumMods(const `TArray<FAggregatorMod>`& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters)
{
	float Sum = Bias;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Sum += (Mod.EvaluatedMagnitude - Bias); //乘法的系数是通过加法
		}
	}

	return Sum;
}

如果要修改算法

那么需要修改引擎代码如下

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Multiplicitive = MultiplyMods(Mods[EGameplayModOp::Multiplicitive], Parameters);
	...

	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
float FAggregatorModChannel::MultiplyMods(const `TArray<FAggregatorMod>`& InMods, const FAggregatorEvaluateParameters& Parameters)
{
	float Multiplier = 1.0f;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Multiplier *= Mod.EvaluatedMagnitude;
		}
	}

	return Multiplier;
}

注: MultiplyModes 函数原本是没有的

修改相关标签

SourceTagsTargetTags的运行机制跟GA类似, 如果是有持续时间的GE只会在第一次运行的时候执行Tag操作

AttributeBase模式会有2个TagFilter过滤变量

叠加/堆

每一个GE会实例化一个GameplayEffectsSpec对象,无论之前是否已经存在

从GE对象可以得到当前堆的数量

堆类型有两种

Stacking TypeDescription
Aggregate by Source目标上的每个源“ASC”都有一个单独的实例。每个源可以应用X数量的堆
Aggregate by Target无论源是什么,目标上只有一个堆实例。每个源都可以将堆应用到共享堆限制

image-20201202092332123

授予技能/Grant Abilities

GE可以给ASC新的技能

image-20201202092736123

Removal PolicyDescription
Cancel Ability Immediately当赋予该GAGE从目标上移除时,该GA会立即被取消和移除
Remove Ability on End被授予的GA被允许完成,然后从目标上移除。
Do Nothing授予的GA不受从目标上移除GE的影响。目标具有永久的能力,直到后来被手动删除。

标签

CategoryDescription
Gameplay Effect Asset TagsGameplayEffect 它们自己不做任何功能,只用于描述GE
Granted Tags此类标签伴随着GE的生命周期,同时也会添加到ASC中;在GE移除以后也会移除;只在有持续时间的GE中生效
Ongoing Tag Requirements此类标签会将GE暂时性的开/关,只作用于有持续时间的GE,举个例子: 一个GE来模拟一个5秒的恢复效果, 那么在此标签给与一个Tag, 其他无论是GE还是GA添加了这个Tag后这个恢复就触发了
Application Tag Requirements跟目标相关的标签,如果不符合就不能作用到目标
Remove Gameplay Effects with Tags如果目标GE拥有此类标签,那么在GE生效后会移除这些标签的GE

免疫/减免

image-20201202094459865

可以通过代理FImmunityBlockGE OnImmunityBlockGameplayEffectDelegate来监听免疫能力的改变

SetByCaller

Modifiers中,必须提前在GE中定义好,只能使用GameplayTag版本,如果GE定义后GameplaySpec没有拥有正确的tag,那么会报错

如果在其他地方,那么不需要提前定义

关于SetByCaller相关方法,在蓝图中可以用如下

image-20201202102139353

在cpp中可以用

void FGameplayEffectSpec::SetSetByCallerMagnitude(FName DataName, float Magnitude);
void FGameplayEffectSpec::SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);
float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;

Modifier Magnitude Calculation

Modifier Magnitude CalculationGameplayEffectExecutionCalculations有点类似,但是没有后者功能强大但是更重要

MMC的唯一目的就是通过方法CalculateBaseMagnitude返回一个浮点值

image-20201202103436539

MMC可以在任意的GE中使用

MMC可以捕获任意的不管是Source还是Target的属性,并且完全访问GameplayEffectSpec对象来获取GameplayTagsSetByCaller

image-20201202104135362

Snapshot的属性会在GE创建的时候被捕获,反之在应用时被捕获

SnapshotSource or TargetCaptured on GameplayEffectSpecAutomatically updates when Attribute changes for Infinite or Duration GE
YesSourceCreationNo
YesTargetApplicationNo
NoSourceApplicationYes
NoTargetApplicationYes

重新计算不会触发函数PreAttributeChange,所以必须在这里进行必要的Clamp操作

MMC在蓝图中的使用不是很方便, 蓝图无法从FGameplayEffectSpec获取任何参数, 也无法调用GetCapturedAttributeMagnitude()获取捕获属性的值, 如果非要在蓝图中编辑 ,那么建议封装一个蓝图可见的函数包裹函数GetCapturedAttributeMagnitude(), 那么就可以获取到捕获属性的值了就可以魔改一些内容了,我们在下一篇会详细讲述

注意, CalculateBaseMagnitude()函数是const函数

Gameplay Effect Execution Calculation

Gameplay Effect Execution Calculation下文简称GEEC功能类似MMC, 但是功能更强大

一般只应用于非持续性的GE

关于捕获方式如下表

SnapshotSource or TargetCaptured on GameplayEffectSpec
YesSourceCreation
YesTargetApplication
NoSourceApplication
NoTargetApplication

在蓝图中可以重写方法Excute,但是意义不大, 参数无法在蓝图中获取什么有用的数据

image-20201216152057021

关于这个的详细应用我们会在后续展开

## Custom Application Requirement

自定义GE是否可以被应用

image-20201202110224455

技能消耗/Cost Gameplay Effect

Cost Gameplay Effect(CTGE)定义了GA释放所需要的消耗,

当然可以直接定义一个浮点参数,记得给负值,必定消耗一般是扣除的

目前有两种方式来扩展

  1. 使用MMC,然后重写方法CalculateBaseMagnitude
  2. 重写GA中的GetCostGameplayEffect方法,手动动态创建一个GE,然后读取Cost Value

image-20201205091051810

因为默认情况下的GameplayEffectSpec的变量是没有暴露给蓝图的,上图中我重新定义了一个结构体然后暴露给蓝图使用

冷却系统/Cooldown Gameplay Effect

Cooldown Gameplay Effect(CDGE)定义了技能的冷却时间,在CDGE中,我们可以不做任何属性修改,只需要提供一个Tag来标志冷却时间;当然类似CTGE,我们也可以用MMC来自定义算法

多数情况下,可以为每一个GA提供一个唯一的CDGE,如果想要复用同一个CDGE,那么我们可以在对GE创建的GameplayEffectSpec中的数据进行修改,这种方式只能在实例化技能(Instanced)使用

两种方式来扩展CDGE的计算

  1. 使用SetByCaller

    我们意图把对CD的控制放到GA中,那么我们在自定义的GA中声明一个FScalableFloat变量CD,然后声明FGameplayTagContainer CDTags

    	UPROPERTY(BlueprintReadOnly,EditAnywhere)
    		FScalableFloat CD;
    	UPROPERTY(BlueprintReadOnly, EditAnywhere)
    		FGameplayTagContainer CDTags;
    

然后需要重写两个基类方法,需要一个临时变量FGameplayTagContainer tempTags

const FGameplayTagContainer* UGA_TestCpp1::GetCooldownTags() const
{
	FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&tempTags);
	const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
	if (ParentTags)
	{
		MutableTags->AppendTags(*ParentTags);
	}
	MutableTags->AppendTags(CDTags);
	return MutableTags;
}
void UGA_TestCpp1::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
	UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
	if (CooldownGE)
	{
		FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
		SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CDTags);
		FName tagName = FName(*(CDTags.ToStringSimple()));
		SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(tagName), CD.GetValueAtLevel(GetAbilityLevel()));
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
	}
}

注意

FGameplayTagContainerFName用到的FString需要用ToStringSimple()

RequestGameplayTag()方法在默认情况下如果找不到标签会直接导致引擎崩溃

这样我们可以直接在GA中定义我们自己的冷却时间

  1. 使用MMC

方法类似对Cost的处理, 重写MMC中的CalculateBaseMagnitude方法, 无论从蓝图还是cpp中都可以通过对返回的float值作为CD处理