UE5.8 Gameplay Ability System 基础:从一套 MOBA 技能系统说起

📑 目录

UE5.8 Gameplay Ability System 基础:从一套 MOBA 技能系统说起

引子:技能系统崩掉的那个周五

小李接手项目里的技能系统时,前任已经离职三个月。代码里每个技能都是一个单独的 Actor,火球术是 BP_FireballSkill,治疗术是 BP_HealSkill,闪现是 BP_BlinkSkill。看起来各司其职,真到联调阶段却处处漏风。

策划要求火球术命中敌人后触发眩晕,同时如果目标身上有护盾则先扣护盾再扣血。小李在火球 Actor 里加了一个分支判断;第二天治疗术也要识别护盾状态,他又复制了一份过去。一周后新增了一个禁疗 Tag,火球术和治疗术里的判断逻辑对不上,同样的状态在八个文件里各写各的。冷却时间用 Timer 自己管,网络对战时客户端明明看见图标转好了,服务器却说还在 CD。最麻烦的是伤害数值:策划想调一下法强加成,得翻遍十几个 Blueprint 找那个乘以 1.21.2 的节点。

周五晚上十点,小李把键盘一推,在主程桌上贴了张便签:「我们上 GAS 吧。」

Gameplay Ability System,简称 GAS,是 Epic 为《堡垒之夜》设计、并在 UE4/UE5 里开源给所有人的技能框架。它不是一个插件,而是一套围绕 Ability、Attribute、Gameplay Effect、Gameplay Tag 四个核心概念组织起来的工作流。UE5.8 里这套框架已经相当成熟,网络复制、预测、冷却、消耗、状态标签都有现成支持。这篇文章从小李当时踩的坑出发,把 GAS 里最基础也最容易被忽略的点讲清楚。

architecture-beta
    group player[本地玩家]
        service input[输入组件 Enhanced Input]
        service asc[Ability System Component]
        service avatar[Avatar Actor]
    end
    group data[数据资产]
        service ga[Gameplay Ability 蓝图/C++]
        service ge[Gameplay Effect]
        service tags[Gameplay Tags]
    end
    group server[服务端]
        service attr[Attribute Set]
        service pred[预测与网络复制]
    end
    input:B --> T:asc
    asc:B --> T:ga
    asc:B --> T:ge
    asc:B --> T:tags
    asc:B --> T:attr
    attr:B --> T:pred
    avatar:B --> T:asc

一、GAS 的四个核心组件

GAS 的所有功能都可以拆成四块来理解:Ability 负责「做什么」,Attribute 负责「数值是多少」,Gameplay Effect 负责「怎么改数值或状态」,Gameplay Tag 负责「现在处于什么状态」。这四块通过 Ability System Component(ASC)串在一起。ASC 一般挂在 Player State 上,方便网络对战时死亡换角色还能保留等级、buff 等数据;也可以直接挂在 Character 上,看项目需求。

1.1 Gameplay Ability:一段可授权、可预测、可中断的逻辑

Ability 不是普通的函数调用,而是一段有生命周期的对象。调用 Activate 之后,它会经过 Commit、执行 Task、等待事件、最终 End 或 Cancel。这个生命周期让 GAS 可以做很多传统事件图做不到的事:比如技能吟唱到一半被眩晕打断,网络延迟下客户端先播动画、服务器稍后再确认是否成功,消耗法力时发现蓝不够就自动取消。

一个 Ability 在 UE5.8 里通常继承自 UGameplayAbility。蓝图子类可以在 Event Graph 里连节点;C++ 子类则重写 ActivateAbilityCanActivateAbilityInputPressedInputReleased 等函数。Ability 本身不直接改血蓝,它通过生成 Gameplay Effect 来影响 Attribute,通过 Gameplay Tag 来标记状态。

1.2 Attribute:角色的数值面板

Attribute 是浮点数值,比如 Health、Mana、AttackPower、Armor。它们被组织在 Attribute Set 里,一个 Set 是一组相关属性的集合。常见的做法是把战斗相关属性放在 UMyCharacterAttributeSet 里:Health、MaxHealth、Mana、MaxMana、AttackPower、DefensePower。

每个 Attribute 都有 Current Value 和 Base Value 之分。Base Value 是装备、等级、初始值带来的基础值;Current Value 是加上临时 Buff/Debuff 后的结果。GAS 会自动处理这两层,Gameplay Effect 可以指定改 Base 还是改 Current。

1.3 Gameplay Effect:对 Attribute 和 Tag 的修改指令

Gameplay Effect 不是技能特效,它更像一张「修改单」。这张单子描述:我要把目标的 Health 减多少,或者给目标加上 State.Stunned 标签持续多久,又或者给施法者回复 Mana。Gameplay Effect 是数据资产,策划可以在编辑器里调数值、时长、叠加上限,而不必改 Blueprint 连线。

GE 按持续方式分四种:Instant 立刻生效(例如一次伤害),Duration 持续一段时间(例如中毒),Infinite 永久直到显式移除(例如装备加成),Periodic 周期性生效(例如每秒钟回一次血)。

1.4 Gameplay Tag:用标签取代布尔变量

这是 GAS 里最容易被低估的部分。传统做法是在 Character 里放一堆 bool:bIsStunnedbIsSilencedbIsInvincible。技能系统稍微复杂一点,这些 bool 就会爆炸,网络同步也很痛苦。GAS 的做法是:所有状态都放到一个全局 Tag 表里,运行时通过 Add/Remove 标签来切换。

一个 Tag 是层级结构,例如 State.Debuff.StunState.Buff.ShieldAbility.Fireball.Cooldown。有了层级,你就可以用 State.Debuff.* 一次性查询所有 debuff,或者用 Ability.*.Cooldown 做全局冷却 UI。标签还支持响应回调:ASC 提供了 RegisterGameplayTagEvent,标签变化时自动触发委托。

flowchart TD
    A[玩家按下技能键] --> B{CanActivateAbility}
    B -->|蓝够、无沉默、无眩晕| C[Commit Ability 消耗与冷却]
    B -->|条件不满足| D[取消并播失败提示]
    C --> E[Activate Ability 执行逻辑]
    E --> F[生成 Gameplay Effect]
    F --> G[修改 Attribute 或添加 Tag]
    E --> H[Ability Task 等待动画/碰撞/输入]
    H --> I[Task 完成]
    I --> J[End Ability]
    J --> K[清理资源与标签]

二、Ability Task 与 Blueprint 集成

Ability 的核心逻辑可以在 C++ 里写,但 GAS 真正的开发效率来自 Ability Task。Task 是一段可以「挂起」的异步逻辑:等待一段动画播完、等待玩家再次按键、等待碰撞检测触发、等待一段延迟。没有 Task,ActivateAbility 必须在一帧内跑完,很多技能根本没法做。

2.1 常用 Task 举例

UE5.8 内置了大量 Task:PlayMontageAndWait 播放蒙太奇并等结束;WaitGameplayEvent 等某个 Gameplay Event 触发;WaitInputPress 等玩家按键;WaitTargetData 等玩家选目标;SpawnActor 生成投射物;ApplyGameplayEffectToTarget 对目标应用 GE。

小李的火球术就是靠这几个 Task 拼出来的:Activate 之后先 PlayMontageAndWait 举手动画,动画播到某个 Notify 时 SpawnActor 一个火球 Projectile,Projectile 命中后发送 Gameplay Event,Ability 里用 WaitGameplayEvent 接收,再 ApplyGameplayEffectToTarget 打伤害。整个过程 Ability 一直挂着,直到最后一个 Task 结束才 End。

2.2 蓝图里的连法

在蓝图 Ability 里,你从 Event ActivateAbility 节点开始,右键创建 Task 节点。Task 节点通常有两个输出:On CompleteOn Cancel。把后续逻辑连到 On Complete 上,就能实现「动画结束后再做某事」。

蓝图能力强的项目可以完全用 Blueprint Ability 做原型,等网络性能或代码复用出现瓶颈时,再把高频 Ability 迁到 C++。UE5.8 的 Blueprint Ability 已经支持预测和大部分 Task,中小型项目完全够用。

2.3 任务取消与资源清理

Ability 被 Cancel 时,所有未完成的 Task 会被自动取消。但生成的 Actor、应用的 Effect、添加的 Tag 不会自动清理,需要你手动处理。常见的做法是在 OnEndAbility 里移除本 Ability 添加的 Tag,销毁本 Ability 生成的投射物。UE5.8 提供了一批 K2_EndAbilityK2_CancelAbility 辅助函数, Blueprint 里直接用即可。

三、Attribute Set 与数值设计

Attribute Set 是 GAS 里和数据打交道最多的地方。一个设计良好的 Set 能让数值平衡工作从程序员身上卸下来,交给策划在表格里调。

3.1 定义属性

C++ 里用宏 ATTRIBUTE_ACCESSORS 声明属性:

UCLASS()
class MYGAME_API UMyAttributeSet : public UAttributeSet
{
    GENERATED_BODY()
public:
    UMyAttributeSet();

    UPROPERTY(BlueprintReadOnly, Category="Attributes")
    FGameplayAttributeData Health;
    ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)

    UPROPERTY(BlueprintReadOnly, Category="Attributes")
    FGameplayAttributeData MaxHealth;
    ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)

    UPROPERTY(BlueprintReadOnly, Category="Attributes")
    FGameplayAttributeData Mana;
    ATTRIBUTE_ACCESSORS(UMyAttributeSet, Mana)

    UPROPERTY(BlueprintReadOnly, Category="Attributes")
    FGameplayAttributeData SpellPower;
    ATTRIBUTE_ACCESSORS(UMyAttributeSet, SpellPower)
};

每个属性都是 FGameplayAttributeData,内部同时保存 Base 和 Current。ATTRIBUTE_ACCESSORS 宏会生成 GetHealthSetHealthInitHealthGetMaxHealth 等 getter/setter,蓝图中也能直接读取。

3.2 属性变化回调

Attribute Set 里可以重写 PostGameplayEffectExecute 函数,在 GE 执行后做 clamp、触发事件、计算派生值。比如收到伤害后把 Health 限制在 [0,MaxHealth][0, MaxHealth] 区间,Health 降到 00 时触发死亡事件。

伤害计算本身通常放在 GE 的 Execution Calculation 里,通过 FGameplayEffectCustomExecutionParameters 读取施法者和目标的属性。基础伤害公式可以写成:

Dfinal=max(0,Dbase×(1+Spower100)Adef)D_{final} = \max\left(0, \; D_{base} \times \left(1 + \frac{S_{power}}{100}\right) - A_{def}\right)

其中 DbaseD_{base} 是技能基础伤害,SpowerS_{power} 是施法者法术强度,AdefA_{def} 是目标防御力。把法强按百分比放大、防御按固定值减免,是最常见的 MOBA 伤害模型。策划可以通过调 DbaseD_{base}、法强加成系数、防御减免系数来平衡不同英雄。

3.3 Base 与 Current 的用法

Base Value 代表「去掉所有临时效果后的真实属性」。升级、穿装备、天赋点改的是 Base Value。Current Value 是在 Base 之上叠加 Buff/Debuff 的结果。比如一个装备的被动把 MaxHealth Base 加 100100,一瓶临时药水把 Current MaxHealth 加 5050,二者叠加时角色面板显示 +150+150。GE 里通过 SetByCallerMagnitude 指定具体改哪一层。

四、Gameplay Tags 的状态管理

Gameplay Tag 是 GAS 的黏合剂。它把 Ability、GE、输入、动画、UI 全部拉到了同一套状态语言里。

4.1 标签分层

建议项目早期就定好命名规范。一个常见的分层如下:

  • Ability.Fireball:火球术 Ability 本身的标识。
  • Ability.Fireball.Cooldown:火球术正在冷却。
  • State.Buff.Shield:身上有护盾。
  • State.Debuff.Stun:被眩晕。
  • State.Debuff.Silence:被沉默,无法施放需要吟唱的法术。
  • Input.Skill.1:技能槽位 1 被按下。

层级的好处是查询灵活。HasMatchingGameplayTag(FGameplayTag::RequestGameplayTag("State.Debuff")) 可以判断角色是否处于任何 Debuff 状态;HasAllMatchingGameplayTag 可以判断同时满足多个条件。

graph TD
    Root[Gameplay Tags] --> Ability[Ability]
    Root --> State[State]
    Root --> Input[Input]
    Ability --> A1[Ability.Fireball]
    Ability --> A2[Ability.Heal]
    A1 --> A1c[Ability.Fireball.Cooldown]
    A2 --> A2c[Ability.Heal.Cooldown]
    State --> Buff[State.Buff]
    State --> Debuff[State.Debuff]
    Buff --> B1[State.Buff.Shield]
    Buff --> B2[State.Buff.Haste]
    Debuff --> D1[State.Debuff.Stun]
    Debuff --> D2[State.Debuff.Silence]
    Input --> I1[Input.Skill.1]
    Input --> I2[Input.Skill.2]

4.2 Ability 的 Tag 约束

每个 Ability 都可以配置 Ability TagsActivation Owned TagsActivation Blocked TagsActivation Required TagsCancel Abilities with TagBlock Abilities with Tag。这些字段直接决定了技能能否释放、释放时会被什么状态打断、释放时会取消哪些其他技能。

例如火球术可以配置:

  • Activation Required Tags:空,表示随时可放。
  • Activation Blocked TagsState.Debuff.SilenceState.Debuff.Stun,表示被沉默或眩晕时不能放。
  • Activation Owned TagsState.Casting,表示释放期间角色处于施法状态。
  • Block Abilities with TagAbility.Blink,表示施法期间不能闪现。

这些约束完全在 Ability 的数据资产里配置,不需要写代码。策划后续想加一个「缴械」状态禁止所有普通攻击,只需要给普通攻击 Ability 加一个 Activation Blocked Tags 即可。

4.3 GE 与 Tag 的联动

Gameplay Effect 可以添加或移除 Tag。眩晕 GE 通常是一个 Duration 类型的 GE,激活时给目标加上 State.Debuff.Stun,时间到自动移除。这种设计让状态持续时间天然和数值绑定在一起:眩晕 2 秒就是 GE Duration 为 2 秒。

Ability 被眩晕打断,也依赖 Tag。你可以在 Character 的 ASC 上监听 State.Debuff.Stun 的新增事件,收到后调用 CancelAbilities 并传入 Ability 标签,取消所有非瞬发的技能。UE5.8 的 UAbilitySystemGlobals 还提供了一些全局工具函数,方便批量查询和取消。

五、一个简单的火球术与治疗术实现

下面是一个最简化的火球术 Blueprint Ability 逻辑。为了保持篇幅可控,我把关键节点用伪代码方式描述,实际在蓝图里就是对应节点的连线。

// 火球术 Ability 伪代码
Event ActivateAbility()
{
    if (!CommitAbility()) return;  // 扣蓝并进入冷却

    PlayMontageAndWait("Cast_Fireball", OnComplete, OnCancel);
    OnMontageNotify("SpawnProjectile") => {
        SpawnActor(BP_FireballProjectile, Owner.Socket.Hand);
    };
    OnComplete => {
        EndAbility();
    };
}

// 投射物命中目标时由 Projectile 调用
Event OnProjectileHit(Target)
{
    ApplyGameplayEffectToTarget(
        GE_FireballDamage,          // Instant GE
        Target,
        SetByCaller("Damage", 50.0f)
    );
    EndAbility();
}

治疗术逻辑更简单:

Event ActivateAbility()
{
    if (!CommitAbility()) return;
    ApplyGameplayEffectToOwner(GE_HealInstant);
    PlayMontageAndWait("Cast_Heal", OnComplete => EndAbility());
}

GE_FireballDamage 是一个 Instant Gameplay Effect,只改 Health 属性,数值用 SetByCaller 在运行时传入。GE_HealInstant 也是 Instant,但修改的是正值。策划可以在编辑器里把火球基础伤害调到 4040、法强加成系数调到 0.80.8,而不必碰 Ability 蓝图。

注意这里 CommitAbility 做了三件事:检查消耗(Cost)、检查冷却(Cooldown)、真正扣除和添加冷却 Tag。如果蓝不够或正在冷却,Commit 返回 false,Ability 直接结束。

六、输入绑定与技能冷却

6.1 Enhanced Input 与 ASC 绑定

UE5.8 推荐用 Enhanced Input。每个技能槽位对应一个 Input Action,例如 IA_Skill1IA_Skill2。在 Player Controller 或 Character 里,把输入事件映射到 ASC 的 AbilityActorInfo,然后调用 TryActivateAbilityByClassTryActivateAbilityByTag

输入绑定常见的模式有两种。一种是「按下触发、松开取消」:按下技能键时 TryActivateAbility,松开时如果 Ability 支持蓄力则发送 Input Release 事件。另一种是「按下即释放」:按下直接触发完整 Ability,适合火球术这种点按技能。

6.2 冷却的网络同步

GAS 的冷却通过网络复制自动同步。服务端 Commit 成功后会给 Owner 加上 Ability.Fireball.Cooldown 标签,并设置 Duration。客户端的 ASC 也会收到这个 Tag 和剩余时间,UI 只需要监听 Tag 是否存在即可刷新图标遮罩。

客户端预测在这里也很关键。玩家按下技能键时,客户端会立即开始播放动画和进入冷却;服务端确认后再把正式结果复制回来。如果服务端判定失败(比如网络延迟期间目标已经走出范围),客户端会收到纠正并回滚。UE5.8 的 GAS 把这套预测封装在 UAbilitySystemComponent::ProcessInputBindingsUAbilitySystemComponent::ServerTryActivateAbility 里,常规项目不需要自己实现。

sequenceDiagram
    autonumber
    participant P as 玩家
    participant C as 客户端 ASC
    participant S as 服务端 ASC
    participant T as 目标角色

    P->>C: 按下技能键 1
    C->>C: 本地预测:播动画、扣蓝、进 CD
    C->>S: ServerTryActivateAbility(Fireball)
    S->>S: CanActivate + Commit
    S->>T: Apply GE_FireballDamage
    S-->>C: 确认并复制结果
    C->>P: 更新 UI 与状态

6.3 冷却与消耗的 GE 配置

Cost 和 Cooldown 在 Ability 里指向两个 Gameplay Effect 资产。Cost GE 是 Instant,扣除 Mana;Cooldown GE 是 Duration,添加 Cooldown Tag。你可以让多个技能共享同一个 Cooldown Tag,实现「放了 A 技能后 B 技能也进 CD」的效果;也可以给一个技能配置多个 Cooldown GE,分别管理不同层级的冷却。

策划表通常只维护两个数值:ManaCost 和 CooldownSeconds。Ability 在 Commit 时通过 SetByCaller 把这两个值传进对应的 GE。这样策划改表不需要开编辑器蓝图。

冷却还有一些容易踩坑的边界情况。比如技能被打断时,冷却 Tag 应该保留还是清除?多数 MOBA 的做法是:吟唱期间被打断,技能不进入冷却;已经进入后摇或已经生效,冷却正常计算。这个逻辑可以在 Ability 的 OnEndAbility 里根据 EndReason 判断。再比如冷却缩减属性:可以让 Cooldown GE 的 Duration 在应用前乘上一个由 CooldownReduction 属性算出的系数,实现 Tactual=Tbase×(1rcdr)T_{actual} = T_{base} \times (1 - r_{cdr})

七、常见架构模式

项目用到 GAS 之后,很快会面临几个结构性选择。这些选择没有绝对对错,但前期定好能少走很多弯路。

7.1 ASC 挂在哪里

最常见的两种挂法:挂在 Player State 上,或挂在 Character 上。

挂在 Player State 的好处是角色死亡重生时,等级、Buff、Cooldown 可以保留;坏处是 Authority 判断稍微复杂,因为 Player State 在客户端上不是 always relevant。挂在 Character 上的好处是逻辑直观,死亡即销毁所有状态;坏处是重生时需要手动把属性迁移到新 Character。

MOBA 和 ARPG 通常把 ASC 挂在 Player State 或自定义的 Player Avatar 上,因为这类游戏有长期成长。动作游戏或 Roguelike 更常挂在 Character 上。

7.2 Ability 的粒度

Ability 到底要做到多细?一个方案是把「技能」定义为玩家按一个键释放的完整行为,例如火球术是一个 Ability,治疗术是一个 Ability。另一个方案是把通用逻辑拆成 Ability,例如「生成投射物」是一个 Ability,具体是火球还是冰箭由 GE 决定。

一般推荐前者:一个按键对应一个 Ability。这样 Blueprint 可读性最好,策划调整也最直观。公共逻辑可以抽到 Ability Task 或 C++ 基类里,而不是拆成多个 Ability。

7.3 数值表与 GE 的映射

中大型项目通常会把所有技能数值放到 Data Table 或 Curve Table 里。每一行是一个技能配置,包含 ManaCost、Cooldown、BaseDamage、PowerRatio 等字段。Ability 激活时读取这行数据,通过 SetByCaller 传给 GE。策划改表即可,不需要重新打 Ability 包。

另一种做法是每个技能单独做一个 GE 资产,数值直接写在 GE 里。 prototyping 阶段这样最快,但技能数量上百之后维护成本会上升。

7.4 状态机与 Tag

角色的行为状态机推荐用 Tag 驱动,而不是单独维护 bool。动画蓝图里读取 State.Debuff.StunState.Casting 等 Tag 来决定播放什么动画。Gameplay 逻辑里也用 Tag 判断能否移动、能否攻击。这样做的好处是状态来源统一:眩晕可能来自技能、陷阱、剧情演出,但它们都表现为同一个 Tag。

UI 也一样。HUD 上的技能图标只需要问 ASC:这个 Ability 的 Cooldown Tag 还在不在?有没有 Silence Tag?就能决定图标要不要灰显。

7.5 调试 GAS 的几个小技巧

GAS 的问题定位起来有时比较隐蔽,因为 Ability、GE、Tag、Attribute 四条线会同时参与。第一个技巧是打开 showdebug abilitysystem 控制台命令,它会直接在屏幕上打印当前 ASC 里的 Ability、Tag、Attribute 状态,比断点更快看清全局。

第二个技巧是给每个 GE 配置一个 Granted Tag 或自定义 Tag,在编辑器里用 Gameplay Debugger 观察谁正在影响谁。第三个技巧是注意 NetMode:很多 Ability 问题其实是客户端预测和服务端 Authority 不一致造成的,用 GetNetMode()HasAuthority() 配合日志,可以快速区分是逻辑错还是同步错。

最后一个技巧是建立一套命名规范。Ability、GE、Tag、Attribute 的命名前缀一旦统一,搜代码和查引用会轻松很多。比如所有火球术相关资产都以 Fireball 开头,所有 Buff 都以 Buff_ 开头,所有状态 Tag 都以 State. 开头。

结尾

小李把项目切到 GAS 后,最明显的变化不是某一处代码变短了,而是「技能」这个概念终于有了一个统一的家。策划调数值不用再翻 Blueprint,程序加新状态不用再复制 bool,网络同步和冷却也不再靠手写 Timer 硬撑。GAS 的初期学习成本不低,但当你需要处理超过十个技能、并且它们之间还会互相影响时,这套框架省下来的时间和精力会非常明显。

这篇文章只覆盖了 GAS 最基础的部分:Ability 生命周期、Attribute Set、Gameplay Effect、Gameplay Tag、Ability Task、输入绑定与冷却,以及一个火球术和治疗术的示例。真正落到项目里,你还会遇到预测回滚、技能队列、目标选择、AI 施法、属性快照等更深层的话题。把这些基础先踩实,后面的路会顺很多。希望小李踩过的那些坑,能让你少走几段夜路。