Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Edited 4/16/2026

Howdy. You’ve Reached Borger.

The Problem

In a fast-paced multiplayer video game, latency is the root of all evil. The fundamental problem that you as a developer face is the highly variable amount of time between one player tapping a key and 10 other players seeing it.

Take, for example, a simple racing game in which 3 players drive their cars toward a self destruct button. The first player to cross the finish line explodes violently and wins the game as charred gears rain from the sky.

💥🏁             🚗💨
💥🏁             🚙💨
💥🏁             🏎️💨

However, if you were to sit the 3 players right next to each other and observe all 3 of their screens, you’d see something very odd:

Player 1

💥🏁        🚗💨
💥🏁             🚙💨
💥🏁             🏎️💨

Player 2

💥🏁             🚗💨
💥🏁        🚙💨
💥🏁             🏎️💨

Player 3

💥🏁             🚗💨
💥🏁             🚙💨
💥🏁        🏎️💨

You’re witnessing the effects of network latency (also known as ping in gaming communities): the amount of time it takes for information to disseminate to all players across the game session.

  • When player 1 presses the left arrow key, they see their car move immediately, and the game feels responsive to them.
  • When player 2 presses the left arrow key, player 1 won’t see any change for a fraction of a second.

Player 1 is looking at a delayed, outdated version of player 2’s car. The delay is caused by how slow it is to send information, such as key presses, across the internet. Even players under the same roof have a small but noticeable amount of latency across their devices. Players on opposite sides of the globe are up against the speed of light and face a very high ping; perhaps 200-300 milliseconds and constantly fluctuating.

While the visual discrepancy is annoying enough on its own, it also wreaks havoc while writing game logic. What happens when all 3 players think they got to the finish line first? How do you make sure that only 1 player sees that they won? If 2 cars crash into each other, where do you render the dents? Your brain quickly melts and smells bad from trying to answer each individual question in the form of code.

The Solution

Borger uses a technique called rollback netcode to automatically resolve these types of conflicts. It allows writing multiplayer game logic with code that looks no different from single-player logic, solving one of the hardest problems in gamedev in a way that you don’t even need to think about it.

Edited 5/17/2026

Installation

Hardware Requirements

Although games built with Borger are very small and efficient, running 2 rustc instances and rust-analyzer at the same time during development is absolutely brutal. At bare minimum, you will need at least 10GB of available disk storage, and if using rust-analyzer, at least 12GB of total RAM .

Borger itself was written on a 2014 Dell Precision M2800 equipped with Xubuntu. If that clunker can run it, so can you!

Software Requirements

  • Bash: Required to run Borger’s CLI tool. Installing it depends on your operating system:
    • Linux: Usually already installed, depending on distro
    • macOS: Requires purchasing expensive, unrepairable, unupgradable hardware, but comes with Bash 3.2 (released in 2006) out of the box.
    • Windows: Requires Windows Subsystem for Linux (WSL), a mini Linux virtual machine. High risk of Windows Update forcing a reboot that deletes your unsaved work. It is also highly probable that Bill Gates performed unscrupulous activities with Jeffrey Epstein.
  • Curl: Command for downloading things, such as the CLI tool. Often already installed
  • Git: Used internally by the CLI tool to generate projects

More recommendations

  • Visual Studio Code - Technically any IDE will do the job (or even a plain text editor if you’re severely RAM-constrained), but Borger is preconfigured to work with VSCode. Here’s a good starter pack of optional extensions to install:
    • rust-analyzer - Makes an enormous difference in how easy it is to write Rust
    • Even Better TOML - Syntax highlighting for TOML files
    • CodeLLDB - For debugging the game server. Because game logic is shared between server and client, this is often easier than trying to debug Rust in-browser.
  • If you do find yourself needing to debug Rust with your preferred browser’s DevTools:
    • Chromium-based (Chrome, Brave, Edge, Opera, etc.)
    • Firefox - Unpleasant but surprisingly doable
    • Safari (lol)

Installing Borger

Open a terminal and run the command:

curl -fsSL https://eat.borger.dev | bash

This installs the latest borger CLI tool, as well as Rustup, Node.js, cargo-watch, and wasm-pack if they can’t be found. If asked upon completion, close and restart the terminal. Do note the security implications of downloading and installing several programs from the internet.

Edited 5/4/2026

Development Cycle

Creating a New Project

Replace my_game with the name of the project folder to be created.

borger init my_game
cd my_game
borger dev

Loading an Existing Project

git clone https://whatever/my_game.git
cd my_game
borger setup
borger dev

Using borger dev

The dev command runs several parallel jobs:

RUN-CODEGEN: Run code generator when state.ts is changed
TSC-CODEGEN: TypeScript error detection on state.ts
SERVER-RUST: Watch and recompile the server
CLIENT-RUST: Watch and recompile the client’s WASM binary
CLIENT-VITE: HTTPS development server - specifically Vite, which supports hot reloading HTML, CSS, and sometimes even graphics without having to refresh the page or restart the game.

It can be tricky at first to decipher when dev is finished and it’s safe to load the game. The golden rule is:

  • The most recent output of [SERVER-RUST] is it's alive. The text is highlighted neon green and is hard to miss.
  • AND
  • The most recent output of [CLIENT-RUST] is [Finished running. Exit status: 0]
  • The server usually finishes a few seconds before the client

Here’s an example of a game that’s ready to go: Output of borger dev Sometimes during compilation, you’ll see the harmless error:

Cannot find module '@borger/rs' or its corresponding type declarations.

Missing @borger/rs module This can be safely ignored. For unknown reasons, the wasm-pack tool deletes the old WASM build before beginning, so for a few seconds during compilation, the module doesn’t exist. As seen in the screenshot (Found 0 errors after it tries again), it corrects itself upon completion.

A few more helpful pointers:

  • Visit https://localhost:5173 in your browser to finally see the “game”! (A blank page by default)

  • If you ever forget the URL, the Vite server reminds you:

    [CLIENT-VITE]   ➜  Local:   https://localhost:5173/
    
  • Push F12 or Ctrl+Shift+I to open the DevTools console in order to verify the engine loaded successfully Borger client output

  • Push Ctrl+C in the terminal to close dev mode

  • The dev server uses something called self-signed certificates. If you’ve never worked with these before, you’ll see a terrifying error the first time you try to test the game: Your connection is not private

    In most cases, the browser is correct to scare you, but local web development is a notable exception. Choose Advanced -> Proceed. Essentially what’s happened is the browser is unable to verify that this is a legitimate website, because it hasn’t been deployed anywhere yet.

Project Directory Structure

/src/state.ts - Declaration of networked state
/src/presentation/index.ts - Presentation logic entry point (rendering, UI, audio)
/src/simulation/lib.rs - Simulation logic entry point (game logic)
/src/simulation/input.rs - Input handling callbacks /index.html - Main webpage, client entry point
/assets - Art files loaded by the game
/borger - Source code of the framework, linked via a Git submodule
/Cargo.toml - Rust library dependencies
/package.json - Java/TypeScript library dependencies
/rust-toolchain.toml - Change which version of Nightly Rust to compile with
/vite.config.ts - Install Vite plugins (such as React) for better hot reloading support

The rest can usually be ignored.

Edited 4/14/2026

Workflow

The landing page may be a bit goofy, but “outline, decree, engrave” is genuinely the recommended workflow. This page does not go into detail because each topic has its own section in the Concepts chapter. In slightly less arrogant terms, the overall intention for using Borger as a framework is:

  1. Outline: Additions to the state definition (defined in state.ts) enable storage for new game mechanics that persist throughout a single game session.

  2. Decree: This is simulation logic. Given the shape of data defined by state.ts, write the code that populates that state, governed by the rules of the game.

  3. Engrave: Presentation logic takes the populated state and decides how it should look and sound to the player.

Following these steps results in something that looks vaguely similar to a video game.

Edited 5/20/2026

Deployment

Release build

borger release

This produces a release folder containing:

  • /release/server - The final native executable, compiled to run on your OS
  • /release/client - The static webpage, which can be hosted with any HTTP server capable of serving files

Prerequisites

Borger is free software, and as such, is unable to provide all the necessary infrastructure and services required for server hosting. You will need:

  1. At least one always-on dedicated server (VPS, bare metal, compute instance all commonly used). If you’re just getting started, Oracle Cloud Free Tier offers a single, small compute instance and will never charge you for it. You may choose to have multiple servers across different global regions. In terms of maximum players that a single server can handle, there isn’t yet any concrete data for Borger specifically, but a single powerful server hosting a well-optimized game should theoretically be able to handle more than 1000 CCU (concurrent users). Most web games never even come close to this.

  2. A domain name (either pay for one or find a free subdomain), because Safari has some awful bug that prevents it from connecting to a raw IP address. The domain name should point to/fall back to an IPv4 address because a frightening percentage of the world can’t connect to IPv6. Some free options:

    • https://nip.io/ - No account required, hardcodes the IP address into the domain. If the IP address changes after you’ve given out a link, the link is now dead.
    • https://www.duckdns.org/ - Requires an account, lets you pick out your own name.
  3. TLS certificates for the domain name, which can be obtained from a free service called Let’s Encrypt, via a tool called Certbot. Remember that TLS certificates expire. The game server automatically reloads them daily, making use of the fact that Certbot overwrites the same files each time it renews them. Your HTTP server of choice must also be able to handle this.

If enough demand is shown, a paid service will be introduced in order to automate this process. For now, server orchestration and matchmaking are outside the scope of the problems Borger aims to solve.

Firewall Settings/Port Forwarding

PortTransport Layer ProtocolApplication ProtocolPurpose
6969UDPWebTransportGame Server
6996TCPWebSocketGame Server
80TCPHTTPGame Client + Certbot Renewal
443TCPHTTPSGame Client

Make ’em Move, Hunny

The game server and HTTP server must be running at the same time.

Game server:

#note there are more flags eg. for using different ports
./server --fullchain /etc/letsencrypt/live/my.domain.com/fullchain.pem --privkey /etc/letsencrypt/live/my.domain.com/privkey.pem

HTTP server: There is currently not an included default. You are responsible for choosing one (nginx, Caddy, Apache, plain Node.js script, etc.). It must:

  • Serve static files, with the root pointed to the client directory. This works well with Certbot’s webroot plugin.
  • It must forward/redirect HTTP to HTTPS in order to enable all required web platform features.
  • Use these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Strict-Transport-Security: max-age=31536000; includeSubDomains
Edited 4/18/2026

Server and Client

Borger implements standard client-server architecture. Client-server architecture

  • This diagram represents 3 players with different devices who are in a shared multiplayer game session together.
  • The red lines represent internet connections along which data is sent back and forth.
  • You can see that clients don’t talk to each other directly: all data flows through a central game server that everybody is connected to at the same time.
  • All 4 devices, both server and clients, run the same simulation logic. Borger makes sure that the same code runs everywhere.

Server

The server exists to be the source of truth for all connected clients. Each game session has exactly 1 server.

  • The server’s state is said to be authoritative: regardless of what each client sees, only the server’s version of the game state is considered to be correct.
  • When a client joins, it downloads the current game from the server.
  • It continuously broadcasts small updates known as “diffs” (derived from differences) to client, as the state changes from tick to tick.

Client

The term client is used loosely to refer to a group of a few different things at once. Each individual client is:

  1. A device
  2. The connection between that device and the server
  3. An instance of the game running on the device
  4. The human who’s playing said game
  • The client’s state is said to be a prediction. Because of network latency, the client needs to try to predict what the server will send before the client receives anything.
  • The most important thing to predict is the consequences of inputs. When the player tries to move their character, this should happen immediately without waiting for the server’s response.
  • Classifying and treating the client’s state as a prediction is what prevents certain forms of cheating. It’s not real, so it can’t hurt you!
  • It continuously sends input events to the server: button presses, joysticks, etc. These inputs are the client’s only means of impacting the game session.
  • And of course, clients run presentation logic, unlike the server.
Edited 4/16/2026

Simulation and Presentation

All game code in a Borger project must be categorized as either simulation or presentation logic before writing anything.

  • It’s very difficult to pick incorrectly thanks to the language split: simulation is written in Rust, and presentation in TypeScript.
  • Each type of logic is primarily driven by its own loop that fires at a specific interval. Each iteration of each loop is called a “tick”. Loops

Simulation

The simulation is the core game logic that runs on both the server and client: enforcement of game rules, movement and collision, entity creation and removal, and other state changes/mutation.

  • The tick rate of the simulation loop is a fixed 30Hz (iterations per second), or once every 33 milliseconds (derived by dividing 1000/30Hz).
  • Each tick is assigned an incremental ID. When the server first launches, it starts at ID 0, then the next tick that happens 33ms later has ID 1, etc. When a client joins, they start simulating at whichever ID the server is currently at and automatically stay in sync.
  • Each tick ID may be resimulated multiple times due to rollback. Because of this, the code running in the loop must be deterministic depending on the trade-off used.

Presentation

Presentation asks the question: “How should the simulation be presented to the player?” It deals with rendering, UI, audio, and listening for input events.

  • The tick rate of the presentation loop is variable and tends to hover near (but practically never identical to) the player’s display refresh rate. This is most commonly 60Hz (iterations/frames per second), or higher if they have disposable income.
  • Presentation output state state is read-only. It’s meant to be read and, well, presented, as is.
  • Crucially, presentation does not care why the output is what it is. It blindly presents whatever the simulation tells it to.
  • Although the presentation loop itself does not undergo rollback, it does have to be aware that the output it’s receiving may have undergone rollback. Again, this is achieved by blindly trusting the simulation’s output, even when it makes no sense due to mispredictions: players teleporting, entities spawning and immediately despawning, etc.
  • Presentation is client-only. Servers don’t participate because they are generally headless computers living in Big Bezos warehouses, so “presenting” anything to the warehouse employees would be expensive and wasteful.
Edited 4/17/2026

Input and Output State

State is the data that is available to the game to read, process, and write to.

  • In both Rust and TypeScript, it takes the form of a big chungus struct whose shape is defined entirely by your state.ts.
  • Networking and synchronization happen automagically whenever you mutate state. A game written with Borger never ever manually sends data over the wire.
  • State is susceptible to mispredictions.

Input

Input state holds information such as mouse cursor position, analog stick angle, whether certain controller buttons are currently pressed down, and more. It is the sole means for a client to interact with the game session.

  • It is only writable during a presentation tick.
  • Each client owns their own input state object that they are individually responsible for populating.
  • At the start of each presentation tick, The Borger.Input object starts out in the default, empty state, and must be populated by the end of the tick.
  • Your presentation loop fills it out like a form using standard DOM input events: mousemove, touchstart, keydown, etc.

Output

Output state describes everything in the virtual game world: player positions, health, door hinge angles, mission objectives, and more.

  • It is only writable during a simulation tick.
  • In simulation logic, the output struct is called SimulationState. Clients’ inputs are nested inside each individual client struct for convenience. In presentation, it’s simply known as Borger.Output.
  • The server and client both share a copy of output state. The server has a complete view of everything, while the client can only see what’s in scope.
Edited 4/21/2026

Determinism

From Wikipedia: A deterministic algorithm is an algorithm that, given a particular input, will always produce the same output.

Applying this definition to Borger:

  • The simulation_loop that you write, as a whole, is assumed to be a deterministic algorithm by the underlying engine that calls it. This is an invariant you must uphold.
  • The “input” of the algorithm (not be confused with input state) is the initial state of the GameContext object when simulation_loop first begins.
  • The “output” of the algorithm (again, not to be confused with output state) is the mutated state of the GameContext object when simulation_loop finishes running.
1. INPUT:     GameContext starts out in some kinda way
      ↓
2. ALGORITHM: simulation_loop does stuff to GameContext
      ↓
3. OUTPUT:    GameContext is different now

The same input should always produce the same output.

While this determinism requirement may sound scary and complicated at first, in practice, it simply means avoiding certain patterns that produce unpredictable results. These are the main 3 offenders you’re most likely to run into:

  1. Borger’s API is the only reliable source of truth for understanding when a tick has occurred, because the system clock continues to move forward even when Borger rolls back.

    • std::time
    • web_time crate
    • chrono crate
    • TickInfo::id()
  2. If using the rand crate (or similar) for random number generation, always use a seed derived from game state:

    • let mut rng = thread_rng();
      let n: f32 = rng.gen_range(0.0..10.0);
      
      //ironically thread_rng() is TOO random, and returns something
      //different every time, even if the tick ID hasn't changed
    • let mut rng = SmallRng::seed_from_u64(ctx.tick.id() ^ 2468);
      let n: f32 = rng.gen_range(0.0..10.0);
      
      //2468 can be replaced with any random constant, or even a game
      //state variable, in order to avoid getting the same result from
      //every SmallRNG instance in the same tick
  3. If using Rust’s built-in HashMap or HashSet, keep in mind that the iteration order is randomized, so deterministic code can’t rely on them to iterate in any certain order. You can still use them and their iterators, but whatever value you’re attempting to derive from them must still be deterministic. For example:

    • ✅ You could safely calculate the sum of a HashSet<i32> by iterating it, because integer addition is commutative (1+2 == 2+1).
    • ❌ On the other hand, hash_set.iter().next() is bad because it essentially returns a random value.
    • BTreeMap and BTreeSet DO have deterministic iteration order, but have different performance characteristics.

Historically, floating-point arithmetic (working with fractional, non-integer numbers) has caused a lot of non-deterministic grief for multiplayer games. For example, f32::consts::PI.sin() might return a slightly different number depending on whether it runs on an ARM or Intel CPU, meaning players on the same server who have different hardware don’t see exactly the same thing. With Borger, this is no longer an issue, because the same WASM binary runs on all CPU’s. The official spec guarantees cross-platform determinism for all situations that a game would care about.

Also note that determinism comes in varying degrees of strictness, depending on the trade-off used.

Edited 4/18/2026

Rollback and Misprediction

Misprediction

Latency across the game session means that the simulation must make predictions in order for the game to feel responsive. And predictions, by definition, can be wrong.

Let’s say the server is chugging along as usual:

Tick ID 100
		 ↓
Tick ID 101
		 ↓
Tick ID 101
		 ↓
		etc.

And a client is connected to that server, chugging alongside it in perfect sync:

Tick ID 100
		 ↓
Tick ID 101
		 ↓
Tick ID 101
		 ↓
		etc.

Now, let’s say this particular client decides to press the spacebar key on tick ID 100 in order to jump. The game logic may look something like:

//(pseudocode)

if input.jump
{
	character.y += 10;
}
  • When the client runs the simulation logic on tick ID 100, input.jump == true, because the client has direct access to their own keyboard. No slow internet connection is involved.
  • When the server runs the simulation logic on tick ID 100, input.jump == false, because the spacebar key press is still making its way downtown over the internet. This is an example of misprediction.

The above misprediction is caused by the server having not received all inputs from all clients yet. Clients also experience misprediction; arguably more so. Misprediction is fundamentally caused by the simulation loop’s need to keep running and making predictions even when it doesn’t have all the information it needs to be fully accurate. Its ability to continue blindly running forward is what allows inputs to feel responsive, which is very important in fast-paced multiplayer. The player would be very angry if jumping were to require waiting for consensus across all devices. A platformer would be impossible to play.

In practice, what does misprediction look like to the player?

  • Lag is the simplest and most common form of misprediction. Whether you’re seeing other players stall and stutter during a bad connection, or a slight delay in picking up a weapon, the effects of lag are caused by your client being unable to predict something. It’s waiting for the server to tell you what happened.
  • Imagine 2 players trying to pick up the same gun at the same time. If the collision callback were to run in Immediate mode, both of them equip the gun. This results in a gnarly misprediction when the server picks a winner and rips the gun back out of the loser’s hand. Whether it’s more important to quickly equip weapons or prevent mispredictions entirely depends on the type of game.

Rollback

At its core, Borger is a rollback framework. This is the idea that an older simulation tick ID can rerun, when information pertaining to the past finally arrives over the slow internet connection.

  • It occurs on both the server and client
  • It’s a generic, brute-force, fix-all solution that looks not dissimilar to traveling back in time.
  • It makes mispredictions much easier to reason about, at the cost of being difficult to implement. But you already installed Borger, so you’ll do fine.

Building off the above jumping example in the misprediction section helps to convey how rollback steamrolls mispredictions out of existence:

  1. Tick ID 100 executes on the server. input.jump == false and so character.y is untouched
  2. Server keeps on simulating. Tick ID 100, 101, and 102 all come and go.
  3. Server finally receives the jump input pertaining to tick ID 100.
  4. 🚨😬🚨 Red alert. Server rolls back to tick ID 100. Every single output state change that happened in ticks 102, 101, and 100 are undone in reverse order.
  5. Tick ID 100 re-executes on the server, this time with new a lease on life. input.jump == true now. Character jumps.
  6. Server fasts forward back to where it should be. Tick ID 101 and 102 also re-execute.

So technically, each scheduled simulation tick that happens every 33 milliseconds actually consists of multiple ticks! In reality, the pristine timeline that the player is watching on their screen is nowhere near as linear and pretty as it seems.

They see this:

Tick ID 100
		 ↓   wait 33ms
Tick ID 101
		 ↓   wait 33ms
Tick ID 101
		 ↓   wait 33ms
		etc.

But what’s actually happening underneath the hood is:

Tick ID 98
Tick ID 99
Tick ID 100
		 ↓   wait 33ms
Tick ID 99
Tick ID 100
Tick ID 101
		 ↓   wait 33ms
Tick ID 100
Tick ID 101
Tick ID 102
		 ↓   wait 33ms
		etc.

The engine makes sure that only the final sub-tick is ever presented to the player, and so as long as the code is deterministic, the illusion is complete and they are blissfully unaware of what’s happening. It’s important to note the performance implications here: more latency means more rollback means more ticks having to re-run during the short 33ms time window. The laggiest client hurts everyone.

Edited 4/18/2026

Trade-Offs

The trade-offs annotation system is the heart of what makes Borger unique. All simulation logic grapples with the tension between latency, state visibility, and misprediction risk. Trade-offs allow you to explicitly choose how this tension affects your code (and thus the overall game feel), on a spectrum between “responsive+potentially incorrect” to “laggy+definitely correct”. They are applied as wrappers around functions and other blocks of code, so a quick glance at the start of a block tells you everything you need to know about the multiplayer characteristics of any game mechanic.

Comparison Table

Rule of ThumbWhereLatency/ResponsivenessCorrectnessState VisibilityDeterminism RequirementsExample Use Cases
Immediate (default)Immediate response without waiting for the serverCode runs on both server and client. Use of Immediate mode does NOT give clients any cheating ability or authority over what everyone else sees - it’s simply a local prediction.Instantaneous. Press a button on the client, see result on screen immediately without waiting for server reply.Highest risk of mispredction due to not having access to all relevant state. The server may produce different results than the client.Can only access client-visible state. Accessing private state causes a compile error; out-of-scope access causes a runtime error.Must be deterministic between the server and any clients executing it. They should independently arrive at the same result.Processing client inputs (movement, shooting, etc.) should happen here in Immediate as much as possible for best game feel. Controls are by far the most latency-sensitive aspect of gameplay. Any physics interactions should also be immediate whenever possible: damage-on-collision mechanics, moving platforms, etc.
WaitForServerHide sensitive, private data from clientsCode runs only on the serverSlower if used for processing client inputs/interactions (their individual RTT), otherwise feels smooth. The server executes this regardless of whether it has received inputs from all clients, and clients will render the result whenever they receive it.If it is possible for a client’s input to change the results (eg. someone stepping in front of an NPC), there is a risk of mispredction.Has full access to all game state, including private/hidden data.Must only be locally deterministic (consistent results on the same device across multiple runs, but may vary between devices)Logic that affects entities seen by clients but are unable to be predicted by clients due to having some private state (NPC)
WaitForConsensusGuaranteed correctness at all costsCode runs only on the serverSlowest. The server waits to receive all inputs from all clients (up to 1.5 seconds before timing out) before executing. Clients won’t see the result until RTT of the slowest client.Always correct. No mispredction.Has full access to all game state, including private/hidden data.Determinism is not required because this code will only ever run once.Backend calls (updating the leaderboard in the database), large/important game state change events that would look horrible if rolled back/mispredicted (game over, level change)

Identification

Trade-offs are implemented as a no-op generic parameter on the DiffSerializer struct (and GameContext by extension), which forces any function that mutates output state to specify under which trade-off context it can execute. This is what’s meant by “annotations”: you see trade-offs at the top of every function signature and every multiplayer_tradeoff!() call, lending an understanding to the multiplayer implications when it runs.

In this example, note the WaitForConsensus in the signature. It immediately (pun intended) tells you that this function is non-deterministic, never rolls back, and only happens on the server (from the information in the above table)

pub fn on_client_connect
(
	state: &mut SimulationState,
	client_id: usize32,
	tick_id: TickID,
	diff: &mut DiffSerializer<WaitForConsensus>,
)

And the simulation loop of course is tagged with the most flexible trade-off, Immediate.

fn simulation_loop(ctx: &mut GameContext<Immediate>)

Sometimes it can make sense for a function to be called under multiple contexts:

//a character may be client-controlled (Immediate) or an NPC (WaitForServer)
fn update_character(ctx: &mut GameContext<impl ImmediateOrWaitForServer>)

multiplayer_tradeoff!()

This macro is what allows toggling between trade-offs. Notably, trade-offs can only be nested in order of increasing latency:

fn simulation_loop(ctx: &mut GameContext<Immediate>)
{
	multiplayer_tradeoff!(WaitForServer,
	{
		multiplayer_tradeoff!(WaitForConsensus,
		{
			//game logic here

			//(you can skip directly from Immediate to WaitForConsensus
			//if needed. this is demonstration only)
		});
	});
}

#[server]

Functions who are meant to only be called in WaitForServer and WaitForConsensus contexts must be tagged with this attribute macro, in order to strip the code from the client build. Ideally the compiler could flag this automatically through static analysis, but for now it must be manually inserted in this early release of Borger. Forgetting to include it could result in a failed client build, a client-sided runtime crash if unwrap()ing on out-of-scope state, or it may just work normally if the function doesn’t attempt to read private state.

Edited 4/11/2026

Clients and Scopes

Edited 4/11/2026

Cheating

Edited 5/21/2026

Listening for Input

Edited 4/16/2026

Entities

Edited 4/16/2026

Rapier Integration

Edited 4/16/2026

Complete Examples

  • Hello Multiplayer (2D, Minimal players moving around)
  • Physics Demo (3D, Three.js, Rapier, Rigid Bodies, Character controllers, Gru vs. Nose)
Edited 4/11/2026

state.ts

Edited 4/11/2026

Primitives and Structs

Simulation API (Rust)

Edited 4/17/2026
Edited 4/15/2026

Click here if not redirected

Edited 4/16/2026

Presentation API (TypeScript)

Edited 4/11/2026

Debugging

Edited 4/11/2026

Common Oopsydinkies

Edited 4/11/2026

Session Replay

Edited 4/11/2026

Cross-Platform Support

Edited 4/11/2026

Feedback

Edited 4/11/2026

Roadmap