4、GAS相关概念4.8 Gameplay Cues

4.8 Gameplay Cues

4.8.1 Gameplay Cues定义

GameplayCues(简称 GC)用于执行与游戏玩法无关的内容,如音效、粒子效果、摄像机震动等。GameplayCues 通常是复制的(除非显式地在本地执行 ExecutedAddedRemoved),并且是预测的。

我们通过发送相应的 GameplayTag(必须具有父标签 GameplayCue.)和事件类型(ExecuteAddRemove)到 GameplayCueManager 来触发 GameplayCues,该操作通过 ASC 实现。GameplayCueNotify 对象和其他实现了 IGameplayCueInterfaceActor 可以根据 GameplayCueGameplayTagGameplayCueTag)订阅这些事件。

注意: 重申一下,GameplayCueGameplayTags 必须以父标签 GameplayCue 开头。例如,有效的 GameplayCue GameplayTag 可能是 GameplayCue.A.B.C

GameplayCueNotifies

为两类:StaticActor。它们响应不同的事件,且不同类型的 GameplayEffect 可以触发它们。重写相应的事件并实现你的逻辑。

GameplayCue事件GameplayEffect 类型描述
GameplayCueNotify_StaticExecuteInstantPeriodic静态 GameplayCueNotifiesClassDefaultObject 上操作(即没有实例),非常适合用于一次性效果,比如击中冲击。
GameplayCueNotify_ActorAddRemoveDurationInfiniteActor 类型的 GameplayCueNotifiesAdd 时会生成一个新实例。由于这些是实例化的,它们可以持续执行直到被 Remove。它们适合循环音效和粒子效果,当相应的 DurationInfinite 类型的 GameplayEffect 被移除时,或者通过手动调用移除时。它们还带有选项来管理允许同时 Add 的数量,避免相同效果的多次应用只启动一次音效或粒子效果。

GameplayCueNotifies 理论上可以响应任何事件,但这通常是我们如何使用它们的方式。

注意: 使用 GameplayCueNotify_Actor 时,检查 Auto Destroy on Remove,否则后续调用 Add 相同的 GameplayCueTag 时将无法生效。

当使用 ASC复制模式 为非 Full 模式时,AddRemoveGC 事件会在服务器玩家(监听服务器)上触发两次——一次是应用 GE,另一次是从 “Minimal” 的 NetMultiCast 发送到客户端。然而,WhileActive 事件仍然只会触发一次。所有事件只会在客户端触发一次。

示例项目中包含了一个 GameplayCueNotify_Actor 用于眩晕和冲刺效果,还包含了一个 GameplayCueNotify_Static 用于 FireGun 的射击冲击。这些 GC 可以通过 本地触发进行进一步优化,而不是通过 GE 进行复制。在示例项目中,我选择展示初学者使用的方式。

4.8.2 触发Gameplay Cues

GameplayEffect 成功应用时(未被标签或免疫阻止),填写应触发的所有 GameplayCueGameplayTags

从 GameplayEffect 触发的 GameplayCue

UGameplayAbility 提供了蓝图节点来 ExecuteAddRemove GameplayCues

从 GameplayAbility 触发的 GameplayCue

在 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)

GameplayAbilitiesASC 触发的游戏提示默认是同步的。每个 GameplayCue 事件是一个多播 RPC。这会导致大量的 RPC。GAS 还强制每个网络更新最多只允许有两个相同的 GameplayCue RPC。为了避免这一点,我们在可能的情况下使用本地的 GameplayCues。本地的 GameplayCues 仅在单独的客户端上执行 ExecuteAddRemove

可以使用本地 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 的额外信息。如果你从 GameplayAbilityASC 的函数中手动触发 GameplayCue,那么你必须手动填写传递给 GameplayCueGameplayCueParameters 结构。如果 GameplayCue 是由 GameplayEffect 触发的,那么以下变量会自动填充到 GameplayCueParameters 结构中:

  • AggregatedSourceTags
  • AggregatedTargetTags
  • GameplayEffectLevel
  • AbilityLevel
  • EffectContext
  • Magnitude(如果 GameplayEffectGameplayCue 标签容器上选择了一个用于幅度的 Attribute 并且有一个相应的影响该 AttributeModifier

在手动触发 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 事件发送到 TargetSourceASC

如果你永远不希望某个特定的 ASC 上的任何 GameplayCues 被触发,你可以设置 AbilitySystemComponent->bSuppressGameplayCues = true;

4.8.7 Gameplay Cue 批处理

每个触发的 GameplayCue 都是一个不可靠的 NetMulticast RPC。在我们同时触发多个 GCs 的情况下,有一些优化方法可以将它们压缩为一个 RPC 或通过发送更少的数据来节省带宽。

4.8.7.1 手动 RPC

假设你有一把发射八个弹丸的霰弹枪。这是八个轨迹和冲击 GameplayCuesGASShooter 采用了一种懒惰的方法,将它们组合成一个 RPC,通过将所有的轨迹信息藏入 EffectContext 作为 TargetData。虽然这将 RPC 从八个减少到一个,但它仍然在一个 RPC 中发送了大量数据(大约 500 字节)。一种更优化的方法是发送一个带有自定义结构的 RPC,在其中有效地编码命中位置,或者你可以给它一个随机种子号,以在接收端重现/近似冲击位置。客户端然后解包这个自定义结构并转化为本地执行的 GameplayCues

这样运作:

  1. 声明一个 FScopedGameplayCueSendContext。这会抑制 UGameplayCueManager::FlushPendingCues() 直到它超出范围,这意味着所有 GameplayCues 将会被排队直到 FScopedGameplayCueSendContext 超出范围。
  2. 重写 UGameplayCueManager::FlushPendingCues() 以根据一些自定义 GameplayTag 将可以批处理在一起的 GameplayCues 合并到你的自定义结构中并将其 RPC 到客户端。
  3. 客户端接收自定义结构并解包成本地执行的 GameplayCues

此方法也可用于当你需要特定参数来适应 GameplayCues,但不适合 GameplayCueParameters 提供的内容,并且你不希望将它们添加到 EffectContext 中,如伤害数字、暴击指示器、破盾指示器、致命命中指示器等。

https://forums.unrealengine.com/development-discussion/c-gameplay-programming/1711546-fscopedgameplaycuesendcontext-gameplaycuemanager

4.8.7.2 一个GameplayEffect上的多个GameplayCue

所有在 GameplayEffect 上的 GameplayCues 都已经在一个 RPC 中发送。默认情况下,UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec() 将发送整个 GameplayEffectSpec(但转换为 FGameplayEffectSpecForRPC),即使在 ASCReplication Mode 下也是不可靠的。这可能会消耗大量带宽,具体取决于 GameplayEffectSpec 中的内容。我们可以通过设置 cvar AbilitySystem.AlwaysConvertGESpecToGCParams 1 来优化这一点。这将 GameplayEffectSpecs 转换为 FGameplayCueParameter 结构并 RPC 这些而不是整个 FGameplayEffectSpecForRPC。这可能会节省带宽,但也会有更少的信息,这取决于 GESpec 如何转换为 GameplayCueParameters 以及你的 GCs 需要知道什么。

4.8.8 Gameplay Cue Events

GameplayCues 响应特定的 EGameplayCueEvents

EGameplayCueEvent描述
OnActiveGameplayCue 被激活(添加)时调用。
WhileActiveGameplayCue 处于活动状态时调用,即使它实际上并没有刚刚被应用(例如进行中加入等)。这不是 Tick!当 GameplayCueNotify_Actor 被添加或变得相关时,它会像 OnActive 一样被调用一次。如果需要 Tick(),只需使用 GameplayCueNotify_ActorTick()。毕竟它是一个 AActor
RemovedGameplayCue 被移除时调用。响应此事件的 Blueprint GameplayCue 函数是 OnRemove
ExecutedGameplayCue 被执行时调用:即时效果或周期性 Tick()。响应此事件的 Blueprint GameplayCue 函数是 OnExecute

使用 OnActive 处理在 GameplayCue 开始时发生的任何事情,但如果迟到的加入者错过也没关系。使用 WhileActive 处理 GameplayCue 中的持续效果,你希望迟到的加入者看到。例如,如果你有一个 MOBA 中塔结构爆炸的 GameplayCue,你会把初始爆炸粒子系统和爆炸声放在 OnActive 中,而任何残留的持续火焰粒子或声音放在 WhileActive 中。在这种情况下,迟到的加入者重播来自 OnActive 的初始爆炸是没有意义的,但你会希望他们在爆炸发生后从 WhileActive 看到地面上的持续循环火焰效果。OnRemove 应该清理在 OnActiveWhileActive 中添加的任何东西。每次 Actor 进入 GameplayCueNotify_Actor 的相关范围时,WhileActive 将被调用。每次 Actor 离开 GameplayCueNotify_Actor 的相关范围时,OnRemove 将被调用。

4.8.9 Gameplay Cue Reliability(Gameplay Cue 可靠性)

一般来说,GameplayCues 应被认为是不可靠的,因此不适合直接影响游戏玩法的任何事情。

Executed GameplayCues: 这些 GameplayCues 是通过不可靠的多播应用的,并且始终是不可靠的。

GameplayCues 应用于 GameplayEffects:

  • 自主代理可靠接收 OnActiveWhileActiveOnRemoveFActiveGameplayEffectsContainer::NetDeltaSerialize() 调用 UAbilitySystemComponent::HandleDeferredGameplayCues() 来调用 OnActiveWhileActiveFActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers() 调用 OnRemoved
  • 模拟代理可靠接收 WhileActiveOnRemoveUAbilitySystemComponent::MinimalReplicationGameplayCues 的复制调用 WhileActiveOnRemoveOnActive 事件是通过不可靠多播调用的。

GameplayCues 未通过 GameplayEffect 应用:

  • 自主代理可靠接收 OnRemoveOnActiveWhileActive 事件是通过不可靠多播调用的。
  • 模拟代理可靠接收 WhileActiveOnRemoveUAbilitySystemComponent::MinimalReplicationGameplayCues 的复制调用 WhileActiveOnRemoveOnActive 事件是通过不可靠多播调用的。

如果你需要 GameplayCue 中的某些东西是“Reliable”,那么从 GameplayEffect 应用它,并使用 WhileActive 添加效果,使用 OnRemove 移除效果。