Appearance
Fixed-Point Math
This page describes how to use fixed-point math in your game logic.
Appearance
This page describes how to use fixed-point math in your game logic.
Fixed-point numerics achieve deterministic computations, enabling logic to execute identically on both client and server. Floating-point types (float and double) cannot guarantee this because different CPUs and compilers can produce slightly different results.
The Metaplay SDK provides two fixed-point types: F64 (signed 32.32 format) and F32 (signed 16.16 format). Use F64 by default for its larger range and precision. Use F32 only when performance or storage is critical, taking care to avoid overflows.
| Type | Format | Range | Precision | Storage |
|---|---|---|---|---|
F32 | 16.16 | ±32,767 | ~0.000015 | 4 bytes (int) |
F64 | 32.32 | ±2,147,483,647 | ~0.00000000023 | 8 bytes (long) |
Both types store their value as a raw integer where the lower bits represent the fractional part. For F32, 16 bits are used for the integer part and 16 bits for the fraction. For F64, 32 bits are used for each.
You can create fixed-point values from integers, floats, doubles, or by parsing strings:
// From integers
F64 fromInt = F64.FromInt(42);
// From floating-point (useful for initialization, but loses determinism guarantee)
F64 fromDouble = F64.FromDouble(3.14159);
// From string (slow, deterministic parsing)
F64 parsed = F64.Parse("3.14159");
// Using constants
F64 pi = F64.Pi;
F64 half = F64.Half;
F64 zero = F64.Zero;
// From ratios (fast and deterministic)
F64 third = F64.Ratio(1, 3); // 1/3
F64 tenth = F64.Ratio10(7); // 0.7 (7/10)
F64 percent = F64.Ratio100(15); // 0.15 (15/100)
F64 permille = F64.Ratio1000(125); // 0.125 (125/1000)The Parse() method is deterministic and uses the dot (.) as the decimal separator regardless of the current culture settings. This makes it safe for parsing values from game configs and other data files.
Fixed-point types support standard arithmetic operators and comparisons:
F64 a = F64.FromInt(10);
F64 b = F64.FromInt(3);
// Arithmetic
F64 sum = a + b; // 13
F64 diff = a - b; // 7
F64 product = a * b; // 30
F64 quotient = a / b; // 3.333...
F64 remainder = a % b; // 1
// Comparisons
bool isGreater = a > b; // true
bool isEqual = a == b; // false
// Negation
F64 negative = -a; // -10Convert fixed-point values back to standard types when needed:
F64 value = F64.FromDouble(3.7);
// To integer (various rounding modes)
int floored = F64.FloorToInt(value); // 3
int ceiled = F64.CeilToInt(value); // 4
int rounded = F64.RoundToInt(value); // 4
// To floating-point (for display or external systems)
float asFloat = value.Float;
double asDouble = value.Double;
// To string (deterministic)
string asString = value.ToString(); // "3.7"
// Between F32 and F64
F32 smaller = value.F32;
F64 larger = F64.FromF32(smaller);The library provides a comprehensive set of math functions. Most functions come in three variants:
Sqrt(), Sin(), etc.) - Best precisionSqrtFast(), SinFast(), etc.) - Good balance of speed and precisionSqrtFastest(), SinFastest(), etc.) - Maximum speed, lower precisionF64 x = F64.FromInt(2);
// Square root
F64 root = F64.Sqrt(x);
F64 rootFast = F64.SqrtFast(x);
// Trigonometric functions
F64 angle = F64.Pi / F64.FromInt(4); // 45 degrees
F64 sine = F64.Sin(angle);
F64 cosine = F64.Cos(angle);
F64 tangent = F64.Tan(angle);
// Inverse trigonometric
F64 asin = F64.Asin(F64.Half);
F64 atan2 = F64.Atan2(F64.One, F64.One);
// Exponential and logarithmic
F64 exp = F64.Exp(F64.One);
F64 log = F64.Log(exp);
F64 log2 = F64.Log2(F64.FromInt(8));
// Power
F64 squared = F64.Pow(x, F64.FromInt(2));
// Utility functions
F64 absolute = F64.Abs(F64.FromInt(-5));
F64 minimum = F64.Min(a, b);
F64 maximum = F64.Max(a, b);
F64 clamped = F64.Clamp(x, F64.Zero, F64.One);
F64 clamped01 = F64.Clamp01(x);
// Interpolation
F64 lerped = F64.Lerp(F64.Zero, F64.FromInt(10), F64.Half); // 5Vector types are included for both 2D and 3D math: F32Vec2, F32Vec3, F64Vec2, and F64Vec3. They follow the same precision tradeoffs as their scalar counterparts.
// From F64 components
F64Vec2 pos = new F64Vec2(F64.FromInt(10), F64.FromInt(20));
// From integers
F64Vec2 fromInt = F64Vec2.FromInt(10, 20);
// From doubles (for initialization)
F64Vec2 fromDouble = F64Vec2.FromDouble(1.5, 2.5);
// Using constants
F64Vec2 origin = F64Vec2.Zero;
F64Vec2 unit = F64Vec2.One;
F64Vec2 right = F64Vec2.Right;
F64Vec2 up = F64Vec2.Up;F64Vec2 a = F64Vec2.FromInt(3, 4);
F64Vec2 b = F64Vec2.FromInt(1, 2);
// Arithmetic
F64Vec2 sum = a + b;
F64Vec2 diff = a - b;
F64Vec2 scaled = a * F64.FromInt(2);
F64Vec2 divided = a / F64.FromInt(2);
// Length and distance
F64 length = F64Vec2.Length(a); // 5 (3-4-5 triangle)
F64 lengthSqr = F64Vec2.LengthSqr(a); // 25 (faster, no sqrt)
F64 distance = F64Vec2.Distance(a, b);
// Normalization
F64Vec2 normalized = F64Vec2.Normalize(a);
// Dot product
F64 dot = F64Vec2.Dot(a, b); // 3*1 + 4*2 = 11
// Cross product (3D only)
F64Vec3 v1 = F64Vec3.FromInt(1, 0, 0);
F64Vec3 v2 = F64Vec3.FromInt(0, 1, 0);
F64Vec3 cross = F64Vec3.Cross(v1, v2); // (0, 0, 1)
// Interpolation
F64Vec2 lerped = F64Vec2.Lerp(a, b, F64.Half);
// Component-wise operations
F64Vec2 min = F64Vec2.Min(a, b);
F64Vec2 max = F64Vec2.Max(a, b);
F64Vec2 clamped = F64Vec2.Clamp(a, F64Vec2.Zero, F64Vec2.One);When choosing between precision variants, consider these guidelines:
Sqrt(), Sin(), etc.) for gameplay-critical calculations where precision matters, such as physics simulations or financial calculations.For type selection:
F64 by default. The extra precision prevents subtle bugs and overflow issues.F32 only when memory is constrained (lots of values are needed) or when profiling shows significant performance gains.Fixed-point types integrate seamlessly with the game config system. When you define a config class with F32 or F64 fields, they are automatically parsed from your CSV or spreadsheet data:
[MetaSerializable]
public class ProducerInfo : IGameConfigData<ProducerTypeId>
{
[MetaMember(1)] public ProducerTypeId Id { get; private set; }
[MetaMember(2)] public string Name { get; private set; }
[MetaMember(3)] public F64 BaseValue { get; private set; }
[MetaMember(4)] public F64 GrowthRate { get; private set; }
public ProducerTypeId ConfigKey => Id;
}In your spreadsheet data, you can write decimal values just as you would for floats:
| Id | Name | BaseValue | GrowthRate |
|---|---|---|---|
| Producer1 | Gold Mine | 1.5 | 0.15 |
| Producer2 | Lumber Mill | 2.25 | 0.12 |
The config parser automatically detects fixed-point types and uses the deterministic Parse() method, ensuring that the same values are loaded on both client and server.
Fixed-point types are fully supported by the Metaplay serializer. You can use F32, F64, and their vector variants in any [MetaSerializable] class:
[MetaSerializable]
public class PlayerPosition
{
[MetaMember(1)] public F64Vec2 Position { get; private set; }
[MetaMember(2)] public F64 Rotation { get; private set; }
}The serializer handles these types automatically with no additional configuration required.