4.8 Gameplay Cues
4.8.1 Gameplay Cues定义
GameplayCues
(简称 GC
)用于执行与游戏玩法无关的内容,如音效、粒子效果、摄像机震动等。GameplayCues
通常是复制的(除非显式地在本地执行 Executed
、Added
或 Removed
),并且是预测的。
我们通过发送相应的 GameplayTag
(必须具有父标签 GameplayCue.
)和事件类型(Execute
、Add
或 Remove
)到 GameplayCueManager
来触发 GameplayCues
,该操作通过 ASC
实现。GameplayCueNotify
对象和其他实现了 IGameplayCueInterface
的 Actor
可以根据 GameplayCue
的 GameplayTag
(GameplayCueTag
)订阅这些事件。
注意: 重申一下,GameplayCue
的 GameplayTags
必须以父标签 GameplayCue
开头。例如,有效的 GameplayCue
GameplayTag
可能是 GameplayCue.A.B.C
。
GameplayCueNotifies
分
为两类:Static
和 Actor
。它们响应不同的事件,且不同类型的 GameplayEffect
可以触发它们。重写相应的事件并实现你的逻辑。
GameplayCue 类 | 事件 | GameplayEffect 类型 | 描述 |
---|---|---|---|
GameplayCueNotify_Static | Execute | Instant 或 Periodic | 静态 GameplayCueNotifies 在 ClassDefaultObject 上操作(即没有实例),非常适合用于一次性效果,比如击中冲击。 |
GameplayCueNotify_Actor | Add 或 Remove | Duration 或 Infinite | Actor 类型的 GameplayCueNotifies 在 Add 时会生成一个新实例。由于这些是实例化的,它们可以持续执行直到被 Remove 。它们适合循环音效和粒子效果,当相应的 Duration 或 Infinite 类型的 GameplayEffect 被移除时,或者通过手动调用移除时。它们还带有选项来管理允许同时 Add 的数量,避免相同效果的多次应用只启动一次音效或粒子效果。 |
GameplayCueNotifies
理论上可以响应任何事件,但这通常是我们如何使用它们的方式。
注意: 使用 GameplayCueNotify_Actor
时,检查 Auto Destroy on Remove
,否则后续调用 Add
相同的 GameplayCueTag
时将无法生效。
当使用 ASC
的 复制模式 为非 Full
模式时,Add
和 Remove
的 GC
事件会在服务器玩家(监听服务器)上触发两次——一次是应用 GE
,另一次是从 “Minimal” 的 NetMultiCast
发送到客户端。然而,WhileActive
事件仍然只会触发一次。所有事件只会在客户端触发一次。
示例项目中包含了一个 GameplayCueNotify_Actor
用于眩晕和冲刺效果,还包含了一个 GameplayCueNotify_Static
用于 FireGun 的射击冲击。这些 GC
可以通过 本地触发进行进一步优化,而不是通过 GE
进行复制。在示例项目中,我选择展示初学者使用的方式。
4.8.2 触发Gameplay Cues
当 GameplayEffect
成功应用时(未被标签或免疫阻止),填写应触发的所有 GameplayCue
的 GameplayTags
。
UGameplayAbility
提供了蓝图节点来 Execute
、Add
或 Remove
GameplayCues
。
在 C++ 中,你可以直接在 ASC
上调用这些函数(或者在你的 ASC
子类中暴露它们给蓝图):
/** 游戏提示也可以独立触发。这些可以带有一个可选的效果上下文来传递击中结果等 */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** 添加一个持久的游戏提示 */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** 移除一个持久的游戏提示 */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
/** 移除任何独立添加的游戏提示,即不是作为 `GameplayEffect` 一部分的游戏提示 */
void RemoveAllGameplayCues();
4.8.3 Local Gameplay Cues(客户端Gameplay Cues)
从 GameplayAbilities
和 ASC
触发的游戏提示默认是同步的。每个 GameplayCue
事件是一个多播 RPC。这会导致大量的 RPC。GAS 还强制每个网络更新最多只允许有两个相同的 GameplayCue
RPC。为了避免这一点,我们在可能的情况下使用本地的 GameplayCues
。本地的 GameplayCues
仅在单独的客户端上执行 Execute
、Add
或 Remove
。
可以使用本地 GameplayCues
的场景:
- 投射物冲击
- 近战碰撞冲击
- 从动画蒙太奇触发的
GameplayCues
你应该将以下本地 GameplayCue
函数添加到你的 ASC
子类:
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
void UPAAbilitySystemComponent::ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Executed, GameplayCueParameters);
}
void UPAAbilitySystemComponent::AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::OnActive, GameplayCueParameters);
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::WhileActive, GameplayCueParameters);
}
void UPAAbilitySystemComponent::RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Removed, GameplayCueParameters);
}
如果一个 GameplayCue
是本地 Add
的,它应该本地 Remove
。如果它是通过复制 Add
的,它应该通过复制 Remove
。
4.8.4 Gameplay Cue 参数
GameplayCues
接收一个 FGameplayCueParameters
结构,包含作为参数传递给 GameplayCue
的额外信息。如果你从 GameplayAbility
或 ASC
的函数中手动触发 GameplayCue
,那么你必须手动填写传递给 GameplayCue
的 GameplayCueParameters
结构。如果 GameplayCue
是由 GameplayEffect
触发的,那么以下变量会自动填充到 GameplayCueParameters
结构中:
- AggregatedSourceTags
- AggregatedTargetTags
- GameplayEffectLevel
- AbilityLevel
- EffectContext
- Magnitude(如果
GameplayEffect
在GameplayCue
标签容器上选择了一个用于幅度的Attribute
并且有一个相应的影响该Attribute
的Modifier
)
在手动触发 GameplayCue
时,GameplayCueParameters
结构中的 SourceObject
变量可能是传递任意数据给 GameplayCue
的一个好地方。
注意: 参数结构中的一些变量如 Instigator
可能已经存在于 EffectContext
中。EffectContext
也可以包含一个 FHitResult
,用于在世界中生成 GameplayCue
的位置。子类化 EffectContext
可能是向 GameplayCues
传递更多数据的好方法,特别是那些由 GameplayEffect
触发的。
参见 UAbilitySystemGlobals
中的 3 个函数,它们填充 GameplayCueParameters
结构以获取更多信息。它们是虚拟的,所以你可以重写它们以自动填充更多信息。
/** Initialize GameplayCue Parameters */
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectSpecForRPC &Spec);
virtual void InitGameplayCueParameters_GESpec(FGameplayCueParameters& CueParameters, const FGameplayEffectSpec &Spec);
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectContextHandle& EffectContext);
4.8.5 Gameplay Cue Manager
默认情况下,GameplayCueManager
将扫描整个游戏目录中的 GameplayCueNotifies
并在播放时将它们加载到内存中。我们可以通过在 DefaultGame.ini
中设置 GameplayCueManager
的扫描路径来改变扫描路径。
[/Script/GameplayAbilities.AbilitySystemGlobals]
GameplayCueNotifyPaths="/Game/GASDocumentation/Characters"
我们希望 GameplayCueManager
扫描并找到所有的 GameplayCueNotifies
;然而,我们不希望它在播放时异步加载每一个。这会把每个 GameplayCueNotify
及其所有引用的声音和粒子加载到内存中,无论它们是否在关卡中使用。在一个像 Paragon 这样的大型游戏中,这可能会在内存中造成数百兆字节的不必要资产,并导致启动时的卡顿和游戏冻结。
一种替代在启动时异步加载每个 GameplayCue
的方法是只在游戏中触发时异步加载 GameplayCues
。这样可以缓解不必要的内存使用和在异步加载每个 GameplayCue
时可能造成的游戏硬冻结,以交换在首次触发特定 GameplayCue
时可能的效果延迟。对于 SSD 来说,这种潜在延迟是不存在的。我没有在 HDD 上测试过。如果在 UE 编辑器中使用此选项,GameplayCues 第一次加载时,如果编辑器需要编译粒子系统,可能会有轻微的卡顿或冻结。在构建版本中,这不是问题,因为粒子系统已经编译好。
首先我们必须子类化 UGameplayCueManager
并在 DefaultGame.ini
中告诉 AbilitySystemGlobals
类使用我们的 UGameplayCueManager
子类。
[/Script/GameplayAbilities.AbilitySystemGlobals]
GlobalGameplayCueManagerClass="/Script/ParagonAssets.PBGameplayCueManager"
在我们的 UGameplayCueManager
子类中,重写 ShouldAsyncLoadRuntimeObjectLibraries()
。
virtual bool ShouldAsyncLoadRuntimeObjectLibraries() const override
{
return false;
}
4.8.6 防止 Gameplay Cues 触发
有时我们不希望 GameplayCues
被触发。例如,如果我们阻止了一次攻击,我们可能不希望播放附加到伤害 GameplayEffect
的命中效果,或者希望播放一个自定义的效果。我们可以通过在 GameplayEffectExecutionCalculations
中调用 OutExecutionOutput.MarkGameplayCuesHandledManually()
来实现这一点,然后手动将我们的 GameplayCue
事件发送到 Target
或 Source
的 ASC
。
如果你永远不希望某个特定的 ASC
上的任何 GameplayCues
被触发,你可以设置 AbilitySystemComponent->bSuppressGameplayCues = true;
。
4.8.7 Gameplay Cue 批处理
每个触发的 GameplayCue
都是一个不可靠的 NetMulticast RPC。在我们同时触发多个 GCs
的情况下,有一些优化方法可以将它们压缩为一个 RPC 或通过发送更少的数据来节省带宽。
4.8.7.1 手动 RPC
假设你有一把发射八个弹丸的霰弹枪。这是八个轨迹和冲击 GameplayCues
。GASShooter 采用了一种懒惰的方法,将它们组合成一个 RPC,通过将所有的轨迹信息藏入 EffectContext
作为 TargetData
。虽然这将 RPC 从八个减少到一个,但它仍然在一个 RPC 中发送了大量数据(大约 500 字节)。一种更优化的方法是发送一个带有自定义结构的 RPC,在其中有效地编码命中位置,或者你可以给它一个随机种子号,以在接收端重现/近似冲击位置。客户端然后解包这个自定义结构并转化为本地执行的 GameplayCues
。
这样运作:
- 声明一个
FScopedGameplayCueSendContext
。这会抑制UGameplayCueManager::FlushPendingCues()
直到它超出范围,这意味着所有GameplayCues
将会被排队直到FScopedGameplayCueSendContext
超出范围。 - 重写
UGameplayCueManager::FlushPendingCues()
以根据一些自定义GameplayTag
将可以批处理在一起的GameplayCues
合并到你的自定义结构中并将其 RPC 到客户端。 - 客户端接收自定义结构并解包成本地执行的
GameplayCues
。
此方法也可用于当你需要特定参数来适应 GameplayCues
,但不适合 GameplayCueParameters
提供的内容,并且你不希望将它们添加到 EffectContext
中,如伤害数字、暴击指示器、破盾指示器、致命命中指示器等。
4.8.7.2 一个GameplayEffect上的多个GameplayCue
所有在 GameplayEffect
上的 GameplayCues
都已经在一个 RPC 中发送。默认情况下,UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec()
将发送整个 GameplayEffectSpec
(但转换为 FGameplayEffectSpecForRPC
),即使在 ASC
的 Replication Mode
下也是不可靠的。这可能会消耗大量带宽,具体取决于 GameplayEffectSpec
中的内容。我们可以通过设置 cvar AbilitySystem.AlwaysConvertGESpecToGCParams 1
来优化这一点。这将 GameplayEffectSpecs
转换为 FGameplayCueParameter
结构并 RPC 这些而不是整个 FGameplayEffectSpecForRPC
。这可能会节省带宽,但也会有更少的信息,这取决于 GESpec
如何转换为 GameplayCueParameters
以及你的 GCs
需要知道什么。
4.8.8 Gameplay Cue Events
GameplayCues
响应特定的 EGameplayCueEvents
:
EGameplayCueEvent | 描述 |
---|---|
OnActive | 当 GameplayCue 被激活(添加)时调用。 |
WhileActive | 当 GameplayCue 处于活动状态时调用,即使它实际上并没有刚刚被应用(例如进行中加入等)。这不是 Tick !当 GameplayCueNotify_Actor 被添加或变得相关时,它会像 OnActive 一样被调用一次。如果需要 Tick() ,只需使用 GameplayCueNotify_Actor 的 Tick() 。毕竟它是一个 AActor 。 |
Removed | 当 GameplayCue 被移除时调用。响应此事件的 Blueprint GameplayCue 函数是 OnRemove 。 |
Executed | 当 GameplayCue 被执行时调用:即时效果或周期性 Tick() 。响应此事件的 Blueprint GameplayCue 函数是 OnExecute 。 |
使用 OnActive
处理在 GameplayCue
开始时发生的任何事情,但如果迟到的加入者错过也没关系。使用 WhileActive
处理 GameplayCue
中的持续效果,你希望迟到的加入者看到。例如,如果你有一个 MOBA 中塔结构爆炸的 GameplayCue
,你会把初始爆炸粒子系统和爆炸声放在 OnActive
中,而任何残留的持续火焰粒子或声音放在 WhileActive
中。在这种情况下,迟到的加入者重播来自 OnActive
的初始爆炸是没有意义的,但你会希望他们在爆炸发生后从 WhileActive
看到地面上的持续循环火焰效果。OnRemove
应该清理在 OnActive
和 WhileActive
中添加的任何东西。每次 Actor 进入 GameplayCueNotify_Actor
的相关范围时,WhileActive
将被调用。每次 Actor 离开 GameplayCueNotify_Actor
的相关范围时,OnRemove
将被调用。
4.8.9 Gameplay Cue Reliability(Gameplay Cue 可靠性)
一般来说,GameplayCues
应被认为是不可靠的,因此不适合直接影响游戏玩法的任何事情。
Executed GameplayCues
: 这些 GameplayCues
是通过不可靠的多播应用的,并且始终是不可靠的。
GameplayCues
应用于 GameplayEffects
:
- 自主代理可靠接收
OnActive
、WhileActive
和OnRemove
。FActiveGameplayEffectsContainer::NetDeltaSerialize()
调用UAbilitySystemComponent::HandleDeferredGameplayCues()
来调用OnActive
和WhileActive
。FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers()
调用OnRemoved
。 - 模拟代理可靠接收
WhileActive
和OnRemove
。UAbilitySystemComponent::MinimalReplicationGameplayCues
的复制调用WhileActive
和OnRemove
。OnActive
事件是通过不可靠多播调用的。
GameplayCues
未通过 GameplayEffect
应用:
- 自主代理可靠接收
OnRemove
。OnActive
和WhileActive
事件是通过不可靠多播调用的。 - 模拟代理可靠接收
WhileActive
和OnRemove
。UAbilitySystemComponent::MinimalReplicationGameplayCues
的复制调用WhileActive
和OnRemove
。OnActive
事件是通过不可靠多播调用的。
如果你需要 GameplayCue
中的某些东西是“Reliable
”,那么从 GameplayEffect
应用它,并使用 WhileActive
添加效果,使用 OnRemove
移除效果。