【UE源码精读-ActionRPG】GAS基础:能力与效果容器

📅 2026/7/8 2:12:23
【UE源码精读-ActionRPG】GAS基础:能力与效果容器
[TOC]GASGameplayAbilities最难啃的一关不是属性系统也不是网络复制而是**“效果是如何从一个攻击动作流到目标身上的”**这条隐形管线。ActionRPG 在 C 里做了一个优雅的抽象EffectContainerMap——把这个能力在哪种时机、对哪些目标、施加哪些效果统一配置进一张TMap里留给设计师在蓝图里配置留给程序在运行时查表执行。本文精读这套机制的数据结构和两个核心方法。前置知识GAS 五大件速览在进入 ActionRPG 的具体实现前先用一张表把 GAS 的五大核心组件和它们之间的关系交代清楚组件全称职责在 ActionRPG 中ASCAbilitySystemComponent整个 GAS 的中枢组件管理 GA/GE/AS 的增删查改URPGAbilitySystemComponent挂在角色上GAGameplayAbility一个行为——攻击、使用药水、翻滚、施法都是 GAURPGGameplayAbility本文主角GEGameplayEffect对属性的修改——扣血、加速、回蓝都是 GE。GE 是修改 Attribute 的唯一正规渠道伤害 GE、被动属性初始化 GE 等ASAttributeSet属性集合——Health/Mana/AttackPower 等 float 属性的容器URPGAttributeSet上一篇精读过GCGameplayCue特效/音效等纯表现层不影响逻辑ActionRPG 未使用 GC它们的协作关系可以用一句话概括GA 通过 Apply GE 来修改 AS 上的 Attribute。ASC 是管理这一切的中枢。另一个必须预先理解的概念是Class → Spec 模式GAS 里的 GA、GE 在使用前都有一个类 → 实例的过程。UGameplayAbility和UGameplayEffect都是类蓝图资产或 C 类是静态的数据模板运行时会生成对应的SpecFGameplayAbilitySpec/FGameplayEffectSpec里面填入具体的上下文信息发起者是谁、等级多少。ApplyGE 操作的对象从来不是 GE 类本身而是从 GE 类创建出来的 GE Spec。理解了这一点后面的MakeOutgoingGameplayEffectSpec和MakeEffectContainerSpec就完全不神秘了。GE 的三种持续类型GE 按 Duration Policy 分三种直接影响它修改的是 BaseValue 还是 CurrentValueDuration Policy效果修改的值典型场景Instant瞬时执行一帧完成BaseValue永久改变扣血、治疗、经验值结算Has Duration持续指定时长后自动移除CurrentValue临时叠加5 秒加速、10 秒减防Infinite永久生效只能手动移除CurrentValue临时叠加按住跑步时的移速加成关键点只有Instant 类型的 GE 会触发PostGameplayEffectExecute属性系统精读和伤害管线都在讲这个回调因为它改的是 BaseValue。Duration/Infinite 类型改的是 CurrentValue触发的是PreAttributeChange。ActionRPG 的伤害 GE 是 Instant 类型——这就是为什么伤害管线最终走到PostGameplayEffectExecute里扣血。一、从蓝图配置说起在编辑器里打开任意一个近战能力蓝图如 GA_Sword、GA_Axe可以看到它的 Details 面板里有一个EffectContainerMap再往里点开一个近战能力看到GA_Melee_Base里的 EventActivateAbility 实现蓝图里调用的MakeEffectContainerSpec和ApplyEffectContainerSpec就是本文要剖析的核心。二、两个核心数据结构2.1 FRPGGameplayEffectContainer静态模板USTRUCT(BlueprintType)structFRPGGameplayEffectContainer{GENERATED_BODY()public:/** Sets the way that targeting happens */UPROPERTY(EditAnywhere,BlueprintReadOnly,CategoryGameplayEffectContainer)TSubclassOfURPGTargetTypeTargetType;/** List of gameplay effects to apply to the targets */UPROPERTY(EditAnywhere,BlueprintReadOnly,CategoryGameplayEffectContainer)TArrayTSubclassOfUGameplayEffectTargetGameplayEffectClasses;};它是静态配置在蓝图或数据资产的 Details 面板里填好、存盘——“当这个容器被触发时用TargetType选目标对目标施加TargetGameplayEffectClasses里的所有效果”。关键点TargetType是个类引用TSubclassOf不是对象——运行时才通过GetDefaultObject()拿实例。这样这个 struct 不持有任何运行时状态可以在磁盘上序列化成数据资产。TargetGameplayEffectClasses是一组 GE 类一次命中可以同时施加多个效果伤害 减速 火焰。2.2 FRPGGameplayEffectContainerSpec运行时实例USTRUCT(BlueprintType)structFRPGGameplayEffectContainerSpec{GENERATED_BODY()public:/** Computed target data */UPROPERTY(EditAnywhere,BlueprintReadOnly,CategoryGameplayEffectContainer)FGameplayAbilityTargetDataHandle TargetData;/** List of gameplay effects to apply to the targets */UPROPERTY(EditAnywhere,BlueprintReadOnly,CategoryGameplayEffectContainer)TArrayFGameplayEffectSpecHandleTargetGameplayEffectSpecs;boolHasValidEffects()const;boolHasValidTargets()const;voidAddTargets(constTArrayFHitResultHitResults,constTArrayAActor*TargetActors);};它是FRPGGameplayEffectContainer的运行时展开版TargetData不再是目标类型的类引用而是已经运行过目标逻辑后得到的实际目标列表HitResult 列表或 Actor 列表。TargetGameplayEffectSpecs不再是GE 类引用列表而是已经填好发起者/能力等级等上下文的FGameplayEffectSpecHandle列表——这些 Spec 随时可以被施加。用表格对比两者FRPGGameplayEffectContainerFRPGGameplayEffectContainerSpec性质静态配置蓝图里填运行时实例目标TSubclassOfURPGTargetType类引用FGameplayAbilityTargetDataHandle已计算结果效果TArrayTSubclassOfUGameplayEffect类引用TArrayFGameplayEffectSpecHandle已实例化 Spec生命周期磁盘序列化常驻内存运行时临时创建用完丢弃2.3 为什么要分两步原因只有一个时机不同。MakeSpec建 Spec的时机是能力激活时此时能知道发起者是谁、能力等级是多少可以把这些信息烘进FGameplayEffectSpec。但目标可能还不知道——攻击动画还没到命中帧还不知道打中谁了。ApplySpec施加效果的时机是命中帧动画AnimNotify触发GameplayEventAbilityTask 收到事件此时才有目标信息把目标填进去再施加效果。这就是分两步的本质把和发起者相关的信息能力等级、来源对象和和目标相关的信息目标 Actor、HitResult的填充分开各在恰当时机完成。三、URPGGameplayAbilityEffectContainerMapURPGGameplayAbility继承UGameplayAbility整个类就两件事声明EffectContainerMap提供两对方法。UCLASS()classACTIONRPG_APIURPGGameplayAbility:publicUGameplayAbility{GENERATED_BODY()public:URPGGameplayAbility();/** Map of gameplay tags to gameplay effect containers */UPROPERTY(EditDefaultsOnly,BlueprintReadOnly,CategoryGameplayEffects)TMapFGameplayTag,FRPGGameplayEffectContainerEffectContainerMap;virtualFRPGGameplayEffectContainerSpecMakeEffectContainerSpecFromContainer(...);virtualFRPGGameplayEffectContainerSpecMakeEffectContainerSpec(FGameplayTag ContainerTag,...);virtualTArrayFActiveGameplayEffectHandleApplyEffectContainerSpec(...);virtualTArrayFActiveGameplayEffectHandleApplyEffectContainer(FGameplayTag ContainerTag,...);};EffectContainerMap是核心FGameplayTag → FRPGGameplayEffectContainer的映射。设计师在蓝图里配一个近战攻击能力时可以这样填Event.Montage.Attack.Heavy→ Container{TargetType: UseEventData, Effects:[GE_HeavyMeleeDamage]}Event.Montage.Attack.Light→ Container{TargetType: UseEventData, Effects:[GE_LightMeleeDamage]}不同的 AnimNotify 触发不同的 GameplayEvent不同的 Tag能力就知道该套用哪个 Container。一个能力 BP 可以配置多套不同强度、不同效果的命中逻辑完全靠标签区分。GA 的两种调用方式主动与被动值得补充的是GAS 的 GA 有两种激活方式ActionRPG 都有用到主动调用代码里显式调用TryActivateAbilityby Handle或TryActivateAbilitiesByTagby Tag。ActionRPG 里玩家按攻击键 →ActivateAbilitiesWithItemSlot→TryActivateAbility就是主动调用。被动调用通过 GameplayEvent在 GA 蓝图的 Details 面板里设置 Trigger当 ASC 收到一个匹配 Tag 的GameplayEvent时自动激活该 GA。这种方式的好处是可以通过GameplayEventData传递额外数据如碰撞信息。ActionRPG 的GA_Melee_Base用的就是事件驱动激活——它在蓝图里用ActivateAbilityFromEvent替代了普通的ActivateAbility这样就能直接从EventData里拿到 HitResult传给MakeEffectContainerSpec。四、MakeEffectContainerSpecFromContainer建立 SpecFRPGGameplayEffectContainerSpecURPGGameplayAbility::MakeEffectContainerSpecFromContainer(constFRPGGameplayEffectContainerContainer,constFGameplayEventDataEventData,int32 OverrideGameplayLevel){FRPGGameplayEffectContainerSpec ReturnSpec;AActor*OwningActorGetOwningActorFromActorInfo();ARPGCharacterBase*OwningCharacterCastARPGCharacterBase(OwningActor);URPGAbilitySystemComponent*OwningASCURPGAbilitySystemComponent::GetAbilitySystemComponentFromActor(OwningActor);if(OwningASC){// 1. 如果配了 TargetType现在就运行目标逻辑可选也可以稍后 AddTargetsif(Container.TargetType.Get()){TArrayFHitResultHitResults;TArrayAActor*TargetActors;constURPGTargetType*TargetTypeCDOContainer.TargetType.GetDefaultObject();AActor*AvatarActorGetAvatarActorFromActorInfo();TargetTypeCDO-GetTargets(OwningCharacter,AvatarActor,EventData,HitResults,TargetActors);ReturnSpec.AddTargets(HitResults,TargetActors);}// 2. 确定能力等级if(OverrideGameplayLevelINDEX_NONE)OverrideGameplayLevelthis-GetAbilityLevel();// 3. 为每个 GE 类建立 Spec把发起者、等级、上下文信息烘进去for(constTSubclassOfUGameplayEffectEffectClass:Container.TargetGameplayEffectClasses){ReturnSpec.TargetGameplayEffectSpecs.Add(MakeOutgoingGameplayEffectSpec(EffectClass,OverrideGameplayLevel));}}returnReturnSpec;}逐步拆解第一步如果Container.TargetType有值就立刻调用目标逻辑GetTargets把 HitResult/Actor 填进ReturnSpec.TargetData。这是可选的——注释说 “targets can be added later”。对于近战攻击通常在EventReceived命中帧时才有 HitResult所以这里可能不填之后在蓝图里手动AddTargets。第二步GetAbilityLevel()——能力等级来自FGameplayAbilitySpec是装备时由物品数据填进去的武器等级。如果调用者传了OverrideGameplayLevel则用覆盖值。第三步MakeOutgoingGameplayEffectSpec是 GAS 引擎方法内部把谁发的、能力等级多少、GE 上下文全部烘进一个FGameplayEffectSpec返回 Handle。这个 Handle 之后可以传来传去施加时不需要再重新构建上下文。MakeEffectContainerSpec是一层薄包装FRPGGameplayEffectContainerSpecURPGGameplayAbility::MakeEffectContainerSpec(FGameplayTag ContainerTag,constFGameplayEventDataEventData,int32 OverrideGameplayLevel){FRPGGameplayEffectContainer*FoundContainerEffectContainerMap.Find(ContainerTag);if(FoundContainer)returnMakeEffectContainerSpecFromContainer(*FoundContainer,EventData,OverrideGameplayLevel);returnFRPGGameplayEffectContainerSpec();}用ContainerTag在EffectContainerMap里查表找到FRPGGameplayEffectContainer再调真正的实现。五、ApplyEffectContainerSpec施加效果TArrayFActiveGameplayEffectHandleURPGGameplayAbility::ApplyEffectContainerSpec(constFRPGGameplayEffectContainerSpecContainerSpec){TArrayFActiveGameplayEffectHandleAllEffects;for(constFGameplayEffectSpecHandleSpecHandle:ContainerSpec.TargetGameplayEffectSpecs){AllEffects.Append(K2_ApplyGameplayEffectSpecToTarget(SpecHandle,ContainerSpec.TargetData));}returnAllEffects;}遍历所有SpecHandle用K2_ApplyGameplayEffectSpecToTarget对TargetData里的所有目标逐一施加。返回的FActiveGameplayEffectHandle列表可以用来之后移除比如 Buff 需要被主动解除。ApplyEffectContainer是一步到位的便捷方法TArrayFActiveGameplayEffectHandleURPGGameplayAbility::ApplyEffectContainer(FGameplayTag ContainerTag,constFGameplayEventDataEventData,int32 OverrideGameplayLevel){FRPGGameplayEffectContainerSpec SpecMakeEffectContainerSpec(ContainerTag,EventData,OverrideGameplayLevel);returnApplyEffectContainerSpec(Spec);}六、AddTargets填充目标数据FRPGGameplayEffectContainerSpec::AddTargets的实现也很有意思voidFRPGGameplayEffectContainerSpec::AddTargets(constTArrayFHitResultHitResults,constTArrayAActor*TargetActors){// 把每个 HitResult 包装成 FGameplayAbilityTargetData_SingleTargetHitfor(constFHitResultHitResult:HitResults){FGameplayAbilityTargetData_SingleTargetHit*NewDatanewFGameplayAbilityTargetData_SingleTargetHit(HitResult);TargetData.Add(NewData);}// 把 Actor 数组包装成 FGameplayAbilityTargetData_ActorArrayif(TargetActors.Num()0){FGameplayAbilityTargetData_ActorArray*NewDatanewFGameplayAbilityTargetData_ActorArray();NewData-TargetActorArray.Append(TargetActors);TargetData.Add(NewData);}}FGameplayAbilityTargetDataHandle内部是一个TArrayTSharedPtrFGameplayAbilityTargetData支持多态——HitResult 和 Actor 数组用不同的子类存储统一放进这个 Handle 里一起传递。K2_ApplyGameplayEffectSpecToTarget在施加时会把 Handle 里所有的目标拿出来逐一查 ASC 并施加效果。七、小结这套设计的价值回头看整个EffectContainerMap体系环节数据结构时机静态配置FRPGGameplayEffectContainerTMap 里设计师在编辑器里填建 SpecFRPGGameplayEffectContainerSpecMake 时能力激活时填目标TargetData.AddAddTargetsAnimNotify 命中帧施加K2_ApplyGameplayEffectSpecToTarget命中帧之后这一套把配置、“发起者信息”、“目标信息”、施加彻底分离——设计师只管配表程序员只管在正确时机调正确方法。新增一种攻击效果不需要动任何 C 代码只在蓝图EffectContainerMap里加一行、改一改 GE 资产就完成了。这正是 ActionRPG 示例项目用 GAS 做游戏逻辑的最佳实践展示。