Authoritative Rust server · Unity URP client · Protobuf/WebSocket networking
A fully server-authoritative multiplayer territory-capture game inspired by Voodoo's Paper.io 2.
Custom Rust game server + Unity 3D client with real-time state sync, procedural rendering, flood-fill territory claiming, and client-side prediction.
▶ Play in Browser · Server Repository · Report Bug
** Portfolio Project** — Built as a technical showcase for a Senior Game Developer application at Voodoo, the original creators of Paper.io. All game mechanics are reimplemented from scratch; no original code or assets from Voodoo are used.
A 3D multiplayer Paper.io 2 clone — a territory-capture game where players navigate a grid, leave colored trails, and claim land by circling back to their territory. Every enclosed region becomes yours, but your trail is your vulnerability: if anyone crosses it before you close the loop, you're eliminated.
Desktop: WASD or Arrow Keys · Mobile: Swipe to move
| Feature | Description | |
|---|---|---|
| ◈ | Territory Claiming | Draw trails outside your zone, return home to claim everything enclosed. Edge-based flood-fill determines captured regions — including enemy territory |
| ⚡ | Trail Collisions | Cross any player's trail (or your own) and they're eliminated. Head-on collisions resolve by score. Four elimination types handled server-side |
| ↻ | Respawn System | Eliminated players respawn after a configurable delay with brief invulnerability. Safe-distance spawn algorithm |
| ▦ | 3D Rendering | Procedural mesh generation for territory and trails. Vertex-colored grid, glowing emissive trails, smooth interpolation between server ticks |
| ⟐ | Client Prediction | Input applied locally on the frame it's pressed. Pending inputs buffered with tick IDs. Server reconciliation replays unacknowledged inputs |
| △ | Delta Compression | Only changed cells transmitted per tick. Full state on join + periodic keyframes. RLE encoding for territory rows |
| 🧭 | Minimap & UI | Dedicated minimap camera with per-player indicators, score percentage, leaderboard, territory claim popups |
| 🔌 | Dual Transport | UDP for native builds, WebSocket for WebGL — unified routing through virtual socket addresses |
┌─────────────────┐ Protobuf / WS ┌──────────────────┐
│ │◄──────────────────────────────► │ │
│ Unity Client │ ~50ms ticks │ Rust Server │
│ │◄──────────────────────────────► │ │
└─────────────────┘ └──────────────────┘
Input capture Session management
Client-side prediction Room lifecycle
Position interpolation Game trait interface
Procedural mesh rendering Movement simulation
Trail tube geometry Collision detection
Object-pooled visuals Flood-fill claiming
Camera + minimap Delta state broadcast
The client never determines game outcomes. The server owns simulation, collision, and territory state. The client handles input, rendering, and prediction.
| System | Details |
|---|---|
| Tick Loop | 20 Hz (50ms). Processes queued inputs, advances movement, runs collision, broadcasts state |
| Movement | Grid-based with configurable move interval. Direction changes apply next tick. Boundary kills |
| Trail System | Positions tracked when player is outside own territory. Claim triggered on return home |
| Territory Claiming | Trail cells converted to territory → BFS from all map edges → unreachable cells = enclosed = claimed |
| Collision | Trail-cut (another player crosses your trail), self-intersection, head-on (score-based resolution), boundary |
| State Sync | Keyframe/delta pattern. Full PaperioState at intervals, PaperioDelta with only changes in between. Territory rows use RLE |
| Sessions | Reconnect tokens, sequence numbering, packet loss detection, timeout with auto-cleanup |
| Game Trait | Generic Game trait — networking code is game-agnostic. Adding a new game = implementing one trait |
| System | Details |
|---|---|
| Prediction | ClientPrediction.cs — records pending inputs with estimated tick, replays on correction. Stats tracking for debugging |
| Reconciliation | On PaperioState arrival: snap to server position, discard acknowledged inputs, replay remaining |
| Territory Renderer | Procedural mesh with vertex colors per cell. Updates incrementally on delta arrival |
| Trail Renderer | 3D tube geometry with rounded corners and emissive glow material. Per-player tracking with object pooling |
| Player Visuals | Spawn/despawn with pooling. MaterialPropertyBlock for per-instance colors. Name labels track camera |
| Interpolation | Smooth movement between grid positions over tick duration for all remote players |
| Service Container | Lightweight DI: IService vs ITickableService split. Bootstrap registers, subsystems resolve by interface |
| Layer | Technology |
|---|---|
| Server | Rust, Tokio (async runtime), Prost (protobuf), tokio-tungstenite (WebSocket) |
| Client | Unity 6 with URP, New Input System, Google.Protobuf NuGet |
| Protocol | Protobuf over WebSocket (WebGL) / UDP (native). Sequence-numbered packets |
| Rendering | Procedural mesh generation, vertex colors, custom shaders (ShaderLab/HLSL) |
Game Trait Abstraction — Generic networking modules (network/, session/, room/) never import game-specific code. All communication goes through the Game trait interface. This means adding a second game to the same server infrastructure requires implementing one trait, not rewriting transport.
Flat Array Territory — The territory grid is stored as Vec<Option<PlayerId>> indexed by y * width + x. This gives O(1) cell lookup, cache-friendly iteration, and trivial snapshot diffing for delta compression. No HashMap overhead on the hot path.
Edge Flood-Fill — Instead of tracing enclosed regions inward (which is fragile with complex shapes), the claiming algorithm seeds a BFS from every map edge cell that isn't owned by the player. Anything the BFS can't reach from the edges is enclosed and gets claimed. Simple, correct, handles arbitrary trail shapes.
Virtual Socket Addresses — WebSocket clients receive virtual 127.255.x.x addresses so the entire session/room/game pipeline treats them identically to UDP clients. Think of it like adding a second entrance to the same building while keeping all the internal hallways identical.
Keyframe + Delta Pattern — Full state is sent periodically as a baseline. Between keyframes, only territory cell changes and modified player data are transmitted. Territory rows use RLE encoding. A 3-cell territory change produces a delta that is orders of magnitude smaller than the 100×100 full state.
Service Container DI — Lightweight dependency injection on the Unity side. Services register at bootstrap and resolve by type. IService vs ITickableService split ensures non-ticking services don't participate in the frame loop.
- Unity 6 (URP project)
- Rust (stable toolchain) — for the server
- Protoc — Protocol Buffers compiler
# Clone the server repo
git clone https://github.com/WallerTheDeveloper/paperio-server.git
cd paperio-server
# Build and run
cargo run --bin paperio_serverThe server starts a WebSocket listener (for WebGL clients) and a UDP socket (for native clients).
# Clone this repo
git clone https://github.com/WallerTheDeveloper/paperio-clone.git
# Open in Unity 6
# Set the server address in the connection config
# Build for WebGL or run in EditorFor WebGL builds, the client automatically uses the WebSocket transport. For Editor/standalone, it uses UDP.
This is a portfolio / technical showcase project, not a commercial release. Built to demonstrate multiplayer game architecture, server-authoritative design, and production-level Unity development.
Original game credit: Paper.io and Paper.io 2 are created by Voodoo. This project is an independent clone built for educational and portfolio purposes. All game mechanics are reimplemented from scratch — no original code or assets are used.
Danylo Golosov — Software Engineer / Game Developer, Berlin
4+ years of experience in Unity Engine, C#, and C++. Professional background in AR/VR development (AR Foundation, ARKit, ARCore, OpenXR), cross-platform mobile apps, and backend systems. Previously AR & Web Developer at Zaubar (Berlin).