news 2026/7/11 8:36:39

【UE源码精读-ActionRPG】GAS进阶:目标选择与AbilityTask

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【UE源码精读-ActionRPG】GAS进阶:目标选择与AbilityTask

[TOC]

上一篇讲了EffectContainerMap如何把效果配置和施加分两步走。但有两个问题留着没答:一是"目标怎么选"——谁来决定这次命中打中了哪个目标?二是"命中帧怎么触发结算"——攻击动画里的哪一帧算"打中",如何精确地在那一帧完成从动画到 GAS 的跳转?这两个问题,分别由URPGTargetType(策略模式)和RPGAbilityTask_PlayMontageAndWaitForEvent(自定义 AbilityTask)来回答。


一、URPGTargetType:目标选择的策略模式

URPGTargetType是一个极简的策略接口,只有一个纯虚方法:

UCLASS(Blueprintable,meta=(ShowWorldContextPin))classACTIONRPG_APIURPGTargetType:publicUObject{GENERATED_BODY()public:URPGTargetType(){}/** Called to determine targets to apply gameplay effects to */UFUNCTION(BlueprintNativeEvent)voidGetTargets(ARPGCharacterBase*TargetingCharacter,AActor*TargetingActor,FGameplayEventData EventData,TArray<FHitResult>&OutHitResults,TArray<AActor*>&OutActors)const;};

注意类上的注释:“This does not subclass GameplayAbilityTargetActor because this class is never instanced into the world”。常规的GameplayAbilityTargetActor是一个真正的 Actor,需要 Spawn 进世界场景才能用,生命周期复杂。URPGTargetTypeUObject,从不实例化进世界——通过GetDefaultObject()拿 CDO,直接调GetTargets,调完就扔。这是一种更轻量的策略分发方式,代价是无法感知场景(不能做射线检测这类需要 WorldContext 的操作)。

什么是 CDO(Class Default Object,类默认对象)?UE 里每一个UClass在被加载时,都会自动创建且仅创建一个"默认对象"——这就是 CDO。它有两个关键作用:

  1. 保存这个类所有属性的默认值。你在编辑器"Class Defaults"面板里填的、或标了EditDefaultsOnly的属性(比如上一篇的EffectContainerMap),存的就是这个类 CDO 里的值。
  2. 充当"模板":每次NewObject/SpawnActor创建新实例时,引擎先把 CDO 拷一份,再在上面改——所以新实例的初始值就来自 CDO。

CDO 有几个特点,正好解释了这里为什么用它做策略对象:

  • 全程只有一个、常驻内存:类一加载它就在,不用 new、也不用销毁,GetDefaultObject()每次拿到的都是同一个共享实例。
  • 应当被当作无状态、只读来用:因为全局共享,谁都不该往 CDO 上写运行时数据。URPGTargetTypeGetTargetsconst、结果全部通过Out参数返回、自身不存任何状态——正因如此,"直接拿 CDO 调一下就扔"才是安全的。

一句话:CDO = 这个类的"出厂设置 + 共享单例模板"。对URPGTargetType这种"纯逻辑、无状态"的策略类,直接借 CDO 当函数载体,比 Spawn 一个 Actor 轻量得多。

ActionRPG 提供了两个内置的 C++ 子类:

1.1 URPGTargetType_UseOwner:自我目标

voidURPGTargetType_UseOwner::GetTargets_Implementation(ARPGCharacterBase*TargetingCharacter,AActor*TargetingActor,FGameplayEventData EventData,TArray<FHitResult>&OutHitResults,TArray<AActor*>&OutActors)const{OutActors.Add(TargetingCharacter);}

实现只有一行:把发动技能的角色自己加到目标列表。使用场景:喝药水、使用主动防御技能——效果施加给自身。

1.2 URPGTargetType_UseEventData:从事件数据取目标

voidURPGTargetType_UseEventData::GetTargets_Implementation(ARPGCharacterBase*TargetingCharacter,AActor*TargetingActor,FGameplayEventData EventData,TArray<FHitResult>&OutHitResults,TArray<AActor*>&OutActors)const{constFHitResult*FoundHitResult=EventData.ContextHandle.GetHitResult();if(FoundHitResult){OutHitResults.Add(*FoundHitResult);}elseif(EventData.Target){OutActors.Add(const_cast<AActor*>(EventData.Target));}}

GameplayEventData里取:如果 Context 里有 HitResult(近战扫描命中),就用 HitResult;否则取TargetActor(比如锁定技能已知目标)。使用场景:近战攻击命中目标——动画的AnimNotify时机做了一次碰撞检测,把 HitResult 塞进GameplayEventData,这个类从里面取出来。

两个子类都标NotBlueprintable——它们太简单,不需要蓝图扩展;更复杂的目标选择逻辑应该继承URPGTargetTypeBlueprintable)在蓝图里实现(比如范围扇形检测、锥形 AOE 等)。

1.3 策略模式的优雅之处

这个设计把"目标逻辑"从"技能逻辑"里彻底剥离:一个近战攻击能力配UseEventData,一个喝药水能力配UseOwner,换目标策略只改FRPGGameplayEffectContainer.TargetType那一行配置,能力本身的激活、动画、效果施加逻辑完全不变。


二、AnimNotifyState:动画驱动碰撞的机制

在进入 AbilityTask 之前,先理解"命中帧"是怎么产生的。

ActionRPG 用AnimNotifyState标记动画里的"攻击判定窗口":

NotifyBegin时开始做碰撞检测,NotifyEnd时停止。一旦检测到命中,AnimNotifyState通过AbilitySystemComponent->HandleGameplayEvent把 HitResult 打包成GameplayEventData,发送一个带标签的 GameplayEvent(如Event.Montage.Attack)。

这就是GameplayEventData里有 HitResult 的原因,也是UseEventData能取到目标的前提。


三、RPGAbilityTask_PlayMontageAndWaitForEvent:合二为一

3.0 为什么需要 AbilityTask

一个关键前提:GA 的激活逻辑本身是在一帧内完成的。如果你在ActivateAbility里直接写"播放 2 秒的攻击动画、等命中事件、再结算伤害",那么"等待"这个概念在单帧逻辑里不存在——你需要一种异步机制。

AbilityTask 就是 GAS 的异步方案。它是一种特殊的UGameplayTask,挂在 GA 上运行,可以跨多帧持续等待某个条件,等条件满足后通过 Delegate 回调通知 GA。GA 本身在发起 Task 后就"让出控制"了,等 Task 的回调驱动后续逻辑。这和协程/Promise 的思路类似——只是在 UE 的世界里用了 Delegate + Task 的形式。

GAS 内置了两个常用 AbilityTask:

  • UAbilityTask_PlayMontageAndWait:播放蒙太奇,结束/中断时通知。
  • UAbilityTask_WaitGameplayEvent:等待某个 GameplayEvent,收到时通知。

这两个分开用有个问题:近战攻击需要同时做这两件事——播放攻击动画,并且在动画进行中等待命中事件。ActionRPG 把它们合并成URPGAbilityTask_PlayMontageAndWaitForEvent,在一个节点里搞定。

3.1 五个输出引脚

/** The montage completely finished playing */UPROPERTY(BlueprintAssignable)FRPGPlayMontageAndWaitForEventDelegate OnCompleted;/** The montage started blending out */UPROPERTY(BlueprintAssignable)FRPGPlayMontageAndWaitForEventDelegate OnBlendOut;/** The montage was interrupted */UPROPERTY(BlueprintAssignable)FRPGPlayMontageAndWaitForEventDelegate OnInterrupted;/** The ability task was explicitly cancelled by another ability */UPROPERTY(BlueprintAssignable)FRPGPlayMontageAndWaitForEventDelegate OnCancelled;/** One of the triggering gameplay events happened */UPROPERTY(BlueprintAssignable)FRPGPlayMontageAndWaitForEventDelegate EventReceived;
引脚触发条件典型处理
OnCompleted蒙太奇正常播完能力正常结束
OnBlendOut蒙太奇开始淡出提前解锁输入
OnInterrupted蒙太奇被打断(受击等)取消效果,回滚状态
OnCancelled能力被另一个能力主动取消清理资源
EventReceived注册的 GameplayEvent 标签匹配到结算伤害

在 GA_Melee_Base 蓝图里,接EventReceived的节点调用ApplyEffectContainerSpec,这就是"命中帧触发结算"的完整闭环。

前四个引脚的参数(EventTagEventData)在蒙太奇回调里都是空值——它们是为了保持函数签名一致而统一用FRPGPlayMontageAndWaitForEventDelegate

3.2 Activate:绑定监听的时机

voidURPGAbilityTask_PlayMontageAndWaitForEvent::Activate(){// 1. 先注册 GameplayEvent 监听(必须在播放蒙太奇之前!)EventHandle=RPGAbilitySystemComponent->AddGameplayEventTagContainerDelegate(EventTags,FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this,&URPGAbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent));// 2. 播放蒙太奇RPGAbilitySystemComponent->PlayMontage(Ability,...,MontageToPlay,Rate,StartSection);// 3. 绑定蒙太奇事件(BlendingOut、End、Cancelled)CancelledHandle=Ability->OnGameplayAbilityCancelled.AddUObject(this,...);AnimInstance->Montage_SetBlendingOutDelegate(...);AnimInstance->Montage_SetEndDelegate(...);}

关键顺序:先绑 GameplayEvent 监听,再播蒙太奇。如果反过来,蒙太奇很快(或帧率极低)可能在第一帧就触发 AnimNotify,此时监听还没注册,命中事件会被漏掉。

EventTags是构造时传入的标签集合,只有匹配到其中任意一个 Tag 的 GameplayEvent 才会触发EventReceived。这意味着一个 Task 可以同时等待多种命中类型(重击和轻击都监听,根据收到的EventTag区分处理)。

3.3 OnGameplayEvent:事件转委托

voidURPGAbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent(FGameplayTag EventTag,constFGameplayEventData*Payload){if(ShouldBroadcastAbilityTaskDelegates()){FGameplayEventData TempData=*Payload;TempData.EventTag=EventTag;EventReceived.Broadcast(EventTag,TempData);}}

ShouldBroadcastAbilityTaskDelegates()是 AbilityTask 基类的保护方法,在能力已经结束/取消时返回 false,防止僵尸回调。之后广播EventReceived,把EventTagPayload(含 HitResult)一起送出去——蓝图里接这个引脚拿到 HitResult,填进 ContainerSpec,完成闭环。

3.4 OnDestroy:清理所有监听

voidURPGAbilityTask_PlayMontageAndWaitForEvent::OnDestroy(boolAbilityEnded){// 停止蒙太奇if(Ability&&bStopWhenAbilityEnds)StopPlayingMontage();// 移除 GameplayEvent 监听if(RPGAbilitySystemComponent)RPGAbilitySystemComponent->RemoveGameplayEventTagContainerDelegate(EventTags,EventHandle);Super::OnDestroy(AbilityEnded);// 必须最后调,会 PendingKill}

注释强调:Super::OnDestroy必须放最后调——它会把对象标记为PendingKill,之后访问任何成员都可能 crash。


四、完整的动画事件驱动结算流

把上面所有环节串起来,一次近战攻击的事件流是这样的:

1. 玩家输入攻击 │2. 蓝图调用 ActivateAbilitiesWithItemSlot │ → ARPGCharacterBase::ActivateAbilitiesWithItemSlot │ → ASC::TryActivateAbility │3. GA_MeleeBase 能力激活(EventActivateAbility) │ → 调用 PlayMontageAndWaitForEvent Task(开始等待) │4. 动画播放,AnimNotifyState.NotifyBegin 开始碰撞检测 │5. 检测到命中 → NotifyState 构造 GameplayEventData(含 HitResult) │ → ASC.HandleGameplayEvent("Event.Montage.Attack", EventData)6. AbilityTask.OnGameplayEvent 被触发 │ → EventReceived.Broadcast(EventTag, EventData)7. 蓝图 EventReceived 节点 │ → MakeEffectContainerSpec(用 EventData 里的 HitResult) │ → ApplyEffectContainerSpec │8. PostGameplayEffectExecute 执行伤害计算(见伤害管线篇) │9. OnDamaged / OnHealthChanged 触发,蓝图播放受击特效

五、小结:两个设计哲学

URPGTargetType 体现的是"策略模式":把变化的部分(目标选取逻辑)从不变的部分(能力激活流程)里分离,通过配置切换,而不是继承重写。新增目标策略(范围 AOE、锁定最近目标等),只需继承URPGTargetType写一个新的GetTargets,无需动任何能力代码。

AbilityTask 体现的是"状态机 + 事件驱动":一个 Task 代表一段等待状态,它知道何时进入(Activate)、何时退出(OnDestroy)、中间发生了什么(各 Delegate 广播)。把"播动画"和"等命中事件"合并成一个 Task,消除了两个独立 Task 之间同步状态的问题。

两者结合,让 ActionRPG 的攻击逻辑在蓝图里只需要几个节点就能完整表达:EventActivateAbility → PlayMontageAndWaitForEvent → EventReceived → ApplyEffectContainerSpec——每个节点的背后,都是本文梳理的那套 C++ 机制。


附录:GA 标签系统——能力之间的互斥与协作

ActionRPG 的代码里没有显式展示 GA Tag 的配置(它在蓝图的 Details 面板里设置),但理解这套标签系统对于读懂 AbilityTask 的 Cancel/Interrupt 逻辑至关重要。每个 GA 蓝图的 Details 面板里有一组 Tag 配置项,控制能力之间的相互关系:

标签类型含义典型用法
Ability Tags该 GA 自己的标签身份Ability.Melee——标识这是近战能力
Cancel Abilities with Tag激活本 GA 时,打断拥有指定标签的其他 GA施法打断普攻:施法 CancelAbility.Melee
Block Abilities with Tag激活本 GA 时,阻止激活拥有指定标签的 GA(已激活的不受影响)受击时阻止翻滚:受击 BlockAbility.Roll
Activation Owned Tags本 GA 激活期间,给 ASC 赋予指定标签攻击期间给角色加State.Attacking标签
Activation Required Tags激活本 GA 时,ASC 必须拥有这些标签连击第二段要求State.Combo1Complete
Activation Blocked Tags激活本 GA 时,ASC 不能有这些标签眩晕时禁止所有攻击:Blocked byState.Stunned

这套标签系统让技能之间的互斥与协作完全通过配置而非代码解决——不需要写if (IsAttacking) return;这类硬编码判断。当 AbilityTask 的OnCancelled/OnInterrupted引脚被触发时,往往就是因为另一个 GA 的Cancel Abilities with Tag规则命中了当前 GA 的标签。理解了这一层,那些蓝图里"为什么攻击动画突然被打断"的 bug 就有了排查方向。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/11 8:36:06

AI工程化实践:GEO数据可视化、AI Agent开发与大仓模式架构解析

今天我们来深入分析三个当前AI工程化领域的热点话题&#xff1a;GEO仪表盘的数据陷阱、阿里AI Agent的发布实践&#xff0c;以及腾讯大仓模式的AI工程化体系。这三个方向分别代表了数据监控、智能体开发和工程架构的不同维度&#xff0c;对从事AI系统开发和落地的工程师来说都是…

作者头像 李华
网站建设 2026/7/11 8:34:47

CSP-J/S C++真题精析:从语法基础到算法思维的竞赛备考指南

1. 项目概述&#xff1a;一份真题的价值与使用指南最近在整理资料时&#xff0c;翻出了这份2025年CSP-J1-S1第一轮入门级和提高级的C语言真题&#xff08;不带答案&#xff09;。对于正在备战信息学竞赛&#xff0c;尤其是CSP-J/S认证的同学们来说&#xff0c;一份高质量的、不…

作者头像 李华
网站建设 2026/7/11 8:32:32

OpenCV 4.8 实现摄影测量前方交会:C++ 代码解析与 3 点精度验证

OpenCV 4.8 实现摄影测量前方交会&#xff1a;C 工程实践与精度验证 摄影测量中的前方交会算法是三维重建的核心技术之一&#xff0c;它通过立体像对中的同名像点坐标和相机参数&#xff0c;计算出对应物方点的三维坐标。本文将基于OpenCV 4.8库&#xff0c;从工程实现角度完整…

作者头像 李华
网站建设 2026/7/11 8:32:17

Seedance 2.0 Mini:AI视频生成技术从实验室到实用的关键突破

最近在AI视频生成领域&#xff0c;一个值得关注的新动向是&#xff1a;Luma AI平台正式上线了Seedance 2.0 Mini模型。这不仅仅是又一个AI视频工具的更新&#xff0c;而是标志着高质量视频生成技术正在从"实验室玩具"向"实用工具"的关键转变。 如果你曾经…

作者头像 李华
网站建设 2026/7/11 8:31:05

工业负载控制:TPD2017FN与MKV58F1M0VLQ24的高效方案

1. 项目概述&#xff1a;工业负载控制的核心挑战 在工业自动化领域&#xff0c;电感和电阻负载的控制一直是电气工程师面临的经典难题。不同于简单的阻性负载&#xff0c;电感性负载&#xff08;如继电器、电机、电磁阀等&#xff09;在开关瞬间会产生反向电动势&#xff0c;可…

作者头像 李华