4.8 游戏反馈 - Gameplay Cues

4.8.1 游戏反馈的定义 - Gameplay Cue Definition

GameplayCuesGC)负责执行与游玩无关的事情的处理,比如说声音效果,粒子效果,相机抖动之类。GameplayCues通常是会复制(除非在外部进行ExecutedAdded,或者本地进行Removed)和预测的。

我们可以通过利用ASC发送与某个GameplayCue的合法的父名称对应的GameplayTag以及来发送某个事件类型(ExecuteAdd或者Remove)到GameplayCueManager来触发GameplayCuesGameplayCueNotify对象,以及其他实现了IGameplayCueInterface接口的Actors可以注册到这些基于GameplayCueGameplayTagGameplayCueTag)的事件。

**注意:**需要重申一下,GameplayCueGameplayTags需要以GameplayCue这个GameplayTag作为起始。比如,一个合法的GameplayCueGameplayTag可以是GameplayCue.A.B.C

有两类的GameplayCueNotifiesStaticActor。他们响应不同的事件,并且不同类型的GameplayEffects可以去对他们进行触发。 你可以用你自己的逻辑对响应事件的内容进行重写。

GameplayCue ClassEventGameplayEffect 类型描述
GameplayCueNotify_StaticExecuteInstant或者Periodic静态GameplayCueNotifies是在ClassDefaultObject(即没有对应的实例)上进行操作,这非常适合是实现那些一次性的效果,比如说碰撞冲击这一类的。
GameplayCueNotify_ActorAdd或者RemoveDuration或者InfiniteActor类型的GameplayCueNotifies会在Added的时候生成一个新的实例。因为这些都是实例化出来的,他们可以执行某些操作,一直到他们被Removed掉。他们比较适合来做那些循环的声效和粒子效果,在响应的Duration或者Infinite类型的GameplayEffect被移除掉或者手动调用移除指令时进行中断并移除。他们也提供了一些选项来管理在同一时间允许被Added的数量,这样在不同的程序想要开始某段声音或者粒子时,就不会去重复叠加多个同样的效果。

GameplayCueNotifies从技术层面讲可以响应任意的事件,但是上面这些是我们更加普遍的使用方式。

**注意:**当使用GameplayCueNotify_Actor时,要勾选Auto Destroy on Remove,否则在随后Add那个GameplayCueTag就无法正常生效了。

ASCReplication Mode不是Full时,服务器玩家(监听服务器)的AddRemove GC的事件将会触发两次——一次是应用GE,另一次是通过NetMultiCast广播给客户端们。但是WhileActive事件讲仅会触发一次。所有事件在客户端仅触发一次。

示例项目中包含一个GameplayCueNotify_Actor来处理眩晕和冲刺效果。此外还有一个GameplayCueNotify_Static来处理枪械的子弹命中效果。这些GC可以通过triggering them locally来做进一步的优化,这样就不用通过GE来对他们进行复制。我在示例项目中选择以简单的初学的方法来对他们进行使用展示。

4.8.2 触发游戏反馈 - Triggering Gameplay Cues

GameplayEffect被成功应用时,在相应GameplayTags下的所有的GameplayCues都会被进行触发。

GameplayCue Triggered from a GameplayEffect

UGameplayAbility提供了一些蓝图节点来ExecuteAdd或者Remove GameplayCues

GameplayCue Triggered from a GameplayAbility

在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

GameplayAbilitiesASC暴露出来的用于触发GameplayCues的函数在默认情况下是会被复制的。每个GameplayCue事件都是一个多播的RPC。浙江导致大量的RPC。GAS也强制限制每次网络更新至多有两个同样的GameplayCue的RPC。我们可以使用本地GameplayCues来解决这个问题。本地GameplayCues只会在每个独立的客户端上进行ExecuteAdd或者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,那么你必须手动构建传入到GameplayCueGameplayCueParameters结构体。如果GameplayCue是由GameplayEffect来进行触发的,那么GameplayCueParameters结构体的下列参数将会自动填充:

  • AggregatedSourceTags
  • AggregatedTargetTags
  • GameplayEffectLevel
  • AbilityLevel
  • EffectContext
  • Magnitude (如果GameplayEffect选择了某项Attribute,并且会有对应的Modifier对其产生影响。)

GameplayCueParameters里的SourceObject变量是一个高度自定义的数据位置,你可以利用它在手动触发GameplayCue时,传入任意的数据。

**注意:**在参数结构体中的一些变量,比如Instigator,可能已经存在于EffectContextEffectContext也可以包含一个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或者SourceASC

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

4.8.7 游戏反馈的批处理 - Gameplay Cue Batching

每个触发的GameplayCue都是一个不可靠的NetMulticast的RPC。在某些情况下,我们可能需要同一时间触发多个GC,对应着有着一些优化的处理方法,来将他们合并到一个RPC中,亦或是发送相对更少量的数据从而节省带宽。

4.8.7.1 手动远程过程调用 - Manual RPC

假设你有一把能射八颗子弹的猎枪,这就会有8个射线检测和以及轨迹效果的GameplayCuesGASShooter中采用了一种偷懒的方法,它将所有的轨迹信息打包到一块儿以 TargetData的格式存储到EffectContext 。虽然这种方法将8个RPC减到了1个,但是这1个里直接包含了原先8个的信息,包含了大量的数据(约500b),仍然需要占用很多网络资源。针对这种情况,还有一种更好的处理方法,可以在要发送的RPC中用一个自定义的结构体,其中你可以高效编码命中位置的数据,或者放一个随机数种子,从而在接收端能够重建/拟合出冲击位置的数据信息。然后客户端就可以进行数据解包并将解析出来的数据刷到本地执行的GameplayCues

具体操作步骤:

  1. 声明一个FScopedGameplayCueSendContext。它会自动阻止 UGameplayCueManager::FlushPendingCues()的执行,直到超出其作用域。这意味着其作用域内的所有的GameplayCues将会排成一个队列以供使用。
  2. 重写UGameplayCueManager::FlushPendingCues(),依据GameplayTag来合并GameplayCues到自定义的结构体,然后通过RPC发送到客户端。
  3. 客户端接收自定义结构体然后将其解包到本地执行的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),这一点不会受到ASCReplication Mode影响。依据GameplayEffectSpec中的内容的不同所占据的带宽可能会非常之不同(有可能会非常占用资源)。我们可以在控制台设置AbilitySystem.AlwaysConvertGESpecToGCParams 1来尝试进行优化。这会将GameplayEffectSpecs转换为FGameplayCueParameter结构,这样就不用发送整个 FGameplayEffectSpecForRPC。这样可能会节省一些带宽,但也会相对的少一些信息,具体取决于GESpecGameplayCueParameters的转换方法以及具体你的GCs需要哪些信息。

4.8.8 游戏反馈事件 - Gameplay Cue Events

GameplayCues会去响应特定的EGameplayCueEvents

EGameplayCueEvent描述
OnActiveGameplayCue激活(添加)时调用。
WhileActiveGameplayCue处于激活状态时调用,即使它当前并没有应用。注意,这并不是Tick!它只会被调用一次,即当GameplayCueNotify_Actor被添加或者被引用。如果你需要用到Tick(),可以使用GameplayCueNotify_ActorTick()。其本质上是一个AActor
RemovedGameplayCue被移除时调用。蓝图中对应的函数事件是OnRemove
ExecutedGameplayCue被执行时调用:瞬间的效果亦或是持续一定事件的Tick()。蓝图中对应的函数事件是OnExecute

对于那些在GameplayCue开始时所发生的内容,都可以将其写在OnActive,当然,晚加入者会错过相关的东西。这无妨。如果你希望晚加入者也要能够看到相应的内容的话,可以使用WhileActive。例如,你在MOBA类游戏中有一个炮塔的爆炸要处理,你可以将一开始的爆炸声音和粒子效果放在OnActive中,然后把后续火焰粒子以及声音放在WhileActive。在这种情形下,晚加入者是不需要在连接上之后再去为其播放一遍初始的爆炸效果,相对的,你需要为其播放后续的炮塔燃烧的火焰效果和声音。OnRemove应该要去清理任何通过OnActiveWhileActive添加的东西。WhileActive是每当某个Actor进入到GameplayCueNotify_Actor的关联范围里时调用。OnRemove则是每当有某个Actor离开GameplayCueNotify_Actor的关联范围进行触发。

4.8.9 游戏反馈的可靠性 - Gameplay Cue Reliability

GameplayCues通常被认为是不可靠的,因此不适合用来做那些会直接影响游玩的效果。

**已执行的GameplayCues:**这些GameplayCues是通过不可靠的多播而被应用的,所以全部是不可靠的。

GameplayEffects应用的GameplayCues

  • 主控端可靠得接收到OnActiveWhileActive,以及OnRemove
    FActiveGameplayEffectsContainer::NetDeltaSerialize()调用UAbilitySystemComponent::HandleDeferredGameplayCues()来进行OnActive以及WhileActive的调用。 FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers() 则负责OnRemoved的调用。
  • 模拟端可靠得介绍到WhileActiveOnRemove
    UAbilitySystemComponent::MinimalReplicationGameplayCues的复制调用WhileActive以及OnRemoveOnActive事件则是通过不可靠的多播来进行调用的。

GameplayEffect之外进行的GameplayCues的应用:

  • 主控端可靠得接收到OnRemove
    OnActive以及WhileActive事件是通过一个不可靠得多播来进行调用的。
  • 模拟端可靠得接收到WhileActive以及OnRemove
    UAbilitySystemComponent::MinimalReplicationGameplayCues的复制调用WhileActive以及OnRemoveOnActive事件是由一个不可靠的多播进行调用的。

如果你需要GameplayCue里的某样东西是可靠的,那么就利用GameplayEffect进行应用,并且使用WhileActive来添加特效,使用OnRemove来进行特效的移除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值