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.3 确定性计算

确定性是帧同步的核心要求,保证所有客户端执行相同逻辑得到相同结果。任何不确定性都会导致游戏状态分叉,玩家看到不同的世界。

核心问题:如何保证所有客户端计算一致?

问题场景

// 场景:简单的浮点数运算
// 客户端A(Intel CPU):0.1 + 0.2 = 0.30000000000000004
// 客户端B(AMD CPU):0.1 + 0.2 = 0.30000000000000007
// 客户端C(ARM CPU):0.1 + 0.2 = 0.30000000000000002

// 问题:如何保证所有客户端结果相同?
func calculate() {
    result := 0.1 + 0.2
    // 不同CPU可能产生不同结果
}

确定性挑战

挑战1:浮点数精度

// ❌ 非确定性:浮点数运算
type Position struct {
    X float64
    Y float64
}

func updatePosition(p *Position, vx, vy float64) {
    p.X += vx  // 不同CPU精度不同
    p.Y += vy
}

// ✅ 确定性:定点数
type FixedPosition struct {
    X int64  // 定点数(精确)
    Y int64
}

const SCALE = 1000  // 精度到小数点后3位

func updateFixedPosition(p *FixedPosition, vx, vy int64) {
    p.X += vx  // 精确计算
    p.Y += vy
}

// 定点数工具函数
func FixedFromFloat(f float64) int64 {
    return int64(f * SCALE)
}

func FixedToFloat(i int64) float64 {
    return float64(i) / SCALE
}

// 使用示例
func fixedPointExample() {
    pos := &FixedPosition{
        X: FixedFromFloat(100.5),
        Y: FixedFromFloat(200.3),
    }

    // 移动
    pos.X += FixedFromFloat(10.2)  // 精确
    pos.Y += FixedFromFloat(20.1)

    // 转换为浮点数显示
    x := FixedToFloat(pos.X)  // 110.7
    y := FixedToFloat(pos.Y)  // 220.4
}

挑战2:随机数生成

// ❌ 非确定性:使用系统时间作为种子
func randomDamage() int {
    rand.Seed(time.Now().UnixNano())  // 每个客户端不同
    return rand.Intn(100)  // 结果不同
}

// ✅ 确定性:使用固定种子
type DeterministicRandom struct {
    seed uint64
    current uint64
}

func NewDeterministicRandom(seed uint64) *DeterministicRandom {
    return &DeterministicRandom{
        seed:    seed,
        current: seed,
    }
}

// 线性同余生成器(LCG)
func (dr *DeterministicRandom) Next() int32 {
    dr.current = (dr.current*1103515245 + 12345) & 0x7fffffff
    return int32(dr.current)
}

// 使用示例
func deterministicRandomExample() {
    // 所有客户端使用相同种子
    rng := NewDeterministicRandom(12345)

    // 所有客户端得到相同的随机数序列
    damage1 := rng.Next()  // 所有客户端相同
    damage2 := rng.Next()  // 所有客户端相同
    damage3 := rng.Next()  // 所有客户端相同
}

挑战3:容器遍历顺序

// ❌ 非确定性:map遍历顺序不确定
func processPlayers(players map[uint64]*Player) {
    for _, player := range players {
        player.Update()  // 执行顺序不确定
    }
}

// ✅ 确定性:排序后执行
func processPlayersDeterministic(players map[uint64]*Player) {
    // 1. 提取所有玩家ID
    ids := make([]uint64, 0, len(players))
    for id := range players {
        ids = append(ids, id)
    }

    // 2. 排序(保证顺序一致)
    sort.Slice(ids, func(i, j int) bool {
        return ids[i] < ids[j]
    })

    // 3. 按顺序执行
    for _, id := range ids {
        players[id].Update()
    }
}

确定性状态机

1. 确定性FSM设计

// DeterministicFSM 确定性状态机
type DeterministicFSM struct {
    currentState   string
    states         map[string]*State
    transitions    map[string][]Transition
}

type State struct {
    Name        string
    OnEnter     func()
    OnUpdate    func()
    OnExit      func()
}

type Transition struct {
    Condition   func() bool
    TargetState string
}

// 确定性状态转换
func (fsm *DeterministicFSM) Update() {
    state := fsm.states[fsm.currentState]

    // 1. 执行当前状态更新
    state.OnUpdate()

    // 2. 检查状态转换(按固定顺序)
    transitions := fsm.transitions[fsm.currentState]
    for _, trans := range transitions {
        if trans.Condition() {
            fsm.changeState(trans.TargetState)
            break
        }
    }
}

// 状态转换(确定性)
func (fsm *DeterministicFSM) changeState(newState string) {
    // 1. 退出当前状态
    currentState := fsm.states[fsm.currentState]
    currentState.OnExit()

    // 2. 进入新状态
    fsm.currentState = newState
    newStateObj := fsm.states[newState]
    newStateObj.OnEnter()
}

2. 确定性物理计算

// DeterministicPhysics 确定性物理
type DeterministicPhysics struct {
    gravity      int64  // 定点数
    friction     int64
}

// 更新位置(确定性)
func (dp *DeterministicPhysics) UpdatePosition(obj *PhysicsObject, dt int64) {
    // 1. 应用重力
    obj.VelocityY += dp.gravity * dt / SCALE

    // 2. 应用摩擦力
    obj.VelocityX = obj.VelocityX * (SCALE - dp.friction) / SCALE
    obj.VelocityY = obj.VelocityY * (SCALE - dp.friction) / SCALE

    // 3. 更新位置
    obj.PositionX += obj.VelocityX * dt / SCALE
    obj.PositionY += obj.VelocityY * dt / SCALE
}

// 碰撞检测(确定性)
func (dp *DeterministicPhysics) CheckCollision(a, b *PhysicsObject) bool {
    // AABB碰撞检测(确定性)
    return a.PositionX < b.PositionX + b.Width &&
           a.PositionX + a.Width > b.PositionX &&
           a.PositionY < b.PositionY + b.Height &&
           a.PositionY + a.Height > b.PositionY
}

type PhysicsObject struct {
    PositionX int64
    PositionY int64
    VelocityX int64
    VelocityY int64
    Width     int64
    Height    int64
}

确定性检查工具

1. 哈希校验

// DeterminismChecker 确定性检查器
type DeterminismChecker struct {
    hashHistory []string
}

// 计算状态哈希
func (dc *DeterminismChecker) CalculateHash(state *GameState) string {
    // 1. 序列化状态(确定性顺序)
    data := dc.serializeState(state)

    // 2. 计算哈希
    hash := sha256.Sum256(data)

    // 3. 转换为字符串
    return hex.EncodeToString(hash[:])
}

// 序列化状态(确定性)
func (dc *DeterminismChecker) serializeState(state *GameState) []byte {
    buf := new(bytes.Buffer)

    // 1. 写入帧号
    binary.Write(buf, binary.BigEndian, state.FrameNum)

    // 2. 写入玩家(排序后)
    players := make([]*Player, 0, len(state.Players))
    for _, player := range state.Players {
        players = append(players, player)
    }
    sort.Slice(players, func(i, j int) bool {
        return players[i].ID < players[j].ID
    })

    // 3. 写入每个玩家
    for _, player := range players {
        binary.Write(buf, binary.BigEndian, player.ID)
        binary.Write(buf, binary.BigEndian, player.PositionX)
        binary.Write(buf, binary.BigEndian, player.PositionY)
        binary.Write(buf, binary.BigEndian, player.Health)
    }

    return buf.Bytes()
}

// 检查确定性
func (dc *DeterminismChecker) CheckDeterminism(state *GameState) bool {
    hash := dc.CalculateHash(state)

    // 与历史哈希比较
    if len(dc.hashHistory) > 0 {
        lastHash := dc.hashHistory[len(dc.hashHistory)-1]
        if hash != lastHash {
            // 哈希不同,存在非确定性
            return false
        }
    }

    // 保存哈希
    dc.hashHistory = append(dc.hashHistory, hash)
    return true
}

2. 确定性调试

// DeterminismDebugger 确定性调试器
type DeterminismDebugger struct {
    logEnabled    bool
    divergenceLog []string
}

// 记录状态
func (dd *DeterminismDebugger) LogState(frameNum uint32, state *GameState) {
    if !dd.logEnabled {
        return
    }

    // 记录关键状态
    log := fmt.Sprintf("Frame %d:\n", frameNum)
    for _, player := range state.Players {
        log += fmt.Sprintf("  Player %d: Pos(%.2f, %.2f) HP=%d\n",
            player.ID,
            float64(player.PositionX)/SCALE,
            float64(player.PositionY)/SCALE,
            player.Health)
    }

    dd.divergenceLog = append(dd.divergenceLog, log)
}

// 比较日志
func (dd *DeterminismDebugger) CompareLogs(other *DeterminismDebugger) (int, string) {
    // 找到第一个分叉点
    minLen := min(len(dd.divergenceLog), len(other.divergenceLog))
    for i := 0; i < minLen; i++ {
        if dd.divergenceLog[i] != other.divergenceLog[i] {
            return i, dd.divergenceLog[i]
        }
    }

    return -1, ""
}

真实案例分析

案例:《街霸》的确定性系统

背景

  • 帧同步:Lockstep
  • 确定性要求:100%
  • 特点:格斗游戏,精确判定

技术方案

// StreetFighter 确定性系统
type StreetFighterEngine struct {
    random     *DeterministicRandom
    physics    *DeterministicPhysics
    fsm        *DeterministicFSM
}

// 游戏主循环
func (sfe *StreetFighterEngine) GameLoop(inputs []Input) {
    // 1. 按玩家ID排序输入
    sort.Slice(inputs, func(i, j int) bool {
        return inputs[i].PlayerID < inputs[j].PlayerID
    })

    // 2. 执行输入(确定性顺序)
    for _, input := range inputs {
        sfe.executeInput(input)
    }

    // 3. 更新物理(确定性)
    sfe.physics.Update()

    // 4. 更新状态机(确定性)
    sfe.fsm.Update()

    // 5. 碰撞检测(确定性)
    sfe.checkCollisions()
}

// 执行输入(确定性)
func (sfe *StreetFighterEngine) executeInput(input Input) {
    switch input.Type {
    case "attack":
        sfe.executeAttack(input)
    case "move":
        sfe.executeMove(input)
    case "jump":
        sfe.executeJump(input)
    }
}

// 攻击判定(确定性)
func (sfe *StreetFighterEngine) executeAttack(input Input) {
    attacker := sfe.getPlayer(input.PlayerID)
    defender := sfe.getOpponent(input.PlayerID)

    // 1. 计算攻击范围(确定性)
    attackRange := sfe.calculateAttackRange(attacker)

    // 2. 检查命中(确定性)
    if sfe.checkHit(attacker, defender, attackRange) {
        // 3. 计算伤害(确定性随机数)
        damage := sfe.random.Next() % 20

        // 4. 应用伤害
        defender.Health -= int(damage)
    }
}

// 计算攻击范围(确定性)
func (sfe *StreetFighterEngine) calculateAttackRange(player *Player) int64 {
    // 使用定点数计算
    baseRange := FixedFromFloat(100.0)
    bonusRange := FixedFromFloat(float64(player.Strength) * 0.5)
    return baseRange + bonusRange
}

// 检查命中(确定性)
func (sfe *StreetFighterEngine) checkHit(attacker, defender *Player, range_ int64) bool {
    // 计算距离(确定性)
    dx := attacker.PositionX - defender.PositionX
    dy := attacker.PositionY - defender.PositionY
    distance := abs(dx) + abs(dy)  // 曼哈顿距离

    return distance <= range_
}

踩坑经验

❌ 错误1:使用浮点数计算

// ❌ 错误
func calculateDamage(base float64, multiplier float64) float64 {
    return base * multiplier  // 精度不确定
}

// ✅ 正确
func calculateDamageFixed(base int64, multiplier int64) int64 {
    return base * multiplier / SCALE  // 精确计算
}

❌ 错误2:不保证容器遍历顺序

// ❌ 错误
for id, player := range players {
    player.Update()  // 顺序不确定
}

// ✅ 正确
ids := getSortedPlayerIDs(players)
for _, id := range ids {
    players[id].Update()  // 顺序固定
}

❌ 错误3:使用系统时间

// ❌ 错误
func update() {
    now := time.Now()  // 每个客户端不同
    player.Position += player.Velocity * now.UnixNano()
}

// ✅ 正确
func update(frameNum uint32) {
    dt := int64(16)  // 固定时间步长
    player.Position += player.Velocity * dt
}

小结

确定性计算的核心要点:

  1. 浮点数问题:使用定点数替代
  2. 随机数问题:使用确定性随机数生成器
  3. 顺序问题:排序后处理容器元素

确定性检查

  • 哈希校验:每帧计算状态哈希
  • 日志比较:记录并比较状态日志

真实案例

  • 《街霸》:Lockstep + 确定性物理

踩坑经验

  • ❌ 不要使用浮点数
  • ❌ 不要依赖map遍历顺序
  • ❌ 不要使用系统时间

下一节(4.4)我们将学习:预测与补偿,深入了解如何减少延迟感。