# Borger Documentation # 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](./concepts/rollback.md) 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. # 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 12GB of RAM and 10GB of available disk storage. ### Software Requirements - [**Bash**](https://www.gnu.org/software/bash/): Required to run Borger's CLI tool. Installing it depends on your operating system: - Linux: Any self-respecting distribution should already have it. Borger itself was written on a 2014 Dell Precision M2800 equipped with [Xubuntu](https://xubuntu.org/download/). - macOS: 1. First you must [purchase](https://www.apple.com/mac/) expensive, unrepairable, unupgradable hardware 2. Check your Bash version in the terminal: `bash --version`. If it outputs version 3.2, congrats, your Bash is 20 years out of date. Otherwise, your Bash should be good to go already. 3. Before you can update it, you'll need [Homebrew](https://brew.sh/) installed. 4. `brew install bash` 5. Check the version again to be on the safe side. If it's still stuck at 3.2, restart the terminal. - Windows: Requires [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install), a mini Linux virtual machine. It is highly probable that Bill Gates performed unscrupulous activities with Jeffrey Epstein. - [**Git**](https://git-scm.com/install/): Used internally by the CLI tool to generate projects ### More recommendations - [**Visual Studio Code**](https://code.visualstudio.com/Download) - Technically any IDE will do the job (or even a plain text editor if you hate being productive), but Borger is preconfigured to work with VSCode. - [**rust-analyzer**](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - It's nearly impossible to write Rust without this - [**Even Better TOML**](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) - Syntax highlighting for TOML files - [**CodeLLDB**](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) - 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](https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb) (Chrome, Brave, Edge, Opera, etc.) - [Firefox](https://github.com/jdmichaud/dwarf-2-sourcemap) - Unpleasant but surprisingly doable - Safari (lol) ### Installing Borger Open a terminal and run the command: ```bash curl -fsSL https://eat.borger.dev | bash ``` This installs the latest `borger` CLI tool, as well as Rustup, Bun, Cargo Watch, and wasm-pack if they can't be found. If asked upon completion, close and restart the terminal. # Development Cycle ### Creating a New Project Replace `my_game` with the name of the project folder to be created. ```bash borger init my_game cd my_game borger dev ``` ### Loading an Existing Project ```bash git clone https://whatever/my_game.git cd my_game borger setup borger dev ``` ### Using `borger dev` The `dev` command runs several parallel jobs: - Automatically recompile each time you modify code - Host a local game [**server**](../concepts/server-and-client.md#server) - Host a local game [**client**](../concepts/server-and-client.md#client) web (HTTPS) server - specifically [Vite](https://vite.dev/), which supports hot reloading HTML, CSS, and sometimes even [graphics](https://r3f.docs.pmnd.rs/getting-started/introduction) 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`](dev/startup.png) Sometimes during compilation, you'll see the harmless error: ``` Cannot find module '@borger/rs' or its corresponding type declarations. ``` ![Missing @borger/rs module](dev/missing-module.png) 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 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](dev/devtools.png) - Push Ctrl+C in the terminal to close dev mode - The dev server uses something called [self-signed certificates](https://en.wikipedia.org/wiki/Self-signed_certificate). 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](dev/self-signed.png) 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](../api/state.md) `/src/presentation/index.ts` - [Presentation](../concepts/simulation-and-presentation.md#presentation) logic entry point (rendering, UI, audio) `/src/simulation/lib.rs` - [Simulation](../concepts/simulation-and-presentation.md#simulation) logic entry point (game logic) `/src/simulation/input.rs` - [Input](../concepts/input.md) handling callbacks `/index.html` - Main webpage, client entry point `/assets` - Art files loaded by the game `/borger` - [Source code of the framework](https://github.com/BorgerLand/Borger), linked via a [Git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) `/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](https://react.dev/)) for better hot reloading support The rest can usually be ignored. # Deployment ### Release build ```bash borger release --build ``` 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 ### Hosting Deployment is a bit of a manual procedure at the moment due to requirements that Borger isn't able to provide for free: - A domain name. TLS certificate providers typically refuse to generate certificates for a raw IP address. - At least one always-on dedicated server. Pros with a high concurrent user (CCU) count typically have multiple across different regions of the world to handle demand. 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. The simplest, cheapest possible procedure for a small friend group looks something like this. It's not recommended for any significant amount of CCU, so this guide won't go into great detail. 1. Acquire a domain name. If you don't mind using a subdomain, check out [Duck DNS](https://www.duckdns.org/faqs.jsp). 2. Acquire a server. You could either self-host on your own hardware at home (keep in mind that home IP addresses often change), or rent from a cloud provider. [Oracle Cloud](https://www.oracle.com/cloud/free/) has an "always free" tier for their bottom of the barrel servers. 3. Point the domain name at the server's IP address. Set TTL to 0 to make it propagate fast. For example: ![DNS Config](release/dns.png) 4. [Port forwarding](https://en.wikipedia.org/wiki/Port_forwarding) | Transport Layer | Port | Application Protocol | Purpose | | --------------- | ---- | -------------------- | ----------- | | UDP | 6969 | WebTransport | Game Server | | TCP | 6996 | WebSocket | Game Server | | TCP | 80 | HTTP | Game Client | | TCP | 443 | HTTPS | Game Client | 5. TLS Certificates can be obtained for free through a service called [Let's Encrypt](https://letsencrypt.org/), via a tool called [Certbot](https://certbot.eff.org/instructions) 6. Run the servers, on the server: ```bash borger release --run --fullchain /etc/letsencrypt/live/borger.land/fullchain.pem --privkey /etc/letsencrypt/live/borger.land/privkey.pem ``` # Server and Client Borger implements standard [client-server architecture](https://en.wikipedia.org/wiki/Client%E2%80%93server_model). ![Client-server architecture](server-and-client/client-server-architecture.png) - 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**](./simulation-and-presentation.md#simulation). 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**](./io-state.md#output) 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" 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**](./io-state.md#output) 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**](./io-state.md#input). When the player tries to move their character, this should happen [**immediately**](./trade-offs.md#prediction) without waiting for the server's response. - Classifying and treating the client's state as a prediction is what prevents certain forms of [**cheating**](./cheating.md). 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**](./simulation-and-presentation.md#presentation), unlike the server. # 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". ### Simulation The _simulation_ is the core game logic that runs on both the [**server**](./server-and-client.md#server) and [**client**](./server-and-client.md#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**](./server-and-client.md#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**](./rollback.md). Because of this, the code running in the loop must be [**deterministic**](./determinism.md) depending on the [**trade-off**](./trade-offs.md) 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**](./io-state.md#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 per second), or higher if they have disposable income. - Presentation [**output state**](./io-state.md#output) 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**](./rollback.md), 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: players teleporting, entities spawning and immediately despawning, etc. - Presentation is [**client**](./server-and-client.md#client)-only. [**Servers**](./server-and-client.md#server) don't participate because they are generally [headless](https://en.wikipedia.org/wiki/Headless_computer) computers living in Big [Bezos warehouses](https://aws.amazon.com/), so "presenting" anything to the warehouse employees would be expensive and wasteful. # Input and Output State ### Input ### Output # Determinism From [Wikipedia](https://en.wikipedia.org/wiki/Deterministic_algorithm): 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](./io-state.md#input)) 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](./io-state.md#output)) 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**](./rollback.md). - ❌ `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](https://en.wikipedia.org/wiki/Random_seed) derived from game state: - ❌ ```rust 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 ``` - ✅ ```rust 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` by iterating it, because _integer_ addition is commutative. - ❌ 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**](./server-and-client.md#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](https://webassembly.github.io/spec/core/exec/numerics.html#floating-point-operations) for all situations that a game would care about. Also note that determinism comes in varying degrees of strictness, depending on the [**trade-off**](./trade-offs.md) used. # Rollback At its core, Borger is a rollback framework. This is the idea that an older [**simulation tick ID**](./simulation-and-presentation.md#simulation) can rerun, when information pertaining to the past finally arrives over the slow internet connection. Let's say the [**server**](./server-and-client.md#server) is chugging along as usual: ``` Tick ID 100 ↓ Tick ID 101 ↓ Tick ID 101 ↓ etc. ``` # Trade-Offs ### Immediate ### WaitForServer ### WaitForConsensus # Clients and Scopes # Cheating # state.ts # Primitives and Structs # SlotMap # Event Dispatcher # Rapier Integration # Examples # Debugging # Common Oopsydinkies # Session Replay # Cross-Platform Support # Feedback # Roadmap