Gameplay Targeting System C++ API
Gameplay Targeting System C++ API
本文是 Gameplay Targeting System 系列 的第三篇。
涵盖:模块依赖、核心数据结构、
UTargetingSubsystemAPI、自定义 Task 写法、同步 vs 异步执行细节。
1. 模块依赖
在自己模块的 Build.cs 中加入对 TargetingSystem 的依赖:
PrivateDependencyModuleNames.AddRange(new[]
{
"TargetingSystem",
// 还要用到的:
"GameplayAbilities", "GameplayTags", "GameplayTasks",
});
常用头文件:
#include "TargetingSystem/TargetingSubsystem.h" // 子系统主入口
#include "Types/TargetingSystemTypes.h" // 核心数据结构
// 框架层扩展:
#include "Targeting/FrameworkTargetingSourceInterface.h"
2. 核心数据结构
2.1 FTargetingRequestHandle:请求句柄
USTRUCT(BlueprintType)
struct FTargetingRequestHandle
{
// 所有目标定位请求的唯一标识符。
// 所有数据存储(结果、上下文、任务状态)均以此为 Key。
// 必须显式释放(ReleaseTargetRequestHandle / bReleaseOnCompletion),
// 否则旁挂数据不清理。
uint32 Handle;
FORCEINLINE bool IsValid() const;
void Reset();
};
2.2 FTargetingSourceContext:源上下文
USTRUCT(BlueprintType)
struct FTargetingSourceContext
{
UPROPERTY(BlueprintReadWrite)
TObjectPtr<AActor> SourceActor = nullptr; // 谁发起
UPROPERTY(BlueprintReadWrite)
TObjectPtr<AActor> InstigatorActor = nullptr; // 最终发起者(例:发射子弹的玩家)
UPROPERTY(BlueprintReadWrite)
FVector SourceLocation = FVector::ZeroVector; // 可覆盖 SourceActor 位置
UPROPERTY(BlueprintReadWrite)
FName SourceSocketName = NAME_None; // 起点骨骼插槽
UPROPERTY(BlueprintReadWrite)
TObjectPtr<UObject> SourceObject = nullptr; // 自定义附加数据
};
SourceActor 不能为空,否则后续 Task 拿不到位置;InstigatorActor 在投射物场景下要指向发射者,否则队伍判定容易把"主人的友军"误打成敌人。
2.3 FTargetingDefaultResultData:单条结果
USTRUCT(BlueprintType)
struct FTargetingDefaultResultData
{
UPROPERTY(BlueprintReadOnly)
FHitResult HitResult; // 命中(Actor*、Component*、位置、法线等)
UPROPERTY(BlueprintReadOnly)
float Score = 0.0f; // 排序分数
};
2.4 FTargetingImmediateTaskData:同步请求控制
USTRUCT()
struct FTargetingImmediateTaskData
{
uint8 bReleaseOnCompletion : 1;
static FTargetingImmediateTaskData& FindOrAdd(FTargetingRequestHandle Handle);
};
bReleaseOnCompletion = true 等于"用完就释放",几乎是所有"一次性"请求的默认选择。
3. UTargetingSubsystem 常用 API
3.1 获取子系统
UTargetingSubsystem* TS = UTargetingSubsystem::Get(GetWorld());
// 或者:
UTargetingSubsystem* TS = UTargetingSubsystem::GetTargetingSubsystem(this);
3.2 创建请求句柄
// 推荐:创建句柄并绑定 Preset + SourceContext
FTargetingRequestHandle Handle =
UTargetingSubsystem::MakeTargetRequestHandle(TargetingPreset, SourceContext);
// 空白句柄(很少用)
FTargetingRequestHandle Handle = UTargetingSubsystem::CreateTargetRequestHandle();
3.3 同步执行
// 用 C++ 委托回调
TargetingSubsystem->ExecuteTargetingRequestWithHandle(Handle, CompletionDelegate);
// 用蓝图动态委托回调
TargetingSubsystem->ExecuteTargetingRequest(TargetingPreset, SourceContext, DynamicDelegate);
3.4 异步执行
FTargetingRequestHandle Handle = TargetingSubsystem->StartAsyncTargetingRequest(
TargetingPreset, SourceContext, DynamicDelegate);
// C++ 委托版本
TargetingSubsystem->StartAsyncTargetingRequestWithHandle(Handle, CppDelegate);
// 取消
TargetingSubsystem->RemoveAsyncTargetingRequestWithHandle(Handle);
3.5 读取结果
TArray<AActor*> TargetActors;
TargetingSubsystem->GetTargetingResultsActors(Handle, TargetActors);
TArray<FHitResult> HitResults;
TargetingSubsystem->GetTargetingResults(Handle, HitResults);
FTargetingSourceContext Context = TargetingSubsystem->GetTargetingSourceContext(Handle);
3.6 释放句柄
UTargetingSubsystem::ReleaseTargetRequestHandle(Handle);
4. Task 类速览
4.1 选择任务
UTargetingSelectionTask_Trace:射线/扫描(Line/Sphere/Capsule/Box),所有可"沿方向打过去"的场景。UTargetingSelectionTask_AOE:在某点做几何重叠检测(Box/Cylinder/Sphere/Capsule/SourceComponent)。UTargetingSelectionTask_SourceActor:直接把SourceActor加进结果(用来做"自我"目标)。USimpleTargetingSelectionTask:蓝图基类,覆盖SelectTargets事件即可写自定义逻辑。
4.2 过滤任务
UTargetingFilterTask_BasicFilterTemplate:过滤基类。子类只需要覆盖ShouldFilterTarget()。UTargetingFilterTask_ActorClass:白/黑名单类型过滤。USimpleTargetingFilterTask:蓝图过滤基类,事件ShouldRemoveTarget。
执行逻辑就是一个标准的 in-place 过滤:
For each target in ResultsSet:
if ShouldFilterTarget(Handle, TargetData) == true:
Remove from ResultsSet
4.3 排序任务
UTargetingSortTask_Base:基类,子类覆盖GetScoreForTarget(),最后按bAscending排。UTargetingFilterTask_SortByDistance:开箱即用的距离排序,bUseDistanceToNearestBlockingCollider为 true 时按碰撞边缘距离(更精确但更贵)。USimpleTargetingSortTask:蓝图排序基类。
5. 框架层扩展任务的关键点
下面这些是常见的"在原生 Task 基础上加一层壳"的扩展,每个项目里实现会略有差异,但接口和含义大体一致。
5.1 UFW_TargetingFilterTask_BasicFilterTemplate
相对引擎原生过滤基类多了两件事:
UPROPERTY(EditAnywhere)
uint8 bInvert : 1;
// 反转过滤逻辑:false = 过滤掉不满足条件的(默认);true = 过滤掉满足条件的
UFUNCTION(BlueprintNativeEvent)
bool ShouldRetainTarget(const FTargetingRequestHandle& Handle,
const FTargetingDefaultResultData& TargetData) const;
// 改用"白名单"语义:返回 true = 保留此目标
被踢掉的目标通常会被记录在一个 FFW_Targeting_FilteredOutData 结构里,方便调试 Preset 配置。
5.2 UFW_TargetingFilterTask_Distance
UPROPERTY(EditAnywhere) FScalableFloat MinDistance;
UPROPERTY(EditAnywhere) FScalableFloat MaxDistance;
UPROPERTY(EditAnywhere) FVector XYZMask; // (1,1,0) 只算 XY 平面距离
5.3 UFW_TargetingFilterTask_GenericTeamAttitude
基于 IGenericTeamAgentInterface:
UPROPERTY(EditAnywhere) FAISenseAffiliationFilter DetectionByAffiliation;
UPROPERTY(EditAnywhere) uint8 bLookController : 1;
UPROPERTY(EditAnywhere) uint8 bUseInstigator : 1;
bLookController = true:玩家角色队伍 ID 一般挂在 Controller 上,必须打开。bUseInstigator = true:投射物场景下用,因为 SourceActor 是飞行物自己。
5.4 UFW_TargetingFilterTask_TagQuery
UPROPERTY(EditAnywhere) FGameplayTagQuery TagQuery;
UPROPERTY(EditAnywhere) uint8 bLookingForTagAssetInterface : 1;
默认从角色身上的 ASC 取 Tag;非 ASC 对象勾上 bLookingForTagAssetInterface 走 IGameplayTagAssetInterface。
5.5 UFW_TargetingFilterTask_Obstacle / _TraceObstacle
视线检测两种实现:线性 vs 球形扫描。bDebug 开发期建议开。
5.6 UFW_TargetingSelectionTask_TraceExt
在原生 Trace 上加等级缩放与接口动态参数:
uint8 bUseContextLocationAsSourceLocation : 1;
FScalableFloat MultipleOnContextTraceLevel;
FScalableFloat AddOnContextTraceLevel;
uint8 bTraceLengthLevel : 1;
uint8 bSweptTraceRadiusLevel : 1;
参数从 IFrameworkTargetingSourceInterface 实例化时取,可以让飞行物等 Actor 在运行时按等级、按状态动态决定 trace 长度/半径,而不是死写在 DataAsset 里。
5.7 UFW_TargetingSortTask_SortByUtilityScore
把多个 UFW_Targeting_UtilityScoreFragment 的分数加权累加,得到最终排序分数。片段基类提供常用的归一化/反向归一化/钳制开关:
float Weight = 1.0f;
uint8 bClamp : 1; float ClampMin, ClampMax;
uint8 bNormalize : 1;
uint8 bReverseNormalize : 1;
uint8 bPostNormalize : 1;
uint8 bReversePostNormalize : 1;
距离片段示例(近距离更高分):
[0] UFW_Targeting_UtilityScoreFragment_Distance
Weight = 1.0
bNormalize = true
bReverseNormalize = true
6. IFrameworkTargetingSourceInterface
让 Actor 在运行时向 Task 提供追踪参数,避免在 DataAsset 里写死。投射物之类的 Actor 实现这个接口后,TraceExt 任务可以走 bUseContextLocationAsSourceLocation、bTraceLengthLevel 等开关动态取参数。
class IFrameworkTargetingSourceInterface
{
public:
UFUNCTION(BlueprintNativeEvent) bool GetTraceLevel(float& OutLevel) const;
UFUNCTION(BlueprintNativeEvent) bool GetTraceDirection(FVector& OutDirection) const;
UFUNCTION(BlueprintNativeEvent) bool GetSweptTraceRotation(FRotator& OutRotation) const;
UFUNCTION(BlueprintNativeEvent) bool GetTraceShape(UShapeComponent*& OutShapeComponent) const;
UFUNCTION(BlueprintNativeEvent) void GetAdditionalActorsToIgnore(TArray<AActor*>& OutActors) const;
UFUNCTION(BlueprintNativeEvent) bool GetSourceLocation(FVector& OutLocation) const;
UFUNCTION(BlueprintNativeEvent) bool GetSweptTraceBoxHalfExtents(FVector& OutHalfExtents) const;
UFUNCTION(BlueprintNativeEvent) bool GetSweptTraceScale(FVector& OutScale) const;
UFUNCTION(BlueprintNativeEvent) bool GetTraceLength(const FTargetingSourceContext& SourceContext,
float& OutLength) const;
UFUNCTION(BlueprintNativeEvent) bool GetSweptTraceRadius(float& OutRadius) const;
};
投射物里典型实现就是把数据资产里配的 TraceLength / AOERadius 返回出去:
bool AProjectile::GetSweptTraceRadius_Implementation(float& OutRadius) const
{
if (FlightAsset && FlightAsset->CollisionTraceInterfaceParam.AOERadius > 0.f)
{
OutRadius = FlightAsset->CollisionTraceInterfaceParam.AOERadius;
return true;
}
return false;
}
bool AProjectile::GetTraceLength_Implementation(const FTargetingSourceContext& SourceContext,
float& OutLength) const
{
if (FlightAsset && FlightAsset->CollisionTraceInterfaceParam.TraceLength > 0.f)
{
OutLength = FlightAsset->CollisionTraceInterfaceParam.TraceLength;
return true;
}
return false;
}
7. 端到端使用示例
7.1 投射物一次性命中(同步)
void AProjectile::ExecuteOneTimeTargeting()
{
// 注意:示例为简洁起见用同步加载,生产环境请走异步加载或预加载流程
if (UTargetingPreset* TP = FlightAsset->OneTimeTargetingPreset.LoadSynchronous())
{
UTargetingSubsystem* TargetingSubsystem = UTargetingSubsystem::Get(GetWorld());
if (!TargetingSubsystem) return;
FTargetingSourceContext SourceContext;
SourceContext.SourceActor = this; // 投射物自身
SourceContext.InstigatorActor = MasterActor; // 发射者
SourceContext.SourceObject = this;
FTargetingRequestHandle Handle =
UTargetingSubsystem::MakeTargetRequestHandle(TP, SourceContext);
// 弱引用 Lambda:防止 Actor 在异步加载/调度过程中被 GC 后野指针
FTargetingRequestDelegate Delegate = FTargetingRequestDelegate::CreateWeakLambda(
this,
[this, TargetingSubsystem](FTargetingRequestHandle InHandle)
{
TArray<FHitResult> Results;
TargetingSubsystem->GetTargetingResults(InHandle, Results);
for (const FHitResult& Hit : Results)
{
if (AActor* HitActor = Hit.GetActor())
{
FlightOnHit(HitActor, GenerateHitParam(Hit));
}
}
});
FTargetingImmediateTaskData& ImmediateData =
FTargetingImmediateTaskData::FindOrAdd(Handle);
ImmediateData.bReleaseOnCompletion = true;
TargetingSubsystem->ExecuteTargetingRequestWithHandle(Handle, Delegate);
}
}
7.2 近战目标组件:持续轮询
void UMeleeTargetingComponent::TickComponent(float DeltaTime,
ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
UpdateTimer += DeltaTime;
if (UpdateTimer >= UpdateTargetActorInterval)
{
UpdateTimer = 0.f;
InternalUpdateTargetActor();
}
}
void UMeleeTargetingComponent::InternalUpdateTargetActor()
{
UTargetingSubsystem* TS = UTargetingSubsystem::Get(GetWorld());
if (!TS || !TargetingPreset_FindTargetActors) return;
AActor* OwnerActor = GetOwner();
FTargetingSourceContext SourceContext;
SourceContext.SourceActor = OwnerActor;
SourceContext.InstigatorActor = OwnerActor;
FTargetingRequestHandle Handle =
UTargetingSubsystem::MakeTargetRequestHandle(
TargetingPreset_FindTargetActors, SourceContext);
FTargetingRequestDelegate Delegate = FTargetingRequestDelegate::CreateWeakLambda(
this,
[this, TS](FTargetingRequestHandle InHandle)
{
TArray<AActor*> Results;
TS->GetTargetingResultsActors(InHandle, Results);
AActor* NewTarget = Results.IsEmpty() ? nullptr : Results[0];
InternalSetCurrentTargetActor(NewTarget);
});
FTargetingImmediateTaskData& Data = FTargetingImmediateTaskData::FindOrAdd(Handle);
Data.bReleaseOnCompletion = true;
TS->ExecuteTargetingRequestWithHandle(Handle, Delegate);
}
7.3 目标强制覆盖(CAS 处决)
某些过场/处决动画希望临时锁定一个特定 Actor,而不再走 Preset。常见做法是给目标组件加一个"覆盖栈":
void UMyAbility::ForceTargetOnCAS()
{
if (auto* TC = AvatarChar->FindComponentByClass<UMeleeTargetingComponent>())
{
FGameplayTag OverrideTag = TAG_Combat_CASTargetOverride;
TC->RequestTargetOverride(OverrideTag, CASTargetActor);
}
}
void UMyAbility::OnAbilityEnd()
{
if (auto* TC = AvatarChar->FindComponentByClass<UMeleeTargetingComponent>())
{
TC->RemoveTargetOverride(TAG_Combat_CASTargetOverride);
}
}
按"优先级 Tag"维护一个覆盖栈,能力开始时 push、结束时 pop,这样多层覆盖(处决 > 锁定)之间互不污染。
8. 写一个自定义 Task
8.1 自定义选择任务
UCLASS(Blueprintable)
class GAME_API UTargetingSelectionTask_RegisteredEnemies : public UTargetingTask
{
GENERATED_BODY()
public:
virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override
{
const FTargetingSourceContext* SourceContext =
FTargetingSourceContext::Find(TargetingHandle);
if (!SourceContext) return;
UWorld* World = GetSourceContextWorld(TargetingHandle);
if (auto* GameMode = Cast<AGameMode>(World->GetAuthGameMode()))
{
FTargetingDefaultResultsSet& ResultsSet =
FTargetingDefaultResultsSet::FindOrAdd(TargetingHandle);
for (AActor* Enemy : GameMode->GetRegisteredEnemies())
{
if (Enemy && Enemy != SourceContext->SourceActor)
{
FTargetingDefaultResultData& Data =
ResultsSet.TargetResults.AddDefaulted_GetRef();
Data.HitResult.HitObjectHandle = FActorInstanceHandle(Enemy);
Data.HitResult.Location = Enemy->GetActorLocation();
}
}
}
}
};
8.2 自定义过滤任务
UCLASS(Blueprintable)
class GAME_API UTargetingFilterTask_NotDead
: public UFW_TargetingFilterTask_BasicFilterTemplate
{
GENERATED_BODY()
protected:
virtual bool ShouldRetainTarget(const FTargetingRequestHandle& Handle,
const FTargetingDefaultResultData& TargetData) const override
{
AActor* Target = TargetData.HitResult.GetActor();
if (!Target) return false;
if (IAbilitySystemInterface* ASI = Cast<IAbilitySystemInterface>(Target))
{
if (UAbilitySystemComponent* ASC = ASI->GetAbilitySystemComponent())
{
if (ASC->HasMatchingGameplayTag(DeadTag))
return false;
}
}
return true;
}
};
8.3 自定义排序任务(按残血优先)
UCLASS(Blueprintable)
class GAME_API UTargetingSortTask_ByHealthPercent : public UTargetingSortTask_Base
{
GENERATED_BODY()
protected:
virtual float GetScoreForTarget(const FTargetingRequestHandle& Handle,
const FTargetingDefaultResultData& TargetData) const override
{
AActor* Target = TargetData.HitResult.GetActor();
if (!Target) return 0.f;
if (IAbilitySystemInterface* ASI = Cast<IAbilitySystemInterface>(Target))
{
if (UAbilitySystemComponent* ASC = ASI->GetAbilitySystemComponent())
{
float Health = ASC->GetNumericAttribute(UMyAttributeSet::GetHealthAttribute());
float MaxHealth = ASC->GetNumericAttribute(UMyAttributeSet::GetMaxHealthAttribute());
if (MaxHealth > 0.f)
return 1.f - (Health / MaxHealth); // 越残血分数越高
}
}
return 0.f;
}
};
9. 异步请求的执行模型
异步请求大致按下面这条流程在 UTargetingSubsystem::Tick 里推进:
实践要点:
- 异步 Trace 至少 1 帧延迟:物理线程跨帧才回调。任何"按下按键立刻要结果"的场景(格挡判定、QTE)必须走同步。
- 异步执行时间片:Subsystem 在 Tick 内单帧执行有上限(实现上通常是 ~10ms 这个量级),保证不卡帧。
- 取消异步:
RemoveAsyncTargetingRequestWithHandle(Handle),Actor 销毁时务必清理。
下一篇:蓝图使用。