DETERMINISTIC MATH
FixInt Math
为帧同步、战斗回放与跨平台一致性打造的定点数学基础库
以整数表达逻辑数值,提供标量、向量、数学函数与伪随机能力,让同一份战斗逻辑在不同平台获得可验证、可复现的计算结果。
框架介绍
FixIntMath 是一套面向 Unity 状态同步、帧同步和战斗回放的定点数学库。它使用整数保存逻辑数值,降低不同 CPU、编译器和运行平台之间的浮点计算差异,为确定性战斗逻辑提供统一的标量、数学函数、向量和伪随机数能力。
| 项目 | 说明 |
|---|---|
| 命名空间 | ZM.FixMath |
| 核心类型 | FixInt |
| 缩放倍率 | 1024(左移 10 位) |
| 内部存储 | long |
| Unity 参考版本 | 2023.2.20f1 |
| 开源协议 | MIT |
适用场景
角色属性、移动速度、逻辑帧时间、伤害计算、技能范围、弹道、AI 决策、战斗回放,以及客户端与服务端共用的战斗逻辑。
职责边界
定点数学只能保证其覆盖的数学运算具有一致的计算路径。要实现完整帧同步,仍需统一逻辑帧、输入顺序、随机序列、配置版本和容器遍历顺序,并避免在逻辑层依赖 Unity 浮点物理结果。
1. 工作原理
FixInt 将实际数值放大 1024 倍后保存为整数:
因此,1.5 在逻辑层内部保存为 1536。加减运算可以直接在整数上进行;乘除运算由类型内部处理缩放倍率。
精度
最小量化单位约为 1 / 1024,即 0.0009765625。输入值会被映射到最接近的可表示定点值,连续运算也会产生量化误差。
2. 获取与接入
2.1 获取源码
Unity-ZM-PhysicFixInt GitHub 仓库
集成到现有项目时,将定点数学目录及其 .asmdef、.meta 文件完整复制到工程:
不同仓库版本的目录可能写作 Assets/ZMPackage/FixIntMath,请以实际工程为准。
2.2 引用命名空间
程序集引用
如果调用方使用了自定义 .asmdef,请在该程序集的 Assembly Definition References 中添加 FixIntMath。
3. 快速上手
3.1 声明与运算
FixInt health = 100;
FixInt damage = 12.5f;
FixInt remain = health - damage;
FixInt speed = 3.5f;
FixInt fixedDeltaTime = 0.02f;
FixInt distance = speed * fixedDeltaTime;
3.2 转换到表现层
// 逻辑层
FixInt logicHealth = 87.5f;
// 表现层:UI、动画、Transform 等需要时再转换
float displayHealth = logicHealth.RawFloat;
int roundedHealth = logicHealth.RawInt;
double preciseDisplay = logicHealth.RawDouble;
不要混淆 Value 与 RawValue
Value 和 IntValue 是放大后的内部数据,不是用户看到的实际值。表现层通常应读取 RawFloat、RawInt 或 RawDouble。
4. FixInt API
4.1 常量与属性
| 成员 | 类型 | 说明 |
|---|---|---|
FixInt.Zero |
FixInt |
定点零值 |
FixInt.One |
FixInt |
定点单位值 1 |
FixInt.MUTIPLE |
int |
缩放倍率,当前为 1024 |
Value |
long |
放大后的内部值 |
IntValue |
int |
将放大后的内部值读取为 int |
RawFloat |
float |
还原后的浮点值,供表现层使用 |
RawInt |
int |
还原并取整后的整数值 |
RawDouble |
double |
还原后的双精度值 |
4.2 构造函数
FixInt(long) 的特殊语义
FixInt(long) 认为传入参数已经乘过缩放倍率,不会再次左移。创建普通整数值时应传入 int,例如 new FixInt(10),不要写成 new FixInt(10L)。
4.3 类型转换
数值类型可以隐式转换为 FixInt:
从 FixInt 转出时需要显式转换,或使用 Raw... 属性:
float a = (float)fixValue;
int b = (int)fixValue;
double c = (double)fixValue;
long internalValue = (long)fixValue;
4.4 运算符
| 分类 | 支持的运算符 |
|---|---|
| 算术 | +、-、*、/、%、一元 - |
| 比较 | ==、!=、>、<、>=、<= |
| 位移 | <<、>> |
FixInt a = 10;
FixInt b = 4;
FixInt sum = a + b;
FixInt difference = a - b;
FixInt product = a * b;
FixInt quotient = a / b;
FixInt remainder = a % b;
bool greater = a > b;
4.5 对象接口
| API | 说明 |
|---|---|
Equals(FixInt) |
判断两个定点值是否完全相等 |
CompareTo(FixInt) |
比较两个定点值 |
GetHashCode() |
获取基于内部值的哈希码 |
ToString() |
返回还原后的数值字符串 |
5. FixIntMath API
5.1 基础函数
| API | 说明 |
|---|---|
Abs(value) |
返回绝对值 |
Min(a, b) |
返回较小值 |
Max(a, b) |
返回较大值 |
Sign(value) |
返回符号值 |
Clamp(value, min, max) |
将值限制在闭区间内 |
Clamp01(value) |
将值限制在 [0, 1] |
Round(value) |
四舍五入 |
Floor(value) |
向下取整 |
Ceiling(value) |
向上取整 |
FixInt damage = FixIntMath.Max(rawDamage, 0);
FixInt normalized = FixIntMath.Clamp01(progress);
FixInt gridX = FixIntMath.Floor(position.x);
5.2 幂与开方
| API | 说明 |
|---|---|
Pow(value, count) |
整数次幂 |
Sqrt(value) |
平方根 |
Sqrt(value, iterations) |
指定迭代次数的平方根 |
5.3 三角函数
| API | 输入/输出 | 说明 |
|---|---|---|
Sin(value) |
弧度 | 查表正弦 |
Cos(value) |
弧度 | 查表余弦 |
Acos(value) |
弧度 | 查表反余弦 |
Atan2(y, x) |
弧度 | 四象限反正切 |
FixInt radians = 0.5f;
FixInt sin = FixIntMath.Sin(radians);
FixInt cos = FixIntMath.Cos(radians);
FixInt direction = FixIntMath.Atan2(deltaY.RawFloat, deltaX.RawFloat);
查找表一致性
三角函数依赖仓库中的 Sin/Cos、Acos、Atan2 查找表。参与同步的所有端必须使用完全相同版本的查找表文件。
6. FixIntVector2
6.1 创建与常量
FixIntVector2 position = new FixIntVector2(10, 5);
FixIntVector2 direction = FixIntVector2.right;
FixIntVector2 origin = FixIntVector2.zero;
常用静态向量:zero、one、up、down、left、right。
6.2 属性
| 属性 | 说明 |
|---|---|
x / y |
定点分量 |
magnitude |
向量长度 |
sqrMagnitude |
长度平方,比较距离时优先使用 |
normalized |
单位向量 |
6.3 常用 API
| 分类 | API |
|---|---|
| 插值移动 | Lerp、LerpUnclamped、MoveTowards、SmoothDamp |
| 向量计算 | Dot、Distance、Angle、SignedAngle |
| 变换 | Scale、Normalize、Reflect、Perpendicular |
| 约束比较 | ClampMagnitude、Min、Max、SqrMagnitude |
| Unity 转换 | ToVector2() |
FixIntVector2 current = new FixIntVector2(0, 0);
FixIntVector2 target = new FixIntVector2(10, 5);
FixInt speedPerFrame = 0.1f;
current = FixIntVector2.MoveTowards(current, target, speedPerFrame);
FixInt sqrDistance = (target - current).sqrMagnitude;
// 表现层
transform.position = current.ToVector2();
7. FixIntVector3
常用静态向量:zero、one、forward、back、up、down、left、right。
| 分类 | API |
|---|---|
| 插值移动 | Lerp、LerpUnclamped、MoveTowards |
| 向量计算 | Dot、Cross、Distance、Angle、SignedAngle |
| 投影反射 | Project、ProjectOnPlane、Reflect |
| 约束比较 | ClampMagnitude、Magnitude、SqrMagnitude、Min、Max |
| Unity 转换 | ToVector3() |
FixIntVector3 forward = new FixIntVector3(0, 0, 1);
FixIntVector3 right = new FixIntVector3(1, 0, 0);
FixInt dot = FixIntVector3.Dot(forward, right);
FixIntVector3 normal = FixIntVector3.Cross(forward, right);
FixIntVector3 projected = FixIntVector3.ProjectOnPlane(forward, normal);
8. FixIntRandomSeed
固定种子随机数用于生成可复现序列。只有在种子与调用顺序都相同时,结果才一致。
FixIntRandomSeed randomA = new FixIntRandomSeed(8734);
FixIntRandomSeed randomB = new FixIntRandomSeed(8734);
for (int i = 0; i < 10; i++)
{
int a = randomA.Range(0, 10000);
int b = randomB.Range(0, 10000);
Debug.Assert(a == b);
}
| API | 说明 |
|---|---|
SeedId |
当前随机种子,只读 |
Range(int min, int max) |
返回 [min, max) 范围内的整数 |
Range(FixInt min, FixInt max) |
使用定点范围生成结果 |
随机调用顺序
任一同步端多调用或少调用一次随机函数,后续整个随机序列都会错位。建议按战斗实例持有随机对象,不要在逻辑中临时创建无种子随机数。
9. 推荐实践
- 逻辑层全程使用
FixInt和定点向量,表现层最后一步再转换。 - 使用固定逻辑帧,不要把
Time.deltaTime直接作为权威逻辑时间。 - 距离比较优先使用
sqrMagnitude,减少不必要的开方运算。 - 所有端锁定相同代码、查找表和配置版本。
- 为关键战斗状态增加帧哈希或校验和,尽早发现不同步。
- 限制业务数值范围,避免乘法、除法和位移造成
long溢出。
10. 常见问题
为什么显示值和输入的小数略有差异?
定点数只能表示 1 / 1024 的整数倍,输入会发生量化。这是定点表示的正常特性。
为什么不能在逻辑层频繁转回 float?
转回浮点后继续参与逻辑运算,会重新引入跨平台浮点差异,削弱使用定点数的意义。
FixInt(long) 为什么得到的值不符合预期?
long 构造函数接收的是放大后的内部值。普通整数请使用 int 构造或隐式转换。
用了定点数就一定能实现帧同步吗?
不能。定点数只解决数学路径的一部分问题。执行顺序、随机序列、物理系统、时间步长、配置和网络输入都必须保持一致。