NullTick networking core
Ten thousand matches at once. One server. Thirty-two megabytes.
Measured, not modeled. Press Play and you're multiplayer — no server to run, no netcode to write.
Built to drop into your engine
Survival, farming, turn-based. Zero networking code. All built from drag-and-drop components — no netcode written.
Unity, Unreal and Godot — the demos page lists what ships where.
Billed on peak CCU — the most players connected at once. Server-owned NPCs never count.
What you get
Six things that are true before you write a line.
Server-authoritative by default
The server owns where you are — there's no client-authoritative mode to opt into. Input leads, server integrates.
Ten thousand matches at once
~2% of one core, in 32 MB. Measured on a 16-core box — not modelled.
Events, not ticks — only what moved goes on the wire
A tick loop walks every entity. NullTick touches only the ones that moved. The sleeping half doesn't exist to the CPU.
Zero networking code
Hover to assemble. Every demo is built from inspector components, not code. Press Play; nothing to install.
Every genre, turn-based included
Between turns, a table sends almost nothing. Network traffic is reserved solely for active play.
Three engines, one core
Unity, Unreal and Godot ship bindings. Hover to see them call the same flat 16-symbol C ABI.
01 — The tick
Sixty times a second, the server asks if anything happened.
The classical loop iterates every entity on a fixed clock — evaluating state deltas, checking deadzones, serializing floats — mostly to learn that nothing changed. The compute scales with the size of the world, not the amount of play: a map holding 65,000 entities with 400 people moving still walks all 65,000, sixty times a second.
NullTick doesn't optimize that loop. It removes it. Updates are events, so update work is O(Nactive) — the sleeping half of the map doesn't exist to the CPU, and it doesn't exist to the network either.
This holds against even a well-built tick server: a competent one skips sending unchanged positions, but the loop itself still pays a fixed compute cost for every entity on every tick — walking the whole list sixty times a second whether or not anything moved. NullTick touches only the ones that do.
In short A tick walks sixty-five thousand entities to learn that nothing changed. Events touch only the ones that moved.
02 — The physics
A node is a deadzone, a filter, and a scheduler — for free.
Each entity is an event-driven node holding three state values, one per axis. Kinetic input — mouse deltas, stick vectors — arrives and the node charges toward a threshold and decays toward rest, resolved retroactively at the exact microsecond an event arrives. No clock anywhere — decay is applied on packet arrival from the node's stored timestamp. The decay is the deadzone: jitter, stick drift and packet bunching decay to nothing as physics.
When an axis crosses threshold it emits a discrete output, +1 or -1, and resets to zero on the spot. Hold the stick below the bound and the node coasts forever, never costing a packet.
In short One leak does the deadzone, the filter and the scheduler at once — so jitter and stick drift die as physics, not as a conditional you have to tune.
03 — Memory
The whole world's network state fits in L3.
The router is one flat arena — nodes indexed directly by entity ID. No hash maps, no pointer chasing, no per-entity heap. Each node holds state values, threshold, timestamp, and bookkeeping — compact enough that the full state table stays resident in a modern CPU's L3 cache. The size is load-bearing; it does not grow casually.
Stale packets die at the door.
Packets outlive entities: a client can fire at an ID the server just recycled. Every registration bumps the node's generation counter, and every packet carries the generation it was aimed at. A mismatch is dropped at the threshold of the critical section, before any math runs.
struct EntityHandle { uint32_t id; // arena slot, dense uint32_t generation; // fenced against recycled slots };
In short 48 bytes an entity, and 65,536 of them in a 3 MiB arena — the whole world's network state sits in L3, reached by offset math rather than a pointer chase.
04 — Concurrency
One spinlock per entity. None for the world.
route_packet takes no lock on the arena — a handle resolves to its node by offset math, O(1). All synchronization is a per-node spinlock, with _mm_pause / yield backoff so contended cores don't saturate the coherency bus. Events fire outside the lock, so a slow network write never stalls the node.
In short One byte of lock per entity and none for the world, so two players in the same room never contend — and a slow socket can't stall the entity it belongs to.
05 — Cadence
Animation phase-locked to the server's rhythm.
Continuous movement makes a node trigger rhythmically — the receiving client hooks events directly into root motion, so every arrival steps the skeletal phase forward by a fixed fraction.
Because playback advances only on event arrival, interpolation delay can't smear a character's feet — foot-sliding requires the animation clock and the network clock to disagree, and here they are the same clock. If the network goes silent, velocity and animation freeze on the exact same phase step for every client.
In short Feet slide when the animation clock and the network clock disagree. Here they're the same clock, so there's nothing left to disagree.
06 — The engine boundary
A C ABI on one side, a UDP socket on the other.
The core is a small header library with one compiled translation unit and no I/O — it embeds in any engine. Unity, Unreal, and Godot ship first-class bindings today; every other engine consumes it through the flat C ABI. The wire is a versioned little-endian UDP protocol with a runnable reference host.
C ABI v1
16 symbols, no C++
An opaque NullTickRouter*, POD handle and event structs, flat nulltick_* functions. No exceptions or std::function cross the boundary — the smoke test compiles with gcc, not g++, to prove it. Callers verify nulltick_abi_version() before trusting a binary.
Engine bindings
Three engine SDKs
Unity and Unreal bind the native ABI — Unity as a GameObject path plus a Burst-compiled ECS path, Unreal as the NullTickRuntime plugin — each running embedded or networked and refusing to load on an ABI mismatch. Godot ships too, a pure-GDScript client. Every SDK carries rooms, reliable messaging, netvars, and an in-editor dashboard.
Server-authoritative UDP
No position on the movement wire
A client cannot move itself. Kinetic deltas in, discrete signals out — position is the server's integral of the spikes it chose to emit, and speed is clamped server-side, so there is no coordinate on the movement wire to forge mid-game.
Re-joining can't move you either: a reconnect resumes the server's ledger and ignores any declared start. Taking over a live seat requires that player's own session token. A server-issued 64-bit session token that every input packet must echo blocks spoofing; per-packet input clamps cap speed hacks and floods.
Test discipline
Sanitizer-clean
The suite runs under ASan, UBSan and ThreadSanitizer. One test fires exactly 1,000 events from 100,000 interleaved injections — a broken lock loses the count. Explicit-timestamp overloads keep every decay assertion deterministic.
In short Sixteen C symbols and no C++ on the boundary, so the engine you use is a binding rather than a rewrite — and the same core answers Unity, Unreal, Godot and anything else that can call C.
07 — Get in touch
Tell us what you're building.
We'll demo live, walk the architecture, and share results under NDA.