BlueRoseNote/03-UnrealEngine/Gameplay/GAS/(11)GAS的TargetData与TargetActor相关机制.md
2023-06-29 11:55:02 +08:00

11 KiB
Raw Permalink Blame History

前言

TargetActor是GAS用于获取场景中物体、空间、位移等数据的机制同时也可以用于制作可视化debug工具。,所以非常有必要掌握它。

一般流程为使用WaitTargetData_AbilityTask生成TargetActor之后通过TargetActor的内部函数或者射线获取场景信息最后通过委托传递携带这些信息构建的FGameplayAbilityTargetDataHandle。

本文部分描述摘自GASDocumentation_Chinese翻译的还不错也请大家给此项目点赞。

TargetData

TargetData也就是FGameplayAbilityTargetData是用于通过网络传输定位数据的通用结构体。它主要用于存储目标数据(一般是TArray<TWeakObjectPtr >)、FHitResult。当然可以传递一些自定义数据这个可以参考源码中的FGameplayAbilityTargetData_LocationInfo。

TargetData一般由TargetActor或者手动创建(较少), 供AbilityTask或者GameplayEffect通过EffectContext使用. 因为其位于EffectContext中,所以Execution,MMC,GameplayCue和AttributeSet的后处理函数都可以访问该TargetData.

GAS不会直接传递数据而是会借助FGameplayAbilityTargetDataHandle来进行传递。具体过程是

  1. 创建TargetData并且填充数据。
  2. 创建FGameplayAbilityTargetDataHandle对象也可以使用带参的构造函数直接构建并且调用Add()添加上面创建的TargetData。
  3. 进行传递。

源代码中GameplayAbilityTargetTypes.h中实现了以下TargetData

  • FGameplayAbilityTargetData_LocationInfo
  • FGameplayAbilityTargetData_ActorArray
  • FGameplayAbilityTargetData_SingleTargetHit

基本上是够用了如果需要创建新的TargetData类型就需要视携带的数据类型实现以下虚函数

virtual TArray<TWeakObjectPtr<AActor>> GetActors() const;
virtual bool SetActors(TArray<TWeakObjectPtr<AActor>> NewActorArray);

virtual bool HasHitResult() const;
virtual const FHitResult* GetHitResult();
virtual void ReplaceHitWith(AActor* NewHitActor, const FHitResult* NewHitResult)

virtual bool HasOrigin() const;
virtual FTransform GetOrigin() const;

virtual bool HasEndPoint() const;
virtual FVector GetEndPoint() const;

virtual FTransform GetEndPointTransform() const;

/** See notes on delegate definition FOnTargetActorSwapped */
virtual bool ShouldCheckForTargetActorSwap() const;

debug相关虚函数

/** Returns the serialization data, must always be overridden */
virtual UScriptStruct* GetScriptStruct() const
{
	return FGameplayAbilityTargetData::StaticStruct();
}

/** Returns a debug string representation */
virtual FString ToString() const;

源代码在实现完类型后,还有附带下面这一段代码,看注释应该和网络同步序列化有关,反正依瓢画葫芦复制+替换类型名称即可。

template<>
struct TStructOpsTypeTraits<FGameplayAbilityTargetData_ActorArray> : public TStructOpsTypeTraitsBase2<FGameplayAbilityTargetData_ActorArray>
{
	enum
	{
		WithNetSerializer = true	// For now this is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work
	};
};

TargetActor

你可以把TargetActor理解为一个场景信息探测器用来获取场景中数据以及进行可视化Debug。一般都是在Ability中通过AbilityTask_WaitTargetData来生成TargetActor(WaitTargetDataUsingActor用来监听已有的TargetActor)之后再通过ValidData委托接收TargetData。

GameplayAbilityWorldReticles

当使用non-Instant TargetActor定位时TargetActor可以选择使用ReticleActor(GameplayAbilityWorldReticles)标注当前目标。

默认情况下Reticle只会显示在TargetActor的当前有效Target上,如果想要让其显示在其他目标上就需要自定义TargetActor手动管理确定与取消事件让目标持久化。

ReticleActor可以通过FWorldReticleParameters进行初始化(在TargetActor设置FWorldReticleParameters变量)但FWorldReticleParameters只有一个AOEScale变量很明显完全不够用。所以你可以通过自定义ActorTarget与参数结构来改进这个功能。

Reticle默认是不可同步的,但是如果你想向其他玩家展示本地玩家正在定位的目标,那么它也可以被设置为可同步的。

ReticleActor还带有一些面向蓝图的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);

GAS默认的UAbilityTask_WaitTargetData节点会在FinalizeTargetActor()中调用AGameplayAbilityTargetActor_Trace::StartTargeting(),进行ReticleActor的Spawn。

GASShooter中的逻辑为

  1. GA_RocketLauncherSecondaryAAset在Configure节点中指定为ReticleClass为BP_SingleTargetReticle。
  2. 在UGSAT_WaitTargetDataUsingActor::FinalizeTargetActor()中调用StartTargeting()
  3. 最后会在AGSGATA_Trace(GASShooter中定义的TargetActor的基类)的StartTargeting()中进行Spawn。

GASShooter中的实现了BP_SingleTargetReticle大致实现为

  1. 在根组件下连接一个WidgetComponent
  2. 使用Widget3DPassThrough_Masked_OneSided材质并且绑定一个UI_TargetReticle UMGAsset

GameplayEffectContext

可以认为这是一个传递数据用的结构体。GameplayEffectContext结构体存有关于GameplayEffectSpec创建者(Instigator)和TargetData的信息,可以向ModifierMagnitudeCalculation/GameplayEffectExecutionCalculation, AttributeSet和GameplayCue之间传递任意数据.

其他案例: https://www.thegames.dev/?p=62

使用方法:

  1. 继承FGameplayEffectContext。
  2. 重写FGameplayEffectContext::GetScriptStruct()。
  3. 重写FGameplayEffectContext::Duplicate()。
  4. 如果新数据需要同步的话, 重写FGameplayEffectContext::NetSerialize()。
  5. 对子结构体实现TStructOpsTypeTraits, 就像父结构体FGameplayEffectContext有的那样.
  6. 在AbilitySystemGlobals类中重写AllocGameplayEffectContext()以返回一个新的子结构体对象。(AbilitySystemGlobals还需要注册请看下节)
FGameplayEffectContext* UGSAbilitySystemGlobals::AllocGameplayEffectContext() const
{
	return new FGSGameplayEffectContext();
}

在ExecutionCalculation中,你可以通过FGameplayEffectCustomExecutionParameters获取FGameplayEffectContext

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(Spec.GetContext().Get());

在GameplayEffectSpec中获取EffectContext:

FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(MutableSpec->GetContext().Get());

在ExecutionCalculation中修改GameplayEffectSpec时要小心.参看GetOwningSpecForPreExecuteMod()的注释.

/** Non const access. Be careful with this, especially when modifying a spec after attribute capture. */
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;

PS.GASShooter实现了带有FGameplayAbilityTargetDataHandle变量的FGSGameplayEffectContext以此来实现在GameplayCue中访问TargetData, 特别是对于霰弹枪来说十分有用, 因为它可以击打多个敌人并且产生效果。

InitGlobalData()

从UE 4.24开始, 必须调用UAbilitySystemGlobals::InitGlobalData()来使用TargetData, 否则你会遇到关于ScriptStructCache的错误, 并且客户端会从服务端断开连接, 该函数只需要在项目中调用一次. Fortnite从AssetManager类的起始加载函数中调用该函数, Paragon是从UEngine::Init()中调用的. 我发现将其放到UEngineSubsystem::Initialize()是个好位置, 这也是样例项目中使用的. 我觉得你应该复制这段模板代码到你自己的项目中以避免出现TargetData的使用问题.

如果你在使用AbilitySystemGlobals GlobalAttributeSetDefaultsTableNames时发生崩溃, 可能之后需要像Fortnite一样在AssetManager或GameInstance中调用UAbilitySystemGlobals::InitGlobalData()而不是在UEngineSubsystem::Initialize()中. 该崩溃可能是由于Subsystem的加载顺序引发的, GlobalAttributeDefaultsTables需要加载EditorSubsystem来绑定UAbilitySystemGlobals::InitGlobalData()中的委托.

上面说的意思是初始化需要通过Subsystem与其节点来进行初始化这样就不需要通过继承来实现GASDocument推荐在UEngineSubsystem(UEngine阶段)初始化时执行。下面是自定义AbilitySystemGlobals的注册方式。

UAbilitySystemGlobals::Get().InitGlobalData();

可以参考UGSEngineSubsystem。

AbilitySystemGlobals类保存有关GAS的全局信息. 大多数变量可以在DefaultGame.ini中设置. 一般你不需要和该类互动, 但是应该知道它的存在. 如果你需要继承像GameplayCueManager或GameplayEffectContext这样的对象, 就必须通过AbilitySystemGlobals来做.

想要继承AbilitySystemGlobals, 需要在DefaultGame.ini中设置类名:

[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"

RPGTargetType

这是GameplayEffectContainer提供的一种方便的产生TargetData方法。但因为使用CDO(Class Default Object)运行所以不支持用户输入与取消等功能功能上也不如TargetActor多。在官方的ActionRPG可以看到演示。这些Target类型定义于URPGTargetType你可以根据获取Target的方式进行拓展调用TargetType获取目标的逻辑位于MakeEffectContainerSpecFromContainer

if (Container.TargetType.Get())
{
	TArray<FHitResult> HitResults;
	TArray<AActor *> TargetActors;
	const URPGTargetType *TargetTypeCDO = Container.TargetType.GetDefaultObject();
	AActor *AvatarActor = GetAvatarActorFromActorInfo();
	TargetTypeCDO->GetTargets(OwningCharacter, AvatarActor, EventData, HitResults, TargetActors);
	ReturnSpec.AddTargets(HitResults, TargetActors);
}

ActionRPG在c++中实现了UseOwner与UseEventData类型在蓝图中实现了SphereTrace与LineTrace。

GASShooter中的用法

GASShooter在以下Asset中使用了TargetData与TargetActor

  • GA_RiflePrimaryInstant
  • GA_RocketLauncherPrimaryInstant
  • GA_RocketLauncherPrimaryInstant

GASShooter中实现了AGSGATA_LineTrace与AGSGATA_SphereTrace等AGameplayAbilityTargetActor拥有共同的父类AGSGATA_Trace

步骤: 在AbilityActivate中

  1. 类内指定AGSGATA_LineTrace对象作为变量。
  2. 调用AGSGATA_LineTrace对象的ResetSpread(),初始化参数。
  3. 调用UGSAT_ServerWaitForClientTargetData并将ValidData委托绑定HandleTargetData。
  4. 之后执行客户端逻辑。
  5. 调用AGSGATA_LineTrace对象的Configure(),来设置具体参数。
  6. 将AGSGATA_LineTrace对象传入UGSAT_WaitTargetDataUsingActor并将ValidData委托绑定HandleTargetData。