导语:某个周五晚上,策划在群里发了三条新需求:火球术对被冰冻的目标伤害翻倍;点燃 DoT 不吃这条加成;暴击时点燃伤害也要跟着翻倍。用 if-else 写技能的程序盯着屏幕沉默了半分钟——这三条规则要分别插进伤害公式、DoT 结算、暴击判定三个互不相干的函数,而下周还要合入四个新技能。这个晚上,这个项目决定重写技能系统。MMO 的技能数量是以百计的,组合规则是以千计的,本章讲的就是那个把「千级组合」驯服成「五种资产」的框架:游戏能力系统(Gameplay Ability System, GAS)。
本章地图
mindmap root((第22章
GAS 一阶)) 系统策划补课 火球术解剖:五个子系统 组合复杂度杀死 if-else SDD 技能描述文档 GAS 五大件 ASC 结算中心 AttributeSet 账本 GameplayAbility 业务流程 GameplayEffect 统一货币 GameplayTag 通用语言 属性结算 Base 与 Current 两本账 Modifier 四种运算符 A["(Base+Add)×Mul 链"] 统一货币 GE 掉血/回血/加速/Buff 全是 GE 四种时长策略 DoT 即 Periodic Tag 三位一体 状态:眩晕中 权限:沉默禁放 标识:火系技能 远港 Online 落地 冲锋:位移+眩晕 顺劈:扇形 AOE 盾墙:减伤 Buff 诚实警告 学习曲线陡峭 什么时候别碰 GAS
本章的顺序是先破后立:先拆一个 WOW 技能看清复杂度从哪来,再引入 GAS 的五个核心概念,最后把「远港 Online」战士的三个技能完整配置一遍。读到一半觉得抽象是正常的——GAS 是 UE 所有系统里概念密度最高的一个,值得来回读两遍。
22.1 补课·系统策划:一发火球术里藏着五个系统
补课:本节补系统策划的建模思维——把技能看作一组规则的交点,比看成一个功能更有用。策划出身的读者可以直接看 22.1 末尾的 SDD 表格。
以《魔兽世界》的火球术为标本。不同资料片数值有出入,机制结构二十年没变过,拆开看是五行描述:
| 维度 | 数值(示例口径) | 挂钩的子系统 |
|---|---|---|
| 读条 | 2.25 秒,可被打断 | 施法状态机、打断/沉默规则 |
| 消耗 | 法力值 85 点 | 资源系统(蓝条) |
| 直接伤害 | 320 + 法强 × 0.95 | 伤害公式、属性系统 |
| 暴击 | 伤害 ×2,概率取自暴击率 | 属性系统、随机判定 |
| 点燃 | DoT 每 2 秒 48 伤,持续 8 秒 | 持续效果(DoT)系统 |
五行字,每一行都是另一个系统的入口。火球术的生命周期早已超越了"按一下键掉一管血"——它是一条跨越好几秒的流水线:
stateDiagram-v2
[*] --> 读条中: 按下技能键
读条中 --> 已取消: 移动/被打断/被沉默
读条中 --> 出手: 2.25s 读满
出手 --> 命中判定: 弹道飞行
命中判定 --> 结算直接伤害: 未抵抗
命中判定 --> 未命中: 抵抗/免疫
结算直接伤害 --> 附加点燃: 天赋已点出
附加点燃 --> DoT跳动: 每 2s 一跳
DoT跳动 --> DoT跳动: 剩余跳数 > 0
DoT跳动 --> [*]: 8s 结束或被驱散
已取消 --> [*]
未命中 --> [*]这一发技能要命吗?不——真正作祟的是叠在它上面的修饰规则。回到导语里策划的三条需求:「对被冰冻的目标伤害翻倍」要插进伤害公式;「点燃不吃这条加成」要在 DoT 结算里加一个例外分支;「暴击时点燃也翻倍」要求 DoT 能读到直接伤害的暴击结果。三条规则,落进三个互不相干的函数。
再往上叠:天赋「火焰强化」全局火系 +10%,装备词条「施法速度 +8%」,团队 Buff「奥术光辉」法强 +40,目标身上的 Debuff「元素诅咒」火系易伤 +15%,还有吸收盾要优先扣减……每个来源都想在公式里占一个位置。
还有一类规则连公式都不进,直接横切在状态机上:沉默禁止读条起手,眩晕打断进行中的施法,移动取消吟唱,目标死亡丢失弹道。读条、瞬发、引导三种施法形态,与这些横切规则的交互各不相同——引导类技能(比如暴风雪)被击退后是重新定向而非中断,沉默却只拦截读条的起手瞬间。策划随口一句"这个技能被打断时返还一半蓝耗",程序就要在取消分支里再接一根管子到资源系统。
算一笔规模账。一个中型 MMO 按 12 个职业、每职业 24 个技能估算,约 300 个技能;每个技能平均挂 6 类修饰来源。if-else 写法的维护成本是 O(技能数 × 修饰来源数)——任何一个新需求都要回归 1800 个交点。项目到第二年,改一条规则引出三个线上事故是常态。复杂度藏在哪里?组合里。它几乎从来不被"写错",但一定会在某个转折点被组合撑爆。
出路只有一条:把"效果"从"技能"里抽出来,立成一等公民。技能不再直接改数字,它只负责签发标准化的效果凭证;结算中心统一处理凭证与属性的换算。这正是 GAS 的世界观。
SDD:策划与程序之间的合约
框架落地的第一步不在引擎里,在文档里。技能描述文档(Skill Design Document, SDD)是系统策划的标准产出物,一个技能一页,字段固定:
| 字段 | 火球术示例 | 说明 |
|---|---|---|
| ID / 名称 | Skill_Mage_Fireball | 全局唯一,程序按 ID 索引 |
| 类型 | 主动 / 读条 | 主动、瞬发、被动、光环四类 |
| 资源消耗 | 法力 85 | 指向哪个资源属性 |
| 冷却 | 无(GCD 1.5s) | 独立冷却与公共冷却分开写 |
| 读条 | 2.25s,可打断 | 移动是否取消要写明 |
| 效果列表 | 伤害 320+SP×0.95;点燃 48×4 跳 | 每条效果:数值 / 系数 / 来源属性 |
| 标签 | 火系、法术、投射物 | 供免疫、易伤、套装检索 |
| 网络需求 | 服务器权威结算 | 命中与伤害以服务器为准 |
「效果列表」和「标签」两栏是重点——它们直接预告了本章后半的两个主角:GameplayEffect 和 GameplayTag。策划按这个格式写,程序按这个格式建资产,两边不再靠口头约定。「远港 Online」从第一天起就执行一条纪律:技能没进 SDD 表,程序有权不接。
22.2 GAS 五大件:一张图先看懂
GAS 的概念多到劝退,但骨架只有五根。先给一组类比,把抽象名词钉在直觉上——把 GAS 想成一家银行的结算体系:
- AbilitySystemComponent(ASC):总行结算中心。一切业务都经过它,它不知道"火球术"是什么,只认凭证和账本。
- AttributeSet:账本。血量、法力、攻击力,每个科目一页账,记两栏数字。
- GameplayAbility:业务流程单。开户、转账、冻结,是"能做的事"的标准描述。
- GameplayEffect(GE):统一货币,或者说转账凭证。一切数值变化都必须兑换成它才能入账。
- GameplayTag:全行通用的科目代码与状态代码,所有单据靠它对暗号。
五者的持有与引用关系,这是本章的灵魂图:
classDiagram
class UAbilitySystemComponent {
<<结算中心>>
+TryActivateAbility()
+ApplyGameplayEffectToTarget()
+持有 AttributeSet 数组
+持有 AbilitySpec 列表
}
class UAttributeSet {
<<账本>>
+Health: FGameplayAttributeData
+AttackPower: FGameplayAttributeData
+PostGameplayEffectExecute()
}
class UGameplayAbility {
<<业务流程>>
+CanActivateAbility()
+CommitAbility()
+技能 Tag 容器
}
class UGameplayEffect {
<<数值凭证>>
+Modifiers 数组
+DurationPolicy
+授予/需求/屏蔽 Tag
}
class FActiveGameplayEffect {
<<运行中的效果实例>>
+剩余时长
+来源与目标
}
class FGameplayTagContainer {
<<通用语言>>
+State.Debuff.Stun
+Ability.School.Fire
}
UAbilitySystemComponent "1" *-- "n" UAttributeSet : 挂账
UAbilitySystemComponent "1" *-- "n" UGameplayAbility : 授予为 Spec
UAbilitySystemComponent ..> FActiveGameplayEffect : 应用 GE 后生成
UGameplayEffect "1" *-- "n" FActiveGameplayEffect : 实例化
UGameplayAbility ..> UGameplayEffect : 签发(伤害/消耗/冷却/Buff)
UGameplayEffect ..> UAttributeSet : Modifier 改写属性
UGameplayAbility ..> FGameplayTagContainer : 激活条件与分类
UGameplayEffect ..> FGameplayTagContainer : 授予状态与权限通用语言>运行中的效果实例>数值凭证>业务流程>账本>结算中心>读图要点:ASC 是唯一的交通枢纽,Ability 和 Effect 都不直接碰对方,更不直接碰属性——一切经由 ASC 结算。运行时真正"活着"的效果是 FActiveGameplayEffect,它有剩余时长、能被查询、能被移除;UGameplayEffect 只是它的资产模板,好比合同模板与已签署合同的关系。
这里埋着 GAS 最容易拧巴的一层:资产与实例分离,而且是两层实例。UGameplayAbility 是资产,授予到 ASC 上变成 FGameplayAbilitySpec,带等级、带输入绑定;UGameplayEffect 是资产,应用时先打包成 FGameplayEffectSpec(带等级、上下文、来源),落到目标身上才成为 FActiveGameplayEffect。调试时在编辑器细节面板里看到的是实例层,改数值改的却是资产层——新手最常见的乌龙,就是对着运行时实例找半天,忘了数据在 .uasset 里。
启用层面只有两步:在 .uproject 里打开 GameplayAbilities 插件,在模块的 Build.cs 里加 GameplayAbilities、GameplayTags、GameplayTasks 三个依赖(对应官方文档:Unreal Engine → Gameplay Ability System 词条,5.8 的设置细节以文档为准)。
ASC 住在哪,是个有讲究的问题。玩家角色的 ASC 建议挂在 PlayerState 上(见第 3 章):角色死亡换 Pawn,ASC 和属性账本跟着 PlayerState 走,Buff 不掉。AI 的 ASC 直接挂 Character 即可,死了连同 Actor 一起销毁,没有续存需求。「远港 Online」的战士、法师都用前一方案,野怪用后一方案。
输入怎么接到 Ability 上?两条路。原型期走 AbilityLocalInputPressed 绑整型 ID,够用但僵硬;正规做法是按 Tag 绑定——Enhanced Input 的动作(见第 5 章)触发时带着 InputTag.Ability.Charge 去找 Spec 上挂了同一 Tag 的技能激活。技能换键位、一键多技能、手柄映射,全是配置层的事,代码零改动。「远港 Online」从第二个技能起就只用 Tag 绑定。
22.3 属性:Base 是存款,Current 是可用余额
AttributeSet 里每个属性都是 FGameplayAttributeData,里面有两栏账,而非孤零零一个 float:
- BaseValue:基础值。等级成长、永久加成写在这里,是"存款本金"。
- CurrentValue:当前值。装备、Buff、Debuff 全部结算完之后玩家实际生效的数字,是"可用余额"。
GE 从不改写 BaseValue(Instant 类除外,后面讲)。它携带的是修饰器(Modifier),挂在账本旁边参与计算。修饰器有四种运算符:Additive(加)、Multiplicitive(乘)、Division(除)、Override(覆盖)。ASC 的聚合器按固定顺序结算:
若存在 Override 修饰器,则忽略其余全部,直接取覆盖值。文字版读法:先把所有加法修饰器求和加到基础上,再依次乘上所有乘法修饰器,最后除以除法修饰器。顺序是写死的,所以「先加后乘」和「先乘后加」的歧义在 GAS 里不存在——这是框架消灭的第一类 bug。
来一遍完整演算。「远港 Online」的战士,基础攻击力来自等级成长:Base = 100。身上挂着三个效果:
- 装备「远航巨斧」:Additive +30
- Buff「战吼」:Additive +15
- 装备「潮汐护符」:Multiplicitive ×1.2
代入公式:Final = (100 + 30 + 15) × 1.2 = 174。下面这张层级图把结算管道画出来,注意巨斧和战吼在同一层求和,护符在下一层做乘法——层与层之间不可交换:
两本账解决的是「效果可撤销」问题。if-else 时代,脱下装备要人肉把 +30 减回去,Buff 到期要人肉恢复——只要漏一处,玩家的攻击力就永远漂移。GAS 里装备和 Buff 都是 GE,脱下或到期就是移除这个 GE,聚合器重新结算,Current 自动归位。没人改 Base,就没有还原的负担。
血量是个特例:掉血直接改 BaseValue(用 Instant 类型的 GE),因为伤害属于"已经发生的既成事实"——它不是挂着的可撤销效果。CurrentValue 永远从 Base 加修饰器重算,所以回血后当前血量也要被 MaxHealth 钳制——这类收尾逻辑写在 AttributeSet 的 PostGameplayEffectExecute 里。
顺手分清两个回调的工种。PreAttributeChange 在修饰器即将应用时触发,做前置钳制,比如"新血量不许越过上限";PostGameplayEffectExecute 在整个 GE 结算落账之后触发,掉血后的死亡判定、怒气攒满后的技能高亮,都挂在这里。另一点容易想岔:怒气、连击点、能量条这类"资源"同样建模为属性,放技能消耗 30 怒气就是一次 Instant GE 的扣减——资源系统和属性系统同属一本账——只是科目不同而已。
一个最小可用的 AttributeSet 长这样,解决"属性声明 + 网络复制 + 血量钳制"三件事:
// 战士属性集:血量与攻击力。配合 ASC 挂在 PlayerState 上。
UCLASS()
class FHO_API UWarriorAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UPROPERTY(ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UWarriorAttributeSet, Health)
UPROPERTY(ReplicatedUsing = OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UWarriorAttributeSet, MaxHealth)
UPROPERTY(ReplicatedUsing = OnRep_AttackPower)
FGameplayAttributeData AttackPower;
ATTRIBUTE_ACCESSORS(UWarriorAttributeSet, AttackPower)
virtual void PostGameplayEffectExecute(
const FGameplayEffectModCallbackData& Data) override;
UFUNCTION() void OnRep_Health(const FGameplayAttributeData& OldValue)
{ GAMEPLAYATTRIBUTE_REPNOTIFY(UWarriorAttributeSet, Health, OldValue); }
UFUNCTION() void OnRep_MaxHealth(const FGameplayAttributeData& OldValue)
{ GAMEPLAYATTRIBUTE_REPNOTIFY(UWarriorAttributeSet, MaxHealth, OldValue); }
UFUNCTION() void OnRep_AttackPower(const FGameplayAttributeData& OldValue)
{ GAMEPLAYATTRIBUTE_REPNOTIFY(UWarriorAttributeSet, AttackPower, OldValue); }
};要点只有三行:ATTRIBUTE_ACCESSORS 宏生成 Get/Set 访问器,让 GE 能按名字找到属性;每个属性配一个 OnRep 走复制通道(机制见第 21 章);钳制逻辑放 PostGameplayEffectExecute,比如 SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()))。属性的初始值(1 级战士血量 500 之类)用 DataTable 配合 InitStats 灌入,别硬编码。
22.4 GameplayEffect:一切数值变化的统一货币
现在给 GE 一个砸实的定位:在 GAS 的世界里,任何数值变化都必须先兑换成 GE,不允许私改账本。
掉血是 GE(Instant,攻击力 −40)。回血是 GE(Instant,血量 +60)。加速是 GE(Duration,移动速度 ×1.3,持续 6 秒)。上 Buff 是 GE(Duration,授予 Tag + 挂修饰器)。甚至连技能的"消耗"和"冷却"也是 GE——CommitAbility 干的事就是把 Cost GE 和 Cooldown GE 各应用一次。
为什么要这样自缚手脚?类比数据库:GE 就是事务。它带来源、带目标、带撤销语义、带网络同步。全部变化走同一个管道,才谈得上统一的日志、统一的回放、统一的反作弊校验(第 28 章会用到这一点)。谁绕过 GE 直接 SetHealth,谁就破坏了这个闭合循环——代码评审时见到直接改属性的调用,一律打回。
GE 按时长分四种策略,选对策略是配置的一半:
| 时长策略 | 语义 | 典型用途 |
|---|---|---|
| Instant | 立即结算,直接改写 BaseValue | 掉血、回血、扣蓝 |
| Duration | 挂修饰器,到期自动移除 | 加速、盾墙减伤 |
| Infinite | 挂修饰器,直到被显式移除 | 装备词条、被动光环 |
| Periodic | 在 Duration/Infinite 上按周期执行 Instant | 点燃 DoT、回血 HoT |
一个"点燃"Debuff 拆开看就是:Duration = 8 秒、Period = 2 秒的 Periodic GE,每跳执行一次"血量 −48"的 Instant 结算,到期自动销毁。22.1 里那条让程序沉默的状态机,翻译成资产配置只有五行字段。
一次 GE 从签发到入账的完整管道:
flowchart TD
A["技能/装备/场景触发"] --> B["MakeOutgoingGameplayEffectSpec
封装 GE 模板 + 等级 + 上下文"]
B --> C{"目标 ASC 校验
免疫 Tag / 应用条件"}
C -- "不通过" --> D["拒绝入账,记日志"]
C -- "通过" --> E["生成 FActiveGameplayEffect"]
E --> F{"时长策略?"}
F -- "Instant" --> G["立即改写属性 BaseValue"]
F -- "Duration / Infinite" --> H["修饰器挂入聚合器
重算 CurrentValue"]
F -- "Periodic" --> I["注册周期计时器
每跳执行一次 Instant"]
G --> J["广播属性变化 + 复制到客户端"]
H --> J
I --> J
H -- "到期/被移除" --> K["修饰器摘除
Current 自动回落"]
I -- "跳数耗尽" --> K管道里有两个扩展点值得知道名字。Modifier 的数值不想写死,可以换 ModMagnitudeCalculation(MMC)从来源属性算——比如"点燃每跳伤害 = 施法者法强 × 0.3"。整套结算不够用时还有 Execution Calculation 做完全自定义,「远港 Online」的伤害公式(攻防换算、暴击、易伤)就收在一个自定义 Execution 里,这是后话,配置细节以官方文档 GameplayEffect 词条为准。
回头把导语留下的第三道题解掉:暴击时点燃也翻倍。钥匙是 GE 的上下文(Effect Context)——每次应用 GE 都随身带一包信息:来源 ASC、施法者、命中结果(含这次伤害是否暴击)。点燃 GE 的 MMC 从上下文里读到"直接伤害已暴击",系数乘 2,收工。if-else 时代要横跨三个函数人肉传参的活,在框架里只是读一个字段——凭证自带附件,这是统一货币的隐藏福利。
数值来源实际有三种写法,按复用粒度选:固定值写在 GE 资产里,一个效果一份;MMC 从属性实时算,公式归一处;SetByCaller 由调用方按 Tag 名传参——技能激活时把"系数 1.6"塞进 Spec,同一个伤害 GE 模板被冲锋、顺劈、盾击三个技能复用,各自注入不同系数。「远港 Online」的伤害 GE 全走 SetByCaller,这是"第四个技能不造新 GE"承诺的技术底座。
把上面管道落成伪代码,看聚合器怎么工作:
# 伪代码:ASC 对一个属性做聚合结算(对应引擎 FAggregator 的简化模型)
def evaluate_attribute(attr, active_effects):
adds, muls, divs, override = [], [], [], None
for ge in active_effects: # 遍历挂在 ASC 上的所有活跃 GE
for mod in ge.modifiers:
if mod.target != attr: continue # 只取关心本属性的修饰器
mag = mod.get_magnitude(ge) # 固定值或经 MMC 计算
if mod.op == ADD: adds.append(mag)
elif mod.op == MULTIPLY: muls.append(mag)
elif mod.op == DIVIDE: divs.append(mag)
elif mod.op == OVERRIDE: override = mag # 后到的覆盖先到的
if override is not None:
return override
value = attr.base_value + sum(adds) # 先加
for m in muls: value *= m # 再乘
for d in divs: value /= d # 后除
return value
# 调用:战士攻击力
# active_effects = [GE_巨斧(+30), GE_战吼(+15), GE_护符(×1.2)]
# → (100 + 45) × 1.2 = 174,与 22.3 的演算一致这段伪代码同时是调试口诀:线上发现属性不对,先列出目标身上全部活跃 GE,再逐个查修饰器——范围永远收敛在"GE 列表 × Modifier 列表"里,不用翻全工程。
22.5 GameplayTag:状态、权限、标识,三位一体
Tag 是层级化的字符串标签,写成点分路径:State.Debuff.Stun、Damage.School.Fire、Ability.Warrior.Charge。层级的意义在匹配:State.Debuff 是父节点,查询"是否中了任意 Debuff"时用父 Tag 一问,眩晕、沉默、减速全部命中。Tag 在 Project Settings → GameplayTags 里集中登记,禁止代码里随手造——散落在代码各处的裸字符串,是 GAS 项目腐化的第一个征兆。
Tag 在 GAS 里同时干三件事,这是它"通用语言"地位的来历:
其一,状态:描述"我现在是什么样"。 被冲锋眩晕,就往目标 ASC 上挂一个授予 State.Debuff.Stun 的 GE,到期 Tag 自动摘除。动画系统、AI 系统、UI 系统都来查这同一个 Tag:动画播僵直、AI 停止决策、头像框亮眩晕图标。一个状态,四处消费,源头只有一处。
其二,权限:描述"我现在能干什么"。 每个技能的 ActivationBlockedTags 配上 State.Debuff.Silence,被沉默的目标所有技能统统点不亮——不用给每个技能单独写"沉默时禁用"。反过来,ActivationRequiredTags 表达"需要某状态才能放",比如战士的处决技要求目标带 State.LowHealth。
其三,标识:描述"这是个什么东西"。 火球术挂 Damage.School.Fire,冰甲术给自己挂"受到的 Damage.School.Fire 伤害减半"的 MMC 判断,套装效果检索"装备 4 件后所有 Damage.School.Fire 技能暴击 +10%"。新加一个火系技能,只要打上同样的 Tag,存量规则自动生效——这是 22.1 那个 O(技能×修饰) 灾难的解药。
三种身份拧在同一张关系网里:
erDiagram
ABILITY ||--o{ GAMEPLAY_EFFECT : "签发(伤害/消耗/冷却/Buff)"
GAMEPLAY_EFFECT ||--o{ MODIFIER : "携带"
MODIFIER }o--|| ATTRIBUTE : "改写"
GAMEPLAY_EFFECT ||--o{ GAMEPLAY_TAG : "授予 GrantedTags"
GAMEPLAY_EFFECT ||--o{ GAMEPLAY_TAG : "免疫/屏蔽"
ABILITY ||--o{ GAMEPLAY_TAG : "标识 AbilityTags"
ABILITY ||--o{ GAMEPLAY_TAG : "激活条件 Required/Blocked"
ABILITY {
string 技能ID
float 冷却
string 消耗指向属性
}
GAMEPLAY_EFFECT {
string 时长策略
float 周期
}
MODIFIER {
string 运算符
float 数值或MMC
}
ATTRIBUTE {
float Base
float Current
}
GAMEPLAY_TAG {
string 层级路径
}读法:ABILITY 与 TAG 之间有两条边——"我是什么"(标识)和"我何时能放"(权限);GE 与 TAG 之间也有两条边——"我给你挂什么状态"和"你免疫我吗"。技能、效果、属性三方的协作全部由 Tag 这个中间语言撮合,谁也不需要 import 谁。
两个工程细节提前打个桩。匹配规则:HasTag 默认父匹配子,查 State.Debuff 能命中 State.Debuff.Stun;引擎也提供精确匹配开关,但九成场景不该用它——精确匹配会悄悄废掉层级设计的意义,新增 State.Debuff.Freeze 时旧代码集体漏判。复制规则:Tag 随 GE 的活跃状态走既有网络通道,客户端查"目标是否被眩晕"拿到的是复制回来的结果,不必自写 RPC,与第 21 章的复制条件联动。
权限校验落到代码里很轻。技能基类的激活检查大致长这样:
// 技能激活前的 Tag 闸门:对应 CanActivateAbility 的核心逻辑
bool UWarriorAbilityBase::CheckActivationTags(
const UAbilitySystemComponent* ASC) const
{
// 沉默中:一切技能禁放(策划在 ActivationBlockedTags 配好)
if (ASC->HasAnyMatchingGameplayTags(ActivationBlockedTags))
return false;
// 需要目标处于某状态(如处决要求目标低血)
if (!ActivationRequiredTags.IsEmpty() &&
!ASC->HasAllMatchingGameplayTags(ActivationRequiredTags))
return false;
return true;
}
void UWarriorAbilityBase::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* Info,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerData)
{
if (!CheckActivationTags(Info->AbilitySystemComponent.Get()))
{ EndAbility(Handle, Info, ActivationInfo, true, true); return; }
// CommitAbility:扣消耗 GE、挂冷却 GE,一次提交
if (!CommitAbility(Handle, Info, ActivationInfo))
{ EndAbility(Handle, Info, ActivationInfo, true, true); return; }
ApplySkillEffects(Info); // 各技能签发自己的伤害/控制 GE
}注意 CommitAbility 的位置:消耗和冷却在激活成功的同一刻入账。顺序错了(先放技能后扣费),就会出现"没蓝也能出手"的线上事故。
22.6 「远港 Online」:战士三技能落地
理论收拢,上配置。先看一次技能释放在网络上的完整旅程——GAS 天生为服务器权威设计,结算全部发生在服务器,客户端拿复制回来的结果(复制机制见第 21 章;按下去立刻有反馈的本地预测是第 23 章的主菜):
sequenceDiagram
autonumber
participant P as 玩家客户端
participant ASC_C as 客户端 ASC
participant S as 服务器
participant ASC_S as 服务器 ASC
participant ATTR as 目标 AttributeSet
P->>ASC_C: 按下冲锋(Enhanced Input,见第 5 章)
ASC_C->>S: RPC: TryActivateAbility(冲锋)
S->>ASC_S: 服务器侧激活
ASC_S->>ASC_S: Tag 闸门 + CommitAbility(扣费/冷却)
ASC_S->>ATTR: ApplyGE: 眩晕 GE 挂到目标
ATTR-->>ASC_S: 修饰器入账 / Tag 授予
ASC_S-->>P: 复制:技能进入冷却(GE 复制)
ASC_S-->>P: 复制:目标属性与 Tag 变化(OnRep)
P->>P: UI 刷新冷却圈 / 目标头像亮眩晕图标三个技能,按 22.1 的 SDD 格式配齐。这张表就是交给策划填、程序照着建资产的合约:
| 字段 | 冲锋 | 顺劈 | 盾墙 |
|---|---|---|---|
| AbilityTags | Ability.Warrior.Charge | Ability.Warrior.Cleave | Ability.Warrior.ShieldWall |
| 冷却 GE | 20s(Tag: Cooldown.Charge) | 8s | 60s |
| 消耗 | 无,命中回复怒气 20 | 怒气 30 | 无 |
| 主效果 GE | GE_Charge_Stun:Duration 1.5s,授予 State.Debuff.Stun | GE_Cleave_Damage:Instant,攻强 ×1.6 | GE_ShieldWall:Duration 8s,承伤系数 ×0.6 |
| 目标 | 单体,8–25m 冲锋距离 | 前方 5m、120° 扇形 | 自身 |
| 关键 Tag 规则 | 目标带 State.Immune.Stun 时眩晕被拒 | 仅对敌人阵营生效 | 激活时授予 State.Buff.ShieldWall |
| 附加行为 | 位移由冲刺动画的根运动承担 | 扇形内每个目标各结算一次 | 期间格挡动作替换站立 |
冲锋拆成两个职责:位移归动画与移动(根运动冲过 25m,表现层的事),控制归 GE。眩晕 GE 挂到目标 ASC 后,目标身上所有技能因为 ActivationBlockedTags 含 State.Debuff.Stun 而瞬间灰掉——"被晕了放不出技能"这条规则一行代码都没有,全是配置。怪物侧加一个 State.Immune.Stun 的常驻 Tag,BOSS 自然免疫,策划想给谁免疫就给谁配。
冲锋还藏着一个设计盲点:它自己也是技能,同样过 Tag 闸门。战士被眩晕时冲锋图标跟着灰掉,天经地义;但被减速 60% 时冲锋距离缩不缩短?这不是程序能拍板的事,SDD 里写死"不缩短,位移表现由根运动保证",程序照抄即可。策划文档与框架配置咬合得越紧,"程序自由发挥"引发的线上争议就越少。
顺劈的关键是"一次激活,多次结算"。技能采集到扇形内三个目标,就对每个目标各签发一份伤害 GE——三份凭证各自独立入账,每个目标身上的减伤、易伤、护盾各自参与结算。伤害数值不写死 160,而是走 MMC 取"来源攻强 ×1.6",这样 22.3 里算出的 174 点攻强自动流进伤害公式,换装备不用改技能。扇形采集本身属于目标数据(TargetData)的范畴,客户端瞄得准不准、服务器认不认,涉及预测与确认,第 23 章专门处理;本章只立一条底线:结算永远发生在服务器。
盾墙演示"减伤"的正确建模:不要做成"血量 +X"的假减伤,而是给属性 DamageTakenMult(承伤系数)挂一个 Multiplicitive ×0.6 的修饰器。伤害 Execution 的最后一步统一乘以这个系数,盾墙期间一切伤害打六折,到期修饰器摘除,系数回落到 1.0。它与治疗、护盾的吸收顺序(先护盾扣减、再系数打折、最后扣血)也写死在同一个 Execution 里——规则只有一处,不会南北两个模块各算一遍。
冷却也归 GE 管,而且是"一个模板走天下"的最佳案例。建一份 GE_Cooldown 模板,时长用 SetByCaller 注入,再授予一个专属冷却 Tag(冲锋给 Cooldown.Charge,顺劈给 Cooldown.Cleave);技能侧只需在自己的 BlockedTags 里声明对应冷却 Tag。CommitAbility 挂冷却的那一瞬间,技能图标自动转灰,冷却结束 Tag 摘除、图标回亮。新增技能时复制三行配置,冷却系统零代码扩张——UI 查冷却进度,查的也是这份 GE 的剩余时间,数据和表现天然同步。
三个技能合计:技能资产 3 个、GE 资产 5 个(3 主效果 + 3 冷却合一通用模板 + 消耗模板)、新属性 2 个(怒气、承伤系数)、新 Tag 约 12 个。策划再加第四个技能,大概率一个新 GE 都不用造——复用既有模板改参数即可。这就是框架的复利。
22.7 诚实警告:什么时候别碰 GAS
GAS 的学习曲线是 UE 所有系统里最陡的,没有之一。官方示例 Lyra 把它和一堆插件缠在一起,新人往往第三周还在跟 Prediction Key 搏斗。团队平均要烧掉三到四周才谈得上顺手,这期间产出几乎为零。这笔学费,只有特定项目交得值。
quadrantChart
title 该不该上 GAS:技能复杂度 × 联机需求
x-axis "技能/效果数量少" --> "技能/效果数量多"
y-axis "单机" --> "强联机"
quadrant-1 "闭眼上 GAS"
quadrant-2 "上 GAS(为后续联机铺路)"
quadrant-3 "别碰:组件+数据表足够"
quadrant-4 "轻量方案,留好接口"
"远港Online": [0.9, 0.95]
"中型ARPG联机": [0.65, 0.7]
"回响峡谷": [0.15, 0.1]
"独立小品单机": [0.1, 0.05]
"卡牌构筑原型": [0.55, 0.2]判断清单,命中左边三条以上就该用,命中右边任意一条就别碰:
| 该用 GAS 的信号 | 不该用的信号 |
|---|---|
| 技能 + Buff + 装备词条总数会破百 | 单机小项目,技能一只手数得过来 |
| 有联机,且结算必须服务器权威 | 没有联机规划 |
| 效果需要"挂上/到期/驱散/免疫"语义 | 团队没人愿意读 Lyra 的 GAS 代码 |
| 策划要持续加技能,改参数不改代码 | 三个月内要出可玩 Demo,人力紧张 |
| 存在 DoT、冷却、消耗、连招 | 战斗只是点缀(步行模拟、解谜) |
本书自己的态度就是证据:「回响峡谷」全程没用 GAS,单机竖切的十几个交互用普通组件加 UPROPERTY 就解决了,硬上 GAS 只会拖垮前两卷的进度;「远港 Online」从第一章就确定要用,因为它的技能规模注定破百。框架选型的唯一标准是复杂度匹配——GAS 的学费是前期的重税,回报是后期的红利。项目活得不够久,税就白交了。
还有一个折中路线值得记:先按 SDD 格式把技能全写进数据表,自写一个两百行的轻量结算器顶住原型期,但接口严格模仿 GAS 的"技能签发效果凭证"模型。等技能数冲破五十、或者联机需求立项,再平移到 GAS——数据表和配置思路原封保留,沉没成本可控。
决定用了,再记住三个翻车重灾区,全是代码评审的高频打回项:绕过 GE 直接 Set 属性(破坏可撤销性与复制闭合循环);代码里裸写 Tag 字符串(绕过集中登记,重构时搜不全);拿 GE 当技能使——GE 没有激活逻辑,"按下 Q 键要做什么"永远归 Ability 管。一句话口诀:Ability 决定做不做,GE 决定变多少,Tag 决定让不让。
排障别用蛮力。控制台输入 showdebug abilitysystem,屏幕上直接列出目标 ASC 的全部活跃 GE、Tag 与属性当前值,配合 AbilitySystem.Debug.NextTarget 切换观察对象。线上定位"伤害不对"的标准流程就是:showdebug 看 GE 列表 → 对 SDD 查修饰器 → 查 MMC 的来源属性——三步收敛,比断点快得多。
小结
MMO 技能系统的复杂度是组合爆炸:300 技能 × 6 类修饰来源,if-else 的维护成本是乘法。框架的本质是把"效果"抽成一等公民,技能只签发凭证。GAS 五大件一条流水线串联:ASC 是结算中心,AttributeSet 是两栏账本,Ability 是业务流程,GE 是统一货币,Tag 是通用语言。属性结算顺序固定为 (Base + ΣAdd) × ΠMul ÷ ΠDiv,Override 一票否决——效果不碰 Base,移除即回落,可撤销性是白送的。一切数值变化走 GE:掉血、回血、加速、Buff、消耗、冷却,四种时长策略覆盖全部场景,DoT 只是 Periodic GE 的特例。Tag 三位一体:状态(眩晕中)、权限(沉默禁放)、标识(火系技能),新技能打上旧 Tag,存量规则自动生效,这正是组合灾难的解药。GAS 的学费是三到四周的陡峭爬坡,技能规模破百、有联机权威结算需求才交得值;单机小品离它远点。
上手任务
- 给「远港 Online」战士建最小属性集。 按 22.3 的代码建 Health/MaxHealth/AttackPower/Rage/DamageTakenMult 五个属性,用 InitStats 灌入 1 级初始值。验收标准:PIE 里用控制台给目标 Apply 一个 +20 攻强的 Duration GE,属性面板实时变 174→194,GE 移除后回落。
- 把盾墙配出来。 建 GE_ShieldWall(Duration 8s,DamageTakenMult ×0.6),用一个打 100 血的 Instant GE 验证。验收标准:无盾墙掉 100 血,盾墙期间掉 60 血,8 秒后恢复掉 100,全程没有手写一行减伤代码。
- 用 Tag 实现沉默。 给所有战士技能配 ActivationBlockedTags = State.Debuff.Silence,做一个施加该 Tag 的 GE。验收标准:被沉默期间冲锋/顺劈/盾墙全部无法激活,沉默移除后立即可用;新增第四个技能只改配置不改代码即同样受沉默约束。
- 写一页顺劈的 SDD。 按 22.1 表格格式补齐顺劈的全部字段,交给一个不懂 GAS 的同事看。验收标准:对方只看文档能复述出冷却、消耗、伤害系数、目标范围,且能指出"扇形内每个目标独立结算伤害"这一条。
下一章
本章所有结算都发生在服务器,客户端要等一个 RTT 才能看到冷却转圈——在 100ms 延迟下,按下去"愣一下"的感觉会杀死动作游戏的手感。第 23 章讲 GAS 的本地预测:Prediction Key、回滚重放、TargetData 的网络之旅,让战士在延迟下依然指哪打哪,顺便立下 500 个技能不打架的目录宪法。
延伸阅读
- 官方文档:Unreal Engine → Gameplay Ability System(五大件定义与设置步骤,5.8 细节以该词条为准)
- 官方示例:Lyra Starter Game,重点看
LyraAbilitySystemComponent与 ALyraPlayerState 上 ASC 的挂法 - 社区文档:tranek/GASDocumentation(GitHub 上流传最广的 GAS 中文友好教程,概念与引擎源码对得上号)
- 引擎源码:Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/ 下的
AggregatorEvaluateQualification相关聚合器实现,对照 22.4 伪代码阅读 - 设计参考:《魔兽世界》技能数据库(任意资料片),用 22.1 的解剖法拆五个技能,体会"效果复用"在真实商业 MMO 里的密度