不能直接用 float 做帧同步,因为其运算结果在不同平台、编译器或 CPU 指令路径下存在微小差异,破坏确定性;定点数(如 Fixed16_16)通过整数运算确保跨平台一致性,缩放因子 2¹⁶,乘法后需手动右移 16 位归一化。

为什么不能直接用 float 做帧同步?
因为不同平台、编译器、甚至同一 CPU 的不同指令路径(SSE vs x87)下,float 运算结果可能有微小差异,而帧同步要求「完全确定性」——同一输入在任何机器上必须产生完全相同的输出。定点数把小数变成整数运算,绕开了浮点的不确定性。
手写不是为了炫技,而是控制精度、范围和溢出行为。标准库没有跨平台确定性定点类,std::fixed_point(C++23 草案)尚未落地且实现不统一,不能用于生产帧同步逻辑。
Fixed16_16 是最常用起点:16 位整数 + 16 位小数
选 int32_t 作底层类型,高位 16 位存整数部分,低位 16 位存小数部分(即缩放因子为 2¹⁶ = 65536)。这是游戏里平衡精度与范围的常见折中:
- 能表示 ±32767.99998(足够地图坐标、速度等)
- 加减乘除全可由整数指令完成,无分支、无平台差异
- 乘法后需手动右移 16 位,这是最容易漏掉的归一化步骤
示例构造和乘法:
立即学习“C++免费学习笔记(深入)”;
struct Fixed16_16 {
int32_t value;
explicit Fixed16_16(int32_t v) : value(v << 16) {}
explicit Fixed16_16(float f) : value(static_cast<int32_t>(f * 65536.0f)) {}
<pre class="brush:php;toolbar:false;">Fixed16_16 operator*(const Fixed16_16& rhs) const {
// 关键:先转 int64_t 防溢出,再右移
return Fixed16_16(static_cast<int32_t>((static_cast<int64_t>(value) * rhs.value) >> 16));
}};
溢出检测必须显式做,别依赖未定义行为
帧同步里一次溢出就导致全服状态分裂,而 C++ 整数溢出是未定义行为(UB),编译器可能优化掉你的检查。不能只靠 value > INT32_MAX 这类判断——乘法中间结果早超了。
实操建议:
- 所有四则运算前,用
__builtin_add_overflow/__builtin_mul_overflow(GCC/Clang)或SafeInt类做饱和或 panic 检查 - 调试期强制开启
-fsanitize=undefined,它能捕获隐式溢出,但运行时有开销,上线前必须关掉 - 避免在热循环里做溢出检查;更稳妥的是预估输入范围,在协议层或输入校验阶段就拒绝越界值
序列化必须用 raw int32_t,别转成 float 再传
帧同步网络同步的是状态快照,如果把 Fixed16_16 先转 float 再用 protobuf 序列化,等于又引入了浮点不确定性。而且 float 只有 24 位有效精度,会截断定点数的全部 32 位信息。
正确做法:
- 序列化时直接写
value字段(int32_t) - 反序列化时直接读
int32_t构造,不做任何浮点转换 - 调试打印时才用
static_cast<float>(value) / 65536.0f</float>,仅限日志,不参与逻辑
这点容易被忽略:只要代码里出现一次 to_float() 并用于比较或计算,就可能埋下同步隐患。



















