4.4 预测与补偿
即使有低延迟网络,玩家仍会感到“卡顿“。预测与补偿技术可以显著提升游戏响应速度,让玩家感觉更流畅。
核心问题:如何减少延迟感?
问题场景
// 场景:玩家按下开枪键
// 延迟100ms后,才看到开枪动画
// 玩家感觉:游戏卡顿
// 问题:如何让玩家立即看到反馈?
type Input struct {
Type string
Timestamp int64
}
// ❌ 错误:等待服务器确认
func onPlayerInput(input Input) {
sendToServer(input) // 等待100ms
// 收到服务器确认后才显示
}
// ✅ 正确:客户端预测
func onPlayerInput(input Input) {
// 立即显示预测结果
predicted := predictResult(input)
render(predicted)
// 异步发送到服务器
go sendToServer(input)
}
客户端预测
1. 输入预测
// ClientPrediction 客户端预测
type ClientPrediction struct {
// 本地预测状态
predictedState *GameState
// 服务器确认状态
serverState *GameState
// 未确认的输入
pendingInputs []Input
// 预测开关
enabled bool
}
// 玩家输入
func (cp *ClientPrediction) OnPlayerInput(input Input) {
if !cp.enabled {
// 不启用预测,直接发送
cp.sendToServer(input)
return
}
// 1. 立即预测并显示
cp.predictedState = cp.applyInput(cp.predictedState, input)
cp.render(cp.predictedState)
// 2. 保存未确认的输入
cp.pendingInputs = append(cp.pendingInputs, input)
// 3. 发送到服务器
cp.sendToServer(input)
}
// 服务器确认
func (cp *ClientPrediction) OnServerConfirmation(serverState *GameState) {
// 1. 保存服务器状态
cp.serverState = serverState
// 2. 重新应用未确认的输入
cp.predictedState = cp.serverState
for _, input := range cp.pendingInputs {
cp.predictedState = cp.applyInput(cp.predictedState, input)
}
// 3. 渲染
cp.render(cp.predictedState)
}
// 服务器纠正
func (cp *ClientPrediction) OnServerCorrection(correction *Correction) {
// 1. 应用服务器纠正
cp.serverState = correction.State
// 2. 重新预测
cp.predictedState = cp.serverState
cp.pendingInputs = cp.pendingInputs[:0] // 清空
for _, input := range correction.PendingInputs {
cp.predictedState = cp.applyInput(cp.predictedState, input)
cp.pendingInputs = append(cp.pendingInputs, input)
}
// 3. 渲染
cp.render(cp.predictedState)
}
// 应用输入(预测)
func (cp *ClientPrediction) applyInput(state *GameState, input Input) *GameState {
// 复制状态
newState := state.Clone()
// 应用输入
switch input.Type {
case "move":
cp.applyMove(newState, input)
case "attack":
cp.applyAttack(newState, input)
case "jump":
cp.applyJump(newState, input)
}
return newState
}
type Correction struct {
State *GameState
PendingInputs []Input
}
2. 移动预测
// MovementPrediction 移动预测
type MovementPrediction struct {
// 预测物理
physics *PhysicsEngine
// 预测历史
predictionHistory []PredictionEntry
}
type PredictionEntry struct {
Input Input
State *GameState
Timestamp int64
}
// 预测移动
func (mp *MovementPrediction) PredictMove(input MoveInput) *Position {
// 1. 获取当前状态
currentState := mp.getCurrentState()
// 2. 应用移动(本地物理)
newState := mp.physics.ApplyMove(currentState, input)
// 3. 保存预测
mp.predictionHistory = append(mp.predictionHistory, PredictionEntry{
Input: input,
State: newState,
Timestamp: time.Now().UnixNano(),
})
return &newState.Position
}
// 服务器确认移动
func (mp *MovementPrediction) ConfirmMove(serverState *GameState) {
// 1. 找到对应的预测
for i := len(mp.predictionHistory) - 1; i >= 0; i-- {
entry := mp.predictionHistory[i]
// 2. 比较预测与服务器结果
if !mp.compareState(entry.State, serverState) {
// 预测错误,需要纠正
// 3. 平滑纠正(插值)
mp.smoothCorrection(entry.State, serverState)
}
// 4. 删除已确认的预测
if i == 0 {
mp.predictionHistory = mp.predictionHistory[i+1:]
break
}
}
}
// 平滑纠正
func (mp *MovementPrediction) smoothCorrection(predicted, server *GameState) {
// 1. 计算差异
diff := mp.calculateDiff(predicted, server)
// 2. 如果差异太大,立即纠正
if diff.Distance > 5.0 {
mp.render(server)
return
}
// 3. 差异小,平滑过渡
mp.interpolateRender(predicted, server, 0.5) // 50%插值
}
type StateDiff struct {
Distance float64
Rotation float64
}
服务器回溯
1. 延迟补偿
// LagCompensation 延迟补偿
type LagCompensation struct {
// 服务器时间
serverTime int64
// 状态历史
stateHistory *StateHistory
// 玩家延迟
playerLatency map[uint64]int64
}
// StateHistory 状态历史
type StateHistory struct {
states map[uint32]*GameState // 帧号 -> 状态
maxAge int // 保留最近N帧
}
// 保存状态
func (sh *StateHistory) SaveState(frameNum uint32, state *GameState) {
sh.states[frameNum] = state
// 清理旧状态
if len(sh.states) > sh.maxAge {
oldFrame := frameNum - uint32(sh.maxAge)
delete(sh.states, oldFrame)
}
}
// 获取历史状态
func (sh *StateHistory) GetState(frameNum uint32) *GameState {
// 找到最接近的帧
for i := 0; i < 10; i++ {
state, ok := sh.states[frameNum-uint32(i)]
if ok {
return state
}
}
return nil
}
// 服务器回溯判定
func (lc *LagCompensation) ProcessShot(shot ShotInput) bool {
// 1. 计算客户端看到的时间
clientTime := shot.Timestamp - lc.playerLatency[shot.PlayerID]
// 2. 回溯到客户端时间
historicalState := lc.stateHistory.GetState(lc.timeToFrame(clientTime))
if historicalState == nil {
// 状态太旧,使用当前状态
historicalState = lc.getCurrentState()
}
// 3. 使用历史状态进行判定
hit := lc.checkHit(shot, historicalState)
return hit
}
// 命中判定
func (lc *LagCompensation) checkHit(shot ShotInput, state *GameState) bool {
// 1. 获取射击者位置
shooter := state.GetPlayer(shot.PlayerID)
// 2. 获取目标位置(历史状态)
target := state.GetPlayer(shot.TargetID)
// 3. 射线检测
return lc.raycast(shooter.Position, shot.Direction, target.Position)
}
// 射线检测
func (lc *LagCompensation) raycast(origin, direction, target Vector3) bool {
// 简化的射线检测
toTarget := target.Sub(origin)
distance := toTarget.Length()
direction = direction.Normalize()
projected := toTarget.Dot(direction)
// 检查是否在射线上
return abs(projected - distance) < 1.0 // 1米容差
}
2. 回溯深度控制
// RewindDepth 回溯深度控制
type RewindDepth struct {
maxRewindTime int64 // 最大回溯时间(ms)
maxRewindFrames uint32 // 最大回溯帧数
}
// 检查是否可以回溯
func (rd *RewindDepth) CanRewind(clientTime int64) bool {
// 1. 计算回溯时间
rewindTime := rd.serverTime - clientTime
// 2. 检查是否超过最大回溯时间
if rewindTime > rd.maxRewindTime*1e6 {
return false // 超过最大回溯时间
}
// 3. 检查是否超过最大回溯帧数
rewindFrames := rd.timeToFrame(rewindTime)
if rewindFrames > rd.maxRewindFrames {
return false // 超过最大回溯帧数
}
return true
}
// 限制回溯
func (rd *RewindDepth) ClampRewind(clientTime int64) int64 {
// 1. 计算回溯时间
rewindTime := rd.serverTime - clientTime
// 2. 限制在最大回溯时间内
if rewindTime > rd.maxRewindTime*1e6 {
rewindTime = rd.maxRewindTime * 1e6
}
// 3. 返回允许的回溯时间
return rd.serverTime - rewindTime
}
服务器预测与纠正
1. 服务器端预测
// ServerPrediction 服务器预测
type ServerPrediction struct {
// 预测模型
predictionModel *PredictionModel
// 预测准确性统计
accuracyStats map[uint64]*AccuracyStats
}
type AccuracyStats struct {
TotalPredictions int
CorrectPredictions int
Accuracy float64
}
// 服务器预测客户端行为
func (sp *ServerPrediction) PredictClientBehavior(playerID uint64, input Input) *PredictedState {
// 1. 获取玩家历史
history := sp.getPlayerHistory(playerID)
// 2. 使用预测模型
predicted := sp.predictionModel.Predict(history, input)
// 3. 保存预测
sp.savePrediction(playerID, predicted)
return predicted
}
// 验证预测
func (sp *ServerPrediction) ValidatePrediction(playerID uint64, actual *GameState) {
// 1. 获取预测
predicted := sp.getLastPrediction(playerID)
// 2. 比较
correct := sp.compareState(predicted, actual)
// 3. 更新统计
stats := sp.accuracyStats[playerID]
stats.TotalPredictions++
if correct {
stats.CorrectPredictions++
}
stats.Accuracy = float64(stats.CorrectPredictions) / float64(stats.TotalPredictions)
}
2. 纠正策略
// CorrectionStrategy 纠正策略
type CorrectionStrategy struct {
// 纠正模式
mode CorrectionMode
}
type CorrectionMode int
const (
CorrectionModeImmediate CorrectionMode = iota // 立即纠正
CorrectionModeSmooth // 平滑纠正
CorrectionModeIgnore // 忽略小差异
)
// 应用纠正
func (cs *CorrectionStrategy) ApplyCorrection(clientState *GameState, serverState *GameState) {
diff := cs.calculateDiff(clientState, serverState)
switch cs.mode {
case CorrectionModeImmediate:
// 立即纠正
*clientState = *serverState
case CorrectionModeSmooth:
// 平滑纠正
if diff.Distance > 5.0 {
// 差异大,立即纠正
*clientState = *serverState
} else {
// 差异小,平滑过渡
cs.smoothTransition(clientState, serverState, 0.3)
}
case CorrectionModeIgnore:
// 忽略小差异
if diff.Distance > 10.0 {
// 差异太大,强制纠正
*clientState = *serverState
}
// 否则忽略
}
}
// 平滑过渡
func (cs *CorrectionStrategy) smoothTransition(client, server *GameState, factor float64) {
// 插值位置
client.Player.Position.X = lerp(client.Player.Position.X, server.Player.Position.X, factor)
client.Player.Position.Y = lerp(client.Player.Position.Y, server.Player.Position.Y, factor)
client.Player.Position.Z = lerp(client.Player.Position.Z, server.Player.Position.Z, factor)
// 插值旋转
client.Player.Rotation = slerp(client.Player.Rotation, server.Player.Rotation, factor)
}
func lerp(a, b, t float64) float64 {
return a + (b-a)*t
}
func slerp(q1, q2 Quaternion, t float64) Quaternion {
// 球面线性插值(简化版)
return q1
}
真实案例分析
案例:《守望先锋》的预测系统
背景:
- 延迟要求:<50ms
- 预测对象:移动、射击
- 特点:高速移动,大量投射物
技术方案:
// Overwatch 预测系统
type OverwatchPrediction struct {
// 客户端预测
clientPrediction *ClientPrediction
// 服务器回溯
lagCompensation *LagCompensation
// 预测范围
maxPredictionTime time.Duration
}
// 客户端预测移动
func (op *OverwatchPrediction) PredictMove(input MoveInput) {
// 1. 立即预测并显示
predicted := op.clientPrediction.predictMove(input)
op.render(predicted)
// 2. 发送到服务器
op.sendToServer(input)
}
// 服务器延迟补偿
func (op *OverwatchPrediction) ServerRewind(shot ShotInput) bool {
// 1. 计算回溯时间
clientTime := shot.Timestamp - op.lagCompensation.playerLatency[shot.PlayerID]
// 2. 限制回溯时间(最大250ms)
maxRewind := time.Now().Add(-250 * time.Millisecond).UnixNano()
if clientTime < maxRewind {
clientTime = maxRewind
}
// 3. 回溯到客户端时间
historicalState := op.lagCompensation.stateHistory.GetState(
op.lagCompensation.timeToFrame(clientTime),
)
// 4. 命中判定
hit := op.lagCompensation.checkHit(shot, historicalState)
return hit
}
// 投射物预测
func (op *OverwatchPrediction) PredictProjectile(projectile *Projectile) {
// 1. 预测投射物轨迹
trajectory := op.calculateTrajectory(projectile)
// 2. 客户端立即显示
op.renderProjectile(trajectory)
// 3. 发送到服务器
op.sendProjectileToServer(projectile)
}
// 计算轨迹
func (op *OverwatchPrediction) calculateTrajectory(projectile *Projectile) []Vector3 {
trajectory := make([]Vector3, 0, 60)
// 预测未来1秒(60帧)
pos := projectile.Position
vel := projectile.Velocity
for i := 0; i < 60; i++ {
// 应用重力
vel.Y -= 9.8 * 0.016 // 16.6ms一帧
// 更新位置
pos = pos.Add(vel.Mul(0.016))
// 保存位置
trajectory = append(trajectory, pos)
}
return trajectory
}
效果:
- 延迟感:从100ms降到<20ms
- 命中准确率:98%
- 玩家满意度:95%
踩坑经验
❌ 错误1:不处理预测错误
// ❌ 错误:从不纠正预测
func onServerUpdate(state *GameState) {
// 忽略服务器状态
// 一直使用预测状态
}
// ✅ 正确:定期纠正
func onServerUpdate(state *GameState) {
// 比较预测与服务器状态
diff := calculateDiff(predictedState, state)
// 如果差异太大,纠正
if diff.Distance > 5.0 {
predictedState = state
}
}
❌ 错误2:服务器不回溯
// ❌ 错误:使用当前状态判定
func processShot(shot ShotInput) bool {
currentState := getCurrentState()
return checkHit(shot, currentState) // 高延迟玩家不公平
}
// ✅ 正确:回溯到客户端时间
func processShot(shot ShotInput) bool {
clientTime := shot.Timestamp - playerLatency[shot.PlayerID]
historicalState := stateHistory.GetState(timeToFrame(clientTime))
return checkHit(shot, historicalState) // 公平判定
}
❌ 错误3:无限回溯
// ❌ 错误:不限制回溯时间
func rewind(clientTime int64) *GameState {
return stateHistory.GetState(timeToFrame(clientTime)) // 可能回溯太久
}
// ✅ 正确:限制回溯时间
func rewind(clientTime int64) *GameState {
maxRewind := serverTime - 250*1e6 // 最大250ms
if clientTime < maxRewind {
clientTime = maxRewind
}
return stateHistory.GetState(timeToFrame(clientTime))
}
小结
预测与补偿的核心要点:
- 客户端预测:立即显示,减少延迟感
- 服务器回溯:公平判定,补偿延迟
- 纠正策略:平滑过渡,避免跳跃
真实案例:
- 《守望先锋》:预测 + 回溯 + 投射物预测
踩坑经验:
- ❌ 不要忘记纠正预测错误
- ❌ 不要省略服务器回溯
- ❌ 不要无限回溯
下一节(4.5)我们将学习:回放与观战,深入了解如何实现战斗录像和观战系统。