11 网络预测(Prediction)
目标:吃透 GAS 的客户端预测机制——为什么需要预测、预测边界、PredictionKey 如何工作、ScopedPredictionWindow 与 WaitNetSync 的用法、预测失败的回滚原理、预测生成 Actor 的分工、预测的局限(GE 移除/冷却)与 Instant GE 预测的"闪烁"问题。这是 GAS 最核心也最容易被"背错"的部分。
11.1 为什么需要预测
在标准客户端-服务端模型里,客户端任何操作都要等服务端确认后才看到结果(网络 RTT 往往 50~150ms),导致输入延迟感。GAS 让拥有者客户端先本地执行(播放动画、扣蓝、出特效),再异步等服务端"验证",从而获得即时手感——这就是预测(Prediction)。
通俗理解:就像在餐厅点菜,服务员(客户端)不等厨房(服务器)确认就先说"好的,马上来"。如果厨房能做,一切顺利;如果厨房说"这道菜卖完了",服务员再道歉并更正。
GAS 只对"拥有者客户端 + 本地预测"做预测。Simulated Proxy 永远不预测(其他玩家看到你放技能,是通过复制/表现通道,不是真的在那边跑技能)。
11.2 预测的边界(能预测什么)
可预测
| 内容 | 说明 |
|---|---|
| 能力激活(Ability Activation) | 客户端先激活,服务端后验证 |
| 触发事件(Triggered Events) | 事件驱动的激活 |
| GE 应用 | 只有 Modifier 可预测,ExecCalc 不可预测 |
| GameplayTag 修改 | 预测 GE 附加的标签 |
| GameplayCue 事件 | 含预测性 GE 中的和独立触发的 |
| 动画蒙太奇(Montages) | 播放/停止 |
| 移动 | 由 CharacterMovementComponent 内置支持 |
不可预测
| 内容 | 说明 |
|---|---|
| GE 移除(GameplayEffect Removal) | 必须等服务端确认 |
| 周期效果(Periodic Effects / DOT Tick) | 服务器权威 |
| ExecCalc | 可执行任意复杂逻辑,无法保证两端一致 |
关键要点:只有 Modifier 可以预测,ExecCalc 不可预测。这是"伤害不预测"的根本原因之一。
Epic 的设计理念
"... we are also not all in on a 'predict everything seamlessly and automatically' solution. We still feel player prediction is best kept to a minimum (meaning: predict the minimum amount of stuff you can get away with)." —— Dave Ratti, Epic Games
Paragon 和 Fortnite 都不预测伤害。它们很可能使用 ExecCalc 进行伤害计算,而 ExecCalc 本身就不可预测。
11.3 预测要解决的六个核心问题
| 问题 | 说明 | GAS 的解决方案 |
|---|---|---|
| "我能做吗?" | 基本的预测协议 | 客户端先执行,服务器后验证 |
| "撤销" | 预测错误时如何回滚 | 移除所有用该 PredictionKey 创建的预测 GE |
| "重做" | 避免重复执行服务器复制的效果 | 匹配 PredictionKey,客户端移除预测版本 |
| "完整性" | 确保预测了所有副作用 | 要求开发者手动确保所有副作用都被预测 |
| "依赖关系" | 管理预测链 | Scoped Prediction Window 管理依赖 |
| "覆盖" | 预测性覆盖服务器拥有的状态 | 预测 GE 覆盖服务器状态,服务器确认后移除 |
11.4 PredictionKey:预测的"对账单"
FPredictionKey 是客户端预测与服务端确认之间的唯一凭证。它是一对整数(Current + Base),由客户端生成并随 RPC 发给服务端。
完整生命周期
客户端 服务器
│ 1. 激活能力,生成 Activation Prediction Key │
│ 2. 将 Key 添加到所有预测的 GE │
│ 3. CallServerTryActivateAbility(Key) ──────────→ │
│ │ 4. 接收 Key
│ │ 5. 将 Key 添加到服务器的 GE
│ │ 6. 运行能力逻辑
│ ←──────────────────────────────────────────── │ 7. 复制 Key 回客户端
│ 8. 接收到复制的 Key(标记为过期) │
│ 9. 移除所有用该 Key 创建的预测 GE │
│ 10. 服务器的 GE 复制过来(预测正确) │
预测正确的场景
客户端:创建预测 GE(Key=5),生命值 100 → 80
服务器:创建服务器 GE(Key=5),生命值 100 → 80
客户端收到复制的 Key=5:
- 移除预测 GE(临时回到 100)
- 服务器 GE 复制过来(再次变 80)
- 结果:最终生命值 = 80 ✓
预测错误的场景
客户端:创建预测 GE(Key=5),生命值 100 → 80
服务器:拒绝(目标有免疫),不创建 GE
客户端收到复制的 Key=5:
- 移除预测 GE(回到 100)
- 没有服务器 GE 复制过来
- 结果:最终生命值 = 100 ✓(预测被回滚)
核心规则:客户端预测的任何东西,都必须把 PredictionKey 一起发给服务端,服务端用同一个 Key 执行后回传。 没有 Key 的操作,服务端不会"认领"为预测,客户端也永远等不到确认。
11.5 依赖预测键(Dependent Prediction Keys)
当预测嵌套发生时(技能 A 预测地激活技能 B,B 又施加 GE),需要依赖键保持链路:
客户端:激活 A(Key=1)→ 在 A 中激活 B(依赖 Key=1 的新 Key=2)
→ 在 B 中 ApplyGE(依赖 Key=2 的 Key=3)
服务端:以同样顺序执行,把 1、2、3 全部确认
依赖键保证:只要父操作未被确认,子操作也不可能先被确认;父操作被拒绝时,整条链回滚。这就是"为什么链式技能必须显式传递依赖键"的原因。
准确说法:引擎没有"MaxPredictionDepth=4"之类的配置项(旧资料有误)。预测深度受设计约束(每层嵌套都要维护依赖键),合理组织预测边界远比"调大上限"重要。经验法则是:每个技能内只预测"这一个技能的完整生命周期",不要把跨技能链无限嵌套。
11.6 ScopedPredictionWindow(核心 API)
FScopedPredictionWindow 是一个 RAII 作用域对象:构造时让 ASC 进入"预测窗口",析构时退出,窗口内的操作被标记为"本预测键的一部分"。
// 标准用法:施加一个"可预测的 GE"
void UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf_Predictive(
const FGameplayEffectSpec& Spec, FPredictionKey InPredictionKey)
{
FPredictionKey PredictionKey;
if (IsOwnerActorAuthoritative()) // 拥有者本地:生成键
{
PredictionKey = CreatePredictionKeyForNewServerAction();
}
else // 服务端:用客户端带来的键
{
PredictionKey = InPredictionKey;
}
FScopedPredictionWindow ScopedPredictionWindow(this, PredictionKey, true);
ApplyGameplayEffectSpecToSelf(Spec); // 窗口内的操作被记录
}
要点:
- 拥有者客户端:窗口内新生成键,操作立即本地执行(预测);
- 服务端:用客户端传来的键开窗口,权威执行并回传确认;
- 不加 ScopedPredictionWindow 就直接 Apply 的 GE,不会走预测链路(服务端会正常执行,但客户端没有"本地先行")。
预测技能的标准生命周期
以 LocalPredicted 技能为例(TryActivateAbility 内部就是此流程):
客户端调用 TryActivateAbility
→ ASC 客户端:
CreatePredictionKey(Current=++, Base=前键)
FScopedPredictionWindow(Key)
ServerTryActivateAbility(Handle, InputPressed, Key) // RPC
ClientTryActivateAbility → 本地立即执行 ActivateAbility
→ 服务端收到 ServerTryActivateAbility:
FScopedPredictionWindow(Key)
校验 CanActivate → 权威 ActivateAbility
执行 CommitAbility(扣蓝/启 CD)
复制结果回客户端
→ 客户端收到复制结果:
预测键被确认 → 本地表现采用服务端最终状态
若被拒绝 → 回滚本地效果
客户端预测性地"扣蓝"为何不会双扣
CommitAbility 在客户端预测性执行(本地立刻看到 MP 下降),服务端用同一个预测键权威执行。由于该 GE 带预测键,客户端本地那份"效果"在服务端结果到达时被标记为"已被服务端认领",不会再次应用——这就是为什么"预测扣蓝不会双扣"。
11.7 Scoped Prediction Window:在 Task 回调中开新窗口
为什么需要新窗口
Prediction Key 在能力激活时生成,只在同一帧内有效。如果一个 AbilityTask 的回调在后续帧执行(等待输入、等待延迟),原来的 Prediction Key 就过期了——回调中应用的 GE 无法被预测。
哪些 Task 自带新窗口
一些输入相关 AbilityTask 内置 Synch Point,会自动生成新的 Scoped Prediction Key:
WaitInputPressWaitInputReleaseWaitConfirmCancel- 其他输入相关任务
手动开新窗口:WaitNetSync
对没有内置 Synch Point 的 Task(如 WaitDelay),用 WaitNetSync(OnlyServerWait 模式)手动创建:
- 客户端:生成新的 Scoped Prediction Key → RPC 发给服务器 → 继续执行;
- 服务器:等待接收客户端的 Key → 收到后继续执行。
// 等待延迟后的回调需要新的预测窗口
void UMyGameplayAbility::OnDelayFinished()
{
UAbilityTask_WaitNetSync* SyncTask =
UAbilityTask_WaitNetSync::WaitNetSync(this, EAbilityTaskNetSyncType::OnlyServerWait);
SyncTask->OnSync.AddDynamic(this, &UMyGameplayAbility::OnSyncReady);
SyncTask->ReadyForActivation();
}
void UMyGameplayAbility::OnSyncReady()
{
// 现在有了新的 Scoped Prediction Key,可以预测性地应用 GE
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CostGE, GetAbilityLevel());
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
}
WaitNetSync 的安全问题
注意:
WaitNetSync(OnlyServerWait)会让服务器阻塞等待客户端的 RPC。恶意玩家可以故意延迟发送,造成"延迟开关"式作弊。
Epic 在 Paragon 的应对:需要即时确认的 Targeting Task 加超时机制(超时后服务器自行生成 TargetData 或取消能力);需要玩家确认的 Targeting 模式不设超时(玩家本来就需要时间瞄准)。
最佳实践:安全优先时避免 WaitNetSync,改用其他方式。Fortnite 的武器系统完全不使用 WaitNetSync——它把武器能力的激活、TargetData 发送、能力结束全部合并到一个 RPC 中(Ability Batching)。
场景:冲刺能力持续消耗体力的预测
void UGA_Sprint::ActivateAbility(...)
{
GetCharacterMovement()->SetMovementMode(MOVE_Custom, CMOVE_Sprint);
StartSprintCostLoop(); // 每帧消耗体力需要持续预测
}
void UGA_Sprint::StartSprintCostLoop()
{
UAbilityTask_WaitDelay* WaitTask =
UAbilityTask_WaitDelay::WaitDelay(this, SprintCostInterval);
WaitTask->OnFinish.AddDynamic(this, &UGA_Sprint::OnCostIntervalReady);
WaitTask->ReadyForActivation();
}
void UGA_Sprint::OnCostIntervalReady()
{
// 每次消耗前创建新的预测窗口
UAbilityTask_WaitNetSync* SyncTask =
UAbilityTask_WaitNetSync::WaitNetSync(this, EAbilityTaskNetSyncType::OnlyServerWait);
SyncTask->OnSync.AddDynamic(this, &UGA_Sprint::ApplySprintCost);
SyncTask->ReadyForActivation();
}
void UGA_Sprint::ApplySprintCost()
{
FGameplayEffectSpecHandle SpecHandle =
MakeOutgoingGameplayEffectSpec(SprintCostGE, GetAbilityLevel());
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
StartSprintCostLoop(); // 继续下一个周期
}
11.8 预测生成 Actor(投射物分工)
GAS 不提供开箱即用的预测 Actor 生成功能。SpawnActor AbilityTask 只在服务器上生成 Actor。
纯表现 Actor
如果 Actor 只是表现性的,限制服务器的复制范围即可:
// 拥有者用本地生成的版本,其他客户端用服务器版本
bool AMyCosmeticActor::IsNetRelevantFor(
const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
return !IsOwnedBy(ViewTarget);
}
游戏逻辑 Actor(投射物)——UnrealTournament 方案
客户端:生成"虚拟投射物"(Dummy Projectile,立即看到飞出)
服务器:生成真实的复制投射物(权威命中判定)
当服务器的投射物复制到客户端时 → 同步位置 → 虚拟投射物与服务器投射物"汇合"
标准分工(GASShooter 等项目的做法):
客户端(拥有者):
├─ 生成"预测投射物"(视觉本体,能立即看到飞出去)
└─ 发送 ServerSpawnProjectile(带预测键)→ 服务端
服务端:
├─ 用同一预测键生成"权威投射物"(负责命中判定/伤害)
└─ 命中后按权威数据广播伤害
客户端:
├─ 服务端确认 → 销毁/同步预测本体为权威体
└─ 若被拒 → 销毁预测本体(没放出来)
核心原则:预测只管"视觉与手感",伤害权威永远在服务端。
11.9 Instant GE 的预测特性
对自己的 Cost GE:无缝预测
Instant GE(如 Cost GE)对自己属性修改可无缝预测:
// 法力消耗 Cost GE(Instant)
// 预测时:客户端立即扣蓝
// 服务器确认后:服务器的 GE 复制过来(同一预测键,不会双扣)
对其他角色的 Instant GE:短暂"闪烁"
当你预测性地对其他角色应用 Instant GE 时,会出现短暂属性值"闪烁":
客户端预测:敌人生命值 100 → 80(预测的 Instant GE 被当作 Infinite GE 处理,为了能回滚)
服务器确认:敌人生命值 100 → 80(服务器 Instant GE 复制到客户端)
问题:客户端同时有两个 GE 存在(预测的 Infinite + 服务器的 Instant)
结果:生命值可能出现短暂的 60(100-20-20)然后回到 80
原因:预测的 Instant GE 被当作 Infinite GE 处理(这样预测错误才能回滚)。服务器 Instant GE 到达时,两个效果短暂叠加。
最佳实践:不要预测对其他角色的 Instant GE 伤害。这是 Paragon 和 Fortnite 不预测伤害的主要原因之一。
11.10 预测的局限性与应对
GE 移除不可预测
限制:GAS 不能预测 GE 的移除。移除减速效果必须等服务端确认,玩家会感受到延迟。
Dave Ratti 表示曾尝试添加"GE 移除预测",但未解决所有边界情况。
变通方案:预测性地添加"反向效果"来抵消:
// 预测一个 40% 减速
ApplyGameplayEffectSpecToOwner(SlowGE_40);
// 想要移除减速时,预测性地添加一个 40% 加速
ApplyGameplayEffectSpecToOwner(SpeedBuff_40);
// 然后同时移除两个效果
注意:不适用于所有场景,特别是冷却。无法用"反向冷却"预测冷却结束。
冷却预测的固有缺陷
玩家 A(50ms 延迟)和玩家 B(200ms 延迟)都有 1 秒冷却的技能:
A:激活 → 50ms 后服务器确认 → 1s 后冷却结束 → 50ms 后客户端收到 ≈ 1.1s 间隔
B:激活 → 200ms 后服务器确认 → 1s 后冷却结束 → 200ms 后客户端收到 ≈ 1.4s 间隔
→ 高延迟玩家射速天然劣势!
Fortnite 的解决方案:不使用 GE 做武器冷却,用自定义计时器管理射击速率,完全绕过 GAS 冷却系统。
Dave Ratti: "Fortnite does its own bookkeeping for weapon firing rates: it does not use GEs for cooldowns on weapons. I would recommend this if this is a critical problem for your game."
Network Prediction Plugin(未来)
Epic 正在开发新插件,旨在替代 CharacterMovementComponent 并与 GAS 深度集成:移除 Prediction Key 显式交换、改用 NetworkPrediction frames、支持 GE 移除预测、更好的物理预测。目前仍在早期开发中,未稳定到生产可用。
11.11 回滚(Rollback)与预测失败
发生回滚的典型场景:
| 场景 | 结果 |
|---|---|
| 客户端预测激活,服务端判定 CanActivate 失败(如其实在冷却) | 服务端回绝该 Key → 客户端回滚动画/状态,技能取消 |
| 客户端预测造成属性变化,服务端计算出不同值 | 以服务端值为准覆盖 |
| 依赖键链路中父级失败 | 整条链回滚 |
GAS 的回滚机制:
- 属性回滚:ASC 记录预测期间的属性快照(
PredictionMap),Key 被拒时恢复; - 效果回滚:预测性添加的 ActiveGE 在 Key 被拒时移除;
- 动画回滚:Montage 的预测/回滚由 Task 的键管理,被拒时回到预测前状态。
调试预测问题要同时看客户端与服务端两份日志(
LogAbilitySystem),比对"哪个键被拒绝、为什么"。别只盯一端。
11.12 常见陷阱清单
| 陷阱 | 说明与应对 |
|---|---|
| 忘记预测键已过期 | Task 回调中直接 ApplyGE(回调在后续帧,原 Key 已过期)→ 效果"闪烁"。应对:回调前用 WaitNetSync 或自带 Synch Point 的 Task 开新窗口 |
| 试图预测 GE 移除 | 客户端移除了但服务器不知道 → 状态不一致。应对:用"反向效果"模拟移除 |
| 预测敌人伤害 | 客户端预测敌人死亡 → 布娃娃;服务器说没死 → 敌人"复活",体验极差。应对:伤害只在服务器算 |
| 冷却预测延迟劣势 | 高延迟玩家射速天然劣势。应对:参考 Fortnite 自定义计时器 |
| WaitNetSync 安全风险 | 恶意延迟 RPC 卡住服务器能力。应对:超时机制 / 尽量少用 / 用 Ability Batching 替代 |
| Instant GE 对他人预测闪烁 | 预测的 Instant 被当 Infinite 处理导致双叠。应对:不预测对他人伤害 |
11.13 使用场景分析
场景一:预测冲刺(移动速度变化)
按下 Shift → 预测性应用冲刺 GE(Modifier 改移速,可预测)
→ 客户端立即加速 → 服务器确认 → 移除预测 GE 保留服务器 GE → 无缝
- Duration GE + Modifier(可预测)+ 不用 ExecCalc。
场景二:预测射击(动画预测,不预测伤害)
点击鼠标 → 预测播放射击动画和音效 → RPC 到服务器
→ 服务器用 ExecCalc 计算伤害 → 应用伤害 GE → 生成命中 GameplayCue
- 动画/音效预测 ✓;伤害不预测 ✓;GameplayCue 通过 GE 自动触发。
场景三:预测消耗(法力/体力扣除)
按技能键 → 预测扣除法力(Cost GE, Instant)→ 动画开始 → 服务器确认
→ 法力不足则服务器拒绝 → 客户端回滚
- Cost GE 用 Modifier(可预测)、对自己 Instant 无缝预测、持续消耗用 WaitNetSync。
场景四:为什么不该预测伤害(四理由)
1. 预测错误 → "血条跳动"(-20 血跳回原值,玩家困惑)
2. 预测死亡更危险 → 敌人布娃娃后"复活"继续攻击
3. ExecCalc 不可预测 → 复杂公式无法保证两端一致
4. Epic 经验 → Paragon/Fortnite 都不预测伤害,Dave Ratti: "I personally do not recommend it"
11.14 本章验证
- [ ] 配置一个
LocalPredicted技能,验证客户端"立即看到动画/扣蓝"而服务端权威结算; - [ ] 故意让服务端拒绝(把冷却调短让客户端误判),观察客户端回滚动画;
- [ ] 实现预测性投射物(客户端视觉本体 + 服务端权威体),验证命中判定只认服务端;
- [ ] 用 WaitNetSync 实现冲刺持续消耗体力的预测;
- [ ] 能解释"预测扣蓝为什么不会双扣";
- [ ] 能解释"预测 Instant GE 对他人为什么会闪烁";
- [ ] 设计一个射击能力的预测策略:哪些预测、哪些不预测、为什么。