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 移除效果。