帧同步场景下,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 的运算速度并不快。