LOADING

加载过慢请开启缓存 浏览器默认开启

为实时帧同步而生的定点库

[TOC]

前言:

写这篇文章和库其实也是方便自己温习和改进,毕竟阅读过不少技术文章后,总会想着自己实现一套。

在客户端–服务器的实时游戏中,玩家输入、物理模拟和渲染必须跨多台设备严格同步,否则会出现“瞬移”、“抖动”或“不同步”的尴尬场景。传统浮点数运算由于舍入差异和平台差异往往难以保证位级一致;而定点数不仅精度固定、可预测,还能在多平台上实现完全相同的计算结果。

本文将实现一个基于 .NET Standard 2.1 定点数学库(以下简称“定点库”),并分析设计思路、关键实现。


1. 浮点数的不确定性

1.1 IEEE-754 双精度简述

  • 总位宽:64 位
  • 符号(1 位) + 指数(11 位,偏移 1023) + 尾数(52 位,隐含最高位 1)
  • 任意实数都要被映射到 1.52×10⁻¹⁶ 的相对精度范围内。

0.1 的二进制表示

十进制 0.1₁₀ 转换为二进制是无限循环:

0.1₁₀ = 0.00011001100110011…₂

而硬件只能存 52 位尾数(加上隐含 1):

≈ 0.0001100110011001100110011001100110011001100110011010₂
             └───────────────┬───────────────┘
             精确截断到 52 位尾数

对应十进制:

0.1000000000000000055511151231257827021181583404541015625

与真实的 0.1 相差约 5.6×10⁻¹⁸


1.2 累加示例:误差如何放大

下面用 C# 演示 0.1 的累加误差随迭代增长的实际输出:

double sum = 0.0;
for (int i = 1; i <= 12; i++) {
    sum += 0.1;
    Console.WriteLine($"i={i}\t sum={sum:R}\t error={sum - 0.1 * i:R}");
}
i sum (R) error (sum − i×0.1)
1 0.1 +0.0
2 0.2 +0.0
3 0.30000000000000004 +4.4408920985006262E−17
4 0.4 +0.0
5 0.5 +0.0
6 0.6 -1.1102230246251565E-16
7 0.7 -1.1102230246251565E-16
8 0.7999999999999999 -1.1102230246251565E-16
9 0.8999999999999999 -1.1102230246251565E-16
10 0.9999999999999999 -1.1102230246251565E-16
11 1.0999999999999999 -2.220446049250313E-16
12 1.2 -2.220446049250313E-16
  1. 误差并非单调增长,而是“振荡”后整体漂移;
  2. 每次尾数对齐与舍入都可能引入 ± 几个 ULP(单位最后有效位);
  3. 长时间迭代,会累积到可见规模,影响物理模拟一致性。

2. 定点数的优势

2.1 位级确定性

定点数「全整型」运算,无需尾数对齐,也无指数变化。只要各端 raw 值相同,加/减/移位/乘除都产生完全相同的结果。

2.2 可控精度与范围

选用 Q32.32(64 位)平衡精度与范围:

格式 范围 最小增量
Q32.32 ±2³¹ ≈ ±2.1×10⁹ 2⁻³² ≈ 2.3×10⁻¹⁰
  • 足够覆盖游戏世界坐标
  • 精度高于像素级与物理判定需求

2.3 性能与优化

  • 整型加减:与原生 long 相同,极高效率。
  • 乘除:可选用
    1. 大整数BigInteger)保证正确;
    2. 手动拆分 64→32 高低位;
    3. SIMD/硬件指令(Burst、PMULLD)。
  • 无垃圾分配:避免 GC 影响帧率。

3. 定点库总体架构与模块划分

在打通了定点算术与误差背景之后,我们来看整个库的高层组织——如何划分模块、解耦依赖,以及设计背后的考量。


3.1 模块划分

XFixedPoint/
├─ Core/ ← 定点算术、比较、数学函数
├─ Matrices/ ← 4×4 定点矩阵变换
├─ Networking/ ← 状态快照、输入缓冲与回滚系统
├─ Physics/ ← 刚体、碰撞、物理系统
├─ Quaternions/ ← 定点四元数与旋转运算
├─ Utilities/ ← Time/Debug/Random工具集
└─ Vectors/ ← 定点向量(2D/3D/4D)

  • 单一职责
    • 每层只关心自己领域:Core 不依赖任何其它模块;Vectors 只用 CoreQuaternionsMatrices 各司其职,Physics 聚合前几层;Networking 构建在物理之上。
  • 可替换性
    • 未来如果要换用更高效的三角函数(CORDIC → 查表),只需替换 Core/XFixedMath;不必触及 PhysicsNetworking

3.2 CoreVectors 解耦

  • 依赖方向
    • Vectors 依赖 Core.XFixedCore.XFixedMath
    • Core 不引用 Vectors,只提供基础算术。
  • 好处
    • 最小可测单元Core 的单元测试独立、运行快速;
    • 低耦合:即使删除或替换 Vectors,也不破坏 Core

3.3 聚合层:PhysicsSystem

namespace XFixedPoint.Physics
{
    public class PhysicsSystem
    {
        List<FixedRigidbody> _bodies;
        List<FixedCollider>  _colliders;
        public void Step(XFixed dt) { … }
    }
}
  • 职责
    • 应用全局重力、积分各 FixedRigidbody
    • N² 碰撞检测 + 响应;
    • 对外只暴露 Step(dt),内部依赖 CoreVectorsQuaternionsMatrices
  • 解耦策略
    • PhysicsSystem 仅持有接口 FixedCollider,不管具体形状;
    • 碰撞体子类(AABB/Sphere/OBB)各自实现 OverlapsComputeManifold,利于扩展。

3.5 “回滚系统” 架构

namespace XFixedPoint.Networking
{
  public class RollbackSystem<T>
  {
      void SubmitInput(int tick, T input);
      void AdvanceTo(int targetTick, XFixed dt, Action<T> applyInput);
  }
}
  • 依赖
    • 引用 PhysicsSystemSnapshotInputBuffer
  • 分层设计
    • **Snapshot**:纯粹的二进制序列化状态,无业务逻辑;
    • **InputBuffer**:按 tick 累积输入;
    • **RollbackSystem**:核心回滚/重放逻辑,保证“一切状态恢复”可重现。

4. 定点算术详解:加、减、乘、除

在完成模块划分与整体架构后,下一步就是打磨核心——最基础、最频繁使用的四则运算。“定点”在此处最直接体现在对整数位与小数位的一体化管理,以及对精度与性能的微妙平衡。


4.1 加法与减法:简洁、安全、零误差

public static XFixed operator +(XFixed a, XFixed b)
    => FromRaw(unchecked(a._raw + b._raw));
public static XFixed operator -(XFixed a, XFixed b)
    => FromRaw(unchecked(a._raw - b._raw));

为什么这么做?

  • 位级对齐
    • 定点数使用同一个“原始值”_raw 存储,只要保证相同格式(Q32.32),两数小数位天然对齐,比浮点运算省去了尾数对齐步骤。
  • 零舍入误差
    • 与整数运算等价,不会产生舍入;只要不溢出,结果完全精确。
  • 性能最优
    • 直接两个 long 相加,相较浮点加法更轻量(无指数与尾数拆分)。

溢出如何处理?

  • C# 默认带溢出检查;unchecked 关闭检查以追求性能。
  • 如果想严格捕获溢出,可在外层用 checked 块或在 XFixed API 中提供 AddChecked 等方法。

4.2 乘法:128 位中间,确保精度

public static XFixed operator *(XFixed a, XFixed b)
    => XFixedArithmetic.Multiply(a, b);

背后逻辑(伪代码):

BigInteger prod = (BigInteger)a._raw * b._raw;
// 原始相乘:rawA × rawB → 128 位中间
long resultRaw  = (long)(prod >> SHIFT);
// 右移 f 位,将小数位“对齐”回原始位置
return FromRaw(resultRaw);

这样设计的好处

  • 无精度丢失
    • 任何 rawA × rawB 的精确值都保留在 128 位中,右移再截断只丢弃真正低于最小量化单位的部分。
  • 位级一致
    • BigInteger 计算与移位运算在所有平台都保证位级相同,不受 JIT 优化影响。
  • 开发效率
    • 一次实现,功能正确,后续可针对性能瓶颈单独优化。

为什么不手写 64×64→128 拆分?

  • 可读性与安全性:手动拆分高低 32 位、合并时要考虑符号扩展、进位,高风险且不易维护。
  • 优化时机:先验证正确性,再在需要的热点(如大量向量点乘)引入 SIMD 或查表加速,保持核心算法清晰。

4.3 除法:左移扩精,除零保护

public static XFixed operator /(XFixed a, XFixed b)
    => XFixedArithmetic.Divide(a, b);

核心(伪代码):

if (b._raw == 0) throw new DivideByZeroException();
// 先左移 f 位,放大被除数
BigInteger dividend = (BigInteger)a._raw << SHIFT;
long resultRaw      = (long)(dividend / b._raw);
// 整除后即为对齐好的定点结果
return FromRaw(resultRaw);

设计思路

  • 提升分子精度
    • 左移保证小数计算不丢失,类似于 “浮点先 scale 然后标准除法” 的思路。
  • 显式除零检查
    • 在最底层抛异常,剥离业务层对“不合法输入” 的判断。
  • 性能与精度平衡
    • 大整数除法虽然慢一些,但只在必要时调用,比如 1/xnormalize、物理碰撞计算等;大量非除法场景不受影响。

4.4 舍入模式与误差控制

  • 截断舍入(toward zero):对乘、除的结果直接截断最低位,不做四舍五入,保证模拟行为可预测。
  • 误差边界
    • 乘法后最多丢失 1 小数单位:≈ 2⁻³² ≈ 2.3×10⁻¹⁰
    • 除法后同理。
  • 浮点对比
    • 浮点乘/除误差通常为相对误差(ulp),定点是固定绝对误差,更易评估误差边界。

5. 高级数学函数:Sqrt、三角、Exp/Log

定点四则运算奠定了基础,但游戏/物理中离不开开方、三角函数和指数对数运算。


5.1 平方根(Sqrt):牛顿迭代

为什么不直接用二分法?

  • 二分法 对定点也适用,但每次“中点运算”都要乘除,迭代次数多、收敛慢。
  • 牛顿迭代 通常 4–6 步即可达到 Q32.32 精度。

实现要点

public static XFixed Sqrt(XFixed x)
{
    if (x.Raw <= 0) return XFixed.Zero;
    // 初始猜测:用位操作近似 x 的指数一半
    long raw = x.Raw;
    int exp = (int)(raw >> (SHIFT - 1));      // approximate exponent
    XFixed guess = XFixed.FromRaw((1L << (SHIFT / 2)) << (exp / 2));
    // 牛顿迭代: g_{n+1} = (g_n + x/g_n) / 2
    for (int i = 0; i < 5; i++)
        guess = (guess + x / guess) * XFixed.Half;
    return guess;
}
  • 初始猜测:通过位移提取近似指数,再构造 2^(exp/2),让迭代更快收敛。
  • 迭代次数:5 步可以在 Q32.32 下保证误差 < 1 ULP。
  • 边界处理x ≤ 0 返回 0,避免除零。

5.2 三角函数:CORDIC 算法

为什么不直接查表?

  • 查表 精度依赖表格大小,表过大内存占用高;表过小插值误差大。
  • CORDIC 纯加减移位,无乘法,适合定点和硬件。

CORDIC 迭代原理(旋转模式)

  • 预先计算一系列 atan(2⁻ⁱ) 的定点常量表。

  • (x₀=1, y₀=0, z₀=θ) 开始,每步旋转 ± atan(2⁻ⁱ),更新:

if zₙ ≥ 0:
  xₙ₊₁ = xₙ  - (yₙ >> i)
  yₙ₊₁ = yₙ  + (xₙ >> i)
  zₙ₊₁ = zₙ  - atan_table[i]
else:
  xₙ₊₁ = xₙ  + (yₙ >> i)
  yₙ₊₁ = yₙ  - (xₙ >> i)
  zₙ₊₁ = zₙ  + atan_table[i]
  • 最终 (x_N, y_N)(cos θ, sin θ),并乘上一个事先计算好的 K = ∏ (1/√(1+2⁻²ⁱ)) 常量。

代码示例

static readonly XFixed[] AtanTable = { /* atan(2⁻⁰), atan(2⁻¹), ..., atan(2⁻ⁿ) */ };
static readonly XFixed CORDICK = /* ∏ 1/√(1+2⁻²ⁱ) precomputed */;

public static (XFixed sin, XFixed cos) CordicSinCos(XFixed theta)
{
    XFixed x = CORDICK, y = XFixed.Zero, z = theta;
    for (int i = 0; i < AtanTable.Length; i++)
    {
        bool dir = z.Raw >= 0;
        XFixed dx =  y >> i;
        XFixed dy =  x >> i;
        x = dir ? x - dx : x + dx;
        y = dir ? y + dy : y - dy;
        z = dir ? z - AtanTable[i] : z + AtanTable[i];
    }
    return (y, x);
}
  • 优点:无乘法,仅移位和加减,性能高且易定点化。
  • 缺点:迭代次数决定精度,通常取 32 步保证 Q32.32 误差 < 1 ULP。

5.3 反三角与 Atan2

  • XFixedMath.Atan2(y, x) 中,可先调用 CordicAtan(已实现 atan 通用迭代),再根据象限调整。
  • Atan 版本可用矢量模式 CORDIC:迭代压缩到一条线,从而只输出角度。

5.4 指数与对数:混合双精度

实现 Exp(x)Log(x) 要求既有动态范围又要高效。常见策略:

  • 分拆指数部分
    • x = N·ln2 + r,其中 N = floor(x/ln2)r ∈ [−ln2/2, ln2/2]
    • exp(x) = 2^N × exp(r)
  • 多项式近似
    • 在小区间 r 上,用最小二乘或泰勒多项式近似 ln(1+r)
  • 定点实现
    • 先计算 N(整数运算)
    • r 转为 XFixed,再用 Horner 法评估多项式(几次乘加)。
    • 2^N 相当于 <fixed raw> << N>> N

伪代码

public static XFixed Exp(XFixed x)
{
    int N = (int)(x.ToDouble() / Ln2);
    XFixed r = x - XFixed.FromDouble(N * Ln2);
    // 用 5 阶泰勒:1 + r + r²/2! + … + r⁵/5!
    XFixed y = EvaluatePoly(r);
    // 左移或右移 N 位:y * 2^N
    return y * XFixed.FromRaw(1L << N);
}
  • 精度:多项式次数越高,区间越大,误差越小。
  • 性能:每次需几次乘加,不如浮点一条指令,但可接受。

5.5 其它辅助:Clamp、Lerp、Pow

public static XFixed Clamp(XFixed x, XFixed min, XFixed max) 
    => x < min ? min : (x > max ? max : x);

public static XFixed Lerp(XFixed a, XFixed b, XFixed t) 
    => a + (b - a) * Clamp(t, XFixed.Zero, XFixed.One);

public static XFixed Pow(XFixed x, XFixed exp) 
    => Exp(exp * Log(x));
  • Clamp/Lerp:纯乘加,O(1),常用于插值与范围限制。
  • Pow:复合函数,性能最低,但功能完备。

6. 向量、四元数与矩阵:构建物理仿真基石

在完成底层算术和高级数学函数后,接下来就要看“如何将这些函数拼装”成游戏物理常用的向量、旋转和矩阵运算了,来吧来吧。


6.1 定点向量:XFixedVector3

public readonly struct XFixedVector3 : IEquatable<XFixedVector3> {
    public readonly XFixed X, Y, Z;

    public XFixedVector3(XFixed x, XFixed y, XFixed z) {
        X = x; Y = y; Z = z;
    }

    public static XFixedVector3 Zero => new XFixedVector3(XFixed.Zero, XFixed.Zero, XFixed.Zero);
    public static XFixedVector3 UnitX => new XFixedVector3(XFixed.One, XFixed.Zero, XFixed.Zero);
    public static XFixedVector3 UnitY => new XFixedVector3(XFixed.Zero, XFixed.One, XFixed.Zero);
    public static XFixedVector3 UnitZ => new XFixedVector3(XFixed.Zero, XFixed.Zero, XFixed.One);

    public static XFixedVector3 operator +(XFixedVector3 a, XFixedVector3 b)
        => new XFixedVector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
    public static XFixedVector3 operator -(XFixedVector3 a, XFixedVector3 b)
        => new XFixedVector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
    public static XFixedVector3 operator *(XFixedVector3 v, XFixed s)
        => new XFixedVector3(v.X * s, v.Y * s, v.Z * s);
    public static XFixedVector3 operator /(XFixedVector3 v, XFixed s)
        => new XFixedVector3(v.X / s, v.Y / s, v.Z / s);

    public XFixed Dot(XFixedVector3 o)
        => X * o.X + Y * o.Y + Z * o.Z;
    public XFixedVector3 Cross(XFixedVector3 o)
        => new XFixedVector3(
            Y * o.Z - Z * o.Y,
            Z * o.X - X * o.Z,
            X * o.Y - Y * o.X
        );

    public XFixed SqrMagnitude => Dot(this);
    public XFixed Magnitude    => XFixedMath.Sqrt(SqrMagnitude);
    public XFixedVector3 Normalized {
        get {
            var mag = Magnitude;
            return mag == XFixed.Zero
                ? Zero
                : this / mag;
        }
    }
}

设计要点

  • 操作符重载
    • 保持与数学符号一致,阅读性高。
  • Normalized 的安全性
    • 先检查 Magnitude == 0,避免除以零;
    • 返回 Zero 而不是抛异常,更符合“零向量归一化得零向量”常用习惯。
  • 精度与性能
    • DotCross 都是 O(1) 定点乘加运算;
    • Magnitude 调用 Sqrt,迭代次数可控。

6.2 四元数:XFixedQuaternion

public readonly struct XFixedQuaternion : IEquatable<XFixedQuaternion> {
    public readonly XFixed X, Y, Z, W;

    public static XFixedQuaternion Identity => new XFixedQuaternion(XFixed.Zero, XFixed.Zero, XFixed.Zero, XFixed.One);

    public XFixedQuaternion(XFixed x, XFixed y, XFixed z, XFixed w) {
        X = x; Y = y; Z = z; W = w;
    }

    public XFixedQuaternion Conjugate => new XFixedQuaternion(-X, -Y, -Z, W);

    public XFixedQuaternion Normalized {
        get {
            var mag = XFixedMath.Sqrt(X*X + Y*Y + Z*Z + W*W);
            return mag == XFixed.Zero
                ? Identity
                : new XFixedQuaternion(X/mag, Y/mag, Z/mag, W/mag);
        }
    }

    public static XFixedQuaternion operator *(XFixedQuaternion a, XFixedQuaternion b)
        => new XFixedQuaternion(
            a.W*b.X + a.X*b.W + a.Y*b.Z - a.Z*b.Y,
            a.W*b.Y - a.X*b.Z + a.Y*b.W + a.Z*b.X,
            a.W*b.Z + a.X*b.Y - a.Y*b.X + a.Z*b.W,
            a.W*b.W - a.X*b.X - a.Y*b.Y - a.Z*b.Z
        );

    public XFixedVector3 Rotate(XFixedVector3 v) {
        var qv = new XFixedQuaternion(v.X, v.Y, v.Z, XFixed.Zero);
        var r = (this * qv * Conjugate).Normalized;
        return new XFixedVector3(r.X, r.Y, r.Z);
    }

    public static XFixedQuaternion FromAxisAngle(XFixedVector3 axis, XFixed angle) {
        var half = angle * XFixed.Half;
        var s = XFixedMath.Sin(half);
        var c = XFixedMath.Cos(half);
        var n = axis.Normalized;
        return new XFixedQuaternion(n.X*s, n.Y*s, n.Z*s, c);
    }
}

解读与取舍

  • 乘法实现
    • 直接按四元数乘法公式实现,无需矩阵转换,兼顾性能与可读性。
  • Rotate 方法
    • 使用 q * (v,0) * q⁻¹ 完整保留旋转性质;
    • 最后 .Normalized 防止累积长度漂移。
  • FromAxisAngle
    • 先标准化 axis,再构造 (sinθ/2, cosθ/2)
    • 参数 angle 单位为弧度,配合定点 Sin/Cos

6.3 矩阵:XFixedMatrix4x4

public readonly struct XFixedMatrix4x4 : IEquatable<XFixedMatrix4x4> {
    // 行优先 16 元素 M00…M33
    public readonly XFixed M00, M01, M02, M03;
    …
    public static readonly XFixedMatrix4x4 Identity = new XFixedMatrix4x4(
        XFixed.One, XFixed.Zero, XFixed.Zero, XFixed.Zero,
        XFixed.Zero, XFixed.One, XFixed.Zero, XFixed.Zero,
        XFixed.Zero, XFixed.Zero, XFixed.One, XFixed.Zero,
        XFixed.Zero, XFixed.Zero, XFixed.Zero, XFixed.One);

    public XFixedMatrix4x4(/*16 elements*/) { … }

    public static XFixedMatrix4x4 operator *(XFixedMatrix4x4 a, XFixedMatrix4x4 b) { … }

    public XFixedVector3 MultiplyPoint(XFixedVector3 p) {
        // p.w = 1
        var x = M00*p.X + M01*p.Y + M02*p.Z + M03;
        var y = M10*p.X + M11*p.Y + M12*p.Z + M13;
        var z = M20*p.X + M21*p.Y + M22*p.Z + M23;
        var w = M30*p.X + M31*p.Y + M32*p.Z + M33;
        return new XFixedVector3(x/w, y/w, z/w);
    }
}

关键点

  • 行优先存储
    • 与 DirectX / Unity 矩阵布局一致,减少转置开销。
  • 乘法实现
    • 16×16 累加,全部定点乘加,O(64) 运算量。
  • MultiplyPoint
    • 支持齐次坐标 w 除法,方便实现透视和仿射变换。
  • MultiplyVector
    • w=0 特殊分支,不做除法,用于法线或方向向量。

6.4 在物理仿真中的应用

  • 刚体积分:平移 Position += Velocity * dt,旋转 Rotation = Δq * Rotation
  • 碰撞检测:OBB vs. OBB 用 Quaternion.Rotate 提取世界轴;
  • 坐标变换:将局部顶点批量 MultiplyPoint 到世界空间;
  • 插值Vector3.LerpQuaternion.Slerp 中底层用定点乘加和 Atan2 计算权重。

7. 物理仿真框架:刚体、碰撞与响应

完成基础数学与几何结构后,下一大板块就是物理仿真——如何让角色受力、碰撞并做出合理反应。

接下来就是刚体和碰撞体以及物理系统的实现,定点环境下的动力学模拟可离不开它们。


7.1 刚体积分:FixedRigidbody

public class FixedRigidbody
{
    public XFixedVector3 Position;
    public XFixedQuaternion Rotation;

    public XFixedVector3 Velocity;
    public XFixedVector3 AngularVelocity;

    public XFixed Mass { … }           // 设置时自动更新 InverseMass
    public XFixed InverseMass { get; }

    private XFixedVector3 _forceAcc;   // 本帧累积的外力
    private XFixedVector3 _torqueAcc;  // 本帧累积的扭矩

    public void AddForce(XFixedVector3 f)  => _forceAcc += f;
    public void AddTorque(XFixedVector3 τ) => _torqueAcc += τ;
    public void ClearAccumulators()       { _forceAcc = _torqueAcc = XFixedVector3.Zero; }

    public void Integrate(XFixed dt)
    {
        if (dt == XFixed.Zero || Mass == XFixed.Zero) { ClearAccumulators(); return; }

        // —— 线性积分 —— 
        var accel = _forceAcc * InverseMass;         // a = F/m
        Velocity += accel * dt;                      // v += a·dt
        Position += Velocity * dt;                   // x += v·dt

        // —— 角运动 —— (简化的标量惯量)
        var α     = _torqueAcc * InverseMass;        // α ≈ τ / I
        AngularVelocity += α * dt;
        var ωmag  = AngularVelocity.Magnitude;
        if (ωmag != XFixed.Zero)
        {
            var axis   = AngularVelocity / ωmag;
            var deltaQ = XFixedQuaternion.FromAxisAngle(axis, ωmag * dt);
            Rotation   = (deltaQ * Rotation).Normalized;
        }

        ClearAccumulators();
    }
}

设计解析

  • 显式欧拉
    • 简单、易实现;累积误差可受定点积分零漂移控制。
    • 对刚体旋转使用“轴–角增量”近似,避免复杂四元数微分。
  • 质量与倒数
    • InverseMass 预先计算,避免每帧做 1/m
    • 质量为零时视为“静态”或“运动学”刚体,仅响应位置/旋转外部设置,不参与积分。
  • 累积力/力矩
    • 一帧内多次 AddForce/AddTorque,最后一起积分;清零后准备下一帧。
    • 确保“∆v = ∑F · dt / m”,而非“∑(F/m · dt)”累积舍入。
  • 旋转更新
    • 将角速度向量分解为“轴”和“角度”,构造四元数增量 δq,左乘旧旋转并正规化。
    • 正规化防止舍入累积让四元数不再单位长度。

7.2 碰撞体抽象:FixedCollider

public abstract class FixedCollider
{
    public FixedRigidbody Rigidbody { get; set; }
    public XFixedVector3    LocalOffset   { get; set; } = Zero;
    public XFixedQuaternion LocalRotation { get; set; } = Identity;

    public XFixedVector3 WorldPosition => Rigidbody == null
      ? LocalOffset
      : Rigidbody.Position + Rigidbody.Rotation.Rotate(LocalOffset);

    public XFixedQuaternion WorldRotation => Rigidbody == null
      ? LocalRotation
      : (Rigidbody.Rotation * LocalRotation).Normalized;

    public abstract bool Overlaps(FixedCollider other);
    public abstract bool ComputeManifold(FixedCollider other, out CollisionManifold m);
}

7.3 具体碰撞器:AABB、球体、OBB


1. AABB (Axis-Aligned Bounding Box)

1.1 技术分析

  • 定义

    • $$盒子中心 C,半尺寸 H=(h_x,h_y,h_z).$$

    • 沿三轴的投影区间:

      $$[C_x - h_x,C_x + h_x],[C_y - h_y,C_y + h_y],[C_z - h_z,C_z + h_z].$$

  • 碰撞判定

    • $$对每轴 u\in{x,y,z},区间重叠当且仅当A_{\min u} \le B_{\max u}\quad\land\quad A_{\max u} \ge B_{\min u}.$$
    • $$三轴同时重叠 \implies AABB 相交.$$
  • 穿透深度

    $$d_u = \min\bigl(A_{\max u},B_{\max u}\bigr) - \max\bigl(A_{\min u},B_{\min u}\bigr).$$

  • 碰撞法线

    $$令 u^* = \underset{u\in\{x,y,z\}}{\arg\min}d_u$$

    $$法线方向为单位向量 \mathbf{e}_{u^*}$$

    $$符号由 (B.C_{u^*} - A.C_{u^*}) 的正负决定.$$

  • 接触点

    $$P_{u^*} = \frac{\max\bigl(A_{\min u^*},B_{\min u^*}\bigr) + \min\bigl(A_{\max u^*},B_{\max u^*}\bigr)}{2}.$$

1.2 C# 实现

private static bool OverlapsAABBvsAABB(AABBCollider a, AABBCollider b)
{
    var amin = a.WorldPosition - a.HalfSize;
    var amax = a.WorldPosition + a.HalfSize;
    var bmin = b.WorldPosition - b.HalfSize;
    var bmax = b.WorldPosition + b.HalfSize;

    return
        amin.X <= bmax.X && amax.X >= bmin.X &&
        amin.Y <= bmax.Y && amax.Y >= bmin.Y &&
        amin.Z <= bmax.Z && amax.Z >= bmin.Z;
}

private static CollisionManifold ComputeAABBvsAABB(AABBCollider a, AABBCollider b)
{
    var amin = a.WorldPosition - a.HalfSize;
    var amax = a.WorldPosition + a.HalfSize;
    var bmin = b.WorldPosition - b.HalfSize;
    var bmax = b.WorldPosition + b.HalfSize;

    var dx = XFixedMath.Min(amax.X, bmax.X) - XFixedMath.Max(amin.X, bmin.X);
    var dy = XFixedMath.Min(amax.Y, bmax.Y) - XFixedMath.Max(amin.Y, bmin.Y);
    var dz = XFixedMath.Min(amax.Z, bmax.Z) - XFixedMath.Max(amin.Z, bmin.Z);

    if (dx <= XFixed.Zero || dy <= XFixed.Zero || dz <= XFixed.Zero)
        return new CollisionManifold { Colliding = false };

    var penetration = dx;
    var normal = new XFixedVector3(XFixed.One, XFixed.Zero, XFixed.Zero);
    if (dy < penetration) { penetration = dy; normal = new XFixedVector3(0,1,0); }
    if (dz < penetration) { penetration = dz; normal = new XFixedVector3(0,0,1); }

    var diff = b.WorldPosition - a.WorldPosition;
    normal = (diff.Dot(normal) < XFixed.Zero) ? -normal : normal;

    var contact = new XFixedVector3(
      (XFixedMath.Max(amin.X,bmin.X) + XFixedMath.Min(amax.X,bmax.X)) * XFixed.Half,
      (XFixedMath.Max(amin.Y,bmin.Y) + XFixedMath.Min(amax.Y,bmax.Y)) * XFixed.Half,
      (XFixedMath.Max(amin.Z,bmin.Z) + XFixedMath.Min(amax.Z,bmax.Z)) * XFixed.Half
    );

    return new CollisionManifold {
      Colliding        = true,
      Normal           = normal,
      PenetrationDepth = penetration,
      ContactPoint     = contact
    };
}

2. Sphere 碰撞

2.1 技术分析

  • 定义

    $$中心 c_A,c_B,半径 r_A,r_B.$$

  • 相交判定

    $$||\Delta||^2 = (\Delta x)^2+(\Delta y)^2+(\Delta z)^2 \le(r_A+r_B)^2.$$

  • 法线

    $$n = \frac{\Delta}{||\Delta||}, \quad \Delta = c_B - c_A.$$

    $$若 ||\Delta||=0,任选单位向量.$$

  • 穿透深度

    $$\delta = r_A + r_B - ||\Delta||.$$

  • 接触点

    $$p = c_A + n \bigl(r_A - \tfrac{\delta}{2}\bigr).$$

2.3 C# 实现

private static CollisionManifold ComputeSphereVsSphere(SphereCollider a, SphereCollider b)
{
    var centerA = a.WorldPosition;
    var centerB = b.WorldPosition;
    var diff    = centerB - centerA;
    var distSq  = diff.Dot(diff);
    var rSum    = a.Radius + b.Radius;

    if (distSq > rSum * rSum)
        return new CollisionManifold { Colliding = false };

    var dist   = XFixedMath.Sqrt(distSq);
    var normal = (dist == XFixed.Zero)
        ? new XFixedVector3(XFixed.One, 0, 0)
        : diff / dist;
    var penetration = rSum - dist;
    var contact = centerA + normal * (a.Radius - penetration * XFixed.Half);

    return new CollisionManifold {
      Colliding        = true,
      Normal           = normal,
      PenetrationDepth = penetration,
      ContactPoint     = contact
    };
}

3. OBB (Oriented Bounding Box)

3.1 技术分析(分离轴定理)

  • 轴集

    • $$A 本地轴 \mathbf{A}_0,\mathbf{A}_1,\mathbf{A}_2$$
    • $$B 本地轴 \mathbf{B}_0,\mathbf{B}_1,\mathbf{B}_2$$
    • $$交叉轴 \mathbf{A}_i \times \mathbf{B}_j$$
  • 半投影长度

    $$R_A(L) = \sum_{i=0}^2 h_{A,i}\bigl|\mathbf{A}_i \cdot \hat{L}\bigr| /2,B同理$$

  • 中心投影距

    $$D = \bigl|(\mathbf{c}_B - \mathbf{c}_A)\cdot \hat{L}\bigr|.$$

  • 判定

    $$若 \exists L: D > R_A+R_B ⇒ 无碰撞;否则相交。$$

  • 穿透轴

    $$最小 (R_A+R_B - D) 对应的 L。$$

  • 接触点

    $$\tfrac{1}{2}\bigl(\mathrm{Support}_A(-L) + \mathrm{Support}_B(L)\bigr).$$

3.3 C# 实现(片段)

// 1. 计算本地轴
var axesA = new[]{
  a.WorldRotation.Rotate(XFixedVector3.UnitX),
  a.WorldRotation.Rotate(XFixedVector3.UnitY),
  a.WorldRotation.Rotate(XFixedVector3.UnitZ)
};
var axesB = …;

// 2. 构建 R 与 absR
var R = new XFixed[3,3];
var absR = new XFixed[3,3];
var eps = XFixed.FromRaw(XFixedConstants.EPS);
for (int i=0; i<3; i++)
for (int j=0; j<3; j++){
  R[i,j]    = axesA[i].Dot(axesB[j]);
  absR[i,j] = XFixedMath.Abs(R[i,j]) + eps;
}

// 3. 中心投影
var tVec = b.WorldPosition - a.WorldPosition;
var tA = new XFixed[3];
for (int i=0; i<3; i++) tA[i] = tVec.Dot(axesA[i]);

// 4. SAT 测试 & 最小穿透
XFixed bestPen = XFixed.FromRaw(long.MaxValue);
XFixedVector3 bestAxis = XFixedVector3.Zero;

// A 本地轴
for (int i=0; i<3; i++){
  var ra = (i==0 ? a.HalfSize.X : i==1 ? a.HalfSize.Y : a.HalfSize.Z);
  var rb = a.HalfSize.X*absR[i,0] + a.HalfSize.Y*absR[i,1] + a.HalfSize.Z*absR[i,2];
  var D  = XFixedMath.Abs(tA[i]);
  var pen= (ra+rb) - D;
  if (pen < XFixed.Zero) return noCollision;
  var axis = axesA[i] * (tA[i]<XFixed.Zero ? -XFixed.One : XFixed.One);
  if (pen < bestPen){ bestPen=pen; bestAxis=axis; }
}

// … 同理 B 轴与交叉轴 …

// 5. 接触点
var supportA = Support(a, -bestAxis);
var supportB = Support(b,  bestAxis);
var contact  = (supportA + supportB) * XFixed.Half;

return new CollisionManifold {
  Colliding        = true,
  Normal           = bestAxis,
  PenetrationDepth = bestPen,
  ContactPoint     = contact
};

7.4 物理系统:PhysicsSystem

public class PhysicsSystem
{
    List<FixedRigidbody> _bodies;
    List<FixedCollider>  _colliders;
    XFixedVector3         Gravity     = new(0, -9.81, 0);
    XFixed                Restitution = XFixed.FromFloat(0.5f);

    public void AddBody(FixedRigidbody b, FixedCollider c = null) { … }
    public void Step(XFixed dt)
    {
        // 1. 重力
        foreach (b in _bodies) b.AddForce(Gravity * b.Mass);

        // 2. 积分
        foreach (b in _bodies) b.Integrate(dt);

        // 3. 碰撞检测与响应(N²)
        for (i=0; i<…; i++)  
          for (j=i+1; j<…; j++)  
            if (A.ComputeManifold(B, out m) && m.Colliding)  
              ResolveCollision(A.Rigidbody, B.Rigidbody, m);
    }
}

响应逻辑

  • 位置校正
    • 按逆质量比例将重叠沿法线分离,消除穿透。
  • 冲量反冲
    • 计算相对速度沿法线分量 v_rel = (vB−vA)·n
    • 冲量 j = −(1+e)·v_rel / (invM_A + invM_B)
    • 更新速度 vA -= j·n·invM_AvB += j·n·invM_B

7.5 设计取舍与优化

因为写这么一个库还挺累的,有些能优化的工作就暂时放到后面做个TODO吧……

环节 方案 权衡与优化方向
积分方式 显式欧拉 简单易用;如需高精度可考虑 Verlet 或 RK4
碰撞检测 N² 遍历 对少量刚体足够;大规模可引入空间分区(四叉树、BVH)
OBB 算法 SAT(12 轴测试) 精确;性能可用 SSE 加速或提前剔除平行轴
物理参数 简化惯性张量(标量质量) 可扩展到 3×3 矩阵惯性张量
多线程 单线程 Unity Job / Burst 可并行检测和积分

8. 网络帧同步与回滚系统

在多人实时对战中,除了本地物理模拟的一致性,网络延迟包乱序 带来的输入时序错位是最大挑战。传统的“确认-应用”模型会导致玩家间画面不同步或卡顿;而回滚(Rollback)技术能在收到网络延迟输入后,自动“回退”到当时帧状态,重新播放后续所有帧,恢复确定性。


8.1 为什么需要回滚?

  • 延迟补发
    • 玩家输入发出后未必及时到达对端;如果不回滚,后续帧会缺少该输入,导致逻辑偏差。
  • 一致性保证
    • 帧同步逻辑假设“同样的输入序列必定产生同样的状态”,回滚+重演确保即使落后输入到来,也能恢复到对应点并重演。
  • 流畅体验
    • 本地策略:本地输入立即生效(Client-Side Prediction),远端输入延迟到来时回滚并重演,看上去更连贯。

8.2 核心组件

XFixedPoint.Networking/
├─ InputBuffer.cs ← 按 tick 缓存多条输入
├─ RollbackSystem.cs ← 回滚检测、状态恢复与重演
└─ Snapshot.cs ← 二进制快照序列化与恢复

8.2.1 Snapshot

  • 职责:将一整帧所有刚体状态“按顺序”序列化为字节数组;恢复时按同样顺序反序列化回刚体列表。
  • 实现细节
    • 固定顺序——快照时按 _bodies 列表顺序写入每个刚体的 Position.RawRotation.RawVelocity.RawAngularVelocity.Raw
    • 位级一致——直接写入 long,保证二进制完全可还原,无浮点差异(示例代码见 Snapshot.Create / Restore)。
  • 设计取舍
    • 使用 BinaryWriter 写原始 long,比 JSON、XML 轻量且无格式歧义;
    • 全快照简单易实现,但内存与带宽占用大;可扩展为“增量快照”或“差分快照”以节省空间。

8.2.2 InputBuffer<TInput>

  • 职责:收集并存储到达时序不定的所有玩家输入,按 tick (帧号)分桶缓存,多条输入不被覆盖。
  • 实现要点
    • 内部用 SortedDictionary<int, List<TInput>> 存储:键为 tick,值为该帧所有输入列表。
    • AddInput(tick, input):新增一条,不覆盖;
    • TryGetInputs(tick, out List<TInput>):获取本帧所有输入;
    • RemoveOld(beforeTick):丢弃早已确认后不再回滚的帧输入,释放内存。
  • 为什么不只存一条?
    • 一帧可能同时收到本地与远端多条操作;
    • 网络补发机制可能在同一帧内到来多次,需要全部重演。

8.2.3 RollbackSystem<TInput>

public class RollbackSystem<TInput>
{
    void SubmitInput(int tick, TInput input);  
    void AdvanceTo(int targetTick, XFixed dt, Action<TInput> applyInput);
}
核心流程
  • 检测延迟输入(伪代码)
    • 找到所有已模拟帧中回来的“晚到输入”,回滚到最早那帧。
earliestLate = min(tick ≤ _lastAppliedTick from InputBuffer.Ticks)
if exists:
  Restore snapshot at earliestLate
  _lastAppliedTick = earliestLate - 1
  • 重演重播(伪代码)
    • 先清零速度:本 Demo 中每帧速度由输入决定,不累积前帧速度;
    • 逐条调用applyInpu:可以是设定刚体速度、触发技能、发弹道等;
    • 物理步进 保证每帧状态准确。
for tick in (_lastAppliedTick+1)..targetTick:
  SaveSnapshot(tick)
  // —— 本帧重置所有刚体速度 —— 
  bodies.ForEach(b => b.Velocity = Vector3.Zero)
  // 应用本帧**所有**输入
  if InputBuffer.TryGetInputs(tick, out inputs):
    foreach inp in inputs: applyInput(inp)
  physics.Step(dt)
  _lastAppliedTick = tick
  • 清理历史
    • 保留最近 N 帧快照/输入(通常 N=200);
    • 自动删除更老数据,防止内存或磁盘无限增长。
设计考量
  • 全帧快照 简单、易实现;缺点是占用高。
  • 增量快照/状态压缩 可减小内存与网络压力,但实现复杂。
  • 多输入支持 让一帧内多个玩家或补发操作都能正确重演。
  • 回滚边界:若输入延迟超过保留历史,需触发“网络重同步”或“状态快照传输”机制。

8.3 示例:MoveOp 的回滚重演

// 每帧 Update 中:
var op = new MoveOp { Tick = curTick, PlayerIndex = idx, RawX = ..., RawZ = ... };
rollback.SubmitInput(curTick, op);
client.SendMoveOp(op);

rollback.AdvanceTo(curTick, dt, op => {
  // 重置后再应用:设置刚体速度 = dir * speed
  bodies[op.PlayerIndex].Velocity = new Vector3(op.RawX, 0, op.RawZ) * speed;
});
  • 本地预测 + 网络广播:本地立刻生效,远端输入到来时全局回滚重演。
  • AdvanceTo 保证所有玩家输入“准时”按帧执行,画面保持一致。

终于暂时告一段落了,这文档可太难写了,希望能够帮助到各位吧。

有问题或者优化也欢迎来交流,过段时间应该会上传到github上,上传完后会把连接加到文章末尾,后面推荐几个 文章/视频/库 给想学习计算机图形学、网络同步技术的小伙伴

推荐阅读:

Github: XFixedPointLibrary