帧同步场景下,float 精度问题尤为突出。解决方案是用定点数替代浮点数。
方案一:自己实现简易定点数
最简单的方案是全程用 long 进行运算,只在需要转换时才做类型转换。基本思路是:数值运算时直接在 long 之间进行;若需要与 float 运算,则先用 Parse 函数将其转换为 long 再参与计算。这种方式比 Fix64 方案速度更快,也更具可读性。
严格来说,逻辑层的输入和运算都应保持 long(即 int64),只有在需要输出其他数据类型时(如 UI 显示、输出到 Unity)才进行转换。
这套简易定点数方案有一个明显缺陷:非常容易溢出。根本原因在于,表示小数部分的数字参与运算时容量有限。float 之所以不会溢出,是因为它采用科学计数法;而这套简易方案并没有引入科学计数法来表示数值。
public struct Decimal
{
public const long MaxValue = long.MaxValue;
public const long MinValue = long.MinValue;
//转换规则,用大精度转换为小精度,比如float就用double来转换
//对于乘除来说, 他们之间并不能直接相除 long_a*long_a / split_xxx 才是真正的结果
public const long SPLIT_LONG = 100000;
public const int SPLIT_INT = 100000;
public const float SPLIT_FLOAT = 100000.0f;
public const double SPLIT_DOUBLE = 100000.0;
public long value;
public Decimal(Decimal other)
{
this.value = other.value;
}
public Decimal(long value)
{
this.value = value;// 默认构造不认为是定点 需要手动调用parse
}
public Decimal(double value)
{
this.value = Parse(value);
}
public Decimal(float value)
{
this.value = Parse(value);
}
public Decimal(int value)
{
this.value = Parse(value);
}
public static implicit operator Decimal(float value)
{
return new Decimal(value);
}
public static implicit operator Decimal(double value)
{
return new Decimal(value);
}
public static implicit operator Decimal(int value)
{
return new Decimal(value);
}
public static implicit operator Decimal(long value)
{
return new Decimal(value);
}
public static long Parse(float v)
{
double tmp = v;
return (long)(tmp * SPLIT_DOUBLE);
}
public static long Parse(int v)
{
long tmp = v;
return (long)(tmp * SPLIT_INT);
}
public static long Parse(double v)
{
double tmp = v;
return (long)(tmp * SPLIT_DOUBLE);
}
public static double ToDouble(long v)
{
return (double)v / SPLIT_DOUBLE;
}
public static int ToInt(long v)
{
return (int)(v / SPLIT_LONG);
}
public static double ToFloat(long v)
{
return (float)ToDouble(v);
}
public static double ToDouble(Decimal v)
{
return (double)v.value / SPLIT_DOUBLE;
}
public static int ToInt(Decimal v)
{
return (int)(v.value / SPLIT_LONG);
}
public static double ToFloat(Decimal v)
{
return (float)ToDouble(v);
}
public static Decimal operator *(Decimal x, Decimal y)
{
}
}
方案二:使用现成的 Fix64 定点数库
另一个选择是使用基于 long 的现成定点数库 Fix64:
https://github.com/jjcat/FixedMath.Net/blob/master/Fix64.cs
Fix64 的核心算法是用末尾 32 位表示小数部分。其 rawvalue 并不直接可读;虽然是 struct、分配在栈上,但运算速度依然相对较慢。可以把它理解为用 long 实现的科学计数法。
long xxxx=0 ;
const int FRACTIONAL_PLACES = 32;
const long ONE = 1L << FRACTIONAL_PLACES;
xxxx += ONE * 5;
xxxx += ONE * 2;
Debug.LogError((float)xxxx / ONE + " " + ONE);
以上代码输出 7。
性能对比测试
以下是对相同数据量运算的简单性能测试:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using FixMath.NET;
using System.Runtime.InteropServices;
public class NewBehaviourScript : MonoBehaviour
{
// Use this for initialization
void Start()
{
// this.GetComponent<Animator>().Play("npc_ani_100011_attack_01");
// this.GetComponent<Animator>().Play("npc_ani_100011_standby_01");
long xx = 2;
decimal bb = 2;
float ff = 2f;
Fix64 fx = (Fix64)2f;
long dd = 2 * Decimal.SPLIT_LONG;
System.Diagnostics.Stopwatch aa = new System.Diagnostics.Stopwatch();
aa.Stop();
aa.Reset();
aa.Start();
for (int i = 0; i < 999999; i++)
{
xx *= 2;
xx /= 2;
}
Debug.LogError("long time=" + aa.ElapsedMilliseconds + " " + xx);
aa.Stop();
aa.Reset();
aa.Start();
for (int i = 0; i < 999999; i++)
{
bb *= 2;
bb /= 2;
}
Debug.LogError("decimal time=" + aa.ElapsedMilliseconds);
aa.Stop();
aa.Reset();
aa.Start();
for (int i = 0; i < 999999; i++)
{
ff *= 2f;
ff /= 2f;
}
Debug.LogError("float time=" + aa.ElapsedMilliseconds + " " + ff);
aa.Stop();
aa.Reset();
aa.Start();
for (int i = 0; i < 999999; i++)
{
fx = Fix64.FastMul((Fix64)2f, fx);
fx /= (Fix64)2f;
}
Debug.LogError("fix64 with float time=" + aa.ElapsedMilliseconds);
Fix64 twof = (Fix64)2f;
aa.Stop();
aa.Reset();
aa.Start();
for (int i = 0; i < 999999; i++)
{
fx = Fix64.FastMul(twof, fx);
fx /= twof;
}
Debug.LogError("fix64 time=" + aa.ElapsedMilliseconds);
aa.Stop();
aa.Reset();
aa.Start();
for (int i = 0; i < 999999; i++)
{
dd *= Decimal.Parse(2f);
dd /= Decimal.Parse(2f);
}
Debug.LogError("light decimal time=" + aa.ElapsedMilliseconds + " " + Decimal.ToDouble(dd));
/* long xxxx=0 ;
const int FRACTIONAL_PLACES = 16;
const long ONE = 1L << FRACTIONAL_PLACES;
xxxx += ONE * 5;
xxxx += ONE * 2;
Debug.LogError((float)xxxx / ONE + " " + ONE + " " + ONE*ONE/ONE);*/
}
// Update is called once per frame
void Update()
{
}
}
从测试结果可以看出,Fix64 的运算速度并不快。