Fixed Point
Introduction
In Quantum the FP
struct (Fixed Point) completely replaces all usages of floats
and doubles
to ensure cross-platform determinism. It offers versions of common math data structures like FPVector2, FPVector3, FPMatrix, FPQuaternion, RNGSession, FPBounds2, etc. All systems in Quantum, including physics and navigation, exclusively use FP
values in their computations.
The fixed-point type implemented in Quantum is Q48.16
. It has proved to a good balance between precision and performance with a bias towards the latter.
Internally FP
uses one long to represent the combined fixed-point number (whole number + decimal part); the long value can be accessed and set via FP.RawValue
.
Quantum's FP Math uses carefully tuned look up tables for fast trigonometric and square root functions (see QuantumSDK\quantum_unity\Assets\Photon\Quantum\Resources\LUT
).
Parsing FPs
The representable FP
fraction is limited and never as accurate as a double
. Parsing is an approximation and will round to the nearest possible FP
precision. This is reflected in:
- parsing an
FP
as thefloat
1.1f
and then converting back tofloat
possibly resulting in1.09999999f
; and, - different parsing methods yielding different results on the same machine.
C#
FP.FromFloat(1.1f).RawValue != FP.FromString("1.1").RawValue
TLDR
- Use from float only during edit time, never inside the simulation or at runtime.
- It is best to convert from raw whenever possible.
FP.FromFloat_UNSAFE()
Converting from float
is not deterministic due to rounding errors and should never be done inside the simulation. Doing such a conversion in the simulation will cause desyncs 100% of the time.
However, it can be used during edit or build time, when the converted (FP) data is first created and then shared with everyone. IMPORTANT: Data generated this way on different machines may not be compatible.
C#
var v = FP.FromFloat_UNSAFE(1.1f);
FP.FromString_UNSAFE()
This will internally parse the string
as a float and then convert to FP
. All caveats from FromFloat_UNSAFE()
apply here as well.
C#
var v = FP.FromFloat_UNSAFE("1.1");
FP.FromString()
This is deterministic and therefore safe to use anywhere but may not be the most performant option. A typical use case is balancing information (patch) that clients load from a server and then use to update data in Quantum assets.
Be aware of the string locale! It only parses English number formatting for decimals and requires a dot (e.g. 1000.01f).
C#
var v = FP.FromFloat("1.1");
FP.FromRaw()
This is secure and fast as it mimics the internal representation.
C#
var v = FP.FromRaw(72089);
This snippet can be used to create a FP converter window in Unity for convient conversion.
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 = FPPropertyDrawer.GetRawAsFloat(_fp.RawValue);
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");
}
}
}
Const Variables
FP
is a struct and can therefore not be used as a constant. It is, however, possible to hard-code and use "FP
" values in const
variables:
- Combine pre-defined
FP._1
static
getters orFP.Raw._1
const
variables.
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);
- Convert the specific float once to FP and save the raw value as a constant
C#
const long MagicNumber = 72089; // 1.1
var foo = FP.FromRaw(MagicNumber);
// or
foo.RawValue = MagicNumber;
- Create the constant inside the Quantum DSL
#define FPConst 1.1
Then use like this:
C#
var foo = default(FP);
foo += Constants.FPConst;
// or
foo.RawValue += Constants.Raw.FPConst;
It will generate code to represent the constant in the following way:
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;
}
}
- Define
readonly
static
variables in the class.
C#
private readonly static FP MagicNumber = FP._1 + FP._0_10;
There is a performance penalty compared to const
variables and don't forget to mark readonly
because randomly changing the value during runtime could lead to desyncs.
Casting
Implicit casting to FP
from int
, uint
, short
, ushort
, byte
, sbyte
is allowed and safe.
C#
FP v = (FP)1;
FP v = 1;
Explicit casting from FP
to float
or double
is possible, but obviously should not be used inside the simulation.
C#
var v = (double)FP._1;
var v = (float)FP._1;
Casting to integer and back is safe though.
C#
FP v = (FP)(int)FP._1;
Unsafe casts are marked as [obsolete]
and will cause a InvalidOperationException
.
C#
FP v = 1.1f; // ERROR
FP v = 1.1d; // ERROR
Inlining
All low-level Quantum systems use manual inlined FP
arithmetic to extract every ounce of performance possible. Fixed point math uses integer division and multiplication. To achieve this, the result or dividend are bit-shifted by the FP-Precision (16) before or after the calculation.
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;
Overflow
FP.UseableMax
represents the highest FP
number that can be multiplied with itself and not cause an overflow (exceeding long
range).
FP.UseableMax
Decimal: 32767.9999847412
Raw: 2147483647
Binary: 1111111111111111111111111111111 = 31 bit
FP.UseableMin
Decimal: -32768
Raw: -2147483648
Binary: 10000000000000000000000000000000 = 32 bit
Precision
The general FP
precision is decent when the numbers are kept within a certain range (0.01..1000)
. FP-math related precision problems usually produce inaccurate results and tend to make systems (based on math) unstable. A very common case is to multiply very high or small numbers and than returning to the original range by division for example. The resulting numbers lose precision.
Another example is this method excerpt from ClosestDistanceToTriangle
. Where t0
is calculated from multiplying two dot products with each other, where a dot-product is already also a result of multiplications. This is a problem when very accurate results are expected. A way to mitigate this issue is shifting the values artificially before the calculation then shift the result back. This will work when the ranges of the input are somewhat known.
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 comes with a set of tools which are useful to express some values in the form of curves. It comes with a custom editor for such curves, which are then serialised and can be used in runtime in order to evaluate the value of the curve in some specific point.
There are many situations where curves can be used, such as expressing steering information when implementing vehicles, the utility value during the decision making for an AI agent (as done in Bot SDK's Utility Theory), getting the multiplier value for some attack's damage, and so on.
The Quantum SDK already comes with its own implementation of an animation curve, named FPAnimationCurve
, evaluated as FPs. This custom type, when inspected directly on Data Assets and Components in Unity, are drawn with Unity's default Animation Curve editors, whose data are then internally baked into the deterministic type.
Polling data from an FPAnimationCurve
The code needed to poll some data from a curve, with Quantum code, is very similar to the Unity's version:
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);
Creating FPAnimationCurves directly on the simulation
Here are the snippets to create a deterministic animation curve from scratch, directly on the simulation:
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);
Baking Unity's AnimationCurve into a FPAnimationCurve
The snippets needed for converting a regular Unity AnimationCurve
into an FPAnimationCurve
can be found HERE.