This document is about: QUANTUM 3
SWITCH TO

This page has not been upgraded to Quantum 3.0 yet. The information may be out of date.

Fixed Point

介紹

在 Quantum 中,FP結構(固定點數)完全取代了所有floatsdoubles的使用,以確保跨平台的確定性。它提供了常見數學數據結構的版本,如 FPVector2、FPVector3、FPMatrix、FPQuaternion、RNGSession、FPBounds2 等。Quantum 中的所有系統,包括物理和導航,都完全使用FP值進行計算。

Quantum 中實現的固定點數類型是Q48.16。它在精度和性能之間取得了良好的平衡,並偏向於後者。

在內部,FP使用一個 long 來表示組合的固定點數(整數部分 + 小數部分);可以通過FP.RawValue訪問和設置該 long 值。

Quantum 的 FP 數學使用精心調整的查找表來實現快速的三角函數和平方根計算(參見Assets/Photon/Quantum/Resources/LUT)。

解析FP

可表示的FP分數是有限的,永遠不如double準確。解析是一個近似值,會四捨五入到最接近的FP精度。這體現在:

  • FP解析為float 1.1f,然後轉換回float可能會得到1.09999999f;及,
  • 在同一台機器上,不同的解析方法會產生不同的結果。

    C#

    FP.FromFloat_UNSAFE(1.1f).RawValue != FP.FromString("1.1").RawValue
    

TLDR

  • 僅在編輯時從浮點數使用,切勿在模擬或運行時內部使用。
  • 盡可能使用原始值轉換。

FP.FromFloat_UNSAFE()

由於捨入誤差,從float轉換不具有確定性,因此絕對 應在模擬內部進行。在模擬中進行此類轉換將 100% 導致不同步。

然而,它可以在 編輯構建 時使用,當轉換後的(FP)數據首次創建並與所有人共享時。重要提示不同 機器上生成的此類數據可能不兼容。

C#

var v = FP.FromFloat_UNSAFE(1.1f);

FP.FromString_UNSAFE()

這會在內部將string解析為浮點數,然後轉換為FPFromFloat_UNSAFE()的所有注意事項同樣適用於此。

C#

var v = FP.FromFloat_UNSAFE("1.1");

FP.FromString()

這是確定性的,因此可以安全地在任何地方使用,但可能不是最高效的選項。典型的用例是平衡信息(補丁),客戶端從服務器加載後用於更新 Quantum 資產中的數據。

注意字符串的區域設置!它僅解析英文數字格式的小數,並需要一個點(例如 1000.01f)。

C#

var v = FP.FromFloat("1.1");

FP.FromRaw()

這是安全且快速的,因為它模仿了內部表示。

C#

var v = FP.FromRaw(72089);

以下代碼片段可用於在 Unity 中創建一個 FP 轉換器窗口,方便轉換。

C#

using System;
using UnityEditor;
using Photon.Deterministic;

public class FPConverter : EditorWindow {
  private float _f;
  private FP _fp;

  [MenuItem("Quantum/FP Converter")]
  public static void ShowWindow() {
    GetWindow(typeof(FPConverter), false, "FP Converter");
  }

  public virtual void OnGUI() {
    _f = EditorGUILayout.FloatField("Input", _f);
    try {
      _fp = FP.FromFloat_UNSAFE(_f);
      var f = _fp.AsFloat;
      var rect = EditorGUILayout.GetControlRect(true);
      EditorGUI.FloatField(rect, "Output FP", f);
      QuantumEditorGUI.Overlay(rect, "(FP)");
      EditorGUILayout.LongField("Output Raw", _fp.RawValue);
    }
    catch (OverflowException e) {
      EditorGUILayout.LabelField("Out of range");
    }
  }
}

常量變量

`FP._1_10`語法無法擴展或生成。

FP是一個結構體,因此不能用作常量。但是,可以硬編碼並在const變量中使用 "FP" 值:

  1. 組合預定義的FP._1 static獲取器或FP.Raw._1 const變量。

C#

FP foo = FP._1 + FP._0_10;
// or
foo.RawValue = FP.Raw._1 + FP.Raw._0_10;

C#

const long MagicNumber = FP.Raw._1 + FP.Raw._0_10;

FP foo = default;
foo.RawValue = MagicNumber;
// or
foo = FP.FromRaw(MagicNumber);
  1. 將特定的浮點數一次性轉換為 FP,並將原始值保存為常量

C#

const long MagicNumber = 72089; // 1.1

var foo = FP.FromRaw(MagicNumber);
// or
foo.RawValue = MagicNumber;
  1. 在 Quantum DSL 中創建常量

#define FPConst 1.1

然後像這樣使用:

C#

var foo = default(FP);
foo += Constants.FPConst;
// or
foo.RawValue += Constants.Raw.FPConst;

它將生成以下代碼來表示常量:

C#

public static unsafe partial class Constants {
  public const Int32 PLAYER_COUNT = 8;
  /// <summary>1.100006</summary>

  public static FP FPConst {
    [MethodImpl(MethodImplOptions.AggressiveInlining)] get {
      FP result;
      result.RawValue = 72090;
      return result;
    }
  }
  public static unsafe partial class Raw {
    /// <summary>1.100006</summary>
    public const Int64 FPConst = 72090;
  }
}
  1. 在類中定義readonly static變量。

C#

private readonly static FP MagicNumber = FP._1 +  FP._0_10;

const變量相比,存在性能損失,並且不要忘記標記readonly,因為在運行時隨機更改值可能導致不同步。

轉換

允許從int, uint, short, ushort, byte, sbyte隱式轉換為FP,並且是安全的。

C#

FP v = (FP)1;
FP v = 1;

FP顯式轉換為floatdouble是可能的,但顯然不應在模擬內部使用。

C#

var v = (double)FP._1;
var v = (float)FP._1;

轉換為整數並返回是安全的。

C#

FP v = (FP)(int)FP._1;

不安全的轉換標記為[obsolete],並會導致InvalidOperationException

C#

FP v = 1.1f;  // ERROR
FP v = 1.1d;  // ERROR

內聯

所有低級 Quantum 系統都使用手動內聯的FP算術,以盡可能提高性能。固定點數學使用整數除法和乘法。為了實現這一點,結果或被除數在計算前或計算後會按 FP 精度(16)進行位移。

C#

var v = parameter * FP._0_01;

// inlined integer math
FP v = default;
v.RawValue = (parameter.RawValue * FP._0_01.RawValue) >> FPLut.PRECISION;

C#

var v = parameter / FP._0_01;

// inlined integer math
FP v = default;
v.RawValue = (parameter.RawValue << FPLut.PRECISION) / FP._0_01.RawValue;

溢出

FP.UseableMax表示可以與自身相乘而不會導致溢出(超過long範圍)的最高FP數字。

FP.UseableMax
    Decimal: 32767.9999847412
    Raw: 2147483647
    Binary: 1111111111111111111111111111111 = 31 bit

FP.UseableMin
    Decimal: -32768
    Raw: -2147483648
    Binary: 10000000000000000000000000000000 = 32 bit

精度

當數字保持在特定範圍內(0.01..1000)時,FP的總體精度是可接受的。FP 數學相關的精度問題通常會產生不準確的結果,並可能使基於數學的系統不穩定。一個非常常見的情況是乘以非常大或非常小的數字,然後通過除法返回到原始範圍。結果數字會失去精度。

另一個例子是ClosestDistanceToTriangle方法中的以下代碼片段。其中t0是通過將兩個點積相乘計算得出的,而點積本身已經是乘法的結果。當需要非常準確的結果時,這是一個問題。緩解此問題的一種方法是在計算之前人為地移動值,然後將結果移回。當輸入的範圍已知時,這將有效。

C#

  var diff = p - v0;
  var edge0 = v1 - v0;
  var edge1 = v2 - v0;
  var a00 = Dot(edge0, edge0);
  var a01 = Dot(edge0, edge1);
  var a11 = Dot(edge1, edge1);
  var b0 = -Dot(diff, edge0);
  var b1 = -Dot(diff, edge1);
  var t0 = a01 * b1 - a11 * b0;
  var t1 = a01 * b0 - a00 * b1;
  // ...
  closestPoint = v0 + t0 * edge0 + t1 * edge1;

FPAnimationCurves

Unity 提供了一組工具,可以以曲線的形式表達某些值。它帶有一個自定義編輯器,用於編輯這些曲線,然後將其序列化並可在運行時用於評估曲線在特定點的值。

有許多情況下可以使用曲線,例如在實現車輛時表達轉向信息,AI 代理在決策過程中的效用值(如 Bot SDK 的效用理論中所做的那樣),獲取某些攻擊傷害的乘數值等。

Quantum SDK 已經自帶了一個動畫曲線的實現,名為FPAnimationCurve,以 FP 形式評估。這種自定義類型在 Unity 中直接檢查數據資產和組件時,會使用 Unity 的默認動畫曲線編輯器繪製,其數據隨後會內部烘焙為確定性類型。

從FPAnimationCurve獲取數據

在 Quantum 代碼中從曲線中獲取數據的代碼與 Unity 版本非常相似:

C#

// This returns the pre-baked value, interpolated accordingly to the curve's configuration such as it's key points, curve's resolution, tangets modes, etc
FP myValue = myCurve.Evaluate(FP._0_50);

直接在模擬中創建FPAnimationCurves

以下代碼片段可以直接在模擬中從頭創建一個確定性動畫曲線:

C#

// Creating a simple, linear curve with five key points
// Change the parameter as prefered
public static class FPAnimationCurveUtils
{
    public static FPAnimationCurve CreateLinearCurve(FPAnimationCurve.WrapMode preWrapMode, FPAnimationCurve.WrapMode postWrapMode)
    {
        return new FPAnimationCurve
        {
            Samples = new FP[5] { FP._0, FP._0_25, FP._0_50, FP._0_75, FP._1 },
            PostWrapMode = (int)postWrapMode,
            PreWrapMode = (int)preWrapMode,
            StartTime = 0,
            EndTime = 1,
            Resolution = 32
        };
    }
}

C#

// Storing a curve into a local variable
var curve = FPAnimationCurveUtils.CreateLinearCurve(FPAnimationCurve.WrapMode.Clamp, FPAnimationCurve.WrapMode.Clamp);

// It can also be used directly to pre-initialise a curve in an asset
public unsafe partial class CollectibleData
{
    public FPAnimationCurve myCurve = FPAnimationCurveUtils.CreateLinearCurve(FPAnimationCurve.WrapMode.Clamp, FPAnimationCurve.WrapMode.Clamp);

將 Unity 的 AnimationCurve 內嵌到 FPAnimationCurve

將常規 Unity AnimationCurve轉換為FPAnimationCurve所需的代碼片段可以在這裡找到。

Back to top