资讯动态

UE5 GAS 次级属性体系:AttributeSet 拆分与 Attribute Based 依赖配置

发布时间:2026/10/27 5:09:07 来源:尧图企业网站定制
1. 为什么要在 GAS 里单独拆一层次级属性做 UE5 GAS 的 RPG 项目属性系统一旦铺开很快就会遇到一个绕不开的坎基础属性Primary Attributes只有那么几个——力量、敏捷、智力、体质之类但角色面板上要显示的东西远不止这些。攻击力、暴击率、暴击伤害、护甲、法术抗性、最大生命、最大法力、生命回复、法力回复、移动速度加成……这些数值没有一个是玩家直接加点加出来的全部是由基础属性推导、叠加装备、再乘上各种系数算出来的。这一层就是所谓的次级属性Secondary Attributes。我见过不少项目一开始图省事把所有数值都塞进一个 AttributeSet 里结果就是属性依赖关系乱成一团麻改一点力量得手动在十几个地方同步更新攻击力和生命上限装备换一件又得写一堆回调去重算。等到后期数值策划要调平衡代码里全是硬编码的乘加公式改一个系数要翻半天。GAS 本身提供了非常清晰的属性分层思路次级属性单独建一个 AttributeSet用Attribute Based的机制让引擎自动帮你做依赖更新这才是正路。这篇内容适合已经跑通过 GAS 基础流程、能创建 AttributeSet、知道 GameplayEffect 怎么加属性的朋友。如果你还在纠结 GAS 是什么、AbilitySystemComponent 怎么挂建议先把基础打牢再来看这块。下面我会从设计思路、属性依赖的底层机制、具体创建步骤、初始化与设置的完整实现一直讲到实际踩过的坑和排查方法尽量把每个为什么这么设计都讲透让你看完能直接在自己的项目里复现一套可维护的次级属性体系。核心关键词先摆出来UE5、GAS、次级属性、AttributeSet、Attribute Based、GameplayEffect、属性初始化、依赖更新。这些词会贯穿全文你对照着自己的工程看就行。2. 整体设计思路与属性分层拆解2.1 基础属性与次级属性的职责边界先把分层这件事说清楚。基础属性是源头是玩家加点、装备词条、buff 直接作用的对象它的特点是变化频率低、语义单一。比如力量就是力量玩家点了一点力量这个值就加一它不关心力量最终换算成多少攻击力。次级属性是结果是面板上给玩家看的、参与战斗结算的数值它的特点是由其他属性推导而来、可能被多层修正。我一般的划分原则是这样的凡是策划能在角色创建界面让玩家分配的归基础属性凡是玩家看到但没法直接加的归次级属性。生命上限、法力上限、攻击力、护甲、暴击率这些典型次级属性全部由基础属性通过公式推导。这样划分的好处是数值策划调平衡时只需要动公式系数不用碰基础属性的加点逻辑两边解耦。这里有个容易忽略的点次级属性本身也可能成为别的属性的依赖源。比如最大生命是次级属性但生命回复可能依赖最大生命来算百分比回复。GAS 的依赖机制是支持链式的A 依赖 BB 依赖 C引擎会按拓扑顺序更新。但链子越长维护成本越高我建议依赖层级控制在两层以内超过两层就要警惕是不是设计过度了。2.2 为什么用 Attribute Based 而不是手动同步这是整个方案的核心决策点必须讲透。GAS 里属性变化有两种驱动方式一种是Instant/Infinite GameplayEffect 直接修改另一种是Attribute Based也就是一个属性的值由另一个属性的当前值通过系数和预乘加成算出来。手动同步的做法是力量变化时触发一个回调在回调里手动算攻击力再调 SetNumericAttributeBase 去改。听起来简单但问题一大堆。第一你得保证所有能改力量的地方都触发这个回调漏一个就出 bug第二多个属性同时变化时更新顺序不可控可能出现攻击力用旧力量算了一次、又用新力量算一次第三网络同步下客户端和服务端的计算时机不一致容易出现数值对不上。Attribute Based 的机制是引擎内建的。你在 AttributeSet 里给攻击力这个属性注册一个依赖声明它依赖力量并给出系数比如 1 点力量 2 点攻击力。之后只要力量通过 GameplayEffect 变化引擎在属性聚合阶段会自动重算攻击力顺序由引擎保证网络同步也走同一套流程。你不需要写任何手动同步代码这就是它最大的价值。代价是什么Attribute Based 的公式表达能力有限它本质是预乘加成 系数的线性组合复杂的非线性公式比如分段函数、开方、查表它表达不了。遇到这种情况我的做法是能用 Attribute Based 表达的走引擎表达不了的用一个专门的 GameplayEffect 在特定时机比如升级、装备变化去重算而不是每帧轮询。这个取舍后面在实操部分会再展开。2.3 属性集拆分策略一个 AttributeSet 塞太多属性会带来两个问题一是网络同步的带宽浪费二是 GameplayEffect 的 modifier 匹配变复杂。我的经验是按变化频率 归属来拆。基础属性一个集比如叫 PrimaryAttributeSet次级属性一个集SecondaryAttributeSet如果项目有战斗相关的临时属性比如连击数、怒气再单独一个集。拆开之后GameplayEffect 在配置 modifier 时就能精确指定作用于哪个 AttributeSet 的哪个属性避免误伤。而且不同 AttributeSet 可以挂不同的初始化逻辑基础属性在角色初始化时赋初值次级属性则完全靠依赖自动推导初始化时甚至不需要手动赋值——这一点很多人第一次做会搞错后面会专门讲。3. 次级属性 AttributeSet 的创建与依赖配置3.1 创建 C AttributeSet 类第一步是建类。在项目里新建一个继承自 UAttributeSet 的 C 类命名上我习惯用USecondaryAttributeSet前缀 U 是 UE 的惯例。头文件里用GAMEPLAYATTRIBUTE_PROPERTY_GETTER和GAMEPLAYATTRIBUTE_VALUE_GETTER/SETTER宏来声明每个属性这些宏会自动生成 Get/Set 函数和属性注册信息省掉大量样板代码。以攻击力和最大生命为例声明大概长这样UPROPERTY(BlueprintReadOnly, Category Secondary Attributes) FGameplayAttributeData AttackPower; ATTRIBUTE_ACCESSORS(USecondaryAttributeSet, AttackPower); UPROPERTY(BlueprintReadOnly, Category Secondary Attributes) FGameplayAttributeData MaxHealth; ATTRIBUTE_ACCESSORS(USecondaryAttributeSet, MaxHealth);ATTRIBUTE_ACCESSORS这个宏是关键它一次性生成 GetAttackPower、SetAttackPower、InitAttackPower 等一整套函数。注意属性类型必须是FGameplayAttributeData不能直接用 float否则 GAS 的聚合机制不认。这里有个实操细节属性声明顺序会影响网络同步的索引。GAS 在同步属性时是按声明顺序打包的如果你在开发中途插入新属性到中间位置会导致旧存档或旧客户端的索引错位。我的做法是新属性一律追加到末尾或者干脆在项目早期就把属性列表定死后期只增不改顺序。3.2 用 GetLifetimeReplicatedProps 处理网络同步多人项目里AttributeSet 的属性需要同步。在 cpp 里重写GetLifetimeReplicatedProps对每个属性调用DOREPLIFETIME_CONDITION_NOTIFY条件用COND_None加REPNOTIFY_Always。这个组合的意思是无论值有没有变都同步并且触发 OnRep 回调。为什么用 Always 而不是默认的变了才同步因为 GAS 的属性聚合在客户端需要完整的值来重算依赖如果漏掉某次同步客户端的次级属性就会算错。void USecondaryAttributeSet::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME_CONDITION_NOTIFY(USecondaryAttributeSet, AttackPower, COND_None, REPNOTIFY_Always); DOREPLIFETIME_CONDITION_NOTIFY(USecondaryAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always); }每个属性还要配一个OnRep_XXX函数里面调用GAMEPLAYATTRIBUTE_REPNOTIFY宏把旧值传进去让 GAS 知道这个属性在客户端更新了需要重算依赖它的属性。这一步漏了客户端面板就会显示旧数值是新手最常见的坑之一。3.3 注册属性依赖Attribute Based 的核心配置依赖配置在 AttributeSet 的构造函数里做。用FAttributeBasedFloat或者更常用的FGameplayAttributeData配合AttributeBased的 modifier 来声明。实际写法上我习惯在构造函数里用FGameplayAttribute找到依赖源然后设置系数。举个具体例子攻击力依赖力量1 点力量 2 点攻击力同时攻击力还受一个攻击力加成百分比的预乘影响。配置逻辑是找到 PrimaryAttributeSet 里的 Strength 属性设置Coefficient 2.0fPreMultiplyAdditiveValue设为 0PostMultiplyAdditiveValue设为加成值。这样最终攻击力 (力量 × 2 预乘) × (1 加成)。需要强调的是依赖配置必须在构造函数里完成且依赖源属性必须已经注册。如果 PrimaryAttributeSet 还没被 GAS 识别构造函数里找属性会失败。所以初始化顺序是先确保 PrimaryAttributeSet 挂上 ASC再挂 SecondaryAttributeSet。这个顺序在蓝图里挂组件时也要注意我一般用 C 在 PossessedBy 或 BeginPlay 里按顺序 AddAttributeSet而不是在蓝图里手动拖。3.4 属性依赖的拓扑顺序问题当次级属性之间存在依赖比如生命回复依赖最大生命引擎会按注册顺序做拓扑排序。但如果你在构造函数里配置依赖时被依赖的属性还没声明排序就会出错。我的经验是在构造函数里按依赖链从底向上声明属性先声明被依赖的最大生命再声明依赖它的生命回复。这样引擎注册时天然就是正确顺序不用依赖它自己排序。如果确实出现了循环依赖A 依赖 BB 又依赖 A引擎不会报错但会陷入无限重算或者算出错误值。这种情况在设计阶段就要杜绝属性依赖必须是有向无环图。我在评审属性表时会画一张依赖关系图肉眼检查有没有环这比事后 debug 省事得多。4. 次级属性的初始化与设置实现4.1 初始化时机的选择次级属性的初始化有个反直觉的点你通常不需要手动给次级属性赋初值。因为它们是依赖推导出来的只要基础属性有值次级属性在第一次聚合时就会自动算出来。手动赋初值反而可能覆盖掉正确的推导结果。但有一个例外如果某个次级属性没有依赖源比如某些项目里护甲是纯装备提供的不依赖基础属性那它就需要一个初始 GameplayEffect 来赋值。我的做法是区分对待有依赖的次级属性初始化时什么都不做靠依赖自动算无依赖的用一个 InitGameplayEffect 在角色初始化时 Apply 一次把基础值设进去。初始化时机上我一般在PossessedBy服务端和OnRep_PlayerState客户端里调用一个统一的InitializeAttributes函数里面 Apply 基础属性的初始化 GE。次级属性不单独初始化等基础属性 GE 生效后依赖链会自动把次级属性带出来。这个流程跑通一次之后后面加新次级属性几乎零成本。4.2 用 GameplayEffect 驱动属性变化属性变化统一走 GameplayEffect这是 GAS 的铁律不要用 SetNumericAttributeBase 直接改除非是初始化这种特殊场景。加点的 GE 配置成 Instantmodifier 作用于基础属性比如力量 1。装备的 GE 配置成 Infinitemodifier 作用于次级属性的加成项。这里要区分两种 modifier 的作用对象作用于基础属性的会触发依赖重算作用于次级属性本身的是直接叠加。比如攻击力 50这种装备词条如果直接加到 AttackPower 上它和依赖推导出来的值是怎么合并的答案是 GAS 会把所有来源的值聚合基础值依赖算出来的 各种 additive modifier 乘法 modifier。所以装备加攻击力不会破坏依赖关系两者是叠加的。配置 GE 时有个细节modifier 的ModifierOp要选对。Add 是加法叠加Multiply 是乘法叠加AddFinal 是在最后加。我一般用 Add 做固定值加成用 Multiply 做百分比加成避免顺序问题。如果同一个属性既有加法又有乘法 modifierGAS 的聚合顺序是先算所有 Add再算 Multiply最后 AddFinal。理解这个顺序配数值时就不会算错。4.3 属性设置的完整代码流程把上面的东西串起来一个完整的次级属性设置流程是这样的角色类里声明TObjectPtrUSecondaryAttributeSet SecondaryAttributeSet在构造函数里 CreateDefaultSubobject。在PossessedBy里拿到 ASC调用AddAttributeSetSubobject把 SecondaryAttributeSet 加进去注意要在 PrimaryAttributeSet 之后加。调用InitializeAttributesApply 基础属性的初始化 GE。基础属性 GE 生效后引擎自动触发依赖重算次级属性得到正确值。客户端通过OnRep_PlayerState走同样的流程保证两端一致。代码上AddAttributeSetSubobject是模板函数直接传指针即可。初始化 GE 用FGameplayEffectContextHandle构造通过ASC-ApplyGameplayEffectToSelf应用。这里要注意 GE 的DurationPolicy必须是 Instant否则初始化值不会立即生效。4.4 面板显示与调试属性算对了还得能看见。我一般在角色里加一个调试用的函数把 ASC 里所有属性的 CurrentValue 和 BaseValue 打印出来。CurrentValue 是聚合后的最终值BaseValue 是不含 modifier 的基础值两者对比能快速定位是依赖算错了还是 modifier 配错了。更直观的做法是做一个简单的 UMG 面板绑定属性的 Get 函数。GAS 提供了GetGameplayAttributeValueChangeDelegate可以监听某个属性的变化变化时刷新 UI。这个委托在属性每次聚合后触发比每帧轮询高效得多。我在项目里会给每个要显示的属性绑一个委托回调里更新对应的 TextBlock实测下来很稳。5. 常见问题与排查技巧实录5.1 次级属性不更新或显示为 0这是最高频的问题。排查顺序我总结成一张表现象可能原因排查方法次级属性始终为 0依赖源属性未注册或未初始化打印基础属性 CurrentValue确认非 0基础属性变了次级不变依赖配置未生效检查构造函数里依赖是否在属性声明之后配置客户端数值与服务端不一致OnRep 未调用或 REPNOTIFY 配置错检查 DOREPLIFETIME 条件和 OnRep 函数数值翻倍或异常大依赖被重复注册检查 AttributeSet 是否被 Add 了两次最常见的是第一种基础属性初始化 GE 没生效导致依赖源是 0次级属性自然算出来也是 0。我踩过一次坑GE 的 DurationPolicy 配成了 Infinite结果初始化值一直不落地改成 Instant 就好了。还有一种情况是 AttributeSet 加到了错误的 ASC 上比如加到了 PlayerState 的 ASC 但角色用的是 Character 的 ASC两边对不上。5.2 依赖链导致的性能问题属性依赖是每次聚合都重算的如果依赖链很长、属性很多聚合开销会上升。我实测过一个极端案例20 个次级属性每个依赖 3 个基础属性每次基础属性变化触发约 60 次重算。单次重算很快但如果在战斗中每帧都有属性变化比如持续伤害每帧扣血累积开销就不可忽视了。优化思路有两个一是减少不必要的依赖能用 GameplayEffect 在特定时机算的不要挂成实时依赖二是控制属性变化频率持续伤害改成每 0.5 秒结算一次而不是每帧。这两个调整下来聚合开销能降一个数量级。另外调试期可以打开 GAS 的LogAbilitySystem日志看属性聚合的调用次数定位热点。5.3 实操心得与避坑清单最后分享几条从实际项目里攒下来的经验都是文档里不会写的属性命名加前缀区分层级基础属性用Primary_前缀次级用Secondary_在 GE 的 modifier 下拉框里一眼就能分清减少配错。依赖系数集中管理不要把 2.0、1.5 这种魔法数字散落在构造函数里抽一个 DataTable 或配置类统一管理策划调平衡时不用改代码。初始化 GE 用单独的类不要和战斗用的 GE 混在一起初始化 GE 只做赋值不挂任何额外逻辑避免初始化时触发意外的回调。客户端预测要谨慎次级属性的依赖重算在客户端预测下可能出现短暂不一致如果项目对数值精度要求高关键属性走服务端权威客户端只做显示。加属性前先想清楚归属新加一个属性时先问它是基础还是次级、依赖谁、被谁依赖想清楚再动手比事后重构省太多事。这套次级属性体系我在几个项目里复用下来最大的感受是前期多花半天把依赖关系理清楚后期能省掉无数个为什么这个数值不对的排查。GAS 的 Attribute Based 机制不是银弹但它把属性依赖这件最容易出错的事交给了引擎你只需要把依赖关系声明对剩下的它帮你兜底。真正需要你操心的是设计层面的分层是否合理、依赖图是否有环、初始化顺序是否正确——这些想明白了代码写起来其实很快。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑