目录
- 4.5.1 游戏效果的定义 - Gameplay Effect Definition
- 4.5.2 应用游戏效果 - Applying Gameplay Effects
- 4.5.3 移除游戏效果 - Removing Gameplay Effects
- 4.5.4 游戏效果的修改器 - Gameplay Effect Modifiers
- 4.5.5 堆叠游戏效果 - Stacking Gameplay Effects
- 4.5.6 赋予技能 - Granted Abilities
- 4.5.7 游戏效果标签 - Gameplay Effect Tags
- 4.5.8 免疫效果 - Immunity
- 4.5.9 Gameplay Effect Spec
- 4.5.10 游戏效果的上下文 - Gameplay Effect Context
- 4.5.11 修改器的幅值计算 - Modifier Magnitude Calculation
- 4.5.12 游戏效果执行的计算 - Gameplay Effect Execution Calculation
- 4.5.13 自定义应用的要求 - Custom Application Requirement
- 4.5.14 消耗的游戏效果 - Cost Gameplay Effect
- 4.5.15 冷却的游戏效果 - Cooldown Gameplay Effect
- 4.5.16 改变激活的游戏效果的持续时间 - Changing Active Gameplay Effect Duration
- 4.5.17 运行时创建动态游戏效果 - Creating Dynamic Gameplay Effects at Runtime
- 4.5.18 游戏效果容器 - Gameplay Effect Containers
4.5.1 游戏效果的定义 - Gameplay Effect Definition
GameplayEffects
(GE
)是技能对自身和他者的Attributes
和GameplayTags
产生影响的容器。具体来讲,他们可以产生一些瞬间的Attribute
的改变效果,比如说伤害或者治疗,以及一些长期的属性buff/debuff效果,比如加速或者眩晕之类的。UGameplayEffect
是一个定义单一游戏效果的数据类,这意味着GameplayEffects
里面不应该添加任何其他的逻辑。通常设计师们只需要创建UGameplayEffect
的派生类就够了。
GameplayEffects
是通过Modifiers
和Executions
(GameplayEffectExecutionCalculation
)来对Attributes
进行修改和调整的。
GameplayEffects
可以按生效时间分为三类:即刻生效Instant
,持续一段时间Duration
,以及无限持续时间Infinite
。
此外,GameplayEffects
也可以添加和执行GameplayCues
。Instant
类型的GameplayEffect
将调用GameplayCue
里的Execute
,而Duration
或者Infinite
类型的GameplayEffect
将会调用GameplayCue
上的Add
和Remove
。
持续类型 | GameplayCue事件 | 使用时机 |
---|---|---|
Instant | Execute | 用于永久性的、立即的对Attribute 的BaseValue 的修改。GameplayTags 将不会被应用,即便是一帧都没有。 |
Duration | Add & Remove | 用于临时的对Attribute 的CurrentValue 的修改并且应用GameplayTags ,该GameplayTags 会随着GameplayEffect 的到期而被移除(或者自行手动删除)。具体的持续时间可以在UGameplayEffect 类/Blueprint中进行指定。 |
Infinite | Add & Remove | 用于临时的对Attribute 的CurrentValue 的修改并且应用GameplayTags ,该GameplayTags 会随着GameplayEffect 被移除时一起移除。他们永远不会过期,所以必须通过技能或者ASC 手动移除掉。 |
Duration
和Infinite
类型的GameplayEffects
中可以通过一个选项来应用周期性的效果Periodic Effects
,在它里面可以通过定义Period
来周期性得每X
秒就调用一次它的Modifiers
和Executions
。Periodic Effects
可以作为Instant
类型的GameplayEffects
来对待,即它会修改Attribute
的BaseValue
并且执行GameplayCues
。这对于实现DOT伤害(持续性伤害)非常有效果。注意: 对Periodic Effects
无法进行预测。
可以依据Duration
和Infinite
类型的GameplayEffects
的Ongoing Tag Requirements
选项是否符合Gameplay Effect Tags来临时对该GameplayEffects
进行关闭或开启。关闭一个GameplayEffect
将会移除掉它的Modifiers
的效果并且应用GameplayTags
,但是并不会移除掉该GameplayEffect
。将GameplayEffect
再开启会再应用它的Modifiers
和GameplayTags
。
如果你需要手动重新计算Duration
和Infinite
类型的GameplayEffects
的Modifiers
(假设你有一个MMC
,而其使用的数据并不是从Attributes
里来的),你可以去调用UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel)
,其中的NewLevel参数可以通过UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel()
来得到。基于自动Attributes
的Modifiers
会随着Attributes
更新而进行更新。用来更新Modifier
的SetActiveGameplayEffectLevel()
的内部的关键函数有:
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
时,它会利用GameplayEffect
的ClassDefaultObject
创建一个GameplayEffectSpec
。成功得应用GameplayEffectSpecs
后它会被添加到一个FActiveGameplayEffect
的结构体,这也就是ASC
的ActiveGameplayEffects
。
4.5.2 应用游戏效果 - Applying Gameplay Effects
在GameplayAbilities
和ASC
里有很多函数可以用来应用某个GameplayEffects
,这些函数名字里通常里面都会带有ApplyGameplayEffectTo
。不同的函数其本质都是一样的,最终都会落到在Target
上去调用其相应的UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()
。
为了在GameplayAbility
之外应用GameplayEffects
,比如子弹的发射,你需要去获取Target
的ASC
,然后调用其ApplyGameplayEffectToSelf
。
你可以监听Duration
或者Infinite
类型的GameplayEffects
被应用到某个ASC
上的事件,通过在相应的委托上绑定回调:
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
The callback function:
virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
无论何种复制模式下服务器都会去调用这个函数。当复制模式为Full
和Mixed
,自主代理会为复制的GameplayEffects
调用此方法。只有当replication mode为Full
时,模拟代理才会调用这个方法。
4.5.3 移除游戏效果 - Removing Gameplay Effects
在GameplayAbilities
和ASC
里有很多函数可以用来移除某个GameplayEffects
,这些函数名字里通常里面都会带有RemoveActiveGameplayEffect
。不同的函数其本质都是一样的,最终都会落到在Target
上去调用其相应的FActiveGameplayEffectsContainer::RemoveActiveEffects()
。
为了在GameplayAbility
之外应用GameplayEffects
,比如子弹的发射,你需要去获取Target
的ASC
,然后调用其RemoveActiveGameplayEffect
。
你可以监听Duration
或者Infinite
类型的GameplayEffects
被从某个ASC
上移除的事件,通过在相应的委托上绑定回调:
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);
对应的回调函数:
virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);
无论何种复制模式下服务器都会去调用这个函数。当复制模式为Full
和Mixed
,自主代理会为复制的GameplayEffects
调用此方法。只有当replication mode为Full
时,模拟代理才会调用这个方法。
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 。 |
Attribute
的CurrentValue
是将其所有的Modifiers
添加到其BaseValue
的一个汇总的结果。对Modifiers
如何进行汇总的公式是在GameplayEffectAggregator.cpp
的FAggregatorModChannel::EvaluateWithBase
中进行定义的:
((InlineBaseValue + Additive) * Multiplicitive) / Division
任意的Override
类型的Modifiers
都将会优先使用最后应用的Modifier
来覆盖最终的值。
**注意:**百分比式的修改要使用Multiply
操作,以确保其是在Add
之后执行。
**注意:**对百分比式的修改的预测是有一些问题存在的。
共有四种类型的Modifiers
:Scalable Float
,Attribute Based
,Custom Calculation Class
,以及Set By Caller
。他们都会生成一些浮点值,然后基于Modifier
的操作类型利用值对Attribute
进行修改。。
Modifier 类型 | 描述 |
---|---|
Scalable Float | FScalableFloats 是一种能够指向Data Table的结构,其中Data Table是将变量作为行、将等级作为列。Scalable Floats 将会自动根据技能的当前等级(或者是在GameplayEffectSpec 中重写的等级)读取指定行的值。这个值可以进一步用一个系数相乘。如果没有指定Data Table/行,该值会被当做是1,从而使用一个单独的硬编码的值作为所有等级的值。 |
Attribute Based | Attribute Based 类型的Modifiers 会获取Source (GameplayEffectSpec 的创建者)或者Target (GameplayEffectSpec 的接收者)上的Attribute 的CurrentValue 或者BaseValue ,然后进一步对其使用系数以及一些前/后处理来进行修改。快照Snapshotting 意味着会在GameplayEffectSpec 被创建时对Attribute 进行捕捉,而no snapshotting 则意味着在GameplayEffectSpec 应用时来对Attribute 进行捕捉。 |
Custom Calculation Class | Custom Calculation Class 为复杂Modifiers 提供了最大的灵活性。这类Modifier 需要一个ModifierMagnitudeCalculation 类,然后可以通过系数和以及一些前/后处理来修改结果值。 |
Set By Caller | SetByCaller 类型的Modifiers 由技能在运行时在GameplayEffect 之外设置或者由GameplayEffectSpec 的创建者进行设置。例如,如果你希望根据玩家按压按钮来对技能进行充能的时间来设置伤害值,那么你就可以使用SetByCaller 。SetByCallers 本质上是一个TMap<FGameplayTag, float> ,存在于GameplayEffectSpec 。Modifier 只是告诉Aggregator 去通过提供的GameplayTag 来查找SetByCaller 值。Modifiers 使用的SetByCallers 只能使用GameplayTag 而不能使用FName 。如果Modifier 被设置为SetByCaller ,但是相应GameplayTag 的SetByCaller 并不存在于GameplayEffectSpec 里的话,游戏就会抛出一个运行时的错误并返回0。这样如果是Divide 运算的话就会出问题了。参阅SetByCallers 获取更多关于如何使用SetByCallers 的信息。 |
4.5.4.1 乘法和除法类型的修改器 - Multiply and Divide Modifiers
默认情况下,所有的Multiply
和Divide
类型的Modifiers
会在将他们应用Attribute
的BaseValue
之前先被加到一起。
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
Multiply
和Divide
类型的Modifiers
中都有一个等于1的偏差值Bias
(Addition
的Bias
为0
)。所以其公式实际上是这样的:
1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...
这样的公式会导致一些无法预料的结果。首先,这个公式会在将其应用到BaseValue
之前把所有的Modifiers
加到一起。大部分人的想法是将他们一起进行乘或者除。例如如果你有两个为1.5
的Multiply
,大部分人对BaseValue
的计算方式是1.5 x 1.5 = 2.25
。而实际上的计算是BaseValue
乘以2
(BaseValue
具有50%的增长 + BaseValue
具有50%的增长 = 100%的增长)。这也是GameplayPrediction.h
中使用的方式,500
的基础速度外加10%
速度buff就得到550
的速度。再施加另一个10%
的速度buff则是600
(500 + 50 x 10% + 50 x 10%)。
其次,这个公式对值如何来使用有一些没有记录在文档的规则,而在Paragon中又确确实实使用了这些规则。
Multiply
和Divide
运算的加法公式:
(小于1的值不超过1个) 和 ([1, 2)范围内的值可以有任意个)
或者 (有一个值 >= 2)
公式中的Bias
基本上是减去[1, 2)
范围内的整数。第一个Modifier
的Bias
从起始的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
,而不是预期的1
(0.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
,而不是预期的10
(5 + 5
)。结果总是sum of the Modifiers - number of Modifiers + 1
。
许多游戏会想要他们的Multiply
和Divide
类型的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都可以设置SourceTags
和TargetTags
。他们的工作原理与GameplayEffect
的Application Tag requirements
是一样的。因此标签只有在效果应用时才会被考虑。即,当有一个周期性的无限持续时间的效果时,他们只在第一次效果应用时考虑而不是在每个执行周期都被重新考虑。
Attribute Based
类型的Modifiers
也可以设置SourceTagFilter
和TargetTagFilter
。当确定作为Attribute Based
的Modifier
的来源的属性的具体大小时,这些过滤器会用于排除该属性的某些Modifier
。那些source
或者target
没有所有过滤器标签的Modifiers
将会被排除在外。
更详细来讲:作为source
和ASC
和作为target
的ASC
的标签会被GameplayEffects
捕捉。作为source
和ASC
的标签会在GameplayEffectSpec
创建时被捕捉, 作为target
的ASC
的标签则是在效果执行时被捕捉。当确定无限持续时间的或者持续一定时间的效果的Modifier
是否合格(即其Aggregator
聚合器符合要求),并且设置这些过滤器时,被捕获的标签将会与过滤器进行比较。
4.5.5 堆叠游戏效果 - Stacking Gameplay Effects
默认情况下,GameplayEffects
会无视已经存在的GameplayEffectSpec
的实例,在应用新的GameplayEffectSpec
时会直接创建新的实例。GameplayEffects
也可以设置为叠加,这是就不是添加新的GameplayEffectSpec
实例,而是修改当前已存在的GameplayEffectSpec
的堆叠数。堆叠只能用于Duration
和Infinite
类型的GameplayEffects
。
共有两种类型的堆叠:源聚合和目标聚合。
堆叠类型 | 描述 |
---|---|
源聚合 | 目标上每一个不同源的ASC 都有一个自己的单独的栈实例。每个源都能够应用X数目个栈。 |
目标聚合 | 无论有多少源,目标上仅有一个栈实例。每一个源能够应用栈的上限不能超过共享栈限制。 |
堆叠对于超时、持续时间刷新以及周期性重置都有相应对的处理办法。在GameplayEffect
的蓝图里在对应条目上悬停都有相应的提示。
示例项目中包括了一个自定义的蓝图节点用来监听GameplayEffect
栈的变化。HUD使用它来更新玩家的被动护甲叠加数。这一异步任务AsyncTask
将会一直持续,知道手动调用EndTask()
,这一步我们会在UMG Widget的Destruct
事件中来做。参阅AsyncTaskEffectStackChanged.h/cpp
。
4.5.6 赋予技能 - Granted Abilities
GameplayEffects
能够为ASCs
赋予新的GameplayAbilities
。只有Duration
和Infinite
类型的GameplayEffects
可以进行赋予技能的操作。
一个常见的用法就是当你想要强制玩家做某些事情,比如将他们击退或者拉近,你就可以给他们一个GameplayEffect
来赋予他们一些自动激活的技能(参阅Passive Abilities获得更多关于如何在赋予技能后进度进行激活的内容),令他们能够做我们想让他们做的事情。。
设计师们可以选择用GameplayEffect
设置具体赋予哪些技能,其具体的技能等级,对应的绑定输入是什么,以及赋予技能后其移除机制又是怎样的.
移除机制 | 描述 |
---|---|
Cancel Ability Immediately 立即取消技能 | 当GameplayEffect 被从目标上移除时,立即取消并移除相应被赋予的技能。 |
Remove Ability on End 结束后移除技能 | 被赋予的技能可以自然执行直到结束,然后从目标上移除。 |
Do Nothing 什么都不做 | 从目标上移除GameplayEffect 并不会影响相应被赋予的技能。目标可以一直拥有对应的能力直到手动移除。 |
4.5.7 游戏效果标签 - Gameplay Effect Tags
GameplayEffects
带有多个GameplayTagContainers
。设计师可以为每个类别编辑相应的Added
和Removed
的GameplayTagContainers
,对应的结果会在编译后展现在Combined
的 GameplayTagContainer
中。 Added
的标签是指这个GameplayEffect
新添加的父类之前所没有的标签。Removed
的标签则是父类拥有而子类没有的。
种类 | 描述 |
---|---|
Gameplay Effect Asset Tags | GameplayEffect 所具有的标签。他们本身并不执行任何函数,仅用于描述GameplayEffect . |
Granted Tags | 存在于GameplayEffect 上的标签,但也会给到GameplayEffect 应用到的目标的ASC 上。当GameplayEffect 移除时他们也会被一并从ASC 上移除。仅用于Duration 和Infinite 类型的GameplayEffects 。 |
Ongoing Tag Requirements | 一旦应用,这些标签将决定GameplayEffect 是开还是关。GameplayEffect 可以被应用时仍然时关闭状态的。如果GameplayEffect 没有满足Ongoing Tag Requirements的就会被关闭,当条件满足时,它又会被再次打开并重新应用它的修改器。仅用于Duration 和Infinite 类型的`GameplayEffects。 |
Application Tag Requirements | 指那些在目标上的标签,它们会决定GameplayEffect 是否可以被应用到目标上。如果相应的要求没有满足,那么GameplayEffect 则不会应用。 |
Remove Gameplay Effects with Tags | 目标上的GameplayEffects 如果在Asset Tags 或Granted 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
去带一个和GameplayTag
或FName
关联的浮点值。他们分别存储在各自对应的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
):
要在蓝图中读取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
/ GameplayEffectExecutionCalculations
,AttributeSets
以及GameplayCues
之间传递数据。
派生GameplayEffectContext
的过程:
- 实现
FGameplayEffectContext
的派生结构 - 重写
FGameplayEffectContext::GetScriptStruct()
- 重写
FGameplayEffectContext::Duplicate()
- 重写
FGameplayEffectContext::NetSerialize()
,如果你有一些新的数据需要复制的话 - 仿照父类结构
FGameplayEffectContext
实现派生类中的TStructOpsTypeTraits
- 在你的
AbilitySystemGlobals
类中重写AllocGameplayEffectContext()
来返回你所建的派生类对象。
GASShooter项目中使用了GameplayEffectContext
的派生类来添加TargetData
,从而可以在GameplayCues
中对其进行访问,这一点特别为霰弹枪设计,因为它可以击中不止一个目标。
4.5.11 修改器的幅值计算 - Modifier Magnitude Calculation
ModifierMagnitudeCalculations
(简称ModMagCalc
或MMC
)是一个功能非常强大的类,使用起来就像是GameplayEffects
中的Modifiers
。他们的功能类似于GameplayEffectExecutionCalculations
,甚至于没有GameplayEffectExecutionCalculations
那么功能繁多,但是最重要的是,他们是可以被预测的。MMC
唯一的目的就是通过CalculateBaseMagnitude_Implementation()
返回一个浮点值。你可以通过蓝图或者C++派生以及重写这个方法。
MMC
可以用于任何持续时间的GameplayEffects
- Instant
,Duration
,Infinite
亦或是Periodic
。
MMC
的优势在于捕捉GameplayEffect
的Source
或者Target
上的任意数量的Attributes
的值的能力并且能够完整得访问GameplayEffectSpec
以读取GameplayTags
以及SetByCallers
。Attributes
可以是快照也可以不是。快照的Attributes
是在GameplayEffectSpec
被创建时进行捕捉的,而非快照的Attributes
则是在GameplayEffectSpec
应用时进行捕捉的,并且会根据Infinite
和Duration
类型的GameplayEffects
对Attribute
进行修改而进行更新。通过已经存在于ASC
的修改捕捉Attributes
然后重新计算他们的CurrentValue
。这里的重计算不会运行AbilitySet
上的PreAttributeChange()
,所以之前提到的那些对数值的预处理操作(截取)必须在这里再做一遍。
快照 | Source 或是 Target | 在GameplayEffectSpec 上被捕捉的时机 | 当Attribute 被Infinite 或Duration 类型的GE 修改时自动更新 |
---|---|---|---|
Yes | Source | Creation | No |
Yes | Target | Application | No |
No | Source | Application | Yes |
No | Target | Application | Yes |
MMC
的结果浮点值可以进一步在GameplayEffect
的Modifier
通过系数、预系数加法或后系数加法等方式进行修改。
下面是一个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
GameplayEffectExecutionCalculations
(ExecutionCalculation
,Execution
(在插件源码中你会经常看到这个的术语),亦或是 ExecCalc
),是GameplayEffects
修改ASC
的最强有力的一种方式。与ModifierMagnitudeCalculations
类似,GameplayEffectExecutionCalculations
可以捕捉Attributes
并且可以对属性们进行快照。而与MMCs
不同的是,他们可以改版不止一个Attribute
,并且高效得执行编程者想要的任何事。当然强大和灵活也伴随着代价,GameplayEffectExecutionCalculations
的代价就是其不支持预测,并且他们也必须在C++中进行实现。
ExecutionCalculations
只能搭配Instant
和Periodic
类型的GameplayEffects
来使用。通常任何带有’Execute’一词的内容基本都是指向这两种类型的GameplayEffects
。
快照会在创建GameplayEffectSpec
时捕捉Attribute
,而非快照会在GameplayEffectSpec
应用时对Attribute
进行捕捉。捕捉Attributes
会去根据ASC
上存在的修改器而重新计算他们的CurrentValue
。这个重计算不会执行AbilitySet
里的PreAttributeChange()
,所以这里需要再做一次数值的处理(截取)。
Snapshot | Source or Target | Captured on GameplayEffectSpec |
---|---|---|
Yes | Source | Creation |
Yes | Target | Application |
No | Source | Application |
No | Target | Application |
为了配置Attribute
的获取,我们可以参考Epic的ActionRPG示例项目中设置好的模板,具体就是定义了一个结构体来保存并且定义我们是如何捕捉Attributes
,并且再结构体的构造函数中创建一份它的拷贝。对每个ExecCalc
你都要有一个类似这样的结构体。**注意:**每个结构体的名称不应该重复,因为他们是在同一命名空间之下的。使用重名结构体会导致在捕捉Attributes
时出现不正确的行为(没有捕捉到预想的那个Attributes
的值)。
对于Local Predicted
,Server Only
以及Server Initiated
的GameplayAbilities
,ExecCalc
仅在服务器上进行调用。
ExecCalc
最常见的应用案例就是基于一个复杂的公式,从Source
和Target
上读取多个Attributes
的值,然后计算出伤害。示例项目中有一个简单的ExecCalc
,从GameplayEffectSpec
的SetByCaller
中读取伤害值,在通过在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.Damage
的GameplayTag
向Temporary Variable
添加了50。
添加后备的Temporary Variables
到ExecutionCalculation
的构造函数中:
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
CustomApplicationRequirement
(CAR
)类为设计者提供了是否去应用某个的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
的技术:
- **使用
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;
- **重写
UGameplayAbility::GetCostGameplayEffect()
。**重写该函数,并且在运行时创建GameplayEffect
,从而读取GameplayAbility
的消耗值。
4.5.15 冷却的游戏效果 - Cooldown Gameplay Effect
GameplayAbilities
中可以有一种专门设计用来处理技能的冷却的GameplayEffect
。冷却指的就是某个技能被施放后直到可以再次施放所需的时间。如果某个GA
仍然出于冷却过程中的话,即意味着它无法被激活。这个Cooldown GE
应是一个Duration
类型的GameplayEffect
,无Modifiers
,并且在GameplayEffect
的GrantedTags
(Cooldown 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
的技术:
- **使用
SetByCaller
。**这是最简单快捷的方法。通过带有GameplayTag
的SetByCaller
设置Cooldown GE
的持续时间。可以在你的GameplayAbility
的子类中,定义一个float/FScalableFloat
作为持续时间,定义一个FGameplayTagContainer
作为唯一的Cooldown Tag
,定义一个临时FGameplayTagContainer
用来返回Cooldown Tag
和Cooldown 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 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
并且添加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.Cooldown
的Data Tag
来进行设置。Data.Cooldown
即是上面代码中的OurSetByCallerTag
。
- **使用
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 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
并且添加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
,这也依赖于他们ASC
的replication 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
。这也依赖于他们ASC
的replication 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
的剩余时间,我们需要改变GameplayEffectSpec
的Duration
,更新它的StartServerWorldTime
、CachedStartServerWorldTime
、StartWorldTime
,并且使用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++创建。Duration
和Infinite
类型的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框架内,但是对于存储GameplayEffects
和TargetData
是极为好用的。它为一些效果实现了自动化,比如从GameplayEffects
创建GameplayEffectSpecs
,并在它的GameplayEffectContext
中设置其默认值。在GameplayAbility
中构建GameplayEffectContainer
,并且将其传递给生成的子弹,这一些列操作是非常简单直接的。我并没有在示例项目中选择去实现GameplayEffectContainers
,这也失去了为你展示如何将寻常GAS项目进行拓展,但是我还是强烈建议在你的项目中去使用这个。
要访问GameplayEffectContainers
中的GESpecs
,做类似添加SetByCallers
的操作,要展开FGameplayEffectContainer
并且通过GESpecs
数组的索引访问其内具体的GESpec
引用。这就需要你提前知晓你想要访问的GESpec
的索引。
GameplayEffectContainers
也包含了一个选项可以高效的目标选取方法。