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

发布时间:2026/6/26 1:17:49
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处理的是没有激活但可能在这期间被激活的GAActivation 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在这里进行严格的校验判断所有必要变量是否存在CooldownCost是否允许激活很关键的是还调用了UGameplayAbility::DoesAbilitySatisfyTagRequirements就是这个函数对Tag之间限制关系进行判断函数源码在GameplayAbility.cpp并不复杂且可读性很高推荐大家去看看。它将AbilitySystemComponent.GetOwnedGameplayTags( )与Tags各个项分别进行比较GA类中各个项都是成员变量可以直接读最后返回一个bool结果确定GA是否通过Tag检查。/** 源码中描述Returns true if none of the abilitys tags are blocked and if it doesnt 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的传递往往都能通过GECueTargetData等实现。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 OnlyGA只能被服务端调用这意味着即使你为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顾名思义这里提供了一个TSubClasssUGameplayEffect ,可以指定一个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判定冷却的设计模式强大许多。TriggersAbilityTriggers有一个很好的说法如果手动触发GA是‘手动挡’那么Trigger就是‘自动挡’通过定义的Source途径添加Tag激活这个GA。Trigger Tag显而易见填入一个Tag作为Source的触发Trigger Source1GameplayEvent最常用的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* HitDatanew FGameplayAbilityTargetData_SingleTargetHit; FHitResult HitResult; HitResult.ImpactNormalPushVel; HitData-HitResultHitResult; EventData.TargetData.Add(HitData); //PassiveGA中设置了以GameplayEventEventTag这里直接触发 UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Target,UGAP_Launch::GetLaunchedAbilityActivationTag(),EventData); } //这里定义TriggerTag和Source UGAP_Launch::UGAP_Launch() { NetExecutionPolicyEGameplayAbilityNetExecutionPolicy::ServerOnly; //收到一个GameplayEvent且事件的EventTagTriggerTag时这个GA才会被触发。 FAbilityTriggerData TriggerData; TriggerData.TriggerSourceEGameplayAbilityTriggerSource::GameplayEvent; TriggerData.TriggerTagGetLaunchedAbilityActivationTag(); //添加到Triggers中 AbilityTriggers.Add(TriggerData); }这个Data相当于一个快递存储了发送Event方的很多信息GA收到这个快递并拆解利用其中的信息实现逻辑。当然如果你的技能只是简单的被动技能需要的信息不多你也可以完全不要这个Data直接传一个FGameplayEventData( )作为参数即可。2Owned Tag Added它监听着ASC的OwnedTags一旦多出了指定的TagGA就开始尝试激活调用TryActivateAbility 它具有严格的边界在Tag数从0-1的一瞬间触发如果已经有了只是从1-2是不会激活这个GA的。特别注意Tag消失的时候并不会自动结束GA如果你想实现这个逻辑应该选择监听这个Tag并回调EndAbility。3Owned Tag Present没有使用经验暂不赘述本文总结了一下GA蓝图类中各个常用选项这些配置基本都能在代码中直接做好也可以先实现代码整体逻辑然后在蓝图中可视化配置了解这些选项的含义和使用方式是非常重要的。感谢你看到这里 下次再见