DEV Community

ShaiDev
ShaiDev

Posted on

Deterministic Physics in TS: Why I Wrote a Fixed-Point Engine

Floating-point math breaks determinism in multiplayer games and replay systems. A tiny CPU rounding difference can desync entire game states. I needed math that returns identical results on every machine, every time.

So I built @shaisrc/fixed-point - a zero-dependency fixed-point library for deterministic simulation in TypeScript.

πŸ“¦ The Library

πŸ›  How it works

Unlike standard number math, this library treats values as integers scaled by a fixed factor (default Q16.16). It guarantees that calculations result in the exact same bit-pattern everywhere.

What makes this library special is the hybrid storage engine:

  1. If you need standard 32-bit precision, it uses native number for speed.
  2. If you need high precision (e.g., 64-bit), it automatically swaps to BigInt arithmetic.

Quick Start

Here is how you perform deterministic operations:

import { fp } from '@shaisrc/fixed-point';

// 1. Create fixed-point numbers from floats or integers
const position = fp.fromFloat(10.5); // Stored as integer: 688128 (10.5 * 2^16)
const velocity = fp.fromInt(2);      // Stored as integer: 131072 (2 * 2^16)

// 2. Perform math (all operations happen on scaled integers)
const newPos = fp.add(position, velocity);

// 3. Convert back only when rendering
console.log(fp.toFloat(newPos)); // 12.5

// 4. Deterministic Trig (uses a LUT, no Math.sin calls!)
const angle = fp.fromFloat(Math.PI / 2);
const sine = fp.sin(angle); // Returns fixed-point 1.0 representation
Enter fullscreen mode Exit fullscreen mode

βš™οΈ Under the Hood

Configurable Precision

By default, it uses Q16.16 (16 bits for the integer, 16 bits for the fraction). But you aren't stuck there. You can configure your own instance:

import { createFixedPoint } from '@shaisrc/fixed-point';

// Need massive precision? Go 64-bit with BigInt backend.
const bigMath = createFixedPoint({ 
  totalBits: 64, 
  fractionBits: 32,
  useBigInt: true 
});

const hugeVal = bigMath.fromString("12345.67890123");
Enter fullscreen mode Exit fullscreen mode

Performance & Features

  • Bitwise Optimized: Shifts and masks are used wherever possible.
  • Lookup Tables (LUT): sin and cos use pre-calculated tables to ensure different browser engines don't return slightly different float results.
  • Newton Iteration: sqrt is implemented with integer-based Newton iteration.
  • Type Safety: Full TypeScript support prevents you from accidentally mixing up raw numbers and fixed-point values.

πŸ“Š Performance

Here's how it compares to native operations (5M iterations):

Benchmark Min (ms) Avg (ms)
native add/mul 6.34 6.70
fixed-point add/mul (number) 116.65 120.24
fixed-point add/mul (bigint) 382.35 389.47
native sin 190.57 197.90
fixed-point sin (LUT, number) 62.75 64.75
fixed-point sin (LUT, bigint) 491.26 501.97

Key takeaway: Basic math is slower than native (you're trading speed for determinism), but the LUT-based trig functions actually beat Math.sin by ~3x when using the number backend.

Why I Open-Sourced This

This was built for my deterministic game engine's lockstep networking. It works-so I'm sharing it. If you're building networked games, replay systems, or physics sims that need bitwise-identical math, this might save you weeks of debugging float drift.

Feedback on edge cases and performance welcome.

Try it out

npm install @shaisrc/fixed-point
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Check it out on GitHub: https://github.com/ShaiSrc/fixed-point

Found a bug? Have a use case I didn't think of? Open an issue or PR.

Top comments (0)