UE5.8 GAS 预测、冷却与网络同步

📑 目录

UE5.8 GAS 预测、冷却与网络同步

一场团战暴露的问题

上周五晚上,我们项目组在测试大厅跑了一场五人竞技。战士职业的玩家阿杰连续按下两次冲锋技能,客户端立刻播放了突进动画,他也看到自己冲到了法师脸上。但不到半秒,画面猛地一跳,阿杰被拉回了原地,法师则悠哉地读完了火球术。会议室里瞬间炸锅:这是 bug 还是网络卡顿?

我打开网络模拟器,把客户端延迟调到 120ms120\,\text{ms},丢包率设为 3%3\%,问题立刻复现。冲锋的冷却时间在本地已经走完,服务器却拒绝第二次释放。更诡异的是,第一次冲锋命中后,目标身上的眩晕标签在本地显示了大约 80ms80\,\text{ms},然后凭空消失,仿佛这次控制从未发生。

这就是 UE5 Gameplay Ability System 在网络环境下的典型症状。GAS 把技能拆成了 Ability、Attribute、Gameplay Effect、Gameplay Tag、Gameplay Cue 等多个概念,客户端和服务端各自持有数据,又要保持一致。 latency 只要稍高,预测失败、冷却不同步、属性回滚就会接踵而至。这篇文章把那天晚上踩到的坑,以及后续一周的源码阅读结论,按模块整理出来。

GAS 网络架构全景

在深入细节之前,先建立一张整体地图。GAS 的核心实体并不神秘,它由 Ability System Component(ASC)统一托管,ASC 同时存在于服务端和客户端。服务端拥有全部真相,客户端根据 relevance 复制数据,并在本地做预测性模拟。

architecture-beta
    group server(cloud)[服务端 Authority]
    group client1(monitor)[客户端 A]
    group client2(monitor)[客户端 B]

    service db(server)[GameState / AttributeSet] in server
    service asc_server(server)[AbilitySystemComponent] in server
    service net(server)[Replication / RPC] in server

    service asc_a(client1)[Local ASC] in client1
    service pred_a(client1)[Prediction] in client1
    service ui_a(client1)[UI / VFX] in client1

    service asc_b(client2)[Local ASC] in client2
    service pred_b(client2)[Prediction] in client2
    service ui_b(client2)[UI / VFX] in client2

    asc_server:R --> L:net
    net:R --> L:asc_a
    net:R --> L:asc_b
    asc_a:R --> L:pred_a
    pred_a:R --> L:ui_a
    asc_b:R --> L:pred_b
    pred_b:R --> L:ui_b
    db:T --> B:asc_server

这张架构图里有两个要点。

第一,ASC 是技能的入口,也是网络同步的入口。Ability、Gameplay Effect、AttributeSet 都挂在它下面。服务端 ASC 负责创建、执行、复制;客户端 ASC 接收复制数据,同时执行本地预测。

第二,客户端的 Prediction 层不直接修改服务端数据。它先在本地生成一个预测性的 Ability 实例,应用预测性的 Gameplay Effect,播放动画和特效。等服务器确认或否认后,再决定保留结果还是回滚。这个确认与否认的机制,核心就是 Prediction Key。

Prediction Key 与客户端预测

Prediction Key 是 GAS 网络同步的灵魂。没有它,客户端永远只能等服务器放行才敢播放动画,手感会像踩棉花。有了它,客户端可以立刻响应玩家输入,但代价是必须承担预测失败的回滚成本。

Prediction Key 的生命周期

当玩家按下技能键,客户端调用 TryActivateAbility。如果 Ability 设置了 ActivationPolicy 允许客户端预测,ASC 会生成一个全局唯一的 Prediction Key。这个 Key 在客户端和服务端保持一致,用于把同一批预测性操作绑定在一起。

flowchart TD
    A[玩家按下技能键] --> B[客户端 TryActivateAbility]
    B --> C{Ability 支持预测?}
    C -->|否| D[等待服务器确认]
    C -->|是| E[生成 Prediction Key]
    E --> F[本地预测性执行]
    F --> G[播放动画/特效/音效]
    G --> H[发送 RPC 到服务器]
    H --> I[服务器权威执行]
    I --> J{结果一致?}
    J -->|是| K[Prediction Key 确认]
    J -->|否| L[Prediction Key 拒绝]
    K --> M[保留本地结果]
    L --> N[回滚本地预测效果]

流程图上可以看到,Prediction Key 像一个挂钩,把本地动画、Gameplay Effect、Gameplay Tag 全部串起来。服务器执行完毕后,会回传一个针对该 Key 的确认或拒绝。如果拒绝,客户端需要把这一批预测性副作用全部撤销。

预测执行的具体表现

在源码里,预测性 Ability 调用的很多函数都会带一个 FPredictionKey 参数。Gameplay Effect 在应用时,如果带有 Prediction Key,会被标记为预测性。预测性 Effect 在客户端会立即生效,但不会真正持久化。服务器确认后,这些 Effect 转为真实;服务器拒绝后,它们会被移除。

我那天晚上看到的眩晕标签闪现,就是因为一个预测性的 Gameplay Effect 在本地生效,随后服务器拒绝或覆盖,标签被回滚。冲锋被拉回原地,则是运动预测和 GAS 预测没有协同好:GAS 确认了技能释放,但角色移动组件的本地预测与服务器快照不一致。

一个常被忽略的细节是:Prediction Key 只在 Authority 为 Server 时由客户端生成。如果项目用了 Listen Server,Host 自己既是客户端也是服务器,不需要跨机器预测,Key 的生命周期会短很多。Dedicated Server 下,客户端必须持有可靠的 Prediction Key,否则 RPC 到达服务器时无法匹配。

Gameplay Cue 与同步

Gameplay Cue 是 GAS 里负责表现层的抽象。爆炸特效、受击闪光、buff 光环、屏幕震动,都应该挂在 Cue 上,而不是直接写在 Ability 蓝图里。这么做的原因是 Cue 有独立的同步策略,能在高延迟下仍保持画面流畅。

Cue 的两种触发方式

Cue 可以通过 Gameplay Effect 自动触发,也可以手动调用 GameplayCueManager 触发。前者最常见,比如一个燃烧 debuff 在应用时自动播放火焰粒子。后者适合一次性表现,比如命中瞬间的爆点。

sequenceDiagram
    participant Player as 玩家客户端
    participant C_A as 客户端 ASC
    participant S as 服务器 ASC
    participant C_B as 目标客户端

    Player->>C_A: 按下技能
    C_A->>C_A: 本地预测:播放 Cue
    C_A->>S: RPC ActivateAbility
    S->>S: 权威判定命中
    S->>C_A: Confirm Prediction Key
    S->>C_B: 多播 GameplayCue
    C_B->>C_B: 目标客户端播放受击特效

注意序列图中的时序。客户端 A 在发送 RPC 之前就已经播放了 Cue,这是预测。目标客户端 B 在服务器判定命中后才会收到 Cue,这是权威同步。如果客户端 A 的预测性 Cue 与服务器最终判定不一致,比如实际没命中,客户端 A 需要把它隐藏或移除。

Cue 的本地播放与同步播放

UE 对 Gameplay Cue 做了分类:GameplayCueNotify_ActorGameplayCueNotify_Static。Actor 版本会生成一个真实 Actor,可以持续存在,适合做持续 buff;Static 版本是一次性事件,开销更小,适合做瞬发特效。

Cue 的同步默认依赖 Gameplay Tag 的网络复制。服务端 TAG 变化后,客户端 ASC 会收到通知,触发对应的 Cue。如果网络抖动严重,Tag 复制可能延迟,导致 Cue 晚出。为了缓解这个问题,可以在调用 ExecuteGameplayCue 时显式指定 PredictionKey,让客户端提前触发。

那次测试里,法师的火球命中特效在本地有轻微延迟,原因就在于 Cue 没有走预测路径,完全等服务端 Tag 复制。后来我们改成在 Ability 的 ActivateAbility 里调用一次预测性 ExecuteGameplayCue,手感改善不少。

Ability 的 Cost 与 Cooldown

Cost 和 Cooldown 是技能释放前的两道闸门。Cost 检查资源是否足够,Cooldown 检查冷却是否结束。GAS 推荐用 Gameplay Effect 实现这两者,因为它们天然享受 GAS 的网络同步和预测机制。

Cooldown 的数学模型

Cooldown 本质是一个持续时间为 CC 的 Gameplay Effect。施法瞬间,ASC 给目标添加 Cooldown Effect,并赋予一个 Cooldown Tag,例如 Ability.Cooldown.Sprint。在效果持续期间,Tag 一直存在;效果结束后,Tag 自动移除。Ability 的冷却检查就是判断这个 Tag 是否存在。

冷却结束时间可以写成:

Tready=Tcast+max(0,Cbase×(1h)Δt)T_{\text{ready}} = T_{\text{cast}} + \max\left(0, C_{\text{base}} \times (1 - h) - \Delta t\right)

其中 TcastT_{\text{cast}} 是施法时间戳,CbaseC_{\text{base}} 是基础冷却,hh 是冷却缩减百分比,Δt\Delta t 是已经经过的时间。这个公式看起来简单,但放在网络同步里会引出很多边界问题。

客户端与服务端的冷却不同步

那天晚上阿杰的冲锋可以连按两次,问题就出在本地 Tag 已经消失,服务器 Tag 还在。客户端预测性施法时,会先把本地 Cooldown Tag 加上,等 Cooldown Effect 时间到就移除。但服务器才是权威,如果两端计时起点不一致,客户端就会先一步认为冷却结束。

常见的原因有:

  • 服务器收到 RPC 时比客户端晚一个 RTT,服务器实际开始冷却的时间更晚。
  • Cooldown Effect 的 Duration 使用了客户端本地时间,而不是服务器时间。
  • Prediction Key 被拒绝后,客户端的 Cooldown Tag 没有被正确清理。

解决思路是:Cooldown Effect 必须让服务器应用,客户端只做预测。使用 UAbilitySystemGlobals::GetServerWorldTimeSeconds() 作为统一时间源,避免两端本地时间漂移。Ability 的 CheckCooldown 要读取当前 ASC 的 Tag 状态,而不是维护一个独立的计时器。

bool UMyGameplayAbility::CheckCooldown(const FGameplayAbilitySpecHandle Handle,
    const FGameplayAbilityActorInfo* ActorInfo,
    FGameplayTagContainer* OptionalRelevantTags) const
{
    if (const UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get())
    {
        const FGameplayTagContainer* CooldownTags = GetCooldownTags();
        if (CooldownTags && CooldownTags->Num() > 0)
        {
            return !ASC->HasAnyMatchingGameplayTags(*CooldownTags);
        }
    }
    return true;
}

这段代码只有十几行,却解决了我们一个长期隐患。之前有人在蓝图中自己维护了一个冷却变量,网络一抖就不同步。改回读取 ASC 的 Tag 后,客户端和服务器的判断依据完全一致。

Cost 的资源一致性

Cost 通常消耗 Mana、Stamina 或者弹药。推荐做法是创建一个 Instant Gameplay Effect,在 Ability 激活时应用,扣减对应的 Attribute。Instant Effect 没有持续时间,一次性修改 Base Value。

Cost 的预测风险在于:客户端先扣了资源,服务器却拒绝施法。如果资源已经用于其他技能,就可能出现负数或透支。GAS 的 Prediction Key 机制能回滚 Cost,但前提是所有资源修改都走 Gameplay Effect,而不是直接改变量。

Attribute 的复制与同步

Attribute 是 GAS 中数值系统的核心。MaxHealth、Health、Mana、AttackPower 都以 Attribute 形式存在,挂在自定义的 AttributeSet 里。AttributeSet 需要注册到 ASC,并设置复制模式。

Base Value 与 Current Value

每个 Attribute 有两层值:Base Value 和 Current Value。Base Value 是原始值,Current Value 是在 Base 之上叠加 Gameplay Effect Modifier 后的结果。比如基础攻击力 Abase=100A_{\text{base}} = 100,装备加成 +20+20,临时 buff +10%+10\%,则 Current Value 为:

Acurrent=(Abase+20)×1.1A_{\text{current}} = (A_{\text{base}} + 20) \times 1.1

这种分层让 buff 管理非常清晰。服务器只同步 Base Value 和 Modifier 来源,客户端自己计算 Current Value。

Replication 模式选择

Attribute 的复制有三种模式:

  • None:不同步。
  • RepNotify:同步且触发回调,适合 HP、MP 这种需要 UI 更新的值。
  • Meta:用于由其他 Attribute 派生出的元属性,例如暴击率由敏捷换算。
graph LR
    A[Base Health] -->|Gameplay Effect| B[Current Health]
    C[Max Health] -->|Clamp| B
    D[Damage GE] -->|Subtract| B
    E[Heal GE] -->|Add| B
    B -->|RepNotify| F[HUD 更新]

选择 RepNotify 时要注意回调频率。如果一个 Attribute 每帧都被修改,网络带宽和 UI 刷新都会吃紧。对于频繁浮动的值,可以在服务器端做阈值或频率限制,再同步到客户端。

预测性 Attribute 修改的回滚

预测性 Attribute 修改是最容易出问题的环节。客户端预测扣血,服务器却说没打中,必须把血加回来。GAS 通过 Prediction Key 把预测性 Modifier 和真实 Modifier 区分开。服务器确认前,客户端看到的 Current Value 是预测值;确认后,预测 Modifier 被真实 Modifier 替换。

如果 Attribute 的 Replication 写得不规范,比如直接在网络回调里改 UI 而不区分预测与权威,就会出现血条闪烁。正确做法是:UI 只订阅 ASC 的 Attribute 变化委托,由 ASC 保证最终一致性。

服务器权威与作弊防范

客户端预测是为了手感,服务器权威是为了公平。任何可能影响胜负的判定,都不能只听客户端的一面之词。

哪些逻辑必须服务器判定

技能命中、伤害计算、资源消耗、冷却开始、状态免疫,这些都该在服务器上跑。客户端可以本地模拟,但只是展示。即使客户端通过修改内存伪造了命中结果,服务器也不会认可。

一个常见的反作弊设计是:伤害 Gameplay Effect 的 Execution Calculation 只在服务器执行。Execution Calculation 可以读取双方的 Attribute、标签、状态,做出复杂判定。客户端只负责播动画和预测性飘字,真正的扣血由服务器下发。

校验输入的合法性

CanActivateAbilityCheckCostCheckCooldown 里,要加入服务器校验。例如:

  • 检查目标是否在技能射程内。
  • 检查施法者是否处于眩晕、沉默状态。
  • 检查资源是否满足 Cost。
  • 检查 Cooldown Tag 是否存在。

这些校验在客户端也会执行,用于预测;但服务器会再次执行,作为权威。两者逻辑要保持一致,否则预测失败率会飙升。

防止 Cooldown 和 Cost 作弊

客户端可以谎报自己的当前时间,也可以谎报资源数量。服务器不能相信客户端传来的 Attribute 值,只能从自己持有的 ASC 数据中读取。Cooldoown 时间戳必须来自服务器世界时钟,而不是客户端 RPC 里的时间。

如果项目需要帧同步或 deterministic rollback,可以考虑把技能冷却也纳入服务器快照。GAS 本身不是帧同步框架,但可以通过 Gameplay Tag 事件和 Snapshot 实现有限度的回滚。

延迟补偿与回滚策略

网络延迟无法消除,只能补偿。补偿有两种思路:延迟补偿和客户端预测回滚。

延迟补偿

延迟补偿指服务器在判定命中时,把目标回退到客户端发送输入时的位置。这样即使目标在客户端眼中已经被打中,服务器也可以用历史状态验证。FPS 游戏里常见的 Lag Compensation 就是这个原理。

GAS 本身没有内置的延迟补偿,但可以和 CharacterMovementComponent 的 NetworkedPositionHistory 结合。服务器收到技能 RPC 时,根据 RTT 估算客户端当时的视角,把目标位置回退到过去某一帧,再判定命中。

延迟补偿的误差可以用下式估算:

Δx=vtarget×RTT2\Delta x = v_{\text{target}} \times \frac{\text{RTT}}{2}

目标移动速度越快,RTT 越大,回退距离越远。对于移动速度 v=600cm/sv = 600\,\text{cm/s}、RTT =120ms= 120\,\text{ms} 的目标,回退距离约为 36cm36\,\text{cm}。这个距离对近战技能可能影响不大,对狙击或瞬发远程技能就很关键。

客户端预测回滚

当服务器拒绝预测时,客户端要把本地的预测效果撤销。GAS 自动处理大部分 Gameplay Effect 和 Tag 的回滚,但表现层需要手动处理。例如已经播放的动画、已经生成的粒子、已经移动的角色位置,都要在回滚时恢复。

sequenceDiagram
    participant C as 客户端
    participant S as 服务器

    C->>C: 预测施法,HP 变为 80
    C->>S: RPC 施法请求
    Note over S: 目标实际已闪避
    S->>C: Reject Prediction Key
    C->>C: 回滚 Gameplay Effect
    C->>C: HP 恢复为 100
    C->>C: 隐藏预测特效
    C->>C: 播放施法失败提示

表现层回滚的一个技巧是:把预测性表现和权威性表现分开管理。预测性动画可以设计为可中断,权威性动画再确认后覆盖。这样玩家不会看到突兀的瞬移,而是看到技能被打断或失效的自然过渡。

回滚与运动同步

阿杰被拉回原地,问题的根因在于运动组件和 GAS 预测没有统一。冲锋技能修改了角色速度或位置,但 CharacterMovementComponent 的本地预测和网络复制独立运行。GAS 确认了技能,但运动组件的服务器快照与客户端不一致。

解决方案通常是:让冲锋通过施加 Root Motion 或受控的 Launch 来移动,而不是直接改 transform。Root Motion 由服务器驱动动画,客户端预测播放,服务器校正。Launch 则由服务器计算落点,客户端平滑插值。无论哪种方式,都要避免客户端单方面决定位置。

调试 GAS 网络同步的技巧

那天晚上排查到后面,日志和可视化工具成了救命稻草。GAS 提供了一系列调试手段,下面是我常用的几个。

showdebug abilitysystem

在控制台输入 showdebug abilitysystem,可以看到当前 ASC 的所有 Ability、Gameplay Effect、Gameplay Tag、Attribute。这个视图会显示每个 Effect 的 Prediction Key、剩余时间、来源。判断一个 Tag 是预测性还是权威性,非常方便。

net_pktlag 与 net pktloss

net pktlagnet pktloss 可以在本地模拟延迟和丢包。测试时建议覆盖几种常见网络环境:

  • 局域网:RTT <10ms< 10\,\text{ms},丢包 0%0\%
  • 国内常见:RTT 30\text{--}60\,\text{ms},丢包 0\text{--}1\%。
  • 跨运营商:RTT 80\text{--}150\,\text{ms},丢包 1\text{--}3\%。
  • 弱网:RTT >200ms> 200\,\text{ms},丢包 >5%> 5\%

在弱网下仍能流畅释放技能,才说明预测和同步做得足够健壮。

Gameplay Debugger

GameplayDebugger 可以在运行时查看 AI、NavMesh、Gameplay Tag 等信息。配合自定义的调试信息,可以实时显示每个目标的冷却状态、免疫标签、Attribute 变化。

日志过滤

DefaultEngine.ini 里提高 GAS 日志级别:

[Core.Log]
LogAbilitySystem=Verbose
LogGameplayEffects=Verbose
LogGameplayTags=Verbose

Verbose 级别的日志会输出每个 Gameplay Effect 的应用、移除、复制、Prediction Key 状态。虽然信息量大,但定位回滚问题时非常有效。建议在开发分支打开,发布前关闭。

可视化 Prediction Key

一个实用的小工具是在 UI 上显示当前激活的 Prediction Key 列表。每个 Key 用颜色区分:绿色表示已确认,黄色表示等待确认,红色表示被拒绝。这样测试人员一眼就能看出预测失败率。

结语

那个晚上的团战 bug,最终被我们拆解成 Prediction Key 生命周期、Cooldown Tag 同步、Gameplay Cue 触发路径、Attribute 复制模式四个问题逐一修复。UE5.8 的 GAS 很强大,但强大意味着细节多。网络同步没有银弹,只能在手感与公平之间找到适合自己项目的平衡点。

如果你也正在调 GAS 网络相关的问题,建议从 showdebug abilitysystem 开始,把每个 Prediction Key 的确认与拒绝都看过一遍。很多时候,问题不在大招有多复杂,而在一个 Tag 早移除了几毫秒。