4、GAS相关概念4.11 Targeting

4.11 Targeting

4.11.1 Target Data

FGameplayAbilityTargetData 是一个通用的目标数据结构,旨在跨网络传递。TargetData 通常会保存 AActor/UObject 引用、FHitResults 和其他通用的位置信息/方向信息/原点信息。然而,你可以将其子类化,以便将你想要的任何内容放入其中,作为在 GameplayAbilities 中在客户端和服务器之间传递数据的简单手段。基础结构 FGameplayAbilityTargetData 并不打算直接使用,而是子类化。GAS 随附一些开箱即用的子类化 FGameplayAbilityTargetData 结构,位于 GameplayAbilityTargetTypes.h 中。

TargetData 通常由 Target Actors 生成或 手动创建,并通过 EffectContextAbilityTasksGameplayEffects 消耗。由于在 EffectContext 中,ExecutionsMMCsGameplayCuesAttributeSet 后端的函数可以访问 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 会通过 WaitTargetDataAbilityTask 创建 TargetActors,以便可视化并从世界中捕捉目标信息。TargetActors 可以选择使用 GameplayAbilityWorldReticles 来显示当前的目标。在确认后,目标信息会作为 TargetData 返回,随后可以传递给 GameplayEffects

TargetActors 基于 AActor,因此可以拥有任何类型的可视组件来表示在哪里以及如何进行目标锁定,例如静态网格或贴花。静态网格可用于可视化角色将要建造的物体的位置。贴花可用于在地面上显示效果范围。例如,示例项目使用 AGameplayAbilityTargetActor_GroundTrace 和地面的贴花来表示流星技能的伤害范围。它们也不需要显示任何内容。例如,对于瞬时目标射击的武器(如 GASShooter 中的例子),显示任何内容都是没有意义的。

它们使用基本的追踪或碰撞重叠来捕捉目标信息,并根据 TargetActor 实现将结果转换为 FHitResultsAActor 数组,然后转换为 TargetDataWaitTargetDataAbilityTask 通过其 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() 以取消目标锁定。
CustomGameplayTargeting 能力负责通过调用 UGameplayAbility::ConfirmTaskByInstanceName() 来决定何时目标数据准备好。TargetActor 也会响应 UGameplayAbility::CancelTaskByInstanceName() 以取消目标锁定。
CustomMultiGameplayTargeting 能力负责通过调用 UGameplayAbility::ConfirmTaskByInstanceName() 来决定何时目标数据准备好。TargetActor 也会响应 UGameplayAbility::CancelTaskByInstanceName() 以取消目标锁定。数据生成后不应结束 AbilityTask

并不是每个 EGameplayTargetingConfirmation::Type 都支持每个 TargetActor。例如,AGameplayAbilityTargetActor_GroundTrace 不支持 Instant 确认。

WaitTargetDataAbilityTask 接受 AGameplayAbilityTargetActor 类作为参数,并且每次激活 AbilityTask 时都会生成一个实例,AbilityTask 结束时会销毁 TargetActorWaitTargetDataUsingActorAbilityTask 接受已经生成的 TargetActor,但在 AbilityTask 结束时仍会销毁它。无论是哪种 AbilityTask,它们都不是特别高效,因为每次使用时都需要生成一个新的 TargetActor 或者要求使用一个新的 TargetActor。它们适用于原型开发,但在生产环境中,如果您有不断生成 TargetData 的情况(例如自动步枪的情况),您可能需要优化它。GASShooter 中有一个 AGameplayAbilityTargetActor 的自定义子类和一个从头编写的 WaitTargetDataWithReusableActor AbilityTask,可以让您在不销毁的情况下重复使用 TargetActor

默认情况下,TargetActors 不会被复制;然而,如果您的游戏需要向其他玩家显示本地玩家的目标,可以使其复制。它们确实包括与服务器通过 WaitTargetDataAbilityTask 进行通信的默认功能。如果 TargetActorShouldProduceTargetDataOnServer 属性设置为 false,那么客户端会在确认时通过 CallServerSetReplicatedTargetData()TargetData 通过 RPC 发送给服务器,方法是在 UAbilityTask_WaitTargetData::OnTargetDataReadyCallback() 中。如果 ShouldProduceTargetDataOnServertrue,客户端会通过 EAbilityGenericReplicatedEvent::GenericConfirm 向服务器发送一个通用的确认事件 RPC,在收到该 RPC 后,服务器将执行追踪或重叠检查并生成服务器上的数据。如果客户端取消了目标锁定,它会通过 EAbilityGenericReplicatedEvent::GenericCancel 发送一个通用取消事件的 RPC。在 TargetActorWaitTargetData AbilityTask 上都有许多代理。TargetActor 响应输入以生成并广播目标数据准备、确认或取消代理。WaitTargetData 监听 TargetActor 的目标数据准备、确认和取消代理,并将这些信息反馈给 GameplayAbility 和服务器。如果您将 TargetData 发送到服务器,您可能需要在服务器上进行验证,以确保 TargetData 合理,以防作弊。直接在服务器上生成 TargetData 可以完全避免这个问题,但可能会导致拥有客户端的预测错误。

根据您使用的 AGameplayAbilityTargetActor 的具体子类,WaitTargetData AbilityTask 节点上会暴露不同的 ExposeOnSpawn 参数。一些常见的参数包括:

常见的 TargetActor 参数定义
Debug如果为 true,每当 TargetActor 执行追踪时(在非发布版本中),会绘制调试追踪/重叠信息。记住,非 InstantTargetActors 会在 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 GameplayTargetDataFilterMake 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(准星)

AGameplayAbilityWorldReticlesReticles)可视化在使用非 Instant 确认的 TargetActors 时正在锁定的目标。TargetActors 负责所有 Reticles 的生成和销毁生命周期。ReticlesAActor 类型,因此它们可以使用任何类型的视觉组件进行表示。在 GASShooter 中常见的实现是使用 WidgetComponent 来显示一个 UMG Widget,在屏幕空间中(始终面朝玩家的摄像机)。Reticles 不知道它们所在的 AActor,但您可以在自定义 TargetActor 上继承该功能。TargetActors 通常会在每个 Tick() 更新 Reticle 的位置到目标的位置。

GASShooter 使用 Reticles 来显示火箭发射器的次要能力的锁定目标的制导火箭。敌人上的红色指示器是 Reticle。类似的白色图像是火箭发射器的准星。 Reticles in GASShooter

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 来创建自己的目标类型。