4.8.1 游戏反馈的定义 - Gameplay Cue Definition
GameplayCues(GC)负责执行与游玩无关的事情的处理,比如说声音效果,粒子效果,相机抖动之类。GameplayCues通常是会复制(除非在外部进行Executed,Added,或者本地进行Removed)和预测的。
我们可以通过利用ASC发送与某个GameplayCue的合法的父名称对应的GameplayTag以及来发送某个事件类型(Execute,Add或者Remove)到GameplayCueManager来触发GameplayCues。GameplayCueNotify对象,以及其他实现了IGameplayCueInterface接口的Actors可以注册到这些基于GameplayCue的GameplayTag(GameplayCueTag)的事件。
**注意:**需要重申一下,GameplayCue的GameplayTags需要以GameplayCue这个GameplayTag作为起始。比如,一个合法的GameplayCue的GameplayTag可以是GameplayCue.A.B.C。
有两类的GameplayCueNotifies:Static和Actor。他们响应不同的事件,并且不同类型的GameplayEffects可以去对他们进行触发。 你可以用你自己的逻辑对响应事件的内容进行重写。
GameplayCue Class | Event | GameplayEffect 类型 | 描述 |
|---|---|---|---|
GameplayCueNotify_Static | Execute | Instant或者Periodic | 静态GameplayCueNotifies是在ClassDefaultObject(即没有对应的实例)上进行操作,这非常适合是实现那些一次性的效果,比如说碰撞冲击这一类的。 |
GameplayCueNotify_Actor | Add或者Remove | Duration或者Infinite | Actor类型的GameplayCueNotifies会在Added的时候生成一个新的实例。因为这些都是实例化出来的,他们可以执行某些操作,一直到他们被Removed掉。他们比较适合来做那些循环的声效和粒子效果,在响应的Duration或者Infinite类型的GameplayEffect被移除掉或者手动调用移除指令时进行中断并移除。他们也提供了一些选项来管理在同一时间允许被Added的数量,这样在不同的程序想要开始某段声音或者粒子时,就不会去重复叠加多个同样的效果。 |
GameplayCueNotifies从技术层面讲可以响应任意的事件,但是上面这些是我们更加普遍的使用方式。
**注意:**当使用GameplayCueNotify_Actor时,要勾选Auto Destroy on Remove,否则在随后Add那个GameplayCueTag就无法正常生效了。
当ASC的Replication Mode不是Full时,服务器玩家(监听服务器)的Add和Remove GC的事件将会触发两次——一次是应用GE,另一次是通过NetMultiCast广播给客户端们。但是WhileActive事件讲仅会触发一次。所有事件在客户端仅触发一次。
示例项目中包含一个GameplayCueNotify_Actor来处理眩晕和冲刺效果。此外还有一个GameplayCueNotify_Static来处理枪械的子弹命中效果。这些GC可以通过triggering them locally来做进一步的优化,这样就不用通过GE来对他们进行复制。我在示例项目中选择以简单的初学的方法来对他们进行使用展示。
4.8.2 触发游戏反馈 - Triggering Gameplay Cues
当GameplayEffect被成功应用时,在相应GameplayTags下的所有的GameplayCues都会被进行触发。

UGameplayAbility提供了一些蓝图节点来Execute,Add或者Remove GameplayCues。

在C++里,你可以直接调用ASC上的函数(或者是在你的ASC类中将其暴露给蓝图):
/** GameplayCues can also come on their own. These take an optional effect context to pass through hit result, etc */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Add a persistent gameplay cue */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Remove a persistent gameplay cue */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
/** Removes any GameplayCue added on its own, i.e. not as part of a GameplayEffect. */
void RemoveAllGameplayCues();
4.8.3 本地游戏反馈 - Local Gameplay Cues
从GameplayAbilities和ASC暴露出来的用于触发GameplayCues的函数在默认情况下是会被复制的。每个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是本地Added,它也相应的应该本地Removed。如果它是通过复制Added,则它相应的也应该通过复制进行 Removed。
4.8.4 游戏反馈的参数 - Gameplay Cue Parameters
GameplayCues接收一个FGameplayCueParameters结构体作为参数,其中包含了关于GameplayCue的一些额外的信息。如果你手动利用GameplayAbility或者ASC上的函数来触发GameplayCue,那么你必须手动构建传入到GameplayCue的GameplayCueParameters结构体。如果GameplayCue是由GameplayEffect来进行触发的,那么GameplayCueParameters结构体的下列参数将会自动填充:
- AggregatedSourceTags
- AggregatedTargetTags
- GameplayEffectLevel
- AbilityLevel
- EffectContext
- Magnitude (如果
GameplayEffect选择了某项Attribute,并且会有对应的Modifier对其产生影响。)
GameplayCueParameters里的SourceObject变量是一个高度自定义的数据位置,你可以利用它在手动触发GameplayCue时,传入任意的数据。
**注意:**在参数结构体中的一些变量,比如Instigator,可能已经存在于EffectContext。EffectContext也可以包含一个FHitResult,用来确定在世界中的哪一位置来生成GameplayCue。继承EffectContext来进行拓展可能是一个不错的方式来向GameplayCues传入更多的数据,特别是对于那些由GameplayEffect进行触发的GameplayCues来说更是如此。
参阅UAbilitySystemGlobals中的三个函数,他们是负责为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;但是,我们并不希望它在游戏时异步加载所有的GameplayCueNotifies。因为这样做的话,每一个GameplayCueNotify及其相关的声音,粒子都会被加载到内存中,不论其到底是否在关卡中进行使用。在大型游戏中,比如Paragon,这可能会是好几百兆的内存消耗,并且可能会让游戏在开启时陷入加载缓慢的境况。
另一个方案是在开启游戏时异步加载真正在关卡中起作用或可能被触发的GameplayCues。这可以一定程度上缓解无用内存的消耗,以及可能出现的游戏卡死,当然代价就是在游玩中每当有某个GameplayCue是第一次被触发时,可能会有一定的延迟效果。在SSD上并不会出现这样的延迟。我还没有在HDD上测试过。如果在UE编辑器内选用这个选项,就可能会在GameplayCue第一次被触发加载时出现轻微的卡顿或甚至卡死,尤其是针对粒子系统(编辑器可能会需要编译粒子系统)。在打包后这就不成问题了,因为粒子系统就已经是编译好的了。
首先我们必须继承UGameplayCueManager并且告诉AbilitySystemGlobals类,去使用我们拓展的UGameplayCueManager的子类,具体是在DefaultGame.ini中。
[/Script/GameplayAbilities.AbilitySystemGlobals]
GlobalGameplayCueManagerClass="/Script/ParagonAssets.PBGameplayCueManager"
在我们的UGameplayCueManager子类中,重写ShouldAsyncLoadRuntimeObjectLibraries()。
virtual bool ShouldAsyncLoadRuntimeObjectLibraries() const override
{
return false;
}
4.8.6 阻止游戏反馈触发 - Prevent Gameplay Cues from Firing
有些时候我们并不希望去触发某些GameplayCue。例如,如果我们阻止某项攻击,我们可能并不希望播放附着在伤害GameplayEffect上的碰撞效果For example if we block an attack, we may not want to play the hit impact attached to the damage GameplayEffect,亦或是想要换另外一个效果。我们可以在GameplayEffectExecutionCalculations内调用OutExecutionOutput.MarkGameplayCuesHandledManually(),并且手动发送GameplayCue事件到Target或者Source的 ASC。
如果你永远不想在特定的ASC上触发任何GameplayCues,你可以设置AbilitySystemComponent->bSuppressGameplayCues = true;。
4.8.7 游戏反馈的批处理 - Gameplay Cue Batching
每个触发的GameplayCue都是一个不可靠的NetMulticast的RPC。在某些情况下,我们可能需要同一时间触发多个GC,对应着有着一些优化的处理方法,来将他们合并到一个RPC中,亦或是发送相对更少量的数据从而节省带宽。
4.8.7.1 手动远程过程调用 - Manual RPC
假设你有一把能射八颗子弹的猎枪,这就会有8个射线检测和以及轨迹效果的GameplayCues。GASShooter中采用了一种偷懒的方法,它将所有的轨迹信息打包到一块儿以 TargetData的格式存储到EffectContext 。虽然这种方法将8个RPC减到了1个,但是这1个里直接包含了原先8个的信息,包含了大量的数据(约500b),仍然需要占用很多网络资源。针对这种情况,还有一种更好的处理方法,可以在要发送的RPC中用一个自定义的结构体,其中你可以高效编码命中位置的数据,或者放一个随机数种子,从而在接收端能够重建/拟合出冲击位置的数据信息。然后客户端就可以进行数据解包并将解析出来的数据刷到本地执行的GameplayCues。
具体操作步骤:
- 声明一个
FScopedGameplayCueSendContext。它会自动阻止UGameplayCueManager::FlushPendingCues()的执行,直到超出其作用域。这意味着其作用域内的所有的GameplayCues将会排成一个队列以供使用。 - 重写
UGameplayCueManager::FlushPendingCues(),依据GameplayTag来合并GameplayCues到自定义的结构体,然后通过RPC发送到客户端。 - 客户端接收自定义结构体然后将其解包到本地执行的
GameplayCues中。
这个方法也还有其他的适用情况,比如说你需要一些特定的参数,但是这些参数与GameplayCueParameters所提供的并不匹配,而且你也并不希望将其添加到EffectContext中,比如说伤害飘字,暴击提示,破盾提示,致命一击的提示等等。
https://forums.unrealengine.com/development-discussion/c-gameplay-programming/1711546-fscopedgameplaycuesendcontext-gameplaycuemanager
4.8.7.2 一个游戏效果上带有多个游戏表现 - Multiple GCs on one GE
一个GameplayEffect的全部GameplayCues都在一个RPC中发送。默认情况下,UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec()将会通过不可靠的NetMulticast的RPC来发送整个GameplayEffectSpec(但会转换成FGameplayEffectSpecForRPC),这一点不会受到ASC的Replication Mode影响。依据GameplayEffectSpec中的内容的不同所占据的带宽可能会非常之不同(有可能会非常占用资源)。我们可以在控制台设置AbilitySystem.AlwaysConvertGESpecToGCParams 1来尝试进行优化。这会将GameplayEffectSpecs转换为FGameplayCueParameter结构,这样就不用发送整个 FGameplayEffectSpecForRPC。这样可能会节省一些带宽,但也会相对的少一些信息,具体取决于GESpec到GameplayCueParameters的转换方法以及具体你的GCs需要哪些信息。
4.8.8 游戏反馈事件 - Gameplay Cue Events
GameplayCues会去响应特定的EGameplayCueEvents:
EGameplayCueEvent | 描述 |
|---|---|
OnActive | 在GameplayCue激活(添加)时调用。 |
WhileActive | 当GameplayCue处于激活状态时调用,即使它当前并没有应用。注意,这并不是Tick!它只会被调用一次,即当GameplayCueNotify_Actor被添加或者被引用。如果你需要用到Tick(),可以使用GameplayCueNotify_Actor的Tick()。其本质上是一个AActor。 |
Removed | 当GameplayCue被移除时调用。蓝图中对应的函数事件是OnRemove。 |
Executed | 当GameplayCue被执行时调用:瞬间的效果亦或是持续一定事件的Tick()。蓝图中对应的函数事件是OnExecute。 |
对于那些在GameplayCue开始时所发生的内容,都可以将其写在OnActive,当然,晚加入者会错过相关的东西。这无妨。如果你希望晚加入者也要能够看到相应的内容的话,可以使用WhileActive。例如,你在MOBA类游戏中有一个炮塔的爆炸要处理,你可以将一开始的爆炸声音和粒子效果放在OnActive中,然后把后续火焰粒子以及声音放在WhileActive。在这种情形下,晚加入者是不需要在连接上之后再去为其播放一遍初始的爆炸效果,相对的,你需要为其播放后续的炮塔燃烧的火焰效果和声音。OnRemove应该要去清理任何通过OnActive 和WhileActive添加的东西。WhileActive是每当某个Actor进入到GameplayCueNotify_Actor的关联范围里时调用。OnRemove则是每当有某个Actor离开GameplayCueNotify_Actor的关联范围进行触发。
4.8.9 游戏反馈的可靠性 - Gameplay Cue Reliability
GameplayCues通常被认为是不可靠的,因此不适合用来做那些会直接影响游玩的效果。
**已执行的GameplayCues:**这些GameplayCues是通过不可靠的多播而被应用的,所以全部是不可靠的。
从GameplayEffects应用的GameplayCues:
- 主控端可靠得接收到
OnActive,WhileActive,以及OnRemove
FActiveGameplayEffectsContainer::NetDeltaSerialize()调用UAbilitySystemComponent::HandleDeferredGameplayCues()来进行OnActive以及WhileActive的调用。FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers()则负责OnRemoved的调用。 - 模拟端可靠得介绍到
WhileActive和OnRemove
UAbilitySystemComponent::MinimalReplicationGameplayCues的复制调用WhileActive以及OnRemove。OnActive事件则是通过不可靠的多播来进行调用的。
GameplayEffect之外进行的GameplayCues的应用:
- 主控端可靠得接收到
OnRemove
OnActive以及WhileActive事件是通过一个不可靠得多播来进行调用的。 - 模拟端可靠得接收到
WhileActive以及OnRemove
UAbilitySystemComponent::MinimalReplicationGameplayCues的复制调用WhileActive以及OnRemove。OnActive事件是由一个不可靠的多播进行调用的。
如果你需要GameplayCue里的某样东西是可靠的,那么就利用GameplayEffect进行应用,并且使用WhileActive来添加特效,使用OnRemove来进行特效的移除。
本文深入探讨了游戏开发中游戏反馈机制(GameplayCues)的工作原理,包括定义、触发方式、参数传递、事件处理等核心内容,并讲解了如何优化网络传输以提高效率。



2072

被折叠的 条评论
为什么被折叠?



