Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Settings

The Escape menu has a Settings button between Play and Fullscreen. It opens a panel with a tab for each kind of setting every game shares, and a Game tab for the game’s own rows when it has any. The tabs run Video, Audio, Game, Controls, Interface and Accessibility, the order of Counter-Strike 2 and Lyra (video, audio, the game’s own, then controls). The panel opens on Video, as Counter-Strike 2 and Lyra do and Fortnite lists first, and within a session it reopens on the tab the player last chose, as Counter-Strike 2 does; a reload opens on Video again. The browser keeps the shared settings across reloads and sessions, and in the player app the player’s own settings follow them from game to game, as Where the settings live says. Escape or Back returns to the menu.

Graphics quality describes the Video tab’s Quality rows and the performance overlay.

A game wires none of it. Each row shows only where it can apply to what the game mounts, so a top-down game with no mouse look never shows Camera sensitivity, and a single-player game never shows the microphone. A game adds its own rows with <SettingRow>, on its own tab or on any of the engine’s.

On a wide screen the tabs run down the left, the rows fill the middle, and a column on the right describes the row under the pointer or the row that holds the focus: its name, what it does, what it costs the device, and an image where the row has one. Before a row is described, the column says what the tab is for. On a narrow screen, such as a phone held upright, the tabs run across the top, and a tapped row opens its description under it.

The keyboard reaches everything. The arrow keys move along the tabs, down the column or across the strip, and wrap at the ends. Each choice, such as the quality preset, is one tab stop, and the arrow keys choose along its options.

A tab whose rows change what is drawn behind the panel is see-through: the world stays in view, lightly dimmed and not blurred, so the player sees each change as she makes it. Video is see-through, for the quality and the field of view, and so is Interface, for the names over other players. Every other tab dims and blurs the world behind it, so its rows read. On a phone the panel covers the game and stays opaque on every tab. Roblox draws its menu over the running game behind a dark translucent layer, Minecraft dims and blurs the world behind its options, and Fortnite and Valorant cover the game with a settings screen that is all but opaque.

Each tab’s Reset puts that tab’s settings back to their defaults and leaves the other tabs as they are. Reset all puts every setting back on its second press, which it asks for by reading Confirm reset all for four seconds.

The other engines give a game a quality API and leave the menu to it: Unity’s quality levels, Unreal’s scalability groups, Godot’s project settings. Roblox has an engine-owned menu. This panel takes its layout from the games players praise: an Accessibility tab of its own, as in Cyberpunk 2077 2.1 and Forza Horizon 5; a description and a cost beside each option, as in Call of Duty; and the world in view behind the graphics, as in the Nixxes ports. The Video tab runs its rows as Fortnite, Lyra and Minecraft do: a Display group first, with fullscreen, the frame rate limit, the field of view and the performance overlay, then a Quality group headed by the preset, with each feature’s row under it and Post-processing last.

Tab Group Row Range Default Shows where
Video Display Fullscreen on Play On or off On The device has a mouse and the page, or the app around it, can go fullscreen.
Video Display Frame rate limit Off, or a divisor of the rate Off The screen’s rate is known and leaves a cap of 20 or more.
Video Display Field of view 60° to 120°, or the game’s range The game’s The camera offers the player’s field of view.
Video Display Performance stats Off, FPS, Graph, Full Off Every game.
Video Quality Quality preset Automatic or a level Automatic The game has its stores, as every Game does.
Video Quality 3D resolution 100%, 85%, 70%, 50% 100% The game has its stores.
Video Quality Dynamic 3D resolution On or off The device’s The game has its stores.
Video Quality A feature’s level The levels that differ The level’s Its group draws more than one look in what the scene mounts, as The player’s rows says.
Video Quality Grass sway On or off The level’s A World’s ground stands. It sits after Foliage.
Audio Master volume 0 to 100 100 Every game.
Audio Music volume 0 to 100 60 Every game.
Audio Effects volume 0 to 100 100 Every game.
Audio Interface volume 0 to 100 100 Every game.
Audio Voice volume 0% to 200% 100% The room has opened voice.
Audio Microphone On or off Off The room has opened voice, or says it runs none, as the development room does.
Controls Camera sensitivity 0.02 to 4; typed, 0.001 to 100 1.00 The camera turns by the player’s look.
Controls Invert vertical look On or off Off The camera turns by the player’s look.
Controls Lock camera with Shift On or off On The camera offers Shift’s lock, and there is a mouse.
Interface Show other players’ names On or off On The scene’s <CharacterSpawn> draws names over other players.
Interface Window layout Reset The player has moved a Window.
Accessibility Reduced motion On or off The device’s Every game.
Accessibility Screen shake 0 to 100 100 The game mounts a camera that can shake.

The feature rows run Anti-aliasing, Shadows, Ambient occlusion, Foliage, Terrain detail and Post-processing. The names are the ones most games use: Quality preset and 3D resolution as Fortnite and Lyra name them, Dynamic 3D resolution as Fortnite does, and Frame rate limit as Fortnite, Lyra, Minecraft Bedrock and Valorant do, where Roblox, Minecraft Java and Counter-Strike 2 say maximum.

A tab with no row to show does not show. The Audio tab ends with People: a row for each other player in the room, by name, whatever the room’s voice. Once the room opens voice, each row says whether that player is speaking, in voice, muted by you or not in voice, and a player this page hears gets a volume and a mute there, as Roblox’s player list and Discord’s user menu offer each person’s volume. In the development room, which runs no voice, the Microphone row shows alone and says that voice runs when the game plays on the platform. Once a room opens voice, the Audio tab also shows the voice rows under Microphone: the voice mode, push to talk or open mic, and the talk key, both on a device with a keyboard, the microphone’s volume, noise suppression, automatic gain, and a volume and a mute for each other player. Voice describes them.

What each row changes:

  • Quality preset: every value a level sets, as Graphics quality sets out. Under Automatic the row says the level the engine picked and the kind of device it read, such as “Automatic chose High for a recent graphics card”.
  • 3D resolution: how sharp the world is drawn, as a share of the level’s pixel ratio. Lower looks softer and runs at a higher frame rate; menus and the HUD stay sharp.
  • Dynamic 3D resolution: whether the world softens for a moment when the frame rate drops, and sharpens again once it recovers, as Dynamic resolution describes. Menus and the HUD stay sharp.
  • The feature rows: the level each feature draws at, apart from the preset, which then reads Custom.
  • Frame rate limit: the most frames a second the game draws, as Frame rate limit describes.
  • Grass sway: whether the grass moves in the wind. Until the player sets it, it follows the quality level, on at Medium and High and off at Low, and off under Reduced motion. Off skips the wind’s work in the blades’ shader.
  • Field of view: the degrees the camera sees across a 16:9 screen, over the game’s figure.
  • Fullscreen on Play: whether the click on Play also fills the screen.
  • Performance stats: how much of the performance overlay shows.
  • Show other players’ names: whether the plate with each other player’s name shows over their head.
  • Window layout: Reset puts every Window the player moved back in its slot.
  • The volumes: every sound, music and ambience, every other sound, menu and HUD sounds, and the other players’ voices, each under the master.
  • Microphone: whether the room gets the player’s voice while she speaks. It starts off in every session, and a tap on the microphone button turns it on and off too.
  • Camera sensitivity: the camera’s look speed and drag speed, as a multiplier on the game’s own.
  • Invert vertical look: moving the mouse up looks down.
  • Lock camera with Shift: whether Shift turns the camera’s lock on and off, in a game whose camera has the toggle.
  • Reduced motion: whether the interface animates less, and whether the camera shakes at all.
  • Screen shake: how much a game’s big moments shake the screen, from none at 0 to the game’s own at 100.

Reset audio puts the voice rows back and leaves the Microphone row alone, since a reset never turns a microphone on. Reset video puts the quality preset back to Automatic, unpins every feature row, puts the 3D resolution back to 100% and dynamic 3D resolution back to its default, Grass sway back to the level’s, the field of view back to the game’s figure, and Fullscreen on Play, the frame rate limit and Performance stats back to theirs. The Microphone row turns her microphone on and off as the microphone button does: the first press asks the browser for it, and nothing keeps it on past the session.

The panel reads what the game mounts, and follows the camera while it is open, so a scene that swaps its camera swaps the camera rows:

  • The camera. The engine’s Camera says on its entity which of the player’s settings reach it. Its look follows Camera sensitivity and Invert vertical look, and its preset says whether it offers Shift’s lock. A game’s own rig built on useCamera says the same through tuning: playerLook: true where it reads the two look settings, and lockToggleOffered where Shift toggles its lock. The field of view shows where the camera offers the player’s figure, and Screen shake where a camera stands that a shake moves.
  • The device. The Shift lock and Fullscreen on Play need a mouse to capture, so a phone never shows them. iPhone Safari has no fullscreen for a page.
  • Voice. The voice rows show once the room opens voice, so a single-player game, or a room run without voice, never shows them.
  • Other players. People shows while the room holds another player, and Show other players’ names while the scene’s <CharacterSpawn> draws their names.
  • Windows. Window layout shows once the player has moved a window.

A camera that keeps its own lens, such as the chase, track, side and fly cameras, shows no Field of view row. A game that offers the setting and then fixes the figure, with fixedFieldOfView, shows the row greyed out at its figure, and its description says the game sets its own.

A game hides one of the engine’s rows by its key with <HideSetting>, for as long as it is mounted. The key is the shared setting the row writes, such as cameraShake or fieldOfView, quality, renderScale, dynamicResolution, a feature group such as shadows, microphone for the voice chat rows and the voice volume, which come and go together, people for the People section, or windowLayout for the Window layout row; the type lists every key that names a row. The microphone key leaves the engine’s microphone button in the top-left corner, the one place every player mutes; a game removes that only by turning voice off, as Voice describes. Hiding a row leaves its setting as it is and still applied: a game that wants one field of view says so on its camera with fixedFieldOfView.

import { HideSetting } from "@spawnite/engine";
export function PuzzleRoom() {
// The puzzle never shakes the camera, so the row would do nothing.
return <HideSetting setting="cameraShake" />;
}

The player sees and types a multiplier on the game’s own look speed, as Roblox’s sensitivity box and every PC shooter show theirs: 1.00 is the game’s speed and 2.00 turns twice as fast. In the shooter camera, 1.00 turns half a degree per mouse count, Roblox’s speed.

The slider is logarithmic. Each step turns about 6% faster, so twelve steps double the speed. It runs from 0.02 to 4, which in the shooter camera is 0.01 to 2 degrees per count. The low end sits well below the defaults of the popular shooters: CS2 at 0.055 degrees per count, Valorant at 0.07, Overwatch at 0.1 and Apex at 0.11. The field beside the slider takes any value from 0.001 to 100, the range of Roblox’s box, and a value past the slider’s ends pins the slider there. Enter, or leaving the field, keeps the typed value; text that is not a number keeps the old one.

The store keeps the multiplier, so a sensitivity saved before the slider changed keeps its speed.

The engine asks the browser for raw mouse input when it captures the cursor, so the system’s pointer acceleration never bends the aim. A browser without raw input gets a plain pointer lock.

The row shows the degrees the camera sees across a 16:9 screen, as Overwatch and Call of Duty measure theirs, and the engine converts them to three.js’s degrees top to bottom. It runs from 60 to 120, Call of Duty’s range, or the narrower range the game’s camera sets. Until the player moves it, it shows the game’s own figure: 102 in the shooter camera, 80 in the others. Reset video clears it back to the game’s figure. A wider screen keeps the height a 16:9 screen sees and shows more at the sides: at 90, a 21:9 screen sees 105 across. A screen held upright keeps that angle across its short side, so its long side sees the figure: 90 top to bottom on a 9:16 phone. The row’s description says both.

The camera’s page says how a game sets its figure, its range and a fixed lens.

A value saved before the row showed degrees across reads as degrees across, so a player who set one may want to set it again.

Each volume slider runs 0 to 100 and is heard in decibels: every point is half a decibel, so every 20 points under 100 is 10 dB quieter, about half as loud to the ear, and 0 is silent. Equal steps of the slider sound like equal steps. The master’s decibels and the channel’s add, so master 80 with effects 80 plays effects 20 dB down.

The voice volume runs to 200%. Past 100%, the slider adds up to 6 dB, so 200% plays every voice at about twice its amplitude. Roblox’s voice volume runs to twice as well, and Discord’s per-user slider has the same two ranges: 50 dB under 100%, and a 6 dB boost over it. A limiter on the voices keeps a boosted voice from clipping.

Master, effects, interface and voice start at 100, the game’s own mix, as Fortnite and Minecraft Java start theirs. Music starts at 60, 20 dB under the effects: CS2 plays its music 16 to 28 dB under its effects, and Minecraft Bedrock lowered its music to 30% in 2025. The following table lists where the top games start:

Volume Here Fortnite Minecraft Java Minecraft Bedrock Roblox
Master 100 none 100% 50% one slider, 0 to 10
Music 60 100% 100% 30% part of the master
Effects 100 100% 100% 100% part of the master
Interface 100 none 100% 100% part of the master
Voice 100% 100% none none 0 to 2, and 1 per player

Fortnite’s figures come from the settings defaults in its 2019 client, and its current defaults are not published. Roblox’s come from its client scripts.

A game that wants the sliders to start elsewhere says so on Game with defaultVolumes. It names each volume by its setting, and each value is the slider’s position:

import { Game, Scene, Sound, VolumeChannel } from "@spawnite/engine";
function Night() {
return <Sound url="/night.mp3" loop channel={VolumeChannel.Music} />;
}
export function App() {
// The file is mixed under the effects already, so music starts at full.
return (
<Game name="night" start="night" defaultVolumes={{ musicVolume: 100 }}>
<Scene name="night" component={Night} />
</Game>
);
}

A player starts at those volumes until they move a slider, and Reset audio puts the volumes back there. A volume the player saved on this device keeps its value.

defaultVoiceMode beside it starts players on a voice mode in the same way, such as defaultVoiceMode={VoiceMode.OpenMic} for a game played with friends. Push to talk is the default everywhere else. Either way the microphone starts off: the mode says what the talk key does.

A phone has no Escape key and no pointer lock to let go, so on a device whose main pointer is a finger the engine shows a menu button in the top-left corner. It stands first in that corner’s row, with the microphone button beside it in a game with voice and the performance overlay under them, and opens the same menu Escape does. Roblox keeps its menu button in the same corner. No game’s HUD uses the top-left slot, and a Panel a game stands there stacks under the row rather than covering it. The row draws above the other slots, so a game’s panel that runs past its own slot on a narrow screen never covers the buttons.

The menus and the overlay draw with ui’s menu tokens, listed on the theme page. A game sets them on :root in its own stylesheet, as Holdfast does, so its Escape menu matches its HUD. A game’s HUD can stand its own panels on the same colours through the engine’s menuPane, the translucent pane on the menu’s surface colour, which the performance overlay also uses.

Mount a <SettingRow> anywhere in the game. It shows for as long as it is mounted, on the Game tab unless it names another. The game keeps the value, and saves it where it likes; the panel calls onChange with each new value.

import { useState } from "react";
import { SettingKind, SettingRow } from "@spawnite/engine";
export function CrosshairSetting() {
const [size, setSize] = useState(1);
return (
<SettingRow
kind={SettingKind.Slider}
label="Crosshair size"
description="How large the crosshair draws."
min={0.5}
max={2}
step={0.1}
value={size}
defaultValue={1}
onChange={setSize}
/>
);
}

A row is one of four kinds:

  • SettingKind.Slider: a number between min and max, moved by step.
  • SettingKind.Toggle: on or off.
  • SettingKind.Choice: one of options, each shown as its own name.
  • SettingKind.Button: a button that says action and calls onPress, for something a player does rather than sets, such as replaying the tutorial.

Every row takes the following, each optional:

  • description: what the row does, in a sentence or two. The panel shows it beside the rows, or under a tapped row on a narrow screen.
  • tab: the tab it shows on, such as SettingsTab.Accessibility for a subtitles switch. The Game tab unless it says otherwise.
  • section: a heading the row stands under within its tab. Rows that name the same section stand together, in the order the first of them mounted, after the rows that name none. On one of the engine’s tabs, a game’s rows follow the engine’s.
  • cost: what the row costs to turn up, as { gpu: CostLevel.High }, shown beside its description as Call of Duty marks each option’s impact.
  • image: the address of an image the panel shows beside the description, such as what each option looks like.
  • defaultValue, on a slider, a switch or a choice: the value the tab’s Reset and Reset all hand back to onChange. A row with none keeps its value on a reset, and a tab whose rows have none shows no Reset.
import { useState } from "react";
import {
CostLevel,
SettingKind,
SettingRow,
SettingsTab,
} from "@spawnite/engine";
export function GrassSetting() {
const [grass, setGrass] = useState("Near");
return (
<SettingRow
kind={SettingKind.Choice}
tab={SettingsTab.Video}
section="Effects"
label="Grass"
description="How much grass draws near you."
cost={{ gpu: CostLevel.Medium }}
options={["Off", "Near", "Far"]}
value={grass}
defaultValue="Near"
onChange={setGrass}
/>
);
}

Epic’s Lyra sample builds its settings screen from the same parts: a value with a default and a reset, a collection that groups settings under a heading, an action that is a button, and an edit condition that hides a setting where it cannot apply. Lyra registers each setting as an object in a registry; here a row is an element the game mounts, so a row that applies only in one scene is mounted in that scene, and unmounting it hides it.

useSettings is a zustand hook over the shared settings. A component reads one setting with a selector, and code outside React reads them all with useSettings.getState():

import { useSettings } from "@spawnite/engine";
const inverted = useSettings((settings) => settings.invertLook);

A game’s own sounds follow the volumes through followVolume, which sets a Web Audio gain from the master volume and one channel’s volume, each through the decibel curve above, now and on every change. convertVolumeToGain turns one slider position into its gain.

import { followVolume, VolumeChannel } from "@spawnite/engine";
const effects = context.createGain();
followVolume(effects, { channel: VolumeChannel.Effects });

The engine’s own <Sound> takes a channel prop: effects unless it names music, interface or voice. A sound the player hears from a menu or the HUD, such as a click or a card turning, plays on VolumeChannel.Interface.

Some settings are the player’s own: her ears, her microphone, her hands and her comfort are the same in every game. The rest are the game’s: its mix and its camera. In the player app, the player’s own settings are carried: she sets them once and every game on the device reads them. The game’s settings stay with each game. The following table sorts every setting:

Setting Kept Why
Master volume Carried, across games How loud her device plays, whatever the game.
Voice volume Carried, across games How loud other people are to her.
Voice mode, talk key, open mic’s threshold Carried, across games How her microphone sends.
Microphone volume, noise suppression, automatic gain Carried, across games Her microphone, the same device in every game.
Microphone on or off Never kept Every session starts it off, in every game.
Camera sensitivity, invert vertical look, lock camera with Shift Carried, across games Her hands: a multiplier on each game’s own speed.
Reduced motion, screen shake Carried, across games Her comfort, which no game should undo.
Fullscreen on Play, performance stats, frame rate cap Carried, across games How she likes the screen, and her device’s battery and heat.
Show other players’ names Carried, across games Whether she wants names over heads, in every game that draws them.
Music volume, effects volume, interface volume Per game Each game mixes its own sound.
Field of view Per game Degrees suit one camera and not the next, and a game can narrow the range.
Graphics quality and its rows, grass sway Per game One game’s scene costs more to draw than another’s.
Keep a replay Per game Only a development build keeps a replay, and none runs in the app.
A game’s own rows Per game The game’s.
Each other player’s volume and mute For the room only A player’s key names them in one room, and each game knows her by another id.

A carried setting the player never touched stays at the game’s default, so a game’s defaultVolumes and defaultVoiceMode still set where she starts. Once she moves a carried setting, every game on the device reads her value, and a tab’s Reset hands it back to each game’s default. Roblox keeps its client settings the same way, on the device and across every experience.

The app keeps the carried settings in its own storage on spawnite.com and hands them to the game’s page over the channel as the page opens. The page sends each change back, and keeps only the game’s settings in its own storage. A game open in another tab of the app takes the change too. The settings are per device, so a desktop and a laptop with different microphones each keep their own. An account that carries them across devices is a later step.

The page keeps every setting in its own local storage under spawnite.settings, and her mute under voice, wherever no app carries them: on a game’s dev server, under spawnite play, or in an app from before carried settings. Each published game runs at an origin of its own, as the trust boundary sets it, and local storage belongs to one origin, so outside the app each game keeps its own. On a development machine each game’s dev server has its own port, and so its own origin too.