Configuration (game.toml)
Your app’s root config file is game.toml in the app directory. It defines [Game]
(name, version, description, terminal), [Window] (size, window state, design size, scale
mode), and optionally [Physics] (layer names and the collision matrix) and
[Network] (dedicated-server settings).
[Game]
Section titled “[Game]”[Game]name = "My Game" # window title; default "Untitled"version = "0.0.1" # default "0.0.1"description = "..." # default "" (empty)terminal = true # default true| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | "Untitled" | Used as the window title. |
version | string | "0.0.1" | Free-form; the engine does not parse it. |
description | string | "" | Free-form metadata. |
terminal | bool | true | When true, the backtick key toggles the in-game terminal overlay. Set false to disable it — a good idea for a shipping build. |
[Window]
Section titled “[Window]”All keys are optional; each one falls back to the engine’s compiled-in default.
[Window]width = 800 # default 800height = 800 # default 800resizable = true # default trueborderless = false # default falsefullscreen = false # default falsemaximized = false # default falseminimized = false # default falsedesign_width = 0 # default 0 (= use window size)design_height = 0 # default 0 (= use window size)scale_mode = "stretch" # default "stretch"| Key | Type | Default | Meaning |
|---|---|---|---|
width | int | 800 | Window width in pixels. |
height | int | 800 | Window height in pixels. |
resizable | bool | true | Whether the user can resize the window. |
borderless | bool | false | Removes the OS title bar and border. |
fullscreen | bool | false | Start fullscreen. |
maximized | bool | false | Start maximized. |
minimized | bool | false | Start minimized. |
design_width | int | 0 | Logical render width. 0 = render at window size. |
design_height | int | 0 | Logical render height. 0 = render at window size. |
scale_mode | string | "stretch" | "stretch", "letterbox", or "integer". Only consulted when design_width/design_height are both > 0. |
Window-state precedence. These three keys are not independent — the engine applies them
in order: if fullscreen = true, it wins and maximized/minimized are ignored. Otherwise
maximized is applied if set; otherwise minimized is applied if set.
Design size and scale mode. design_width/design_height set a logical resolution the
3D pass renders at, which is then scaled to the window. "stretch" fills the window
ignoring aspect ratio, "letterbox" fits while preserving aspect (bars may appear), and
"integer" is letterbox with the scale factor floored to a whole number for sharp pixel
art. Leaving both at 0 renders at the native window size. The same settings can be changed
at runtime — see the Window API.
Physics layers and collision matrix
Section titled “Physics layers and collision matrix”The optional [Physics] section defines tag layer names, collision layer names,
and a collision matrix. These three are largely independent of one another today, and
they do not work the way the names suggest. Read this section before setting
collision_matrix — the wrong value here will make your game’s dynamic bodies fall through
the floor.
tag_layers and collision_layers
Section titled “tag_layers and collision_layers”tag_layersandcollision_layersare arrays of names (e.g.["Default", "Interactable"]).- Order matters: the first name is bit 0, the second is bit 1, and so on.
- In Blender, the Plume3D addon uses these names to label the Tags and Collision
Layers checkboxes when a Game Config Path is set to this
game.toml. This is the primary reason the names exist — the addon reads them purely to render the checkbox UI.
Each scene node ends up with a tags bitmask and a collision_layers bitmask. Bit 0 =
first name, bit 1 = second, etc. (e.g. collision_layers = 1 means “on layer 0”,
collision_layers = 3 means “on layers 0 and 1”).
Where those bitmasks actually go:
collision_layersis read by exactly one system: the mesh raycast layer mask (Raycast). A node whose mask shares no bits with the cast’s mask has its own mesh skipped (its children are still tested). A mask of0disables filtering and tests everything. It is also readable from Wren as a Node property.tagsis not consumed by any engine system — it is metadata you read back from Wren on a Node and act on yourself.- Neither reaches the physics world. A node’s
collision_layersvalue has no effect whatsoever on which bodies collide in Jolt.
collision_matrix
Section titled “collision_matrix”collision_matrix is a list of pairs [a, b] of those motion layers. Symmetry is
implicit — [0, 1] and [1, 0] mean the same thing; you only need one.
The rules the engine actually applies, in order:
- Moving vs moving (
1vs1) always collides, matrix or not. It is hardcoded so that Dynamic bodies always interact with Kinematic (animated) ones. Listing[1, 1]is redundant; omitting it changes nothing. - If no matrix is configured (the key is absent, or the list is empty), everything collides. This is the default and it is the right choice for almost every game.
- If a matrix is configured, any pair not listed in it does not collide.
Put together, the matrix has exactly one meaningful decision to make: whether static
bodies collide with moving bodies, i.e. whether [0, 1] is present.
How to configure collision_matrix:
- Omit it. Unless you specifically want static geometry to be non-solid, delete the key. Everything collides, which is what you want.
- Everything collides, written explicitly:
collision_matrix = [[0, 1]]. This allows static-vs-moving; moving-vs-moving is already on. Equivalent to omitting the key. - Disable static collision entirely (a very unusual thing to want — e.g. a zero-gravity
scene with no level geometry):
collision_matrix = [[1, 1]]. Dynamic bodies still hit each other but pass through all Static bodies.
Parsing quirks worth knowing:
- Each entry must be an array of at least two integers. An entry with fewer —
[[0], [1]]— is silently skipped, not reported as an error. Extra elements past the first two are ignored. - A non-integer element (e.g.
[["a", "b"]]) is silently read as0.
Example game.toml:
[Game]name = "My Game"version = "0.0.1"
[Window]width = 800height = 600resizable = true
[Physics]tag_layers = ["Default", "Interactable"]collision_layers = ["Default", "Interactable"]# collision_matrix omitted on purpose: everything collides.How it’s used
Section titled “How it’s used”- Raycast: Raycast with a layer mask tests only nodes whose
collision_layersbitmask shares a bit with the mask. This is a mesh-level raycast against triangles — it does not consult the physics world, and the collision matrix has no effect on it. - Physics (Jolt): the collision matrix filters body pairs by motion type (static vs
moving), per the rules above. Your named
collision_layersplay no part in it.
Blender addon
Section titled “Blender addon”Set Game Config Path in the Blender addon (on the root collection) to your game.toml.
The addon then shows Tags and Collision Layers as named checkboxes matching
tag_layers and collision_layers from that file. The addon does not read
collision_matrix. See the Blender Addon guide.
Networking ([Network])
Section titled “Networking ([Network])”An optional [Network] section configures the dedicated-server run
mode (plume3d --server). All keys are optional.
[Network]port = 7777 # UDP listen port for the dedicated servertick_rate = 60 # server Game.update(dt) rate, in Hzmax_clients = 32 # hard cap on simultaneous client connections| Key | Type | Default (when [Network] is present) | Meaning |
|---|---|---|---|
port | int | 7777 | UDP port --server binds. Values ≤ 0 fall back to 7777. |
tick_rate | int | 60 | Fixed Game.update(dt) rate in Hz. Values ≤ 0 fall back to 60. |
max_clients | int | 32 | Maximum simultaneous client connections. |
max_clients is enforced, and it is a hard limit. Once the server already holds
max_clients connections, each further inbound connection is refused and closed with the
reason "server full" before it is ever accepted — the client sees a closed connection, and
your Game code never learns about the attempt. It is a real DoS guard, not a hint.
port can be overridden by the environment. If PLUME_SERVER_PORT is set to a valid
port (1–65535), it overrides [Network].port, so an orchestrator can place the same
build on whatever port it assigns.
port is what --server binds and tick_rate drives the loop. The networking transport is
live — see the Dedicated Server guide and the Net API.
Audio mixer ([Mixer])
Section titled “Audio mixer ([Mixer])”An optional [Mixer] section sets default audio-mixer group volumes (see the Mixer API),
applied at startup. master is the Master group’s linear gain; each [[Mixer.group]] sets a named group’s
volume (linear, 1 = unity) and optional mute. The engine pre-creates these groups, so a game’s
Mixer.group("Music") finds them already configured.
[Mixer]master = 1.0
[[Mixer.group]]name = "Music"volume = 0.7
[[Mixer.group]]name = "SFX"volume = 1.0mute = false| Key | Type | Meaning |
|---|---|---|
master | Num | Master group linear gain (default 1.0). |
[[Mixer.group]].name | String | Group name (matched to a Mixer.group). |
[[Mixer.group]].volume | Num | Linear gain, ≥ 0 (default 1.0). |
[[Mixer.group]].mute | Bool | Start muted (default false). |
Player overrides — settings.toml
Section titled “Player overrides — settings.toml”When a player changes the mix, call Mixer.saveSettings() to write their volumes/mute to a
settings.toml in the app directory (same [Mixer] shape). The engine auto-loads it at startup after
the game defaults, so the player’s choices win and survive relaunch; Mixer.loadSettings() re-applies it on
demand. settings.toml is written by the game (safe to delete to reset) and is not part of your source.