4.5 游戏效果 - Gameplay Effects

目录

4.5.1 游戏效果的定义 - Gameplay Effect Definition

GameplayEffectsGE)是技能对自身和他者的AttributesGameplayTags产生影响的容器。具体来讲,他们可以产生一些瞬间的Attribute的改变效果,比如说伤害或者治疗,以及一些长期的属性buff/debuff效果,比如加速或者眩晕之类的。UGameplayEffect是一个定义单一游戏效果的数据类,这意味着GameplayEffects里面不应该添加任何其他的逻辑。通常设计师们只需要创建UGameplayEffect的派生类就够了。

GameplayEffects是通过ModifiersExecutions (GameplayEffectExecutionCalculation)来对Attributes进行修改和调整的。

GameplayEffects可以按生效时间分为三类:即刻生效Instant,持续一段时间Duration,以及无限持续时间Infinite

此外,GameplayEffects也可以添加和执行GameplayCuesInstant类型的GameplayEffect将调用GameplayCue里的Execute,而Duration或者Infinite类型的GameplayEffect将会调用GameplayCue上的AddRemove

持续类型GameplayCue事件使用时机
InstantExecute用于永久性的、立即的对AttributeBaseValue的修改。GameplayTags将不会被应用,即便是一帧都没有。
DurationAdd & Remove用于临时的对AttributeCurrentValue的修改并且应用GameplayTags,该GameplayTags会随着GameplayEffect的到期而被移除(或者自行手动删除)。具体的持续时间可以在UGameplayEffect类/Blueprint中进行指定。
InfiniteAdd & Remove用于临时的对AttributeCurrentValue的修改并且应用GameplayTags,该GameplayTags会随着GameplayEffect被移除时一起移除。他们永远不会过期,所以必须通过技能或者ASC手动移除掉。

DurationInfinite类型的GameplayEffects中可以通过一个选项来应用周期性的效果Periodic Effects,在它里面可以通过定义Period来周期性得每X秒就调用一次它的ModifiersExecutionsPeriodic Effects可以作为Instant类型的GameplayEffects来对待,即它会修改AttributeBaseValue并且执行GameplayCues。这对于实现DOT伤害(持续性伤害)非常有效果。注意:Periodic Effects无法进行预测

可以依据DurationInfinite类型的GameplayEffectsOngoing Tag Requirements选项是否符合Gameplay Effect Tags来临时对该GameplayEffects进行关闭或开启。关闭一个GameplayEffect将会移除掉它的Modifiers的效果并且应用GameplayTags,但是并不会移除掉该GameplayEffect。将GameplayEffect再开启会再应用它的ModifiersGameplayTags

如果你需要手动重新计算DurationInfinite类型的GameplayEffectsModifiers(假设你有一个MMC,而其使用的数据并不是从Attributes里来的),你可以去调用UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel),其中的NewLevel参数可以通过UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel()来得到。基于自动AttributesModifiers会随着Attributes更新而进行更新。用来更新ModifierSetActiveGameplayEffectLevel()的内部的关键函数有:

MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
// Private function otherwise we'd call these three functions without needing to set the level to what it already is
UpdateAllAggregatorModMagnitudes(Effect);

GameplayEffects通常并没有实例化。当某个技能或者ASC想要去应用一个GameplayEffect时,它会利用GameplayEffectClassDefaultObject创建一个GameplayEffectSpec。成功得应用GameplayEffectSpecs后它会被添加到一个FActiveGameplayEffect的结构体,这也就是ASCActiveGameplayEffects

4.5.2 应用游戏效果 - Applying Gameplay Effects

GameplayAbilitiesASC里有很多函数可以用来应用某个GameplayEffects,这些函数名字里通常里面都会带有ApplyGameplayEffectTo。不同的函数其本质都是一样的,最终都会落到在Target上去调用其相应的UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()

为了在GameplayAbility之外应用GameplayEffects,比如子弹的发射,你需要去获取TargetASC,然后调用其ApplyGameplayEffectToSelf

你可以监听Duration或者Infinite类型的GameplayEffects被应用到某个ASC上的事件,通过在相应的委托上绑定回调:

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

The callback function:

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

无论何种复制模式下服务器都会去调用这个函数。当复制模式为FullMixed,自主代理会为复制的GameplayEffects调用此方法。只有当replication modeFull时,模拟代理才会调用这个方法。

4.5.3 移除游戏效果 - Removing Gameplay Effects

GameplayAbilitiesASC里有很多函数可以用来移除某个GameplayEffects,这些函数名字里通常里面都会带有RemoveActiveGameplayEffect。不同的函数其本质都是一样的,最终都会落到在Target上去调用其相应的FActiveGameplayEffectsContainer::RemoveActiveEffects()

为了在GameplayAbility之外应用GameplayEffects,比如子弹的发射,你需要去获取TargetASC,然后调用其RemoveActiveGameplayEffect

你可以监听Duration或者Infinite类型的GameplayEffects被从某个ASC上移除的事件,通过在相应的委托上绑定回调:

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

对应的回调函数:

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

无论何种复制模式下服务器都会去调用这个函数。当复制模式为FullMixed,自主代理会为复制的GameplayEffects调用此方法。只有当replication modeFull时,模拟代理才会调用这个方法。

4.5.4 游戏效果的修改器 - Gameplay Effect Modifiers

修改器Modifiers可以去修改某个Attribute,并且也是唯一的对Attribute的修改进行预测的方式。某个GameplayEffect可以没有也可以有多个Modifiers。每个Modifier只能通过特定的操作对一项Attribute进行修改。

操作描述
Add将结果加到Modifier的指定的Attribute上,减法就是使用相应的负值即可。
Multiply将结果乘以Modifier的指定的Attribute
Divide将结果除以Modifier的指定的Attribute
Override将结果直接替换掉Modifier的指定的Attribute

AttributeCurrentValue是将其所有的Modifiers添加到其BaseValue的一个汇总的结果。对Modifiers如何进行汇总的公式是在GameplayEffectAggregator.cppFAggregatorModChannel::EvaluateWithBase中进行定义的:

((InlineBaseValue + Additive) * Multiplicitive) / Division

任意的Override类型的Modifiers都将会优先使用最后应用的Modifier来覆盖最终的值。

**注意:**百分比式的修改要使用Multiply操作,以确保其是在Add之后执行。

**注意:**对百分比式的修改的预测是有一些问题存在的。

共有四种类型的ModifiersScalable FloatAttribute BasedCustom Calculation Class,以及Set By Caller。他们都会生成一些浮点值,然后基于Modifier的操作类型利用值对Attribute进行修改。。

Modifier类型描述
Scalable FloatFScalableFloats是一种能够指向Data Table的结构,其中Data Table是将变量作为行、将等级作为列。Scalable Floats将会自动根据技能的当前等级(或者是在GameplayEffectSpec中重写的等级)读取指定行的值。这个值可以进一步用一个系数相乘。如果没有指定Data Table/行,该值会被当做是1,从而使用一个单独的硬编码的值作为所有等级的值。在这里插入图片描述
Attribute BasedAttribute Based类型的Modifiers会获取SourceGameplayEffectSpec的创建者)或者TargetGameplayEffectSpec的接收者)上的AttributeCurrentValue或者BaseValue,然后进一步对其使用系数以及一些前/后处理来进行修改。快照Snapshotting意味着会在GameplayEffectSpec被创建时对Attribute进行捕捉,而no snapshotting则意味着在GameplayEffectSpec应用时来对Attribute进行捕捉。
Custom Calculation ClassCustom Calculation Class为复杂Modifiers提供了最大的灵活性。这类Modifier需要一个ModifierMagnitudeCalculation类,然后可以通过系数和以及一些前/后处理来修改结果值。
Set By CallerSetByCaller类型的Modifiers由技能在运行时在GameplayEffect之外设置或者由GameplayEffectSpec的创建者进行设置。例如,如果你希望根据玩家按压按钮来对技能进行充能的时间来设置伤害值,那么你就可以使用SetByCallerSetByCallers本质上是一个TMap<FGameplayTag, float>,存在于GameplayEffectSpecModifier只是告诉Aggregator去通过提供的GameplayTag来查找SetByCaller值。Modifiers使用的SetByCallers只能使用GameplayTag而不能使用FName。如果Modifier被设置为SetByCaller,但是相应GameplayTagSetByCaller并不存在于GameplayEffectSpec里的话,游戏就会抛出一个运行时的错误并返回0。这样如果是Divide运算的话就会出问题了。参阅SetByCallers获取更多关于如何使用SetByCallers的信息。

4.5.4.1 乘法和除法类型的修改器 - Multiply and 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;
}

from GameplayEffectAggregator.cpp

MultiplyDivide类型的Modifiers中都有一个等于1的偏差值BiasAdditionBias0)。所以其公式实际上是这样的:

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

这样的公式会导致一些无法预料的结果。首先,这个公式会在将其应用到BaseValue之前把所有的Modifiers加到一起。大部分人的想法是将他们一起进行乘或者除。例如如果你有两个为1.5Multiply,大部分人对BaseValue的计算方式是1.5 x 1.5 = 2.25。而实际上的计算是BaseValue乘以2BaseValue具有50%的增长 + BaseValue具有50%的增长 = 100%的增长)。这也是GameplayPrediction.h中使用的方式,500的基础速度外加10%速度buff就得到550的速度。再施加另一个10%的速度buff则是600(500 + 50 x 10% + 50 x 10%)。

其次,这个公式对值如何来使用有一些没有记录在文档的规则,而在Paragon中又确确实实使用了这些规则。

MultiplyDivide运算的加法公式:

  • (小于1的值不超过1个) 和 ([1, 2)范围内的值可以有任意个)
  • 或者 (有一个值 >= 2)

公式中的Bias基本上是减去[1, 2)范围内的整数。第一个ModifierBias从起始的Sum值(设置为循环前的Bias)中减去,这就是为什么任何值本身都有效,为什么一个<1的值与范围[1,2)内的数字有效的原因。

Multiply的相关例子:
乘数:0.5
1 + (0.5 - 1) = 0.5,正确

乘数:0.5, 0.5
1 + (0.5 - 1) + (0.5 - 1) = 0,而不是预期的10.5 + 0.5)。小于1的值有多个,这时直接将乘数相加并没有意义。Paragon设计上只使用最大的负值用作Multiply计算类型的Modifiers,因此最多只有一个小于1乘以BaseValue

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

乘数:5, 5
1 + (5 - 1) + (5 - 1) = 9,而不是预期的105 + 5)。结果总是sum of the Modifiers - number of Modifiers + 1

许多游戏会想要他们的MultiplyDivide类型的Modifiers在应用到BaseValue之前共同去乘以及除。为了实现这个目标,你可能需要修改引擎源码,具体的话在FAggregatorModChannel::EvaluateWithBase()

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;
}
4.5.4.2 修改器上的游戏标签 - Gameplay Tags on Modifiers

每个Modifier都可以设置SourceTagsTargetTags。他们的工作原理与GameplayEffectApplication Tag requirements是一样的。因此标签只有在效果应用时才会被考虑。即,当有一个周期性的无限持续时间的效果时,他们只在第一次效果应用时考虑而不是在每个执行周期都被重新考虑。

Attribute Based类型的Modifiers也可以设置SourceTagFilterTargetTagFilter。当确定作为Attribute BasedModifier的来源的属性的具体大小时,这些过滤器会用于排除该属性的某些Modifier。那些source或者target没有所有过滤器标签的Modifiers将会被排除在外。

更详细来讲:作为sourceASC和作为targetASC的标签会被GameplayEffects捕捉。作为sourceASC的标签会在GameplayEffectSpec创建时被捕捉, 作为targetASC的标签则是在效果执行时被捕捉。当确定无限持续时间的或者持续一定时间的效果的Modifier是否合格(即其Aggregator聚合器符合要求),并且设置这些过滤器时,被捕获的标签将会与过滤器进行比较。

4.5.5 堆叠游戏效果 - Stacking Gameplay Effects

默认情况下,GameplayEffects会无视已经存在的GameplayEffectSpec的实例,在应用新的GameplayEffectSpec时会直接创建新的实例。GameplayEffects也可以设置为叠加,这是就不是添加新的GameplayEffectSpec实例,而是修改当前已存在的GameplayEffectSpec的堆叠数。堆叠只能用于DurationInfinite类型的GameplayEffects

共有两种类型的堆叠:源聚合和目标聚合。

堆叠类型描述
源聚合目标上每一个不同源的ASC都有一个自己的单独的栈实例。每个源都能够应用X数目个栈。
目标聚合无论有多少源,目标上仅有一个栈实例。每一个源能够应用栈的上限不能超过共享栈限制。

堆叠对于超时、持续时间刷新以及周期性重置都有相应对的处理办法。在GameplayEffect的蓝图里在对应条目上悬停都有相应的提示。

示例项目中包括了一个自定义的蓝图节点用来监听GameplayEffect栈的变化。HUD使用它来更新玩家的被动护甲叠加数。这一异步任务AsyncTask将会一直持续,知道手动调用EndTask(),这一步我们会在UMG Widget的Destruct事件中来做。参阅AsyncTaskEffectStackChanged.h/cpp

在这里插入图片描述

4.5.6 赋予技能 - Granted Abilities

GameplayEffects能够为ASCs赋予新的GameplayAbilities。只有DurationInfinite类型的GameplayEffects可以进行赋予技能的操作。

一个常见的用法就是当你想要强制玩家做某些事情,比如将他们击退或者拉近,你就可以给他们一个GameplayEffect来赋予他们一些自动激活的技能(参阅Passive Abilities获得更多关于如何在赋予技能后进度进行激活的内容),令他们能够做我们想让他们做的事情。。

设计师们可以选择用GameplayEffect设置具体赋予哪些技能,其具体的技能等级,对应的绑定输入是什么,以及赋予技能后其移除机制又是怎样的.

移除机制描述
Cancel Ability Immediately 立即取消技能GameplayEffect被从目标上移除时,立即取消并移除相应被赋予的技能。
Remove Ability on End 结束后移除技能被赋予的技能可以自然执行直到结束,然后从目标上移除。
Do Nothing 什么都不做从目标上移除GameplayEffect并不会影响相应被赋予的技能。目标可以一直拥有对应的能力直到手动移除。

4.5.7 游戏效果标签 - Gameplay Effect Tags

GameplayEffects带有多个GameplayTagContainers。设计师可以为每个类别编辑相应的AddedRemovedGameplayTagContainers,对应的结果会在编译后展现在CombinedGameplayTagContainer中。 Added的标签是指这个GameplayEffect新添加的父类之前所没有的标签。Removed的标签则是父类拥有而子类没有的。

种类描述
Gameplay Effect Asset TagsGameplayEffect所具有的标签。他们本身并不执行任何函数,仅用于描述GameplayEffect.
Granted Tags存在于GameplayEffect上的标签,但也会给到GameplayEffect应用到的目标的ASC上。当GameplayEffect移除时他们也会被一并从ASC上移除。仅用于DurationInfinite类型的GameplayEffects
Ongoing Tag Requirements一旦应用,这些标签将决定GameplayEffect是开还是关。GameplayEffect可以被应用时仍然时关闭状态的。如果GameplayEffect没有满足Ongoing Tag Requirements的就会被关闭,当条件满足时,它又会被再次打开并重新应用它的修改器。仅用于DurationInfinite类型的`GameplayEffects。
Application Tag Requirements指那些在目标上的标签,它们会决定GameplayEffect是否可以被应用到目标上。如果相应的要求没有满足,那么GameplayEffect则不会应用。
Remove Gameplay Effects with Tags目标上的GameplayEffects如果在Asset TagsGranted Tags里有任何这种类型的标签的话,将会被从目标上移除。

4.5.8 免疫效果 - Immunity

GameplayEffects能够赋予免疫的能力,即基于GameplayTags高效得阻止其他GameplayEffects的应用。虽然免疫的效果也可以通过其他方式实现,比如前面提到的Application Tag Requirements,但是这里介绍的方法会提供一个委托UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate,从而监听GameplayEffects被免疫掉的这一事件。

GrantedApplicationImmunityTags会检查源ASC(包括源上技能的AbilityTags中的那些标签)是否有某些特殊的标签。这是方式是基于标签通过某些角色或者源上的GameplayEffects来提供免疫的效果。

Granted Application Immunity Query会检查GameplayEffectSpec判断其是否匹配从而决定是阻止还是放行。

GameplayEffect的蓝图里悬停到Queries上查看更多相应的提示。

4.5.9 Gameplay Effect Spec

The GameplayEffectSpec (GESpec)可以认为是GameplayEffects的实例化。他会保存一个指向他们所表示的GameplayEffect类的引用,创建它时给定的等级,以及是谁创建了它。这些内容都可以在应用前运行时进行自由得创建和修改,这一点和GameplayEffects不一样(GameplayEffects需要在允许之前由设计师先行创建配置好。在应用一个GameplayEffect时,会先从GameplayEffect中创建一个GameplayEffectSpec出来,然后实际上是把GameplayEffectSpec应用给目标。

GameplayEffects创建GameplayEffectSpecs会用到UAbilitySystemComponent::MakeOutgoingSpec()BlueprintCallable)。GameplayEffectSpecs不是必须立即应用。通常是将GameplayEffectSpec传递给由技能创建的子弹,然后当子弹击中目标时将具体的技能效果应用给目标。当GameplayEffectSpecs成功被应用后,它会返回一个新的结构体FActiveGameplayEffect

GameplayEffectSpec中比较重要的内容有:

  • GameplayEffectSpec创建所依据的GameplayEffect类。
  • GameplayEffectSpec的等级。通常和创建这个GameplayEffectSpec的技能的等级相同,当然也可以不同。
  • GameplayEffectSpec的持续时间。默认是GameplayEffect的持续时间,当然也可以不同。
  • GameplayEffectSpec用于周期效果时,其周期。默认是GameplayEffect的周期,当然也可以不同。
  • GameplayEffectSpec的当前的堆叠数量。具体的堆叠限制在GameplayEffect上。
  • GameplayEffectContextHandle告诉我们是由谁创建的这个GameplayEffectSpec
  • GameplayEffectSpec创建时所对Attributes进行的快照。
  • GameplayEffectSpec赋予目标的DynamicGrantedTags,这个是在GameplayEffect赋予的GameplayTags之外的部分。
  • GameplayEffectSpec赋予目标的DynamicAssetTags,这个是在GameplayEffect赋予的AssetTags之外的部分。
  • SetByCaller``TMaps.

4.5.9.1 SetByCallers

SetByCallers允许GameplayEffectSpec去带一个和GameplayTagFName关联的浮点值。他们分别存储在各自对应的TMaps里:在GameplayEffectSpec上的TMap<FGameplayTag, float>TMap<FName, float>里。他们可以像是GameplayEffect上的Modifiers那样来用或者更加普遍的就是运送浮点值。常常会利用SetByCallers技能中生成的数值传递给GameplayEffectExecutionCalculations或者ModifierMagnitudeCalculations

SetByCaller的使用注意
Modifiers必须在GameplayEffect类中提前定义。只能使用GameplayTag(译者注:对应着不能使用FName)。如果在GameplayEffect类里定义了,但是GameplayEffectSpec并没有对应的标签和浮点值对,游戏将会报一个运行时的错误并返回0。Divide运算可能会有一些潜在的问题。参阅Modifiers
其他地方不需要提前进行定义。去读取GameplayEffectSpec上不存在的SetByCaller会返回一个开发者定义的默认值以及可以配置的警告内容。

要在蓝图中使用SetByCaller的值,可以使用相应的蓝图节点(GameplayTag或者是FName):

Assigning SetByCaller

要在蓝图中读取SetByCaller的值,你需要在Blueprint Library中实现自定义的节点。

要在C++中设置SetByCaller的值,可以使用相应的函数(GameplayTag或者是FName):

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

要在C++中读取SetByCaller的值,可以使用相应的函数(GameplayTag或者是FName):

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版本。这可以在蓝图中防止我们出现拼写错误,另外一个点是GameplayEffectSpec在进行网络复制时,GameplayTags是比FNames更加高效的。

4.5.10 游戏效果的上下文 - Gameplay Effect Context

GameplayEffectContext这个结构体中保存了GameplayEffectSpec的发起者和TargetData的一些信息。这个结构也可以稍作拓展用来在ModifierMagnitudeCalculations / GameplayEffectExecutionCalculationsAttributeSets以及GameplayCues之间传递数据。

派生GameplayEffectContext的过程:

  1. 实现FGameplayEffectContext的派生结构
  2. 重写FGameplayEffectContext::GetScriptStruct()
  3. 重写FGameplayEffectContext::Duplicate()
  4. 重写FGameplayEffectContext::NetSerialize(),如果你有一些新的数据需要复制的话
  5. 仿照父类结构FGameplayEffectContext实现派生类中的TStructOpsTypeTraits
  6. 在你的AbilitySystemGlobals类中重写AllocGameplayEffectContext()来返回你所建的派生类对象。

GASShooter项目中使用了GameplayEffectContext的派生类来添加TargetData,从而可以在GameplayCues中对其进行访问,这一点特别为霰弹枪设计,因为它可以击中不止一个目标。

4.5.11 修改器的幅值计算 - Modifier Magnitude Calculation

ModifierMagnitudeCalculations(简称ModMagCalcMMC)是一个功能非常强大的类,使用起来就像是GameplayEffects中的Modifiers。他们的功能类似于GameplayEffectExecutionCalculations,甚至于没有GameplayEffectExecutionCalculations那么功能繁多,但是最重要的是,他们是可以被预测的。MMC唯一的目的就是通过CalculateBaseMagnitude_Implementation()返回一个浮点值。你可以通过蓝图或者C++派生以及重写这个方法。

MMC可以用于任何持续时间的GameplayEffects - InstantDurationInfinite亦或是Periodic

MMC的优势在于捕捉GameplayEffectSource或者Target上的任意数量的Attributes的值的能力并且能够完整得访问GameplayEffectSpec以读取GameplayTags以及SetByCallersAttributes可以是快照也可以不是。快照的Attributes是在GameplayEffectSpec被创建时进行捕捉的,而非快照的Attributes则是在GameplayEffectSpec应用时进行捕捉的,并且会根据InfiniteDuration类型的GameplayEffectsAttribute进行修改而进行更新。通过已经存在于ASC的修改捕捉Attributes然后重新计算他们的CurrentValue。这里的重计算不会运行AbilitySet上的PreAttributeChange(),所以之前提到的那些对数值的预处理操作(截取)必须在这里再做一遍。

快照Source 或是 TargetGameplayEffectSpec上被捕捉的时机AttributeInfiniteDuration类型的GE修改时自动更新
YesSourceCreationNo
YesTargetApplicationNo
NoSourceApplicationYes
NoTargetApplicationYes

MMC的结果浮点值可以进一步在GameplayEffectModifier通过系数、预系数加法或后系数加法等方式进行修改。

下面是一个MMC的示例,会捕捉Target的魔法值Attribute从而用一个中毒效果来对其进行减少,其中减少的数量会依据Target拥有的魔法值和Target可能拥有的标签来决定:

UPAMMC_PoisonMana::UPAMMC_PoisonMana()
{

	//ManaDef defined in header FGameplayEffectAttributeCaptureDefinition ManaDef;
	ManaDef.AttributeToCapture = UPAAttributeSetBase::GetManaAttribute();
	ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	ManaDef.bSnapshot = false;

	//MaxManaDef defined in header 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
{
	// 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;

	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;
}

如果你没有在MMC的构建方法中将FGameplayEffectAttributeCaptureDefinition添加到RelevantAttributesToCapture中,在尝试去捕捉Attributes时你会在得到一个missing Spec相关的错误信息。如果你不需要捕捉Attributes,那么就不需要上面提到的那一步操作。

4.5.12 游戏效果执行的计算 - Gameplay Effect Execution Calculation

GameplayEffectExecutionCalculationsExecutionCalculationExecution(在插件源码中你会经常看到这个的术语),亦或是 ExecCalc),是GameplayEffects修改ASC的最强有力的一种方式。与ModifierMagnitudeCalculations类似,GameplayEffectExecutionCalculations可以捕捉Attributes并且可以对属性们进行快照。而与MMCs不同的是,他们可以改版不止一个Attribute,并且高效得执行编程者想要的任何事。当然强大和灵活也伴随着代价,GameplayEffectExecutionCalculations的代价就是其不支持预测,并且他们也必须在C++中进行实现。

ExecutionCalculations只能搭配InstantPeriodic类型的GameplayEffects来使用。通常任何带有’Execute’一词的内容基本都是指向这两种类型的GameplayEffects

快照会在创建GameplayEffectSpec时捕捉Attribute,而非快照会在GameplayEffectSpec应用时对Attribute进行捕捉。捕捉Attributes会去根据ASC上存在的修改器而重新计算他们的CurrentValue。这个重计算不会执行AbilitySet里的PreAttributeChange() ,所以这里需要再做一次数值的处理(截取)。

SnapshotSource or TargetCaptured on GameplayEffectSpec
YesSourceCreation
YesTargetApplication
NoSourceApplication
NoTargetApplication

为了配置Attribute的获取,我们可以参考Epic的ActionRPG示例项目中设置好的模板,具体就是定义了一个结构体来保存并且定义我们是如何捕捉Attributes,并且再结构体的构造函数中创建一份它的拷贝。对每个ExecCalc你都要有一个类似这样的结构体。**注意:**每个结构体的名称不应该重复,因为他们是在同一命名空间之下的。使用重名结构体会导致在捕捉Attributes时出现不正确的行为(没有捕捉到预想的那个Attributes的值)。

对于Local PredictedServer Only以及Server InitiatedGameplayAbilitiesExecCalc仅在服务器上进行调用。

ExecCalc最常见的应用案例就是基于一个复杂的公式,从SourceTarget上读取多个Attributes的值,然后计算出伤害。示例项目中有一个简单的ExecCalc,从GameplayEffectSpecSetByCaller中读取伤害值,在通过在Target上捕获到的护甲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 Data Attribute Calculation Modifier

如果你想要将值硬编码到GameplayEffect中,你可以使用一个CalculationModifier将他们传入,这个CalculationModifier会使用捕获到的其中一个Attributes作为后备数据。

在截图所示的例子中,我们添加了50到捕获的伤害Attribute上。你也可以设置其为Override,以直接用硬编码的值进行覆盖。

在这里插入图片描述

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

如果你想要将值硬编码到GameplayEffect中,你可以使用一个CalculationModifier将他们传入,这个CalculationModifier会使用一个Temporary Variable或者Transient Aggregator,就像在C++中调用的那样。Temporary Variable是和GameplayTag相关联的。

在截屏所示的例子中,我们使用Data.DamageGameplayTagTemporary Variable添加了50。

在这里插入图片描述

添加后备的Temporary VariablesExecutionCalculation的构造函数中:

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

你可以通过一个自定义的GameplayEffectSpec上的GameplayEffectContext发送数据到ExecutionCalculation

ExecutionCalculation中你可以从FGameplayEffectCustomExecutionParameters访问EffectContext

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

如果你需要修改GameplayEffectSpec或者EffectContext上的什么东西的话:

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

如果修改ExecutionCalculation中的GameplayEffectSpec的话,请一定小心。参阅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

CustomApplicationRequirementCAR)类为设计者提供了是否去应用某个的GameplayEffect的高级控制,这和简单的在GameplayEffect上进行GameplayTag检查是不同的。这是可以通过在蓝图中重写CanApplyGameplayEffect()函数,或者在C++中重写CanApplyGameplayEffect_Implementation()来实现的。

使用CARs的情形可以有:

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

CARs也可以实现更高级的事,比如检查某个GameplayEffect的实例是否已经应用到Target上,并且在已有其他的同类实例存在的情况下不去做实例的替换而是改变已有实例的持续时间CanApplyGameplayEffect()返回false)。

4.5.14 消耗的游戏效果 - Cost Gameplay Effect

GameplayAbilities中有一种GameplayEffect专门设计用来处理技能的消耗。Costs就是ASC激活某个GameplayAbility所需要的某个Attribute的多少。如果某个GA无法负担对应的Cost GE,那么它就无法被激活使用。这个Cost GE需要是一个Instant类型的GameplayEffect,具备一个或者多个Modifiers,用于对Attributes进行消耗。默认情况下,Cost GEs是支持预测的,建议是不要使用ExecutionCalculations(译者注:上面提到过,ExecutionCalculations不支持预测)。所以最好是只使用MMCs来进行对应的消耗计算。

刚开始时,你可能会为每个有消耗的GA来配备一个单独的Cost GE。更高级一点的做法是为多个GAs重用一个Cost GE,只要根据GA的指定数据修改从Cost GE创建的GameplayEffectSpec(消耗值一般定义在GA上)。这只能用于Instanced的技能。

两种重用Cost GE的技术:

  1. **使用MMC。**这是最简单的方法。创建一个MMC,从GameplayAbility示例中读取消耗值(具体是从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());
}

本例中,消耗值是我添加到GameplayAbility子类上的FScalableFloat类型。

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

Cost GE With MMC

  1. **重写UGameplayAbility::GetCostGameplayEffect()。**重写该函数,并且在运行时创建GameplayEffect,从而读取GameplayAbility的消耗值。

4.5.15 冷却的游戏效果 - Cooldown Gameplay Effect

GameplayAbilities中可以有一种专门设计用来处理技能的冷却的GameplayEffect。冷却指的就是某个技能被施放后直到可以再次施放所需的时间。如果某个GA仍然出于冷却过程中的话,即意味着它无法被激活。这个Cooldown GE应是一个Duration类型的GameplayEffect,无Modifiers,并且在GameplayEffectGrantedTagsCooldown Tag)中配置代表每个GameplayAbility或每个技能插槽(如果你的游戏具有分配给共享冷却时间的插槽的可互换技能)的唯一的一个GameplayTag。实际上GA会检查Cooldown Tag是否存在,而不是Cooldown GE的存在。默认情况下,Cooldown GEs是支持预测的,故而在冷却计算时最好不去使用ExecutionCalculations(译者注:上面提到过,ExecutionCalculations不支持预测)。所以最好是只使用MMCs来进行对应的冷却计算。

刚开始时,你可能会为每个拥有冷却的GA来配备一个单独的Cooldown GE。更高级一点的做法是为多个GAs重用一个Cooldown GE,只要根据GA的指定数据修改从Cooldown GE创建的GameplayEffectSpec(冷却的持续时间和Cooldown Tag是定义在GA上)。这只能用于Instanced的技能。

两种重用Cooldown GE的技术:

  1. **使用SetByCaller。**这是最简单快捷的方法。通过带有GameplayTagSetByCaller设置Cooldown GE的持续时间。可以在你的GameplayAbility的子类中,定义一个float/FScalableFloat作为持续时间,定义一个FGameplayTagContainer作为唯一的Cooldown Tag,定义一个临时FGameplayTagContainer用来返回Cooldown TagCooldown GE的标签集合。
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 TagsCooldown 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并且添加SetByCaller到冷却的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);
		SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName(  OurSetByCallerTag  )), CooldownDuration.GetValueAtLevel(GetAbilityLevel()));
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
	}
}

在下图中,冷却持续时间的Modifier是由SetByCaller通过一个Data.CooldownData Tag来进行设置。Data.Cooldown即是上面代码中的OurSetByCallerTag

在这里插入图片描述

  1. **使用MMC。**这基本上与上面的设置类似,除了在ApplyCooldown中设置SetByCaller作为Cooldown GE上的冷却持续时间,相对的,而是设置Custom Calculation Class并且指向我们创建的新的MMC
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 TagsCooldown 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并且添加SetByCaller到冷却的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());
}

在这里插入图片描述

4.5.15.1 获取冷却的游戏效果的剩余时间 - Get the Cooldown Gameplay Effect’s Remaining Time
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,这也依赖于他们ASCreplication mode

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

要监听冷却的开始,你既可以绑定AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf,从而响应Cooldown GE的应用;或者,可以绑定AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved),从而响应Cooldown Tag的新增。我的建议是使用前者,因为此时你也可以访问应用到其上的GameplayEffectSpec。这样,你可以判断Cooldown GE是本地预测的那个还是服务器矫正过的那个。

要监听冷却的结束,你既可以绑定AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate(),从而响应Cooldown GE的移除;或者,可以绑定AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved),从而响应Cooldown Tag的移除。我的建议是使用后者,因为当服务器矫正的Cooldown GE到达时,它会移除我们本地预测的那个,从而触发OnAnyGameplayEffectRemovedDelegate(),即便我们此时仍然出于冷却过程中。在预测的Cooldown GE的移除过程和服务器矫正的Cooldown GE的应用过程中,Cooldown Tag也不会发生变化。

**注意:**监听GameplayEffect在客户端上的添加和移除要求,客户端们可以接收复制的GameplayEffects。这也依赖于他们ASCreplication mode

示例项目中包含了一个自定义的蓝图节点,用来监听冷却的开始和结束。HUD UMG Widget使用它依照Meteor技能的冷却来更新剩余时间。这个AsyncTask会一直持续,直到手动调用EndTask(),这具体也是在UMG Widget的Destruct时间中调用的。参阅AsyncTaskCooldownChanged.h/cpp

在这里插入图片描述

4.5.15.3 冷却效果的预测 - Predicting Cooldowns

目前冷却并不能够真正得被预测。我们可以在本地预测的Cooldown GE应用时开始UI上的冷却计数器,但是GameplayAbility的实际冷却却与服务器的剩余冷却时间挂钩。由于玩家可能会存在延迟,本地的预测冷却可能已经结束,但是在服务器上GameplayAbility却依然出于冷却,这就会去阻止技能的施放直到服务器端的冷却结束。

示例项目解决这个问题是通过在本地预测的冷却开始时将Meteor技能的图标置灰,然后在服务器矫正的Cooldown GE到达时开启冷却计时器。

这样游戏游玩时,与低延迟的玩家相比,高延迟的玩家在短冷却技能上的开火率较低,从而导致出于劣势。Fortnite则是通过在武器上使用自定义的统计而不是冷却的GameplayEffects从而规避了这个问题。

而真正的可预测的冷却(玩家可以在本地冷却结束而服务器依然出于冷却时激活)会由Epic在Gas未来的迭代计划中实现.

4.5.16 改变激活的游戏效果的持续时间 - Changing Active Gameplay Effect Duration

为了改变某个Cooldown GE或者任意Duration类型的GameplayEffect的剩余时间,我们需要改变GameplayEffectSpecDuration,更新它的StartServerWorldTimeCachedStartServerWorldTimeStartWorldTime,并且使用CheckDuration()重新检查持续时间。在服务器上执行上面的步骤,并且将FActiveGameplayEffect标记为dirty将会把变化复制到客户端。
**注意:**这里会涉及到一个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 运行时创建动态游戏效果 - Creating Dynamic Gameplay Effects at Runtime

运行时创建动态的GameplayEffects是一项高级主题,切忌滥用。

在运行时,只有Instant类型的GameplayEffects可以用C++创建。DurationInfinite类型的GameplayEffects无法在运行时动态创建,因为当他们复制时他们会寻找不存在的GameplayEffect类的定义。为了实现该功能,你应该像通常在编辑器中那样创建一个原型GameplayEffect类。然后在运行时自定义GameplayEffectSpec实例。

运行时创建的Instant类型的GameplayEffects也可以由本地预测GameplayAbility内进行调用。但是,目前还不清楚动态的创建是否会有一些副作用。

示例

示例项目中动态创建了一个GameplayEffect,来当角色受到击杀时(由AttributeSet中处理),发送金币和经验点数到击杀者头上。

// Create a dynamic instant Gameplay Effect to give the bounties
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);
		}

		// Create the GE at runtime.
		UGameplayEffect* GameplayEffect = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeInstantGE"));
		GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // Only instant works with runtime GE.

		// Add a simple scalable float modifier, which overrides MyAttribute with 42.
		// In real world applications, consume information passed via 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;

		// Apply the GE.

		// Create the GESpec here to avoid the behavior of ASC to create GESpecs from the GE class default object.
		// Since we have a dynamic GE here, this would create a GESpec with the base GameplayEffect class, so we
		// would lose our modifiers. Attention: It is unknown, if this "hack" done here can have drawbacks!
		// The spec prevents the GE object being collected by the GarbageCollector, since the GE is a UPROPERTY on the spec.
		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 Sample Project项目实现了一个名为FGameplayEffectContainer的结构。他们并不存在于默认GAS框架内,但是对于存储GameplayEffectsTargetData是极为好用的。它为一些效果实现了自动化,比如从GameplayEffects创建GameplayEffectSpecs,并在它的GameplayEffectContext中设置其默认值。在GameplayAbility中构建GameplayEffectContainer,并且将其传递给生成的子弹,这一些列操作是非常简单直接的。我并没有在示例项目中选择去实现GameplayEffectContainers,这也失去了为你展示如何将寻常GAS项目进行拓展,但是我还是强烈建议在你的项目中去使用这个。

要访问GameplayEffectContainers中的GESpecs,做类似添加SetByCallers的操作,要展开FGameplayEffectContainer并且通过GESpecs数组的索引访问其内具体的GESpec引用。这就需要你提前知晓你想要访问的GESpec的索引。

在这里插入图片描述

GameplayEffectContainers也包含了一个选项可以高效的目标选取方法

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
UE4中的Gameplay框架是一个强大的工具集,用于开发和实现游戏玩法和用户交互。该框架提供了许多功能和组件,以帮助游戏开发者快速构建出丰富、流畅的游戏体验。 UE4的Gameplay框架主要由以下几个方面组成: 1.输入系统:该系统可以处理各种输入设备的操作,如鼠标、键盘和游戏手柄。开发者可以轻松地设置和管理输入映射和按键绑定。此外,还提供了鼠标和手柄的即时响应和移动方向控制功能,使玩家能够自由操作游戏中的角色。 2.角色控制器:角色控制器是游戏玩家在游戏中扮演的角色,他们的控制是通过输入系统和蓝图来实现的。游戏玩家可以移动角色、执行动作、攻击敌人等。角色还可以通过动画系统实现自然的运动和交互。 3.人工智能:UE4的Gameplay框架提供了内置的人工智能系统,可以对NPC和敌人进行编程控制。开发者可以设置敌人的行为模式、路径寻找和攻击策略,让游戏中的敌人具有更真实和智能的表现。 4.物理模拟:UE4的Gameplay框架使用了物理引擎来实现真实的物理模拟效果,比如碰撞、重力和刚体运动等。这使开发者能够创建更真实和具有交互性的游戏世界,使玩家可以与环境进行互动。 总之,UE4的Gameplay框架提供了强大而灵活的工具,帮助开发者轻松地构建出丰富多样的游戏玩法和用户交互。无论是开发动作冒险游戏、射击游戏还是角色扮演游戏,该框架都能满足开发者的需求,并带来令人惊叹的游戏体验。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值