4.11 Targeting
4.11.1 Target Data
FGameplayAbilityTargetData
是一个通用的目标数据结构,旨在跨网络传递。TargetData
通常会保存 AActor
/UObject
引用、FHitResults
和其他通用的位置信息/方向信息/原点信息。然而,你可以将其子类化,以便将你想要的任何内容放入其中,作为在 GameplayAbilities
中在客户端和服务器之间传递数据的简单手段。基础结构 FGameplayAbilityTargetData
并不打算直接使用,而是子类化。GAS
随附一些开箱即用的子类化 FGameplayAbilityTargetData
结构,位于 GameplayAbilityTargetTypes.h
中。
TargetData
通常由 Target Actors
生成或 手动创建,并通过 EffectContext
由 AbilityTasks
和 GameplayEffects
消耗。由于在 EffectContext
中,Executions
、MMCs
、GameplayCues
和 AttributeSet
后端的函数可以访问 TargetData
。
我们通常不直接传递 FGameplayAbilityTargetData
,而是使用 FGameplayAbilityTargetDataHandle
,其中包含一个指向 FGameplayAbilityTargetData
的 TArray 指针。这个中间结构提供了 TargetData
的多态性支持。
一个继承自 FGameplayAbilityTargetData
的例子:
USTRUCT(BlueprintType)
struct MYGAME_API FGameplayAbilityTargetData_CustomData : public FGameplayAbilityTargetData
{
GENERATED_BODY()
public:
FGameplayAbilityTargetData_CustomData()
{
}
UPROPERTY()
FName CoolName = NAME_None;
UPROPERTY()
FPredictionKey MyCoolPredictionKey;
// 这是所有 FGameplayAbilityTargetData 子结构所需的
virtual UScriptStruct* GetScriptStruct() const override
{
return FGameplayAbilityTargetData_CustomData::StaticStruct();
}
// 这是所有 FGameplayAbilityTargetData 子结构所需的
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
// 引擎已经为 FName 和 FPredictionKey 定义了 NetSerialize,感谢 Epic!
CoolName.NetSerialize(Ar, Map, bOutSuccess);
MyCoolPredictionKey.NetSerialize(Ar, Map, bOutSuccess);
bOutSuccess = true;
return true;
}
}
template<>
struct TStructOpsTypeTraits<FGameplayAbilityTargetData_CustomData> : public TStructOpsTypeTraitsBase2<FGameplayAbilityTargetData_CustomData>
{
enum
{
WithNetSerializer = true // 这是 FGameplayAbilityTargetDataHandle 网络序列化工作所需的
};
};
将目标数据添加到句柄的示例:
UFUNCTION(BlueprintPure)
FGameplayAbilityTargetDataHandle MakeTargetDataFromCustomName(const FName CustomName)
{
// 创建我们的目标数据类型,
// 句柄会在句柄销毁时自动清理并删除此数据,
// 如果你不将其添加到句柄中,则要小心,因为这涉及内存管理和内存泄漏,因此最好在某个帧中始终将其添加到句柄中!
FGameplayAbilityTargetData_CustomData* MyCustomData = new FGameplayAbilityTargetData_CustomData();
// 设置结构的信息以使用输入的名称以及我们可能希望进行的其他更改
MyCustomData->CoolName = CustomName;
// 为 Blueprint 使用制作我们的句柄包装器
FGameplayAbilityTargetDataHandle Handle;
// 将目标数据添加到我们的句柄中
Handle.Add(MyCustomData);
// 输出我们的句柄到 Blueprint
return Handle;
}
获取值时需要进行类型安全检查,因为从句柄的目标数据中获取值的唯一方法是使用泛型 C/C++ 转换,这是非类型安全的,可能导致对象切片和崩溃。对于类型检查,有多种方法(实际上你想怎么做就怎么做),两种常见的方法是:
- 游戏标签:你可以使用一个子类层次结构,当你知道任何时候某个代码结构的功能发生时,你可以为基础父类型进行转换并获取其游戏标签,然后与这些标签进行比较以进行继承类的转换。
- 脚本结构和静态结构:你可以直接进行类比较(这可能涉及很多 IF 语句或制作一些模板函数),下面是一个使用这些函数进行类型检查的示例:你可以从任何
FGameplayAbilityTargetData
获取脚本结构(这是它是USTRUCT
的一个好处,并要求任何继承类在GetScriptStruct
中指定结构类型),并比较它是否是你正在寻找的类型。下面是使用这些函数进行类型检查的示例:
UFUNCTION(BlueprintPure)
FName GetCoolNameFromTargetData(const FGameplayAbilityTargetDataHandle& Handle, const int Index)
{
// 注意,这里有两个版本的 '::Get(int32 Index)' 函数;
// 1)返回 'const FGameplayAbilityTargetData*' 的 const 版本,适用于读取目标数据值
// 2)返回 'FGameplayAbilityTargetData*' 的非 const 版本,适用于修改目标数据值
FGameplayAbilityTargetData* Data = Handle.Get(Index); // 这会为你有效检查索引
// 检查我们是否有东西可用,null 数据意味着没有要转换的内容
if (Data == nullptr)
{
return NAME_None;
}
// 这基本上是类型检查过程,static_cast 没有类型安全性,这就是为什么我们要进行此检查。
// 如果我们不这样做,那么它将切片结构对象,因此我们无法确保它是该类型。
if (Data->GetScriptStruct() == FGameplayAbilityTargetData_CustomData::StaticStruct())
{
// 这是你可以进行转换的地方,因为我们知道它已经是正确的类型
FGameplayAbilityTargetData_CustomData* CustomData = static_cast<FGameplayAbilityTargetData_CustomData*>(Data);
return CustomData->CoolName;
}
return NAME_None;
}
4.11.2 目标Actors (Target Actors)
GameplayAbilities
会通过 WaitTargetData
的 AbilityTask
创建 TargetActors
,以便可视化并从世界中捕捉目标信息。TargetActors
可以选择使用 GameplayAbilityWorldReticles
来显示当前的目标。在确认后,目标信息会作为 TargetData
返回,随后可以传递给 GameplayEffects
。
TargetActors
基于 AActor
,因此可以拥有任何类型的可视组件来表示在哪里以及如何进行目标锁定,例如静态网格或贴花。静态网格可用于可视化角色将要建造的物体的位置。贴花可用于在地面上显示效果范围。例如,示例项目使用 AGameplayAbilityTargetActor_GroundTrace
和地面的贴花来表示流星技能的伤害范围。它们也不需要显示任何内容。例如,对于瞬时目标射击的武器(如 GASShooter 中的例子),显示任何内容都是没有意义的。
它们使用基本的追踪或碰撞重叠来捕捉目标信息,并根据 TargetActor
实现将结果转换为 FHitResults
或 AActor
数组,然后转换为 TargetData
。WaitTargetData
的 AbilityTask
通过其 TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType
参数来确定何时确认目标。当不使用 TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant}
时,TargetActor
通常在 Tick()
中执行追踪/重叠,并根据其实现更新位置到 FHitResult
。虽然在 Tick()
中执行追踪/重叠非常响应客户端,但一般不会太糟糕,因为它不会复制并且通常一次只有一个(尽管可以有多个)TargetActor
在运行。只需注意,它使用 Tick()
,而一些复杂的 TargetActors
可能会做很多事情,比如在 GASShooter 中火箭发射器的副技能。尽管在 Tick()
中进行追踪响应速度很快,但如果性能影响太大,您可能会考虑降低 TargetActor
的 tick 率。对于 TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant}
,TargetActor
会立即生成、产生 TargetData
并销毁。Tick()
永远不会被调用。
EGameplayTargetingConfirmation::Type | 确认目标的时机 |
---|---|
Instant | 目标锁定立即发生,无需特殊逻辑或用户输入决定何时“发射”。 |
UserConfirmed | 当用户确认目标时(技能绑定到 Confirm 输入 或调用 UAbilitySystemComponent::TargetConfirm() ),目标锁定发生。TargetActor 也会响应绑定的 Cancel 输入或调用 UAbilitySystemComponent::TargetCancel() 以取消目标锁定。 |
Custom | GameplayTargeting 能力负责通过调用 UGameplayAbility::ConfirmTaskByInstanceName() 来决定何时目标数据准备好。TargetActor 也会响应 UGameplayAbility::CancelTaskByInstanceName() 以取消目标锁定。 |
CustomMulti | GameplayTargeting 能力负责通过调用 UGameplayAbility::ConfirmTaskByInstanceName() 来决定何时目标数据准备好。TargetActor 也会响应 UGameplayAbility::CancelTaskByInstanceName() 以取消目标锁定。数据生成后不应结束 AbilityTask 。 |
并不是每个 EGameplayTargetingConfirmation::Type
都支持每个 TargetActor
。例如,AGameplayAbilityTargetActor_GroundTrace
不支持 Instant
确认。
WaitTargetData
的 AbilityTask
接受 AGameplayAbilityTargetActor
类作为参数,并且每次激活 AbilityTask
时都会生成一个实例,AbilityTask
结束时会销毁 TargetActor
。WaitTargetDataUsingActor
的 AbilityTask
接受已经生成的 TargetActor
,但在 AbilityTask
结束时仍会销毁它。无论是哪种 AbilityTask
,它们都不是特别高效,因为每次使用时都需要生成一个新的 TargetActor
或者要求使用一个新的 TargetActor
。它们适用于原型开发,但在生产环境中,如果您有不断生成 TargetData
的情况(例如自动步枪的情况),您可能需要优化它。GASShooter 中有一个 AGameplayAbilityTargetActor
的自定义子类和一个从头编写的 WaitTargetDataWithReusableActor
AbilityTask
,可以让您在不销毁的情况下重复使用 TargetActor
。
默认情况下,TargetActors
不会被复制;然而,如果您的游戏需要向其他玩家显示本地玩家的目标,可以使其复制。它们确实包括与服务器通过 WaitTargetData
的 AbilityTask
进行通信的默认功能。如果 TargetActor
的 ShouldProduceTargetDataOnServer
属性设置为 false
,那么客户端会在确认时通过 CallServerSetReplicatedTargetData()
将 TargetData
通过 RPC 发送给服务器,方法是在 UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()
中。如果 ShouldProduceTargetDataOnServer
为 true
,客户端会通过 EAbilityGenericReplicatedEvent::GenericConfirm
向服务器发送一个通用的确认事件 RPC,在收到该 RPC 后,服务器将执行追踪或重叠检查并生成服务器上的数据。如果客户端取消了目标锁定,它会通过 EAbilityGenericReplicatedEvent::GenericCancel
发送一个通用取消事件的 RPC。在 TargetActor
和 WaitTargetData
AbilityTask
上都有许多代理。TargetActor
响应输入以生成并广播目标数据准备、确认或取消代理。WaitTargetData
监听 TargetActor
的目标数据准备、确认和取消代理,并将这些信息反馈给 GameplayAbility
和服务器。如果您将 TargetData
发送到服务器,您可能需要在服务器上进行验证,以确保 TargetData
合理,以防作弊。直接在服务器上生成 TargetData
可以完全避免这个问题,但可能会导致拥有客户端的预测错误。
根据您使用的 AGameplayAbilityTargetActor
的具体子类,WaitTargetData
AbilityTask
节点上会暴露不同的 ExposeOnSpawn
参数。一些常见的参数包括:
常见的 TargetActor 参数 | 定义 |
---|---|
Debug | 如果为 true ,每当 TargetActor 执行追踪时(在非发布版本中),会绘制调试追踪/重叠信息。记住,非 Instant 的 TargetActors 会在 Tick() 中执行追踪,因此这些调试绘制调用也会在 Tick() 中发生。 |
过滤(Filter) | [可选] 用于在追踪/重叠时过滤掉(移除)目标的特殊结构体。常见用法包括过滤掉玩家的 Pawn ,或者要求目标是特定类。有关更多高级用法,请参见 目标数据过滤器。 |
Reticle Class | [可选] TargetActor 将生成的 AGameplayAbilityWorldReticle 的子类。 |
Reticle Parameters | [可选] 配置您的 Reticles。请参见 Reticles。 |
起始位置(Start Location) | 一个特殊结构体,用于定义追踪的起始位置。通常,这将是玩家的视点、武器枪口或 Pawn 的位置。 |
对于默认的TargetActor
类,Actors
只有在直接处于跟踪/重叠中时才是有效目标。如果它们离开跟踪/重叠(它们移动或您移开视线),它们就不再有效。如果您希望TargetActor
记住最后一个有效目标,则需要将此功能添加到自定义TargetActor
类中。我将这些称为持久目标,因为它们将一直存在,直到TargetActor
收到确认或取消,TargetActor
在其跟踪/重叠中找到新的有效目标,或者目标不再有效(被破坏)。GASShooter
使用持久目标作为其火箭发射器辅助能力的制导火箭瞄准。
4.11.3 TargetData过滤器
使用 Make GameplayTargetDataFilter
和 Make Filter Handle
节点,您可以过滤玩家的 Pawn
或仅选择特定的类。如果需要更高级的过滤,您可以继承 FGameplayTargetDataFilter
并重写 FilterPassesForActor
函数。
USTRUCT(BlueprintType)
struct GASDOCUMENTATION_API FGDNameTargetDataFilter : public FGameplayTargetDataFilter
{
GENERATED_BODY()
/** 如果演员通过过滤并将成为目标,则返回true */
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
是 AActor
类型,因此它们可以使用任何类型的视觉组件进行表示。在 GASShooter 中常见的实现是使用 WidgetComponent
来显示一个 UMG Widget,在屏幕空间中(始终面朝玩家的摄像机)。Reticles
不知道它们所在的 AActor
,但您可以在自定义 TargetActor
上继承该功能。TargetActors
通常会在每个 Tick()
更新 Reticle
的位置到目标的位置。
GASShooter 使用 Reticles
来显示火箭发射器的次要能力的锁定目标的制导火箭。敌人上的红色指示器是 Reticle
。类似的白色图像是火箭发射器的准星。
Reticles
配备了一些 BlueprintImplementableEvents
,供设计师使用(这些事件旨在通过蓝图开发):
/** 每当 bIsTargetValid 的值发生变化时调用。 */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnValidTargetChanged(bool bNewValue);
/** 每当 bIsTargetAnActor 的值发生变化时调用。 */
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
可以选择使用 FWorldReticleParameters
来进行配置,TargetActor
提供此配置。默认的结构体只提供一个变量 FVector AOEScale
。尽管您可以在技术上继承这个结构体,但 TargetActor
仅接受基本结构体。虽然这限制了默认 TargetActors
的功能,但如果您创建自己的自定义 TargetActor
,您可以提供自定义的准星参数结构体,并在生成时手动传递给您的 AGameplayAbilityWorldReticles
子类。
Reticles
默认情况下不会进行复制,但如果您的游戏需要向其他玩家显示本地玩家正在锁定的目标,可以让其进行复制。
默认的 TargetActors
只会在当前有效的目标上显示 Reticle
。例如,如果您使用 AGameplayAbilityTargetActor_SingleLineTrace
来跟踪目标,则只有当敌人正好位于追踪路径中时,Reticle
才会显示。如果您转移视线,敌人就不再是有效目标,Reticle
会消失。如果您希望 Reticle
始终停留在最后一个有效目标上,您需要自定义 TargetActor
以记住最后一个有效目标,并保持 Reticle
在它们身上。我称之为“持久目标”,因为它们会持续存在,直到 TargetActor
收到确认或取消,或者 TargetActor
在其追踪/重叠中找到新的有效目标,或者目标不再有效(被销毁)。GASShooter 使用持久目标来锁定火箭发射器的次要能力制导火箭的目标。
4.11.5 Gameplay Effect Containers Targeting
GameplayEffectContainers
提供了一种可选的高效方式来生成 TargetData
。当客户端和服务器应用 EffectContainer
时,目标选择会立即发生。它比 TargetActors
更高效,因为它在目标对象的 CDO 上运行(无需生成和销毁 Actors
),但它没有玩家输入,立即发生无需确认,不能取消,也不能从客户端向服务器发送数据(它在客户端和服务器上都会生成数据)。它非常适合用于即时的跟踪和碰撞重叠。Epic 的 Action RPG Sample Project 提供了使用其容器的两种目标选择示例——目标为能力拥有者并从事件中提取 TargetData
。它还通过蓝图实现了一个示例,能够在从玩家的偏移量(由子蓝图类设置)执行即时球形追踪时生成目标数据。您可以在 C++ 或蓝图中继承 URPGTargetType
来创建自己的目标类型。