# 预测与回滚 ⭐
兼顾低延迟和一致性的终极方案;竞技格斗游戏的核心技术
前置知识:第03章 帧同步Lockstep + 第04章 状态同步
阅读指南(初学者必看)
为什么你需要学习预测与回滚?
帧同步操作延迟大(等所有人输入),状态同步画面延迟大(等服务器确认)。预测与回滚结合了两者优点:先预测推进,错了再回滚——这是目前竞技格斗游戏的标配技术(GGPO)。
学完本章,你能回答:
- GGPO 的核心原理是什么?预测+回滚怎么配合?
- Rollback Netcode 的完整实现是怎样的?
- 输入延迟为什么反而能减少回滚?设几帧合适?
- 状态保存与恢复怎么实现?内存开销多大?
- Source引擎的延迟补偿是怎么做的?
本文结构
第一部分:GGPO(Good Game Peace Out)——预测回滚的核心原理
第二部分:Rollback Netcode 完整实现
第三部分:输入延迟——人为增加延迟来减少回滚
第四部分:状态保存与恢复——回滚的工程实现
第五部分:延迟补偿(Lag Compensation)——让高延迟玩家也公平
一、GGPO(Good Game Peace Out)
生活类比:预测回滚就像写文章。你写了一段(预测),编辑说"这里不对"(服务器校正),你就从那个地方重写(回滚+重新模拟)。
核心思想:
1. 客户端预测:不等其他玩家输入,假设他们不变,先推进
2. 输入到达:如果预测正确,继续;如果错误,回滚到预测前状态,用正确输入重新模拟
3. 状态保存:每帧保存完整状态快照,用于回滚
流程:
Frame N:
保存状态快照 snapshot[N]
预测其他玩家输入 = 上一帧输入
模拟一帧
Frame N+1:
收到玩家A的真实输入(和预测不同!)
回滚到 snapshot[N]
用真实输入重新模拟 Frame N
继续模拟 Frame N+1
二、Rollback Netcode 完整实现
class RollbackNetcode {
constructor() {
this.currentFrame = 0;
this.inputBuffer = new Map(); // frame -> { playerId: input }
this.gameStates = new Map(); // frame -> gameState(用于回滚)
this.maxRollbackFrames = 8; // 最多回滚8帧
}
// 保存当前帧的状态
saveState() {
const state = JSON.parse(JSON.stringify(this.gameState));
this.gameStates.set(this.currentFrame, state);
// 只保留最近的N帧状态
const toDelete = this.currentFrame - this.maxRollbackFrames;
this.gameStates.delete(toDelete);
}
// 执行一帧
executeFrame() {
// 保存当前状态
this.saveState();
// 获取所有玩家的输入
const localInput = this.getLocalInput();
const remoteInput = this.inputBuffer.get(this.currentFrame);
// 如果远程输入还没到,预测
const allInputs = {
local: localInput,
remote: remoteInput || this.predictRemoteInput()
};
// 执行游戏逻辑
this.applyInputs(allInputs);
this.updateGameState();
this.render();
this.currentFrame++;
}
// 收到远程输入
onRemoteInput(frame, input) {
this.inputBuffer.set(frame, input);
// 检查是否和预测不同
if (frame < this.currentFrame) {
// 这个帧已经执行过了
// 需要回滚!
this.rollbackToFrame(frame);
}
}
// 回滚到指定帧
rollbackToFrame(targetFrame) {
// 1. 恢复目标帧的状态
this.gameState = JSON.parse(JSON.stringify(this.gameStates.get(targetFrame)));
this.currentFrame = targetFrame;
// 2. 从目标帧重新执行到当前帧
while (this.currentFrame < this.savedCurrentFrame) {
const localInput = this.savedLocalInputs.get(this.currentFrame);
const remoteInput = this.inputBuffer.get(this.currentFrame);
if (remoteInput) {
// 用真实的远程输入
this.applyInputs({ local: localInput, remote: remoteInput });
}
this.updateGameState();
this.currentFrame++;
}
// 3. 恢复渲染
this.render();
}
// 预测远程输入
predictRemoteInput() {
// 最简单的预测:假设和上一帧一样
const prevInput = this.inputBuffer.get(this.currentFrame - 1);
return prevInput || { action: 'idle' };
}
}
三、输入延迟
输入延迟 = 人为增加的操作延迟
为什么要有输入延迟?
- 给网络传输留出时间
- 输入延迟 2 帧 = 33ms → 在这 33ms 内,其他玩家的输入可以到达
- 减少需要回滚的次数
设置:
- 本地网络好:1~2 帧延迟
- 网络一般:3~5 帧延迟
- 网络差:5~7 帧延迟
格斗游戏通常固定 2~3 帧输入延迟
四、状态保存与恢复
class RollbackManager {
constructor(maxFrames = 60) {
this.snapshots = new Map(); // frame → 状态快照
this.maxFrames = maxFrames;
}
save(frame, gameState) {
this.snapshots.set(frame, deepClone(gameState));
// 清理过老的快照
const oldest = frame - this.maxFrames;
for (const key of this.snapshots.keys()) {
if (key < oldest) this.snapshots.delete(key);
}
}
restore(frame) {
return deepClone(this.snapshots.get(frame));
}
rollback(fromFrame, toFrame, correctInputs) {
// 1. 恢复到 toFrame 的状态
let state = this.restore(toFrame);
// 2. 用正确输入重新模拟
for (let f = toFrame; f <= fromFrame; f++) {
simulateOneFrame(state, correctInputs[f]);
this.save(f, state); // 保存重新模拟后的状态
}
return state;
}
}
内存开销:
- 格斗游戏每帧状态可能只有几 KB,保存 60 帧约 300KB
- MMO 的状态可能几 MB,保存 60 帧就是几百 MB
- 所以预测回滚主要用于状态较小的竞技游戏
五、延迟补偿(Lag Compensation)
什么是延迟补偿?
延迟补偿 = 让高延迟玩家和低延迟玩家体验一致
问题:
玩家A(延迟10ms)射击玩家B(延迟100ms)
A看到B在位置X,发射子弹
但B在100ms前已经移动到位置Y
子弹打不中B → A觉得不公平
解决方案:服务器回退时间
服务器收到A的射击请求时,
回退到10ms前的状态(A看到的画面),
在那个状态下判定是否命中。
Source引擎的延迟补偿实现
class LagCompensation {
constructor() {
this.history = []; // 过去的游戏状态
this.maxHistory = 1000; // 保存1秒的历史(1000ms)
}
// 每帧保存状态
saveHistory(timestamp, state) {
this.history.push({ timestamp, state });
// 删除太旧的历史
while (this.history.length > 0 &&
timestamp - this.history[0].timestamp > this.maxHistory) {
this.history.shift();
}
}
// 处理射击,使用延迟补偿
processShot(shooterId, aimPosition, shotTimestamp) {
// shotTimestamp = 射击者的本地时间
// 找到射击者看到的世界状态
const historicalState = this.findStateAt(shotTimestamp);
if (!historicalState) {
// 历史太旧,用当前状态
return this.processShotCurrentState(shooterId, aimPosition);
}
// 在历史状态下判定命中
const hitPlayer = this.checkHit(historicalState, aimPosition);
if (hitPlayer) {
// 命中!应用到当前状态
this.applyDamage(hitPlayer, shooterId);
return { hit: true, playerId: hitPlayer.id };
}
return { hit: false };
}
// 找到指定时间的状态
findStateAt(timestamp) {
for (let i = this.history.length - 1; i >= 0; i--) {
if (this.history[i].timestamp <= timestamp) {
return this.history[i].state;
}
}
return null;
}
}
六、GGPO 同步点校验
// 关键概念:同步点(Sync Point)
// 每隔N帧,所有客户端交换状态哈希,验证一致性
class GGPOClient {
constructor() {
this.syncInterval = 60; // 每60帧同步一次
this.frameCount = 0;
this.stateChecksum = 0;
}
executeFrame() {
this.frameCount++;
super.executeFrame();
// 定期同步
if (this.frameCount % this.syncInterval === 0) {
this.checksum = this.calculateChecksum();
this.sendChecksum(this.checksum);
}
}
onRemoteChecksum(checksum) {
if (checksum !== this.checksum) {
console.error('状态不同步!需要重新同步');
this.resync();
}
}
calculateChecksum() {
// 用游戏状态计算一个哈希值
let hash = 0;
for (const player of Object.values(this.gameState.players)) {
hash = ((hash << 5) - hash + player.x) | 0;
hash = ((hash << 5) - hash + player.y) | 0;
hash = ((hash << 5) - hash + player.hp) | 0;
}
return hash;
}
}
自问自答
Q:预测回滚和帧同步是什么关系? A:预测回滚是帧同步的增强。帧同步必须等所有输入到齐才能推进帧,预测回滚允许先预测推进,错了再回滚。本质是在帧同步基础上加了"预测"和"回滚"两层。
Q:回滚会不会导致画面闪烁? A:会,这是预测回滚的主要视觉问题。解决方案:1)在回滚重模拟后,对角色位置做插值平滑而不是直接跳转;2)适当增加输入延迟减少回滚频率;3)视觉上用动画过渡掩盖位置跳变。
Q:状态快照的内存开销大吗? A:取决于游戏状态大小和保存帧数。一个格斗游戏的状态可能只有几 KB,保存 60 帧约 300KB。但一个 MMO 的状态可能几 MB,保存 60 帧就是几百 MB。所以预测回滚主要用于状态较小的竞技游戏。
Q:为什么格斗游戏固定 2~3 帧输入延迟?
A:23 帧 ≈ 3350ms,人类感知不到这个延迟。但这个延迟足够让大多数本地网络对局中其他玩家的输入到达,从而大幅减少回滚次数。是延迟体验和回滚频率的最佳平衡点。
Q:回滚网络代码能用在H5游戏中吗? A:可以。但H5游戏的性能限制(JavaScript执行速度)意味着回滚帧数不宜过多(建议<5帧)。简单的预测+回滚就足够提升体验。
实践任务
- 任务1:实现 GGPO 风格的预测回滚 demo(双人,预测输入+回滚重模拟)
- 任务2:对比有无预测回滚的操作体验——在 100ms 延迟下,操作手感差异有多大
- 任务3:测试不同网络延迟下的回滚频率,绘制"延迟 vs 回滚次数"图表
- 任务4:实现输入延迟可调功能,找到最佳输入延迟值
- 任务5:优化回滚后的视觉表现——添加位置插值平滑,消除画面闪烁
- 任务6:实现延迟补偿系统,模拟高延迟玩家射击低延迟玩家的场景
与其他章节的关联
| 本章内容 | 关联章节 | 关联点 |
|---|---|---|
| 预测机制 | 第04章 状态同步 | 状态同步也有客户端预测,但不回滚——只校正 |
| 回滚重模拟 | 第03章 帧同步 | 回滚本质是帧同步+预测,确定性是前提 |
| 状态快照 | 第04章 状态同步 | 状态同步的快照用于增量更新,预测回滚的快照用于回滚 |
| 输入延迟 | 第06章 自定义UDP协议与KCP | 输入延迟需要可靠的输入传输,自定义UDP可以优化 |
| 延迟补偿 | 第08章 游戏安全与反作弊 | 延迟补偿可能被滥用,需要服务端校验 |
| 反作弊 | 第08章 游戏安全与反作弊 | 预测回滚的反作弊难度介于帧同步和状态同步之间 |
上一章:04-状态同步 下一章:06-自定义UDP协议与KCP