Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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))
}

小结

预测与补偿的核心要点:

  1. 客户端预测:立即显示,减少延迟感
  2. 服务器回溯:公平判定,补偿延迟
  3. 纠正策略:平滑过渡,避免跳跃

真实案例

  • 《守望先锋》:预测 + 回溯 + 投射物预测

踩坑经验

  • ❌ 不要忘记纠正预测错误
  • ❌ 不要省略服务器回溯
  • ❌ 不要无限回溯

下一节(4.5)我们将学习:回放与观战,深入了解如何实现战斗录像和观战系统。