Fixed Point
介紹
在 Quantum 中,FP
結構(固定點數)完全取代了所有floats
和doubles
的使用,以確保跨平台的確定性。它提供了常見數學數據結構的版本,如 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
解析為浮點數,然後轉換為FP
。FromFloat_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
是一個結構體,因此不能用作常量。但是,可以硬編碼並在const
變量中使用 "FP
" 值:
- 組合預定義的
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);
- 將特定的浮點數一次性轉換為 FP,並將原始值保存為常量
C#
const long MagicNumber = 72089; // 1.1
var foo = FP.FromRaw(MagicNumber);
// or
foo.RawValue = MagicNumber;
- 在 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;
}
}
- 在類中定義
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
顯式轉換為float
或double
是可能的,但顯然不應在模擬內部使用。
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
所需的代碼片段可以在這裡找到。