news 2026/6/26 1:17:40

UE5.6 GAS学习笔记(2)-->GA篇 [2.分析GA类基本内容]

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UE5.6 GAS学习笔记(2)-->GA篇 [2.分析GA类基本内容]

本文继续GAS框架中的GameplayAbility(GA)拆解。

在上一篇中已经实现了如何将一个输入映射关联到一个具体的GA触发。现在我们来考虑如何创建一个GA类,目前有两种通用的方式,一是在IDE(我用的是JetBrains Rider 2025.3.3)中配置好UE环境后,可以直接创建大部分Unreal类,其中就包括GA。二是在编辑器中创建。

一、IDE

选择Unreal类

所有类中找到GA类并创建

当然,一般我们会先基于UGameplayAbility类继承一个项目内的通用GA类作为大部分GA类的基类。

编辑器中在BlueprintClass项找到此GA类并创建蓝图实例即可。

二、编辑器中

创建蓝图GA类

或者在C++ Classes 文件夹中右键找到New C++ Class,从中找到GA项,Create Class即可,这种方法和IDE中创建C++类一样,需要额外编译一次。

现在来看看一个创建好的GA蓝图类有哪些信息

继承自UGameplayAbility的一个基本蓝图类

Tags

这是判断GA能否激活的关键项,通过各种Tag实现了大量GA之间制约和依赖关系。

AssetTag:默认的GA携带的Tag,在这里添加的Tag在会ASC中代表此GA做Tag互斥,通常以Ability作为前缀,然后根据类型不同进行分层。

Cancel Abilities with Tag :配置Tag后,此GA激活时,所有携带这些Tag的GA都会被调用CancelAbility()(因此,为了防止某些不能被取消的GA被我们意外添加到其他GA的Cancel中,我们可以为这个GA重写CanBeCanceled函数,手动return false,就可以不对Cancel操作进行响应)

Block Abilities with Tag:激活该GA时,其他GA的AssetTags中只要带有这些BlockTag都会被阻塞无法激活。

(Tips:和上面的Cancel的区别在于,Cancel处理的是一个正在激活的GA,而Block处理的是没有激活但可能在这期间被激活的GA)

Activation Owned Tags:激活该GA时,将这些Tag临时添加到SourceASC中,在GA结束时会自动移除。

Activation Required Tags:激活GA时,SourceASC上必须有的标签,否则不能激活GA。

Activation Blocked Tags:激活GA时,SourceASC上不能有的标签,否则不能激活GA。

Source Required Tags:激活GA时,SourceTags参数中必须有的Tag。

Source Blocked Tags:激活GA时,SourceTags参数中不能有的Tag。

Target Required Tags:激活GA时,TargetTags参数中需要的Tag。

Target Blocked Tags:激活GA时,TargetTags 参数中不能有的Tag。

这些Tags在GA类中都是一个可以直接配置的FGameplayTagContainer。

SourceTags/TargetTags来自哪里?

bool UGameplayAbility::CanActivateAbility (const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const

这个函数中有参数SourceTags和TargetTags作为形参,用来表示当前技能的临时上下文信息。临时上下文信息指的是:这两个Tags实际上不会注册到ASC中,只是携带一些必要代表信息的Tag,例如,在某次攻击类GA时在SourceTag中添加一个表示火属性的Tag,这意味着这次GA是火属性的,但是SourceASC本身并没有这个火属性Tag。

Source和Target的默认参数都是nullptr,不用的时候直接忽略这两项形参就行。

这些Tags是如何实现对GA的限定的?

不同触发链最终都会从ASC的ActivatableAbilities 中找到目标GA的FGameplayAbilitySpecHandle , 然后调 用TryActivateAbility(FGameplayAbilitySpecHandle Handle),这个函数在真正触发ActiavetAbility( ) 前进行一系列检查,会判断一下网络权限,然后调用InternalTryActivateAbility,在这里检查GA是否已经注册/激活,是否允许再次激活,并且处理Instancing Policy,处理网络预测等,再然后走到CanActivateAbility,在这里,进行严格的校验,判断所有必要变量是否存在,Cooldown,Cost是否允许激活,很关键的是还调用了UGameplayAbility::DoesAbilitySatisfyTagRequirements,就是这个函数对Tag之间限制关系进行判断(函数源码在GameplayAbility.cpp,并不复杂且可读性很高,推荐大家去看看)。它将AbilitySystemComponent.GetOwnedGameplayTags( )与Tags各个项分别进行比较(GA类中各个项都是成员变量,可以直接读),最后返回一个bool结果,确定GA是否通过Tag检查。

/** 源码中描述:Returns true if none of the ability's tags are blocked and if it doesn't have a "Blocking" tag and has all "Required" tags.

Replication Policy

有ReplicateNo和 ReplicateYes 两种选择,分别代表了【在客户端和服务端各创建一个实例】和【在服务端创建实例并通过网络子对象复制机制同步到客户端】两种情况。

前面说过,GA是在服务端于ASC进行注册的,其中大部分逻辑也是在服务端实现,客户端只是拥有一个视口并进行表现。而且Attribute、GE、Cue、Montage等GA逻辑内常用的需要同步的也已经在ASC内部做好了极好的网络同步封装,因此,大部分情况下的GA都是ReplicateNo类型的。

设置GA为Replicate的情况一般是在GA中使用了RPC在两端传输信息,或者对标记了Replicated的成员变量进行网络同步。RPC只能在同一个网络实例间建立,因为其本质上是属于函数调用,只是在网络对象的不同端实现。例如Lyra中的DashGA就是ReplicatePolicy,因为其使用了一个RPC将客户端的Direction和Montage发送给服务端进行调用。

事实上,UE并不推荐使用Replicate,因为一个UObject实例的动态复制对网络带宽和性能消耗都很大,而且变量的复制,修改,RPC的传递,往往都能通过GE,Cue,TargetData等实现。

Instancing Policy

有三种实例化策略:

NonInstanced不实例化,每次激活都是取其CDO的数据直接使用,目前似乎已经很少使用,由于我没有使用经验,故本篇暂且不谈

InstancedPerActor:在注册到ASC时实例化一次,每次激活都调用的都是复用这个实例

InstancedPerExecution每次激活都实例化一次 (每个GameplayAbilitySpec可能创建多个GA实例)

如何选择这些Policy?

大部分GA都能以PerActor Policy实现,这是理想的Policy,因为它只在每次创建的时候在Actor生成一个GA实例,此后每次调用此GA都是复用此实例,而无需每次激活GA都重新实例化一次,这样避免大量新对象的生成(比如普攻GA,尤其是多个小兵情景下,每秒可能会有上百次普攻GA的激活,此时显然不能使用Per Execution,这会带来很大负担)。

也因为复用实例,上一次调用GA操作对GA实例的影响(如修改了某个成员变量【状态】)会在下一次调用的时候保存。相比之下,PerExecution Policy没有这个性质,因为它每次激活都创建一个新的GA实例,不记录上一次的信息。

这有利有弊,对于某些希望每次激活都是独立的,之前的激活不对当前产生影响的GA(如普通的技能,希望每次都调用一模一样的技能产生一样的效果),Per Execution就能符合这一需求。但是注意,这也同时建立在实例化的开销在可接受范围的情况下,如果是一个频繁调用的GA,还是建议以Per Actor进行实例化,如果改变了某些变量又不想影响下一次激活GA,可以在End Ability的时候重置修改了的变量。

对于某些希望记录多次GA激活时信息(比如某个GA根据GA释放次数增加伤害,需要记录激活次数),使用Per Actor就十分合适了。

除此之外,二者在激活操作上也有区别,由于每次激活都创建一个新的实例,Per Execution可以在单个角色上多次激活,而且互不影响。而Per Actor不允许这样做,必须等GA结束了才能继续激活。

如果你希望能够在Per ActorPolicy实现这样的功能:在GA激活期间再次激活时,能够立刻取消GA并重新激活,可以勾选Retrigger Instanced Ability 【eg:跳跃期间再次跳跃】。

Net Execution Policy

网络执行策略决定了当GA被触发时,客户端和服务器之间的权限和同步流程。

这是一个枚举类,有四种Policy,可以看到注释上说:这个Policy决定了GA在网络上如何执行,客户端是“询问并预测执行”还是“询问并等待执行”,还是“只等待执行”。

Local Predicted

最常用的Policy,在本地输入触发GA,调用TryActivateAbility后内部会调用CanActivateAbility判断当前是否满足激活条件,如果校验通过,则开启一个预测窗口生成FPredictionKey,然后调用ServerTryActivateAbility,将包含这个Key的激活请求发送给服务端,此后客户端本地直接激活此GA,无论服务端是否同意此次激活,或者因不合法被服务端拒绝。

这样做的目的是为了降低物理延迟,如果是服务端先执行,判断GA合法后再把Montage,属性集改变,GE应用等同步到客户端,由于网络同步的必然延迟,会导致客户端的手感受到“滞后”影响。

当然,服务端的权威性是GAS的前提,即使客户端先激活了GA,也是以服务器上GA的激活流程为准,一旦服务器GA激活失败,ASC会通过RPC通知客户端这次GA是失败的,作废客户端发送的预测Key,触发GA的回滚(这个回滚与Key绑定,会撤销预测阶段应用的GE,赋予的Tag,停止Montage并停止GA等)。

可以看到,这个Policy具有延迟低的优点,这对玩家的操作体验十分重要,所以它也是GA类的默认配置。

Local Only

完全的单机GA,无法修改任何属性,也无法对其他玩家产生任何实际影响。

一般用于纯客户端表现,如本地动作(当着其他客户端的面播放其他客户端也不会看到的表演性Montage),打开各种菜单(如果你想以GA实现,而非直接使用Widget的各种Button回调的话....),以及各种纯本地的操作。优点就在于绝对的0延迟和网络开销。

Server Only

GA只能被服务端调用,这意味着即使你为GA绑定了输入,并进行触发,GA也完全不进行响应,而是只响应来自服务端的TryActivateAbility

这个Policy常用与AI相关GA,被AIController下的ASC进行GA调用,因为AIC只在服务端上存在,客户端不存在输入触发这一说,直接在服务端决定GA激活并同步属性到各个客户端的Actor上即可。

除此之外,各种被动GA也能够通过这个Policy实现,这是因为相比本地预测,只在服务端激活GA省去了客户端的开销,对于不是客户端自己触发的GA,这时没有了感官最明显的触发滞后感,节省开销带来的优化就变得可观了。

当然,这个技能应该比较简单,只处理数值或者简单的动画,例如一个持续恢复生命值的被动GA,逻辑就是简单的Apply一个HealthRegenGE,这时就非常合适了,客户端只看到自己的血条在回升,并没有手感一说。

它不应该是一个带有复杂表现的GA,否则还是会因为网络延迟导致不合预期的视觉表现。

Server Initiated

由服务端发起此GA,客户端同步属性变化,Montage播放和各种Cue特效等等,一般用来实现由其他客户端/环境触发的,同时客户端也必须同步的被动GA,这里的必须,指的是此GA有一部分逻辑专门在客户端调用。

例如,控制类GA,其他客户端通过服务端,调用SendGameplayEventToActor激活一个GemplayEvent触发的GA,这个GA带有控制效果,会在客户端本地唤起一个眩晕图标,同时闪屏,同时产生一些只有此客户端看到的特效之类的,这些都在 if(!HasAuthority)中运行。

Cost Gameplay Effect Class

顾名思义,这里提供了一个TSubClasss<UGameplayEffect> ,可以指定一个BP_GE类,用于为GA实现Cost(消耗)机制,例如消耗蓝量、耐力、甚至生命值等等。

前面提到在TryActivateAbility时有一个CanActivateAbility检查函数,会检查Cost和Cooldown,其中检查Cost就是一个CheckCost函数,它获取配置好CostGE的CDO对象,然后调用AbilitySystemComponent->CanApplyAttributeModifiers(),会计算GE配置中Modifiers值应用后的结果,如果低于0或自定义的阈值,就会失败,阻止此GA的激活。

注意,此时只是计算预测一下,不会真正应用,因为这个判断之后GA还会因为别的因素失败,不一定就激活。

真正激活这个Cost的地方在CommitAbility函数,这个函数需要手动调用,如果不调用的话Cost和Cooldown都不会生效。在Local Predicted Policy下的GA,这个CommitAbility可以直接在ActivateAbility调用(当然,如果你期望在特别的时候才Cost和Cooldown,也可以在那个位置调用),然后GE被实例化并调用,在客户端和服务端都进行CostGE相关属性值的扣除,此时这个GE属于预测性GE,也就是客户端上的属性值虽然立刻被修改,但其实决定这个值真正结果的还是权威端的属性值,一旦服务器GA激活失败,则Key作废,修改的属性值会立刻回滚,GA激活成功,则因为属性集的Replicated标记,客户端上真正拥有【同步】这个值。

一般来说,这个CostGE的DurationPolicy应该是Instant类型的,这是规范的定义,因为Cost在概念上就是瞬间完成的扣除,虽然也可以用其他的Policy,但可能导致无法产生准确的即时数值判定,如果你的需求是持续性的Cost,应该特别定义一个GE,由GA进行应用。

Cooldown Gameplay Effect Class

和Cost很像,可以指定一个GE类,使用Duration Policy作为GA的冷却时间,在这个期间,激活GA在检查函数期间就会判定失败。

Cooldown GE必须Has Duration, Duration的值就是冷却时间的大小。除此之外,还有一个必不可少的部分,你需要在Components中找到GrantTagToTargetActor,其中可以配置一个CooldownTag。

GE类中

必须有Cooldown Tag的原因是GAS判定GA是否冷却的机制就是在OwnedTags中查询有没有CooldownTag,因为Grant Tags在GE应用时添加到ASC,在GE销毁时从角色身上移除,相当于一个一个方便又简单的跟随符号。

具体来说,在检查函数中调用AbilitySystemComponent->HasAnyMatchingGameplayTags(CooldownTags),如果存在任意一个Cooldown Tag,则激活直接失败。

在Commit函数中,实例化GE并应用到角色身上,和CostGE相同。

以基于Tag判断冷却的设计有什么好处呢?

Grant Tags可以不止一个,这给我们极大的操作空间,可以一个全局Cooldown Tag加到多个GA中,某一个GA一旦被激活,其他拥有这个Tag的GA也无法激活,实现了全局冷却逻辑。其次,多个Tag还能拓展很多操作,相比传统的一个CD值,然后每帧减少冷却时间这种做法而言,基于Tag判定冷却的设计模式强大许多。

Triggers

AbilityTriggers

有一个很好的说法,如果手动触发GA是‘手动挡’,那么Trigger就是‘自动挡’,通过定义的Source途径添加Tag,激活这个GA。

Trigger Tag:显而易见,填入一个Tag,作为Source的触发

Trigger Source

1)GameplayEvent

最常用的Source,一般以ASC->SendGameplayEventToActor来触发,这个函数其中一个形参就是EventTag,即TriggerTag。上文有说,一个被动GA就能如此激活,当目标Actor的被动要被触发时,调用这个函数发特定的Tag激活即可。

我们知道ActivateAbility有一个参数const FGameplayEventData* TriggerEventData,它就是专门响应Trigger Gameplay Event的,那么这个Data来自哪里呢,答案是来自SendGameplayEventToActor,这个函数的参数除了EventTag,还有一个FGameplayEventData EventData参数,这个Data是我们手动创建的,可操作性很强,非常非常好用,它提供了很多信息,如HitResult(最常用)。

这里举个易懂的例子,在此之前先给大家看看EventData数据结构封装的内容

USTRUCT(BlueprintType) struct GAMEPLAYABILITIES_API FGameplayEventData { GENERATED_BODY() // 1. 触发这次事件的 Tag(例如:Event.HitReact) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) FGameplayTag EventTag; // 2. 谁发起的这次事件?(比如:挥刀攻击你的那个敌人) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) const AActor* Instigator; // 3. 谁承受了这次事件?(比如:被命中的你自己) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) const AActor* Target; // 4. 万能对象指针:可以塞入任何自定义的 UObject(比如当前武器的配置、道具数据) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) const UObject* OptionalObject; // 5. 第二个万能对象指针:UE5 新增,进一步扩展自定义空间 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) const UObject* OptionalObject2; // 6. 这次事件的量化表现(比如:暴击造成的伤害数值 150.f) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) float EventMagnitude; // 7. 包含整个伤害流程的原始上下文(包含各种 TargetData、位置信息) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) FGameplayEffectContextHandle ContextHandle; // 8. 精确的命中位置、方向或碰撞到的 Component 数组 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = GameplayEventData) FGameplayAbilityTargetDataHandle TargetData; };

我想实现一个击飞效果,思路是将这个击飞做成被动GA,用Event发送给目标对象进行触发,除此之外,还要知道两个参数,1:击飞对象 2:击飞方向向量。这两个参数就能利用EventData中的HitResult完美获取。

实现过程:用EventData作为载体,TargetData可以存储多个HitResult和ImpactNormal。

void UCGameplayAbility::PushTarget(AActor* Target, const FVector& PushVel) { if (!Target) return; FGameplayEventData EventData; FGameplayAbilityTargetData_SingleTargetHit* HitData=new FGameplayAbilityTargetData_SingleTargetHit; FHitResult HitResult; HitResult.ImpactNormal=PushVel; HitData->HitResult=HitResult; EventData.TargetData.Add(HitData); //PassiveGA中设置了以GameplayEvent+EventTag,这里直接触发 UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Target,UGAP_Launch::GetLaunchedAbilityActivationTag(),EventData); } //这里定义TriggerTag和Source UGAP_Launch::UGAP_Launch() { NetExecutionPolicy=EGameplayAbilityNetExecutionPolicy::ServerOnly; //收到一个GameplayEvent,且事件的EventTag==TriggerTag时这个GA才会被触发。 FAbilityTriggerData TriggerData; TriggerData.TriggerSource=EGameplayAbilityTriggerSource::GameplayEvent; TriggerData.TriggerTag=GetLaunchedAbilityActivationTag(); //添加到Triggers中 AbilityTriggers.Add(TriggerData); }

这个Data相当于一个快递,存储了发送Event方的很多信息,GA收到这个快递并拆解,利用其中的信息实现逻辑。

当然,如果你的技能只是简单的被动技能,需要的信息不多,你也可以完全不要这个Data,直接传一个FGameplayEventData( )作为参数即可。

2)Owned Tag Added

它监听着ASC的OwnedTags,一旦多出了指定的Tag,GA就开始尝试激活,调用TryActivateAbility, 它具有严格的边界,在Tag数从0->1的一瞬间触发,如果已经有了,只是从1->2,是不会激活这个GA的。

特别注意:Tag消失的时候并不会自动结束GA,如果你想实现这个逻辑,应该选择监听这个Tag并回调EndAbility

3)Owned Tag Present:没有使用经验,暂不赘述

本文总结了一下GA蓝图类中各个常用选项,这些配置基本都能在代码中直接做好,也可以先实现代码整体逻辑,然后在蓝图中可视化配置,了解这些选项的含义和使用方式是非常重要的。

感谢你看到这里 ,下次再见!

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

.NET开发者集成YOLO目标检测:yolodotnet实战指南

1. 项目概述&#xff1a;当YOLO遇上.NET如果你是一个.NET开发者&#xff0c;尤其是做桌面应用、工业视觉或者边缘计算方向的&#xff0c;肯定有过这样的烦恼&#xff1a;看到CV领域那些酷炫的实时目标检测模型&#xff0c;比如YOLOv5、YOLOv8&#xff0c;心里痒痒的&#xff0c…

作者头像 李华
网站建设 2026/6/26 1:15:14

2026实测|个人免费AI编程工具全对比,vibe coding副业开发者必看

作为团队里唯一的 Rust 开发&#xff0c;AI 编程工具对非主流语言的支持是我最关心的。5 款工具在 Rust 上的表现参差不齐。我是CS研二在读实习生&#xff0c;平时靠vibe coding接外包、做爬虫数据清洗副业&#xff0c;字节跳动出品的TRAE是我日常主力工具&#xff0c;据CSDN评…

作者头像 李华
网站建设 2026/6/26 1:12:03

铁电MEMS突触技术:神经形态计算新突破

1. 铁电MEMS突触技术背景与核心创新 神经形态计算作为模拟生物神经系统的新型计算范式&#xff0c;其核心挑战在于实现类似生物突触的模拟权重存储与更新机制。传统铁电突触器件&#xff08;如FeFET、FeCAP等&#xff09;通过铁电材料的剩余极化(Pr)状态存储权重信息&#xff0…

作者头像 李华
网站建设 2026/6/26 1:08:05

当智能体真正走进办公室,它的成绩单好看吗?

这项由Frontis.AI旗下Horizon Research团队完成的研究&#xff0c;于2026年6月22日以预印本形式发布&#xff0c;编号为arXiv:2606.23654v1&#xff0c;研究领域归属于计算机科学计算与语言&#xff08;cs.CL&#xff09;。感兴趣的读者可以通过该编号在arXiv平台上查阅完整论文…

作者头像 李华
网站建设 2026/6/26 1:08:03

高阶03:国产EAP vs 进口Applied EAP全维度对比与迁移改造

高阶03&#xff1a;国产EAP vs 进口Applied EAP全维度对比与迁移改造 一、本课学习目标 1、全面吃透进口Applied EAP与国产自研EAP架构、机制、生态、量产差异。 2、掌握新旧系统迁移适配要点、报文兼容、状态机对齐、数据平移、风险控制点。 3、明确老厂替换进口EAP、新厂选型…

作者头像 李华