定点数,浮点数

帧同步中用浮点数计算,容易照成不同步,怎么解决这个问题
浮点数为何不精确:
1.二进制无法精确表示所有十进制小数
根本矛盾:计算机使用二进制(Base-2)存储数据,但许多常见的十进制小数(如 0.1, 0.2, 0.3)在二进制中是无限循环小数。
当这些无限循环小数被强行塞入有限位数(如 32 位单精度浮点数)时,必须进行舍入(Rounding),导致精度损失。
示例:
十进制 0.1 → 二进制 0.0001100110011...(无限循环)
十进制 0.2 → 二进制 0.001100110011...(无限循环)
小数的二进制表示,我不是很懂,后面需要补上
2. 浮点数的存储结构:尾数位数有限
浮点数格式(IEEE 754标准):
单精度(32位):1 位符号位 + 8 位指数位 + 23 位尾数位。
双精度(64位):1 位符号位 + 11 位指数位 + 52 位尾数位。
尾数(Mantissa)的局限性:
尾数存储的是二进制小数部分(如 1.xxxxx₂ 中的 xxxxx)。尾数位数决定了能表示的有效数字精度:
单精度:约 6-9 位十进制有效数字。
双精度:约 15-17 位十进制有效数字
3. 舍入误差(Rounding Error)
一个数无法用有限二进制精确表示时,IEEE 754 会按规则(如“四舍六入五成双”)将其舍入到最接近的可表示值。
4. 大数吃小数(Catastrophic Cancellation)
当两个相差极大的数相加/相减时,较小数的有效数字可能因对齐指数被丢弃
5. 特殊值的非直观表示
非规格化数(Denormal Numbers):接近零的极小值,精度极低。
NaN(Not a Number):无效操作结果(如 √(-1))。
无穷大(Infinity):溢出结果(如 1/0)。
这些值在运算中可能传播,导致意外结果(如 NaN + 1 = NaN)

浮点数因为其科学计数法的表示方式,可以表示很大或者很小的数。这个是定点数无法做到的

为了解决浮点数计算不准确导致各个客户端计算不一致的问题,引入了定点数
定点数(Fixed-Point Number)是一种在计算机中表示实数的数值格式。它的核心特点是小数点(或二进制点)的位置在表示之前就被预先固定,不会像浮点数那样在运算时动态移动。

核心概念
固定的小数点位置: 在定义一种定点数格式时,会约定整数部分占多少位,小数部分占多少位(或者在二进制中,约定二进制点右边有多少位)。这个约定在后续的所有运算中保持不变。
本质是整数: 定点数在内存中通常被存储为一个整数(二进制、补码形式)。这个整数的实际值需要根据预先约定的缩放因子(Scaling Factor)来解释。
缩放因子: 通常是一个固定值 S = 2^F,其中 F 是约定的小数位数(Fractional Bits)。
实际值 = 存储的整数值 * S 或更常见地 实际值 = 存储的整数值 / 2^F
精度: 缩放因子决定了定点数的精度。例如,缩放因子为 2^16 时,它能表示的最小非零小数值是 1/65536 ≈ 0.000015258789。
表示方式
通常用 Qm.n 格式来表示定点数:
m: 表示整数部分占用的位数(包括符号位)。
n: 表示小数部分占用的位数。
总位数 = m + n。
例如 Q16.16 表示一个 32 位的定点数,其中 16 位用于整数部分(包括 1 位符号位和 15 位数值位),16 位用于小数部分。其缩放因子是 2^16。
运算规则
加/减: 直接对存储的整数进行加/减即可,因为小数点对齐是隐含的。
乘: 两个定点数相乘后,结果相当于放大了缩放因子的平方倍。需要将结果右移 n 位(或除以缩放因子)来得到正确的结果。
除: 两个定点数相除前,需要将被除数左移 n 位(或乘以缩放因子)再进行整数除法,以避免精度损失。

游戏帧同步中保持计算准确的例子:物理位置更新
场景描述
想象一个多人实时对战游戏(如 MOBA、RTS、格斗游戏)。为了保持所有玩家屏幕上看到的画面完全一致(帧同步),所有客户端必须基于相同的输入(玩家操作指令)和相同的初始状态,独立地、确定性地计算出每一帧的游戏状态。
其中一个关键状态是游戏单位(角色、子弹等)的位置。位置通常是一个二维或三维向量(如 (x, y))。单位每帧根据其速度 (vx, vy) 更新位置:
新位置 = 旧位置 + 速度 * 帧时间

使用浮点数的问题
如果使用浮点数(如 float 或 double)来计算位置:
非确定性: 不同 CPU 架构、编译器、操作系统甚至不同的优化级别,对浮点数的计算(尤其是涉及超越函数、某些除法、累积误差)可能产生极其微小的差异。这些差异在单机游戏中通常可以忽略。
误差累积放大: 在帧同步中,所有客户端必须绝对一致。即使初始差异只有 0.0000001,经过成百上千帧的累积,这个差异会被放大,最终导致不同客户端上单位的位置出现肉眼可见的偏差。比如:
客户端 A 计算的位置是 (100.0, 50.0)
客户端 B 计算的位置是 (100.000001, 50.0) (由于浮点计算差异)
后果: 当这个单位走到墙边时,客户端 A 认为它撞墙停下了,客户端 B 认为它还能往前走一点点。这会导致严重的不同步(Desync),玩家看到完全不同的游戏状态,游戏无法进行下去。这就是帧同步的噩梦。

使用定点数的解决方案
我们使用 Q24.8 格式(32位整数,24位整数部分 + 8位小数部分,缩放因子 = 2^8 = 256)来表示位置 (x, y) 和速度 (vx, vy)。

定义:
cpp
typedef int32_t fixed_point; // 32位定点数基础类型

define FIXED_SHIFT 8 // 小数部分位数 (n)

define FIXED_SCALE (1 << FIXED_SHIFT) // 缩放因子 = 256

初始化:
假设初始位置是 (10.5, 20.25)。
转换为定点数:
cpp
fixed_point x = (fixed_point)(10.5 * FIXED_SCALE); // 10.5 * 256 = 2688
fixed_point y = (fixed_point)(20.25 * FIXED_SCALE); // 20.25 * 256 = 5184
假设速度是 (1.25, -0.75)。

转换为定点数:
cpp
fixed_point vx = (fixed_point)(1.25 * FIXED_SCALE); // 1.25 * 256 = 320
fixed_point vy = (fixed_point)(-0.75 * FIXED_SCALE); // -0.75 * 256 = -192

每帧更新位置 (核心逻辑):

cpp
// 假设帧时间是固定的,比如 1/60 秒 (0.016666...秒)
// 将帧时间也转换为定点数 (Q24.8)
fixed_point deltaTime = (fixed_point)((1.0f / 60.0f) * FIXED_SCALE); // (1/60)*256 ≈ 4.2666 -> 4 (注意:这里简化处理精度损失)

// 更新位置: position += velocity * deltaTime
// 1. 计算 velocity * deltaTime (结果是 (32+8)=40位,但存储在 int64_t 临时变量)
int64_t temp_x = (int64_t)vx * (int64_t)deltaTime;
int64_t temp_y = (int64_t)vy * (int64_t)deltaTime;

// 2. 将结果右移 FIXED_SHIFT (8) 位,缩放回 Q24.8 格式 (丢弃低位)
temp_x >>= FIXED_SHIFT; // 相当于除以 256
temp_y >>= FIXED_SHIFT;

// 3. 加到当前位置 (32位整数加法,确定性!)
x += (fixed_point)temp_x;
y += (fixed_point)temp_y;
为什么这能保证帧同步的准确性?

确定性整数运算: 整个计算过程的核心(存储、加法、乘法、移位)完全基于整数运算。整数运算在所有遵循相同规则的计算机平台上是绝对一致和确定性的。100 + 200 在任何机器上都等于 300。
避免浮点误差: 彻底规避了不同平台浮点数实现差异和累积误差带来的不确定性。
隐式小数点对齐: 所有参与运算的定点数(位置、速度、帧时间)都使用相同的 Qm.n 格式(相同的缩放因子)。进行加减法时,小数点自动对齐,直接整数加减即可。乘法后虽然需要移位调整,但移位操作本身也是确定性的。
网络传输友好: 位置状态 (x, y) 在需要传输时(比如同步校验点),直接传输 32 位整数即可,比传输浮点数更易于压缩和处理。

posted @ 2025-07-03 15:01  木土无心  阅读(74)  评论(0)    收藏  举报