4、GAS相关概念4.5 Gameplay Effects

4.5 Gameplay Effects

4.5.1 定义Gameplay Effects

GameplayEffects(简称GE)是Ability改变AttributesGameplayTags的载体。它们可以立刻改变Attribute,如伤害或治疗,或施加长期的状态增益/减益,如移动速度提升或眩晕。UGameplayEffect类是一个**Data-Only(仅数据)**类,用于定义单个游戏效果。GameplayEffects中不应添加额外的逻辑。通常,设计师会创建UGameplayEffect的许多蓝图子类。

GameplayEffects通过ModifiersExecutionsGameplayEffectExecutionCalculation改变Attributes

GameplayEffects有三种持续时间类型:InstantDurationInfinite

此外,GameplayEffects可以添加/执行GameplayCuesInstant类型的GameplayEffect会在GameplayCueGameplayTags上调用Execute,而DurationInfinite类型的GameplayEffect会在GameplayCueGameplayTags上调用AddRemove

持续时间类型GameplayCue事件何时使用
Instant (即时)Execute用于对AttributeBaseValue进行即时永久更改。GameplayTags不会被应用,甚至不会持续一帧。
Duration (持续)Add & Remove用于对AttributeCurrentValue进行临时更改,并应用GameplayTags,这些标签将在GameplayEffect过期或被手动移除时被移除。持续时间在UGameplayEffect类/蓝图中指定。
Infinite (无限)Add & Remove用于对AttributeCurrentValue进行临时更改,并应用GameplayTags,这些标签将在GameplayEffect被移除时被移除。这些标签不会自行过期,必须由能力或ASC手动移除。

DurationInfinite类型的GameplayEffects可以选择应用Periodic Effects,这些效果会根据其定义的Period每隔X秒应用其ModifiersExecutions。在更改AttributeBaseValue和执行GameplayCues时,Periodic Effects被视为Instant类型的GameplayEffects。这些效果对于持续伤害(DOT)类型的效果非常有用。注意:Periodic Effects无法被预测

DurationInfinite类型的GameplayEffects可以在应用后暂时关闭和打开,如果它们的Ongoing Tag Requirements未满足/满足(Gameplay Effect Tags)。关闭GameplayEffect会移除其Modifiers和应用的GameplayTags的效果,但不会移除GameplayEffect。重新打开GameplayEffect会重新应用其ModifiersGameplayTags

如果需要手动重新计算DurationInfinite类型GameplayEffectModifiers(例如,您有一个MMC使用的数据不来自Attributes),可以使用UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel),并使用UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel()获取相同的级别。基于支持AttributesModifiers会在这些支持Attributes更新时自动更新。SetActiveGameplayEffectLevel()更新Modifiers的关键功能是:

MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
// 私有函数,否则我们可以在不需要将级别设置为当前级别的情况下调用这三个函数
UpdateAllAggregatorModMagnitudes(Effect);

GameplayEffects通常不会被实例化。当一个能力或ASC想要应用一个GameplayEffect时,它会从GameplayEffectClassDefaultObject创建一个GameplayEffectSpec。成功应用的GameplayEffectSpecs会被添加到一个名为FActiveGameplayEffect的新结构中,这是ASC在一个特殊的容器结构ActiveGameplayEffects中跟踪的内容。

4.5.2 应用Gameplay Effects

GameplayEffects可以以多种方式应用,从GameplayAbilitiesASC上的函数,通常采用ApplyGameplayEffectTo的形式。不同的函数本质上只是方便函数,最终会调用UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()Target

要在外部应用GameplayEffects,例如从投射物,需要获取Target's ASC并使用其函数之一来ApplyGameplayEffectToSelf

可以通过绑定到其委托来监听任何DurationInfinite类型的GameplayEffects何时应用于ASC

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

回调函数:

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

服务器将始终调用此函数,无论同步模式如何。自主代理将仅为此同步GameplayEffectsFullMixed同步模式中调用此函数。模拟代理将仅在此同步Full 同步模式中调用此函数。

4.5.3 移除Gameplay Effects

GameplayEffects可以以多种方式移除,从GameplayAbilitiesASC上的函数,通常采用RemoveActiveGameplayEffect的形式。不同的函数本质上只是方便函数,最终会调用FActiveGameplayEffectsContainer::RemoveActiveEffects()Target

要移除GameplayEffects外部GameplayAbility,需要获取Target's ASC并使用其函数之一来RemoveActiveGameplayEffect

可以通过绑定到其委托来监听任何DurationInfinite类型的GameplayEffects何时从ASC中移除:

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

回调函数:

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

服务器将始终调用此函数,无论同步模式如何。自主代理将仅为此同步GameplayEffectsFullMixed同步模式中调用此函数。模拟代理将仅在此同步Full 同步模式中调用此函数。

4.5.4 Gameplay Effect Modifiers

Modifiers改变一个Attribute,并且是唯一可以预测改变一个Attribute的方法。一个GameplayEffect可以有零个或多个Modifiers。每个Modifier只负责更改一个Attribute,通过指定的操作。

操作描述
Add将结果添加到Modifier指定的Attribute。使用负值进行减法。
Multiply将结果乘以Modifier指定的Attribute
Divide将结果除以Modifier指定的Attribute
OverrideModifier指定的Attribute覆盖为结果。

AttributeCurrentValue是所有Modifiers添加到其BaseValue的结果。聚合公式如下:

((InlineBaseValue + Additive) * Multiplicitive) / Division

任何Override Modifiers将覆盖最终值,最后应用的Modifier优先。

**注意:**对于百分比变化,请确保使用Multiply操作,以便在添加之后发生。

注意:预测对百分比变化有困难。

有四种类型的ModifiersScalableFloat(可缩放浮点)、Attribute-Based(基于属性的)、Custom Calculation Class(自定义计算类)和SetByCaller(设置为调用者)。它们都生成一些浮点值,然后用于根据其操作更改指定的Attribute

Modifier类型描述
ScalableFloatFScalableFloats是一个结构,可以指向具有行和级别作为列的数据表。可缩放浮点将自动读取指定表行的值,该表行在能力当前级别(或不同级别,如果覆盖)。此值可以进一步通过系数进行操作。如果未指定数据表/行,则将其视为1,因此系数可以用于在所有级别中硬编码单个值。 可缩放浮点
Attribute BasedAttribute Based Modifiers获取Source(谁创建了GameplayEffectSpec)或Target(谁接收了GameplayEffectSpec)的CurrentValueBaseValue,并进一步修改它与系数和前系数添加。Snapshotting意味着在创建GameplayEffectSpec时捕获支持Attribute,而无需快照意味着在应用GameplayEffectSpec时捕获Attribute
Custom Calculation ClassCustom Calculation Class提供了最灵活的Modifiers。此Modifier采用ModifierMagnitudeCalculation类,并可以进一步修改结果浮点值与系数和前系数添加。
Set By CallerSetByCaller Modifiers是值,这些值是在运行时通过能力或谁制作GameplayEffectSpecGameplayEffectSpec之外设置的。例如,如果您想根据玩家按住按钮的时间来设置伤害,可以使用SetByCallerSetByCallersTMap<FGameplayTag, float>,它们生活在GameplayEffectSpec上。Modifier只是告诉Aggregator查找与提供的GameplayTag关联的SetByCaller值。SetByCallers用于ModifiersGameplayTag版本,FName版本在此被禁用。如果Modifier设置为SetByCaller,但GameplayEffectSpec上不存在具有正确GameplayTagSetByCaller,游戏将抛出运行时错误并返回值0。这可能会在Divide操作中引起问题。请参阅SetByCallers了解更多信息。

4.5.4.1 Multiply和Divide Modifiers

默认情况下,所有MultiplyDivide Modifiers在将它们应用于AttributeBaseValue之前会一起添加。

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
	float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
	float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
	...
	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
	...
}
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;
}

摘自 GameplayEffectAggregator.cpp

MultiplyDivide Modifiers在公式中都有一个Bias值为1Addition有一个Bias0)。所以它看起来像这样:

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

这个公式导致了一些意外的结果。首先,这个公式在添加或除以数字之前将所有修饰符添加在一起。大多数人会期望它将它们相乘或除以它们。例如,如果您有两个Multiply修饰符为1.5,大多数人会期望BaseValue乘以1.5 x 1.5 = 2.25。相反,这个公式将1.5添加到BaseValue中,使其增加2(50%增加+另一个50%增加=100%增加)。这是从GameplayPrediction.h中的示例,其中10%速度增益在500基础速度上为550。添加另一个10%速度增益,它将为600

其次,这个公式有一些未记录的规则,这些规则是根据Paragon设计的。

MultiplyDivide乘法加法公式规则:

  • (No more than one value < 1) AND (Any number of values [1, 2))
  • OR (One value >= 2)

公式中的Bias基本上从Sum值中减去数字范围[1, 2)中的整数数字。循环开始时的Bias值减去从Sum值中减去,因此任何值本身都可以工作,并且任何小于1的值都可以与数字范围[1, 2)中的数字一起工作。

一些例子与Multiply
Multipliers: 0.5
1 + (0.5 - 1) = 0.5,正确

Multipliers: 0.5, 0.5
1 + (0.5 - 1) + (0.5 - 1) = 0,不正确,预期1?多个小于1的值对于添加乘数没有意义。Paragon被设计为仅使用最大负值用于Multiply Modifiers,因此最多只会有一值小于1乘以BaseValue

Multipliers: 1.1, 0.5
1 + (0.5 - 1) + (1.1 - 1) = 0.6,正确

Multipliers: 5, 5
1 + (5 - 1) + (5 - 1) = 9,不正确,预期10。总是Modifiers的数量减去1的和。

许多游戏将希望他们的MultiplyDivide Modifiers在将它们应用于BaseValue之前将它们相乘或除以它们。要实现这一点,您需要更改引擎代码以覆盖FAggregatorModChannel::EvaluateWithBase()

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Multiplicitive = MultiplyMods(Mods[EGameplayModOp::Multiplicitive], Parameters);
	float Division = MultiplyMods(Mods[EGameplayModOp::Division], 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;
}

4.5.4.2 Modifiers上的Gameplay Tags

每个Modifier都可以设置SourceTagTargetTag。它们的工作方式与Application Tag requirements相同。因此,这些标签仅在效果被应用时考虑。例如,当具有周期性、无限效果时,它们仅在效果第一次应用时被考虑,但在每次周期性执行时被考虑。

Attribute BasedModifiers还可以设置SourceTagFilterTargetTagFilter。在确定属性的大小(这是基于属性的修改器的源)时,这些过滤器用于排除某些Modifier,以该属性。源或目标没有所有过滤器的标签的Modifiers会被排除在外。

这意味着:Source ASC和目标Target ASC的标签被GameplayEffects捕获。Source ASC标签在创建GameplayEffectSpec时捕获,Target ASC标签在效果执行时捕获。当确定是否应用效果的修改器时,如果设置了过滤器,则捕获的标签与过滤器进行比较。

4.5.5 堆叠Gameplay Effects

默认情况下,GameplayEffects会应用新的GameplayEffectSpec实例,这些实例不知道或不关心之前存在的GameplayEffectSpec实例。GameplayEffects可以设置为堆叠,其中不是添加新的GameplayEffectSpec实例,而是更改当前存在的GameplayEffectSpec's堆叠计数。堆叠仅适用于DurationInfinite类型的GameplayEffects

有两种堆叠类型:Aggregate by Source(按源聚合)和Aggregate by Target(按目标聚合)。

堆叠类型描述
Aggregate by Source(按源聚合)每个源ASCTarget上都有一个单独的堆叠实例。每个源可以应用X数量的堆叠。
Aggregate by Target(按目标聚合)Target上只有一个堆叠实例,无论来源如何。每个源可以应用堆叠,直到达到共享堆叠限制。

堆叠也有策略用于过期、持续刷新和周期重置。它们在GameplayEffect蓝图中有一个有用的悬停工具提示。

示例项目包括一个自定义蓝图节点,该节点监听GameplayEffect堆叠更改。HUD UMG小部件使用它来更新玩家被动装甲堆叠的数量。此AsyncTask将永远存在,直到手动调用EndTask(),我们在UMG小部件的Destruct事件中这样做。请参阅AsyncTaskEffectStackChanged.h/cpp

监听GameplayEffect堆叠更改BP节点

4.5.6 授予Abilities

GameplayEffects可以授予新的GameplayAbilitiesASCs。只有DurationInfinite类型的GameplayEffects可以授予能力。

一个常见用例是当您希望强制另一个玩家执行某些操作时,例如将他们从击退或拉出。您将应用GameplayEffect到他们,该GameplayEffect授予他们自动激活能力(请参阅被动Abilities,了解如何自动激活能力),该能力执行所需的操作。

设计师可以选择授予GameplayEffect的能力、授予它们的级别、在何处绑定输入它们以及授予能力的移除策略。

移除策略描述
立即取消Ability 当授予能力的GameplayEffect被移除时,立即取消并移除该能力。
在结束时移除Ability 允许能力完成,然后从目标中移除能力。
Do Nothing (什么都不做)目标不受授予GameplayEffect移除的影响。目标将永久拥有能力,直到手动移除。

4.5.7 Gameplay Effect Tags

GameplayEffects携带多个GameplayTagContainers。设计师将编辑每个类别和结果的AddedRemoved GameplayTagContainers,结果将显示在编译时的Combined GameplayTagContainer上。Added标签是此GameplayEffect添加的新标签,其父类没有先前添加。Removed标签是父类具有但此子类没有的标签。

类别描述
Gameplay Effect Asset Tags标签是GameplayEffect具有的标签。它们本身没有任何功能,仅用于描述GameplayEffect
Granted Tags标签生活在GameplayEffect上,但也提供给应用GameplayEffectASC。它们在移除GameplayEffect时从ASC中移除。此功能仅适用于DurationInfinite类型的GameplayEffects
Ongoing Tag Requirements一旦应用,这些标签确定GameplayEffect是否打开。如果GameplayEffect由于未能通过持续标签要求而关闭,但随后满足要求,GameplayEffect将再次打开并重新应用其修改器。此功能仅适用于DurationInfinite类型的GameplayEffects
Application Tag Requirements目标上的标签,用于确定是否可以将GameplayEffect应用于目标。如果这些要求未满足,则不会应用GameplayEffect
Remove Gameplay Effects with Tags目标上的GameplayEffects具有这些标签中的任何一个,这些标签在Asset TagsGranted Tags中,将从目标中移除目标,当此GameplayEffect成功应用时。

4.5.8 Immunity(免疫)

GameplayEffects可以授予免疫,有效地阻止其他GameplayEffects的申请,基于GameplayTags。虽然免疫可以通过其他方式有效地实现,例如Application Tag Requirements,使用此系统提供了委托,当GameplayEffects由于免疫而被阻止时。

GrantedApplicationImmunityTags检查源ASC(包括从源能力AbilityTags中提取的标签,如果有的话)是否具有任何指定的标签。这是防止从某些角色或来源的所有GameplayEffects的一种方法,基于他们的标签。

Granted Application Immunity Query检查传入的GameplayEffectSpec,如果它匹配任何查询以阻止或允许其应用。

查询有有用的悬停工具提示在GameplayEffect蓝图中。

4.5.9 Gameplay Effect Spec

GameplayEffectSpec(简称GESpec)可以被认为是GameplayEffects的实例化。它们持有对它们表示的GameplayEffect类的引用、创建时所在的级别以及谁创建了它们。这些可以在运行时自由创建和修改,而不像GameplayEffects应该由设计师在运行时之前创建。当应用GameplayEffect时,从GameplayEffect创建GameplayEffectSpec,这实际上是应用到目标的。

GameplayEffectSpecs是从GameplayEffects使用UAbilitySystemComponent::MakeOutgoingSpec()创建的,该UAbilitySystemComponent::MakeOutgoingSpec()BlueprintCallableGameplayEffectSpecs不需要立即应用。通常,将GameplayEffectSpec传递给从能力创建的投射物,该投射物可以在稍后应用于目标。当GameplayEffectSpecs成功应用时,它们返回一个新的结构,称为FActiveGameplayEffect

重要的GameplayEffectSpec内容:

  • GameplayEffect是从哪个GameplayEffect创建的。
  • GameplayEffectSpec的级别。通常与创建GameplayEffectSpec的能力的级别相同,但可以不同。
  • GameplayEffectSpec的持续时间。默认情况下为GameplayEffect的持续时间,但可以不同。
  • GameplayEffectSpec的周期,用于周期效果。默认情况下为GameplayEffect的周期,但可以不同。
  • GameplayEffectSpec的当前堆叠计数。堆叠限制在GameplayEffect上。
  • GameplayEffectContextHandle告诉我们谁创建了这个GameplayEffectSpec
  • 由于快照,捕获了Attributes
  • DynamicGrantedTagsGameplayEffectSpec授予目标,除了GameplayEffect授予的GameplayTags
  • DynamicAssetTagsGameplayEffectSpec具有的AssetTags,除了GameplayEffect具有的AssetTags
  • SetByCaller TMaps

4.5.9.1 SetByCallers(调用者设置)

SetByCallers允许GameplayEffectSpec携带与GameplayTagFName关联的浮点值。它们存储在各自的TMaps中:TMap<FGameplayTag, float>TMap<FName, float>GameplayEffectSpec上。这些可以用于GameplayEffect上的Modifiers或作为通用手段,将浮点数从一个地方传递到另一个地方。通常,将数值数据生成在能力内部传递到GameplayEffectExecutionCalculationsModifierMagnitudeCalculations通过SetByCallers

SetByCaller 用法笔记
Modifiers必须提前在GameplayEffect类中定义。只能使用GameplayTag版本。如果一个在GameplayEffect类中定义,但GameplayEffectSpec上没有相应的标签和浮点值对,游戏将在应用GameplayEffectSpec时抛出运行时错误并返回0。这是Divide操作的问题。请参阅Modifiers
其他用法不需要提前在任何地方定义。读取GameplayEffectSpec上不存在SetByCaller可以返回开发人员定义的默认值,并带有可选警告。

要在蓝图中分配SetByCaller值,请使用您需要的版本(GameplayTagFName)的蓝图节点:

分配SetByCaller

要在蓝图中读取SetByCaller值,您将需要在您的蓝图库中制作自定义节点。

要在C++中分配SetByCaller值,请使用您需要的版本(GameplayTagFName)的函数:

void FGameplayEffectSpec::SetSetByCallerMagnitude(FName DataName, float Magnitude);
void FGameplayEffectSpec::SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);

要在C++中读取SetByCaller值,请使用您需要的版本(GameplayTagFName)的函数:

float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;

我建议使用GameplayTag版本而不是FName版本。这可以防止蓝图中的拼写错误。

4.5.10 Gameplay Effect Context

GameplayEffectContext结构体持有关于GameplayEffectSpec's发起者和TargetData的信息。这也是一个很好的结构,用于继承以传递任意数据,如ModifierMagnitudeCalculations / GameplayEffectExecutionCalculationsAttributeSetsGameplayCues

要继承GameplayEffectContext

  1. 继承FGameplayEffectContext
  2. 重写FGameplayEffectContext::GetScriptStruct()
  3. 重写FGameplayEffectContext::Duplicate()
  4. 如果您的数据需要同步,重写FGameplayEffectContext::NetSerialize()
  5. 对子结构体实现TStructOpsTypeTraits,如父结构FGameplayEffectContext具有的那样
  6. 在您的AbilitySystemGlobals类中重写AllocGameplayEffectContext(),并返回新的子结构体对象

GASShooter使用继承的GameplayEffectContext来添加TargetData,这可以在GameplayCues中访问,特别是对于霰弹枪,因为它可以击中多个敌人。

4.5.11 Modifier Magnitude Calculation(修饰符强度计算)

ModifierMagnitudeCalculations(简称ModMagCalcMMC)是强大的类,用于GameplayEffects中的Modifiers。它们的功能类似于GameplayEffectExecutionCalculations,但最重要的是它们可以被预测。它们的唯一目的是从CalculateBaseMagnitude_Implementation()返回浮点值。您可以在蓝图中子类化和覆盖此函数。

MMCs可以用于任何持续时间的GameplayEffects - InstantDurationInfinitePeriodic

MMCs'强度在于它们捕获任何数量的Attributes的能力,这些AttributesSourceTarget上,完全访问GameplayEffectSpec以读取GameplayTagsSetByCallersAttributes可以快照或不快照。快照Attributes在创建GameplayEffectSpec时捕获,非快照Attributes在应用时捕获并自动更新,当Attribute更改时,对于InfiniteDuration类型的GameplayEffects。捕获Attributes会重新计算它们的CurrentValue,从ASC上的现有修改器中。此重新计算将运行PreAttributeChange()AbilitySet中,因此任何clamping必须在此再次完成。

Snapshot(快照)SourceTargetGameplayEffectSpec中捕获Attribute变为Infinite GEDuration GE自动更新
Source创建
Target应用
Source应用
Target应用

MMC获得的浮点值可以进一步在GameplayEffect's Modifier中修改,通过系数和前系数添加。

一个示例MMC捕获目标的mana Attribute,并将其从中毒效果中减少,该效果取决于目标有多少mana,以及目标可能具有的标签:

UPAMMC_PoisonMana::UPAMMC_PoisonMana()
{
 
	//标头 FGameplayEffectAttributeCaptureDefinition ManaDef 中定义的ManaDef;
    ManaDef.AttributeToCapture = UPAAttributeSetBase::GetManaAttribute();
	ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	ManaDef.bSnapshot = false;
 
	//MaxManaDef 在标头 FGameplayEffectAttributeCaptureDefinition MaxManaDef 中定义;
	MaxManaDef.AttributeToCapture = UPAAttributeSetBase::GetMaxManaAttribute();
	MaxManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	MaxManaDef.bSnapshot = false;
 
	RelevantAttributesToCapture.Add(ManaDef);
	RelevantAttributesToCapture.Add(MaxManaDef);
}
 
float UPAMMC_PoisonMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	// 收集源和目标的标签,因为这会影响应该使用哪些增益
    const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
 
	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;
 
	float Mana = 0.f;
	GetCapturedAttributeMagnitude(ManaDef, Spec, EvaluationParameters, Mana);
	Mana = FMath::Max<float>(Mana, 0.0f);
 
	float MaxMana = 0.f;
	GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvaluationParameters, MaxMana);
	MaxMana = FMath::Max<float>(MaxMana, 1.0f); // Avoid divide by zero
 
	float Reduction = -20.0f;
	if (Mana / MaxMana > 0.5f)
	{
		// Double the effect if the target has more than half their mana
		Reduction *= 2;
	}
	
	if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
	{
		// Double the effect if the target is weak to PoisonMana
		Reduction *= 2;
	}
	
	return Reduction;
}

如果您不在RelevantAttributesToCapture中添加FGameplayEffectAttributeCaptureDefinitionRelevantAttributesToCapture,然后尝试捕获Attributes,您将收到缺少Spec的错误,如果您不需要捕获Attributes,则不需要添加任何内容。

4.5.12 Gameplay Effect Execution Calculation(游戏玩法效果执行计算)

GameplayEffectExecutionCalculations(简称ExecutionCalculationExecution(您将在插件的源代码中经常看到此术语)或ExecCalc)是GameplayEffectsASC进行更改的最强大方式。与ModifierMagnitudeCalculations类似,这些可以捕获Attributes并可选地捕获它们。与MMCs不同,这些可以更改多个Attribute并基本上执行任何其他程序想要执行的操作。此功能的强大和灵活性的代价是它们无法被预测并且必须用C++实现。

ExecutionCalculations只能用于InstantPeriodic类型的GameplayEffects。任何带有’Execute’的单词通常指的是这两种类型。

快照捕获Attribute,当GameplayEffectSpec被创建时,而不是在应用时捕获它们。捕获Attributes会重新计算它们的CurrentValue,从ASC上的现有修改器中。此重新计算将运行PreAttributeChange()AbilitySet中,因此任何clamping必须在此再次完成。

快照源或目标GameplayEffectSpec中捕获
创建
目标应用
应用
目标应用

要设置Attribute捕获,我们遵循Epic的ActionRPG示例项目设置的模式,定义一个结构来捕获和定义我们如何捕获Attributes,并在结构构造函数中创建一个副本。您将有一个结构,如下所示,用于每个ExecCalc。**注意:**每个结构需要一个唯一的名称,因为它们共享相同的命名空间。使用相同的名称会导致捕获Attributes时出现错误行为(主要是捕获错误的Attributes)。

对于Local PredictedServer OnlyServer Initiated类型的GameplayAbilitiesExecCalc仅在服务器上调用。

计算伤害接收基于复杂公式从许多属性在SourceTarget上读取伤害的示例是最常见的示例。包括示例项目有一个简单的ExecCalc,用于计算伤害,从GameplayEffectSpec's SetByCaller读取伤害值,然后根据从Target捕获的armor Attribute来减少该值。请参阅GDDamageExecCalculation.cpp/.h

4.5.12.1 发送数据到Execution Calculations

除了捕获Attributes,还有几种方法可以将数据发送到ExecutionCalculation.

4.5.12.1.1 SetByCaller(调用者设置)

任何在GameplayEffectSpec上设置的SetByCallers都可以直接在ExecutionCalculation中读取。

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
float Damage = FMath::Max<float>(Spec.GetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.0f), 0.0f);

4.5.12.1.2 Backing数据Attribute计算Modifier(Backing Data Attribute Calculation Modifier)

如果您想硬编码值到GameplayEffect中,您可以使用CalculationModifier,该CalculationModifier使用一个捕获的Attributes作为后备数据。

在此截图示例中,我们将50添加到捕获的Damage Attribute。您也可以将其设置为Override,以仅接受硬编码值。

Backing Data Attribute Calculation Modifier

ExecutionCalculation在捕获Attribute时读取此值。

float Damage = 0.0f;
// Capture optional damage value set on the damage GE as a CalculationModifier under the ExecutionCalculation
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);

4.5.12.1.3 Backing Data Temporary Variable Calculation Modifier(Backing数据临时变量计算Modifier)

如果您想硬编码值到GameplayEffect中,您可以使用CalculationModifier,该CalculationModifier使用Temporary VariableTransient Aggregator作为它被调用的C++中的Temporary Variable

在此截图示例中,我们使用Data.Damage GameplayTag将50添加到Temporary Variable

Backing Data Temporary Variable Calculation Modifier

将后备Temporary Variables添加到ExecutionCalculation's构造函数:

ValidTransientAggregatorIdentifiers.AddTag(FGameplayTag::RequestGameplayTag("Data.Damage"));

ExecutionCalculation使用特殊捕获函数类似Attribute捕获函数读取此值。

float Damage = 0.0f;
ExecutionParams.AttemptCalculateTransientAggregatorMagnitude(FGameplayTag::RequestGameplayTag("Data.Damage"), EvaluationParameters, Damage);

4.5.12.1.4 Gameplay Effect Context

您可以通过自定义GameplayEffectContextGameplayEffectSpec上将数据发送到ExecutionCalculation

ExecutionCalculation中,您可以从FGameplayEffectCustomExecutionParameters访问EffectContext

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(Spec.GetContext().Get());

如果您需要更改GameplayEffectSpecEffectContext上的某些内容:

FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(MutableSpec->GetContext().Get());

使用谨慎,如果修改GameplayEffectSpecExecutionCalculation中。请参阅GetOwningSpecForPreExecuteMod()的注释。

/** Non const access. Be careful with this, especially when modifying a spec after attribute capture. */
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;

4.5.13 Custom Application Requirement(自定义应用需求)

CustomApplicationRequirement(简称CAR)类为设计师提供了高级控制,以确定是否可以应用GameplayEffect,而不是简单地检查GameplayEffect上的GameplayTag。这些可以在蓝图中通过覆盖CanApplyGameplayEffect()来实现,在C++中通过覆盖CanApplyGameplayEffect_Implementation()来实现。

使用CARs的示例:

  • Target需要具有一定数量的Attribute
  • Target需要具有一定数量的GameplayEffect堆叠

CARs还可以执行更多高级操作,例如检查目标上是否存在此GameplayEffect的实例,并更改现有实例的持续时间(如果CanApplyGameplayEffect()返回false)。

4.5.14 Cost Gameplay Effect(消耗型 GameplayEffect)

GameplayAbilities具有一个可选的GameplayEffect,专门设计用于作为能力成本。成本是ASC需要拥有的Attribute数量,以便能够激活GameplayAbility。如果GA无法负担Cost GE,则无法激活。此Cost GE应为Instant类型的GameplayEffect,并具有一个或多个Modifiers,这些Modifiers会从Attributes中减去。默认情况下,Cost GEs旨在被预测,并且建议保持这种能力,不要使用ExecutionCalculationsMMCs是完全可接受的,并且鼓励用于复杂的成本计算。

在开始时,您将拥有一个唯一的Cost GE,每个GA具有成本。更高级的技术是重用一个Cost GE来为多个GAs服务,只需修改从Cost GE创建的GameplayEffectSpec,并使用GA特定的数据(成本值在GA上定义)。这只适用于Instanced能力。

两种技术用于重用Cost GE

  1. **使用MMC。**这是最简单的方法。创建一个MMC,该MMCGameplayAbility实例中读取成本值,您可以从GameplayEffectSpec获取该值。
float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());
 
	if (!Ability)
	{
		return 0.0f;
	}
 
	return Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel());
}

在此示例中,成本值是一个FScalableFloat,我将其添加到GameplayAbility子类中。

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cost")
FScalableFloat Cost;

Cost GE With MMC

  1. **覆盖UGameplayAbility::GetCostGameplayEffect()。**覆盖此函数并在运行时创建GameplayEffect,该GameplayEffect读取成本值在GameplayAbility上。

4.5.15 Cooldown Gameplay Effect

GameplayAbilities具有一个可选的GameplayEffect,专门设计用于作为能力冷却。冷却确定激活能力后可以再次激活的时间。如果GA仍在冷却中,则无法激活。此Cooldown GE应为Duration类型的GameplayEffect,并且没有Modifiers,并且具有唯一的GameplayTag,每个GameplayAbility或每个能力槽(如果您的游戏具有可互换的能力分配到槽中,这些槽共享冷却)在GameplayEffect's GrantedTags中(“Cooldown Tag”)。GA实际上检查Cooldown Tag的存在,而不是Cooldown GE的存在。默认情况下,Cooldown GEs旨在被预测,并且建议保持这种能力,不要使用ExecutionCalculationsMMCs是完全可接受的,并且鼓励用于复杂的冷却计算。

在开始时,您将拥有一个唯一的Cooldown GE,每个GA具有冷却。更高级的技术是重用一个Cooldown GE来为多个GAs服务,只需修改从Cooldown GE创建的GameplayEffectSpec,并使用GA特定的数据(持续时间和Cooldown TagGA上定义)。这只适用于Instanced能力。

复用Cooldown GE的两种方法:

  1. **使用SetByCaller。**这是最简单的方法。将Cooldown GE的持续时间设置为SetByCaller,并使用GameplayTagGameplayAbility子类中定义一个浮点数/FScalableFloat,一个FGameplayTagContainer用于唯一的Cooldown Tag,以及一个临时FGameplayTagContainer,我们将使用作为返回指针的联合体,我们的Cooldown TagCooldown GE's标签。
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;
 
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;
 
// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

然后重写UGameplayAbility::GetCooldownTags()以返回我们的Cooldown Tags和任何现有Cooldown GE标签的联合体。

const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
	FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
	MutableTags->Reset(); // MutableTags writes to the TempCooldownTags on the CDO so clear it in case the ability cooldown tags change (moved to a different slot)
	const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
	if (ParentTags)
	{
		MutableTags->AppendTags(*ParentTags);
	}
	MutableTags->AppendTags(CooldownTags);
	return MutableTags;
}

最后,重写UGameplayAbility::ApplyCooldown()以注入我们的Cooldown Tags到冷却GameplayEffectSpec

void UPGGameplayAbility::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(CooldownTags);
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
	}
}

在这张图中,冷却的持续时间 Modifier 被设置为 SetByCaller,并带有一个 Data Tag,名为 Data.CooldownData.Cooldown 在上面的代码中会是 OurSetByCallerTagCooldown GE with SetByCaller

2.使用 MMC。 这与上面的设置相同,唯一不同的是在 Cooldown GEApplyCooldown 中设置 SetByCaller 作为持续时间。相反,将持续时间设置为一个 Custom Calculation Class,并指向我们将创建的新 MMC

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;
 
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;
 
// 临时容器,我们将在 GetCooldownTags() 中返回它的指针。
// 这将是我们 CooldownTags 和 Cooldown GE 的冷却标签的联合。
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

然后重写 UGameplayAbility::GetCooldownTags(),以返回我们 Cooldown Tags 和任何现有的 Cooldown GE 标签的联合。

const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
    FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
    MutableTags->Reset(); // MutableTags 写入 TempCooldownTags 的 CDO,因此在能力冷却标签发生变化时(例如移到不同的槽位)清空它
    const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
    if (ParentTags)
    {
        MutableTags->AppendTags(*ParentTags);
    }
    MutableTags->AppendTags(CooldownTags);
    return MutableTags;
}

最后,重写 UGameplayAbility::ApplyCooldown(),将我们的 Cooldown Tags 注入到冷却 GameplayEffectSpec 中。

void UPGGameplayAbility::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(CooldownTags);
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
    }
}
float UPGMMC_HeroAbilityCooldown::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
    const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());
 
    if (!Ability)
    {
        return 0.0f;
    }
 
    return Ability->CooldownDuration.GetValueAtLevel(Ability->GetAbilityLevel());
}

Cooldown GE with MMC

4.5.15.1 获取 Cooldown GameplayEffect 的剩余时间

bool APGPlayerState::GetCooldownRemainingForTag(FGameplayTagContainer CooldownTags, float & TimeRemaining, float & CooldownDuration)
{
	if (AbilitySystemComponent && CooldownTags.Num() > 0)
	{
		TimeRemaining = 0.f;
		CooldownDuration = 0.f;
 
		FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
		TArray< TPair<float, float> > DurationAndTimeRemaining = AbilitySystemComponent->GetActiveEffectsTimeRemainingAndDuration(Query);
		if (DurationAndTimeRemaining.Num() > 0)
		{
			int32 BestIdx = 0;
			float LongestTime = DurationAndTimeRemaining[0].Key;
			for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
			{
				if (DurationAndTimeRemaining[Idx].Key > LongestTime)
				{
					LongestTime = DurationAndTimeRemaining[Idx].Key;
					BestIdx = Idx;
				}
			}
 
			TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
			CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;
 
			return true;
		}
	}
 
	return false;
}

**注意:**查询冷却的剩余时间在客户端上需要它们可以接收同步GameplayEffects。这取决于它们的ASC同步模式

4.5.15.2 Listening for Cooldown Begin and End(监听冷却开始和结束)

要监听冷却开始,您可以响应Cooldown GE被添加时,也可以响应Cooldown Tag被添加时,我建议监听Cooldown GE被添加,因为您也可以访问应用于GameplayEffectSpecGameplayEffectSpec。从这里,您可以确定它是本地预测的Cooldown GE还是服务器纠正的Cooldown GE

要监听冷却结束,您可以响应Cooldown GE被移除时,也可以响应Cooldown Tag被移除时。我建议监听Cooldown Tag被移除,因为当服务器纠正的Cooldown GE进来时,它会移除我们本地预测的Cooldown GE,导致OnAnyGameplayEffectRemovedDelegate()被触发,即使我们仍然在冷却中。Cooldown Tag不会在本地预测的Cooldown GE被移除和应用服务器纠正的Cooldown GE时更改。

**注意:**监听GameplayEffect添加或移除在客户端上需要它们可以接收同步GameplayEffects。这取决于它们的ASC's 同步模式

示例项目包括一个自定义蓝图节点,该节点监听冷却开始和结束。HUD UMG小部件使用它来更新Meteor冷却剩余时间。此AsyncTask将永远存在,直到手动调用EndTask(),我们在UMG小部件的Destruct事件中这样做。请参阅AsyncTaskCooldownChanged.h/cpp

监听冷却更改BP节点

4.5.15.3 Predicting Cooldowns(预测冷却时间)

冷却无法真正被预测。我们可以开始UI冷却计时器,当本地预测的Cooldown GE被应用时,但GameplayAbility's实际冷却时间与服务器冷却剩余时间相关联。根据玩家的延迟,本地预测的冷却可能会在服务器上过期,但GameplayAbility仍然在冷却中,这将防止GameplayAbility's立即重新激活,直到服务器冷却过期。

示例项目通过在本地预测的Cooldown GE开始时灰化Meteor能力UI图标,然后在服务器纠正的Cooldown GE进来时开始冷却计时器来处理这种情况。

游戏后果是,具有高延迟的玩家在短冷却能力上的射击速度低于具有低延迟的玩家,这使他们在游戏中处于劣势。Fortnite通过他们的武器具有自定义的书籍来避免这种情况,这些武器不使用冷却GameplayEffects

允许真正的预测冷却(玩家可以在本地冷却过期后激活GameplayAbility,但服务器仍在冷却中)是Epic希望在未来迭代中实现的东西

4.5.16 更改已激活的 Gameplay Effect 的持续时间

要更改Cooldown GE或任何Duration类型的GameplayEffect的剩余时间,我们需要更改GameplayEffectSpec's Duration,更新其StartServerWorldTime,更新其CachedStartServerWorldTime,更新其StartWorldTime,并重新运行检查以查看持续时间。在服务器上执行此操作并标记FActiveGameplayEffect脏,以将更改同步到客户端。**注意:**这涉及const_cast,可能不是Epic希望更改持续时间的方式,但它似乎效果很好。

bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
{
	if (!Handle.IsValid())
	{
		return false;
	}
 
	const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
	if (!ActiveGameplayEffect)
	{
		return false;
	}
 
	FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
	if (NewDuration > 0)
	{
		AGE->Spec.Duration = NewDuration;
	}
	else
	{
		AGE->Spec.Duration = 0.01f;
	}
 
	AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
	AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
	AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
	ActiveGameplayEffects.MarkItemDirty(*AGE);
	ActiveGameplayEffects.CheckDuration(Handle);
 
	AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
	OnGameplayEffectDurationChange(*AGE);
 
	return true;
}

4.5.17 在运行时创建动态 GameplayEffects

在运行时创建动态GameplayEffects是一个高级主题。您不应该经常这样做。

只有Instant类型的GameplayEffects可以在运行时从零创建,因为当它们同步时,它们会寻找GameplayEffect类定义,这些定义不存在。要实现此功能,您应该改为制作一个GameplayEffect类原型,然后根据需要定制运行时GameplayEffectSpec实例。

运行时创建的Instant类型的GameplayEffects也可以从本地预测中的本地预测GameplayAbility调用。但是,尚不清楚动态创建是否会有副作用。

示例

示例项目创建一个用于将黄金和经验点返回给角色杀死者的GameplayEffect

// 创建一个动态的即时 Gameplay Effect 以给予奖励
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Bounty")));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;
 
int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);
 
FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
InfoXP.ModifierOp = EGameplayModOp::Additive;
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();
 
FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
InfoGold.ModifierOp = EGameplayModOp::Additive;
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();
 
Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());

第二个示例显示了运行时在本地预测GameplayAbility中创建的GameplayEffect。使用时要小心(请参阅代码中的注释!)!

UGameplayAbilityRuntimeGE::UGameplayAbilityRuntimeGE()
{
	NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}
 
void UGameplayAbilityRuntimeGE::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
	{
		if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
		{
			EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
		}
 
		// 在运行时创建 GE
		UGameplayEffect* GameplayEffect = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeInstantGE"));
		GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // Only instant works with runtime GE.
 
		// 添加一个简单的可缩放浮动修改器,将 MyAttribute 修改为 42。
        // 在实际应用中,可使用 TriggerEventData 中传递的信息。
        const int32 Idx = GameplayEffect->Modifiers.Num();
		GameplayEffect->Modifiers.SetNum(Idx + 1);
		FGameplayModifierInfo& ModifierInfo = GameplayEffect->Modifiers[Idx];
		ModifierInfo.Attribute.SetUProperty(UMyAttributeSet::GetMyModifiedAttribute());
		ModifierInfo.ModifierMagnitude = FScalableFloat(42.f);
		ModifierInfo.ModifierOp = EGameplayModOp::Override;
 
		// 应用 GE
 
        // 在这里创建 GESpec 以避免 ASC 创建基于 GE 类默认对象的 GESpec。
        // 由于这里是动态 GE,这将创建一个带有基本 GameplayEffect 类的 GESpec,因此我们
        // 将丢失修改器。请注意:目前尚不清楚这种 "hack" 是否会产生副作用!
        // Spec 防止 GE 对象被垃圾回收,因为 GE 是 GESpec 上的 UPROPERTY。
        FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // "new", since lifetime is managed by a shared ptr within the handle
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec));
	}
	EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}

4.5.18 Gameplay Effect Containers

Epic的Action RPG示例项目实现了一个称为FGameplayEffectContainer的结构。这些不是vanilla GAS的一部分,但它们非常有用,用于包含GameplayEffectsTargetData。它自动化了一些工作,例如从GameplayEffects创建GameplayEffectSpecs并设置默认值在GameplayEffectContext中。在GameplayAbility中制作GameplayEffectContainer并将其传递给生成的投射物非常容易和直接。我选择不在包含示例项目中实现GameplayEffectContainers,以显示如何在没有它们的情况下工作,但强烈建议查看它们并考虑将它们添加到您的项目中。

要访问GESpecs,请中断FGameplayEffectContainer并访问GESpec引用,方法是按索引访问数组中的GESpec。这需要您提前知道GESpec的索引。

SetByCaller with a GameplayEffectContainer

GameplayEffectContainers还包含一个可选的效率高的Targeting容器方法。