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
}
小结
确定性计算的核心要点:
- 浮点数问题:使用定点数替代
- 随机数问题:使用确定性随机数生成器
- 顺序问题:排序后处理容器元素
确定性检查:
- 哈希校验:每帧计算状态哈希
- 日志比较:记录并比较状态日志
真实案例:
- 《街霸》:Lockstep + 确定性物理
踩坑经验:
- ❌ 不要使用浮点数
- ❌ 不要依赖map遍历顺序
- ❌ 不要使用系统时间
下一节(4.4)我们将学习:预测与补偿,深入了解如何减少延迟感。