4.11.1 目标数据 - Target Data
FGameplayAbilityTargetData
是一个通用的结构体,专为那些可以在网络上传递的目标数据使用。TargetData
通常会保存AActor
/UObject
的引用,FHitResults
以及其他一些通用的位置/方向/原点的信息。当然,你也可以对他进行派生,把你想要的任何东西塞到它里面,这是一种简单的通过GameplayAbilities
来在客户端和服务器之间传递数据的方式。FGameplayAbilityTargetData
结构体需要去派生出子类来进行使用,不要直接去进行使用。GameplayAbilityTargetTypes.h
中罗列了一些为GAS
准备的开箱即用的的FGameplayAbilityTargetData
子类结构体。
TargetData
通常是由Target Actors
产生,或是手动创建,并且由AbilityTasks
和GameplayEffects
通过EffectContext
来进行使用。当TargetData
作为EffectContext
的结果时,Executions
, MMCs
,GameplayCues
以及AttributeSet
的一些默认方法都可以对其进行访问。
通常我们不会去直接传递FGameplayAbilityTargetData
,相对的,我们会去利用FGameplayAbilityTargetDataHandle
,其内部存有一个FGameplayAbilityTargetData
的数组指针。这个中间结构体会为TargetData
的多态性提供支持。
4.11.2 目标Actor - Target Actors
GameplayAbilities
是利用WaitTargetData
的AbilityTask
来生成TargetActors
,以显示和捕捉世界中的目标信息。TargetActors
可以使用GameplayAbilityWorldReticles
来显示当前的目标。在目标确认后,目标的信息会以TargetData
的形式返回,然后可以进一步传递给GameplayEffects
。
TargetActors
是派生自AActor
,因此他们可以拥有任意类型的渲染组件(比如static mesh静态网格体或者decal贴花),来表示他们的位置以及他们如何进行目标选择。静态网格体可以用来显示你的角色所构建的物体。贴花可以用来显示地面上的相应作用区域。示例工程中使用了一个带有地面贴花的AGameplayAbilityTargetActor_GroundTrace
,用以表示Meteor流星技能的伤害效果区域。TargetActors
也可以不去显示任何东西。例如,GASShooter项目中的枪械弹药需要使用射线检测目标,但是这些一瞬间的检测并不需要显示任何东西。
TargetActors
会利用基本的射线或者碰撞检测来捕获目标信息,并且可以根据TargetActor
的具体实现将结果从FHitResults
或者AActor
数组转换为TargetData
。WaitTargetData
的AbilityTask
可以根据TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType
的参数来决定目标在什么时候确定。当不是TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant
时,TargetActor
通常会在Tick()
中去作射线碰撞检测,并且根据其具体实现去将其位置更新到FHitResult
中。尽管这是一个在Tick()
中执行的射线碰撞检测,但是实际上你并不太需要为此担心性能问题,意味它并不需要进行网络复制,而且你通常也同一时间也不会有超过一个的TargetActor
(这说的是更通常的情况,当然系统并没有限制同一时间的数量)。但是这件事你还是需要知道的,有些时候一些复杂的TargetActors
可能会在Tick()
中做非常多的操作,比如GASShooter项目中火箭发射器的第二个技能。尽管在客户端Tick()
是非常灵敏的(即其执行次数高于服务端),但是当其开始对性能产生较大影响时你可能需要考虑降低TargetActor
的Tick()
频率。在TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant
时,TargetActor
会瞬间生成,产生TargetData
数据,然后销毁。这样,就永远不会涉及到Tick()
的调用。
EGameplayTargetingConfirmation::Type | 目标确认的时机 |
---|---|
Instant | 没有特殊逻辑或者用户输入决定何时去“开火”的情况下,目标的选取会立即发生。 |
UserConfirmed | 在用户通过技能绑定到Confirm 的输入进而确认了目标亦或是通过调用UAbilitySystemComponent::TargetConfirm() 表明目标的确认时,目标的选取就发生了。TargetActor 同样也会去相应Cancel 的输入或者UAbilitySystemComponent::TargetCancel() 的调用从而取消目标的选择。 |
Custom | 技能中通过调用UGameplayAbility::ConfirmTaskByInstanceName() 来决定具体的targeting data 准备好的时机。同样,TargetActor 也会响应UGameplayAbility::CancelTaskByInstanceName() 来取消目标的选择。 |
CustomMulti | 同上,其不同之处在于数据生成时不会去结束掉这个AbilityTask 。 |
并不是每个TargetActor
都支持所有的EGameplayTargetingConfirmation::Type
。例如,AGameplayAbilityTargetActor_GroundTrace
就不支持Instant
类型的确认。
WaitTargetData
的AbilityTask
中有一个AGameplayAbilityTargetActor
类作为参数,其会在每次这个技能任务激活时生成一个TargetActor
的实例,而在AbilityTask
结束时销毁这个实例。WaitTargetDataUsingActor
的AbilityTask
可以传入一个已经生成好的TargetActor
,同样会在技能任务结束时销毁该实例。这两种AbilityTasks
都是低效的,在每次使用时都需要生成或者需要一个生成好的TargetActor
。对于开发游戏原型来说他们是可以开箱即用的,但是正式的开发可能就需要进行进一步优化,比如某些情况下你需要不间断得生成TargetData
,像那种自动来复枪。GASShooter项目中有一个自定义的AGameplayAbilityTargetActor
的派生类,以及一个全新的WaitTargetDataWithReusableActor
的AbilityTask
,在这个任务中你可以重复使用TargetActor
而无需一直去处理销毁的问题。
TargetActors
在默认情况下是不进行复制的。但是,如果你的游戏中有需要向其他玩家公开你所选目标的这样的需求的话,就需要用到TargetActors
的复制了,当然这完全是可以实现的。WaitTargetData
的AbilityTask
中也包含了利用RPC和服务器进行通信的默认功能。如果TargetActor
的ShouldProduceTargetDataOnServer
属性设置为false
,那么客户端在确定目标时会通过UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()
中的CallServerSetReplicatedTargetData()
来将TargetData
利用RPC发送给服务器。如果ShouldProduceTargetDataOnServer
是true
,客户端将发送一个通用的确认事件,即EAbilityGenericReplicatedEvent::GenericConfirm
,在UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()
利用RPC方法传递给服务器,然后服务器将会基于接收到的RPC来做射线和碰撞检测,进而在服务器上生成数据。如果客户端取消了目标选择,它会发出一个通用的取消事件,即EAbilityGenericReplicatedEvent::GenericCancel
,在UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback
中利用RPC的方法传递给客户端。如你所见,TargetActor
和WaitTargetData
的AbilityTask
上都存在着非常多的委托。TargetActor
响应输入并且广播TargetData
准备就绪、确认或者是取消的委托。WaitTargetData
监听TargetActor
的TargetData
准备就绪、确认以及取消的委托,并且将这些信息返回给GameplayAbility
和服务器。如果你发送TargetData
给服务器,你可能希望在服务器上进行确认操作,以便确认TargetData
是否是合理的(防止作弊的发生)。直接在服务器上生成TargetData
的话会完全避免这个问题,但是可能会导致所属客户端的预测的失败。
根据你所使用的AGameplayAbilityTargetActor
的特定派生类的不同,WaitTargetData
的AbilityTask
节点上也会相应的暴露不同的ExposeOnSpawn
参数。其中共享的部分有:
共享的 TargetActor 上的参数 | 定义 |
---|---|
Debug | 如果为true 时,那么在非发行版的版本中它会去帮我们绘制所有的射线和碰撞检测的信息。请记住,非Instant 的TargetActors 将会在Tick() 中去做检测,那么自然,这些绘制内容也是在Tick() 中完成的(译者注:相对来说射线检测的消耗还可以接收,但是其绘制的消耗可能会非常爆炸)。 |
Filter | [可选] 一个特定的结构体,用来在射线检测时对Actors 进行过滤。通常的用法是去过滤玩家的Pawn ,只允许某个特定类型的目标。参考Target Data Filters小节获取更多相关内容。 |
Reticle Class | [可选] TargetActor 所要创建的AGameplayAbilityWorldReticle 的派生类。 |
Reticle Parameters | [可选] 配置光标。参考Reticles的小结。 |
Start Location | 一个特定的结构体,其中包含了射线检测的开始位置的信息。通常会是玩家的视口、武器枪口,亦或是Pawn 的位置。 |
默认的TargetActor
类中,Actors
只有在被射线检测到或者碰撞检测到才是有效的目标。如果他们离开射线和碰撞检测的范围(他们移动了或者你的移开了视线),那么他们就不再有效。如果你希望TargetActor
记住之前的有效目标(们),那么你需要去自己实现自定义的TargetActor
类。我称其为永久目标,因为他们的存在会一直持续到TargetActor
接收到目标的确认和取消事件,或者是TargetActor
找到了一个新的有效目标,还有或者是目标不再有效(即其被销毁了)。GASShooter项目中将这种方式应用到火箭发射器的第二个技能上。
4.11.3 目标数据过滤器 - Target Data Filters
通过使用Make GameplayTargetDataFilter
和Make Filter Handle
节点,你可以过滤玩家的Pawn
或者只去选择某个特定的类。如果你需要更多高级的过滤效果,你可以在FGameplayTargetDataFilter
基础上进一步派生,并且重写其中的FilterPassesForActor
函数。
USTRUCT(BlueprintType)
struct GASDOCUMENTATION_API FGDNameTargetDataFilter : public FGameplayTargetDataFilter
{
GENERATED_BODY()
/** Returns true if the actor passes the filter and will be targeted */
virtual bool FilterPassesForActor(const AActor* ActorToBeFiltered) const override;
};
但是,这并不会直接令其在Wait Target Data
节点上生效,因为它还需要一个FGameplayTargetDataFilterHandle
。还需要再构建一个新的自定义的Make Filter Handle
来收扩展出来的派生类:
FGameplayTargetDataFilterHandle UGDTargetDataFilterBlueprintLibrary::MakeGDNameFilterHandle(FGDNameTargetDataFilter Filter, AActor* FilterActor)
{
FGameplayTargetDataFilter* NewFilter = new FGDNameTargetDataFilter(Filter);
NewFilter->InitializeFilterContext(FilterActor);
FGameplayTargetDataFilterHandle FilterHandle;
FilterHandle.Filter = TSharedPtr<FGameplayTargetDataFilter>(NewFilter);
return FilterHandle;
}
4.11.4 游戏技能的世界标线 - Gameplay Ability World Reticles
AGameplayAbilityWorldReticles
,即Reticles
,当你利用非Instant
确定出TargetActors
,它会帮你从视觉表现方面显示你当前正在选择的目标。TargetActors
会负责Reticles
从生成到销毁的整个生命周期。Reticles
本质上是AActors
,因此他们可以使用任意类型的可视化组件。GASShooter实现了一个它的常见的用法,即使用WidgetComponent
来在屏幕空间中显示一个UMG Widget类型的控件(总是朝向玩家摄像机)。Reticles
并不知道他们的目标是具体哪一个AActor
,但是你可以在自定义的TargetActor
进一步实现这个功能。TargetActors
通常会在每个Tick()
内去更新Reticle
的位置到目标的位置(译者注:目标位置的世界控空间位置转到屏幕空间的二维空间位置)。
GASShooter中使用Reticles
来显示被火箭发射器的第二个技能锁定的目标。敌人身上的红色的指示图标就是Reticle
。而相同样式的白色的那个则是火箭发射器的准星。
Reticles
提供了一些对设计者可能有用的BlueprintImplementableEvents
(他们就是被设计来在蓝图中进行拓展开发的):
/** Called whenever bIsTargetValid changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnValidTargetChanged(bool bNewValue);
/** Called whenever bIsTargetAnActor changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnTargetingAnActor(bool bNewValue);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnParametersInitialized();
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamFloat(FName ParamName, float value);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamVector(FName ParamName, FVector value);
Reticles
也可以使用由TargetActor
提供的FWorldReticleParameters
来进行配置。默认的结构体只提供了一个变量FVector AOEScale
。尽管你可以去派生这个结构体,但是TargetActor
只会去接收默认的基类结构体。从设计层面看,默认的TargetActor
在FWorldReticleParameters
上的限制可能看起来显得有些目光短浅。但是,如果你自定义了自己的TargetActor
,你也可以提供你自己自定义的标线参数结构体,然后在生成他们的时候手动将其传递给你的AGameplayAbilityWorldReticles
的派生类中。
Reticles
默认情况下不会被复制,但是如果你的游戏有类似的需求,比如给其他玩家显示本地玩家锁定的敌人是你的游戏的设计之一的话,也可以将复制的选项打开。
默认的TargetActors
中,Reticles
只会在当前有效的目标上显示。例如,如果你使用了一个AGameplayAbilityTargetActor_SingleLineTrace
来检测一个目标,那么Reticle
将只会在目标直接暴露在检测路径上时出现。如果你看向别的什么地方,那么此时敌人就不再是一个有效的目标,那么Reticle
就自动消失了。如果你希望Reticle
停留在最后一个有效目标身上,你就需要自定义你的TargetActor
,让其记住最后一个有效目标并保留Reticle
到目标身上。我建议是将他们作为永久目标,因为他们会一直存在直到TargetActor
接收到确认或者取消的指令,或者TargetActor
通过检测找到了一个新的有效目标,或者目标不再有效(被销毁了)。GASShooter项目中就应用这个方案到火箭发射器的第二个技能上了。
4.11.5 游戏效果容器的目标 - Gameplay Effect Containers Targeting
GameplayEffectContainers
中提供了一个可选的,高效的生成TargetData
的方法。在客户端和服务器上应用EffectContainer
时就会立马进行目标的选择。这个方法要远比TargetActors
的方式更加高效,因为它是直接运行在目标选择的对象的CDO(译者注:Class Default Object,可以理解为类的一个默认单例对象)上的,相对于需要去生成和销毁的Actors
对象来说当然更加高效。但是它会缺少玩家的输入,没有确认取消的过程,也不能够从客户端往服务器发送数据(因为它在两端都会执行)。对于立即触发的射线或者碰撞检测来说这非常有效。Epic的Action RPG Sample Project项目中,在其容器中包含了两种类型的目标选择——选择技能的释放者,以及从某个事件中获取到TargetData
。它也在蓝图中实现了一个去做球体的轨迹检测。你可以派生URPGTargetType
来制作你自己的目标类型。