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
来进行特效的移除。