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

前言

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

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

基础定义

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

Duration Type GameplayCue Event When to use
Instant Execute BaseValue的立即生效的处理,GameplayTags 不会被应用到, 即使在一帧之内
Duration Add & Remove CurrentValue持续修改.可以应用GameplayTags.持续事件在GE类中指定
Infinite Add & Remove 类似Duration,但是是永久的直到手动移除(如通过ASC)

DurationInfinite模式会出现Period选项

image-20201201143832164

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

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

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

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

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

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


一些重要的数据结构

FGameplayEffectContext

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

可以继承他用来扩展参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//发起者,即拥有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的输入参数

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

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

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

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

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

这里也用到了FAggregatorEvaluateParameters

FAggregatorEvaluateParameters

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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效果,可以用如下方式

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

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

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

image-20201201151513828

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

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

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

  • cpp的方式应用GE
1
2
3
//初始效果,用来初始化属性
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "VGAS | Abilities")
TSubclassOf<class UGameplayEffect> DefaultAttributeEffect;
1
2
3
4
5
6
7
8
9
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 Type Description
Scalable Float FScalableFloats 一般可以用一个常量来定义,当然也可以用CurveTable来定义ScalableFloat
Attribute Based 基于一个属性的修改
Custom Calculation Class 通过一个类自定义修改,一般需要在cpp中操作,蓝图无法展开结构体
Set By Caller SetByCaller 一般在外部需要实时修改值的时候用,比如玩家按键时间决定此GE参数大小的情况下

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

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的乘数

源码计算的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

如果要修改算法

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

1
2
3
4
5
6
7
8
float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
...
float Multiplicitive = MultiplyMods(Mods[EGameplayModOp::Multiplicitive], Parameters);
...

return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 Type Description
Aggregate by Source 目标上的每个源“ASC”都有一个单独的实例。每个源可以应用X数量的堆
Aggregate by Target 无论源是什么,目标上只有一个堆实例。每个源都可以将堆应用到共享堆限制

image-20201202092332123

授予技能/Grant Abilities

GE可以给ASC新的技能

image-20201202092736123

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

标签

Category Description
Gameplay Effect Asset Tags GameplayEffect 它们自己不做任何功能,只用于描述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中可以用

1
void FGameplayEffectSpec::SetSetByCallerMagnitude(FName DataName, float Magnitude);
1
void FGameplayEffectSpec::SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);
1
float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
1
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创建的时候被捕获,反之在应用时被捕获

Snapshot Source or Target Captured on GameplayEffectSpec Automatically updates when Attribute changes for Infinite or Duration GE
Yes Source Creation No
Yes Target Application No
No Source Application Yes
No Target Application Yes

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

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

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

Gameplay Effect Execution Calculation

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

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

关于捕获方式如下表

Snapshot Source or Target Captured on GameplayEffectSpec
Yes Source Creation
Yes Target Application
No Source Application
No Target Application

在蓝图中可以重写方法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

    1
    2
    3
    4
    UPROPERTY(BlueprintReadOnly,EditAnywhere)
    FScalableFloat CD;
    UPROPERTY(BlueprintReadOnly, EditAnywhere)
    FGameplayTagContainer CDTags;

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

1
2
3
4
5
6
7
8
9
10
11
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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处理