Skip to content

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]
name = "My Game" # window title; default "Untitled"
version = "0.0.1" # default "0.0.1"
description = "..." # default "" (empty)
terminal = true # default true
KeyTypeDefaultMeaning
namestring"Untitled"Used as the window title.
versionstring"0.0.1"Free-form; the engine does not parse it.
descriptionstring""Free-form metadata.
terminalbooltrueWhen true, the backtick key toggles the in-game terminal overlay. Set false to disable it — a good idea for a shipping build.

All keys are optional; each one falls back to the engine’s compiled-in default.

[Window]
width = 800 # default 800
height = 800 # default 800
resizable = true # default true
borderless = false # default false
fullscreen = false # default false
maximized = false # default false
minimized = false # default false
design_width = 0 # default 0 (= use window size)
design_height = 0 # default 0 (= use window size)
scale_mode = "stretch" # default "stretch"
KeyTypeDefaultMeaning
widthint800Window width in pixels.
heightint800Window height in pixels.
resizablebooltrueWhether the user can resize the window.
borderlessboolfalseRemoves the OS title bar and border.
fullscreenboolfalseStart fullscreen.
maximizedboolfalseStart maximized.
minimizedboolfalseStart minimized.
design_widthint0Logical render width. 0 = render at window size.
design_heightint0Logical render height. 0 = render at window size.
scale_modestring"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.

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 are 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_layers is 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 of 0 disables filtering and tests everything. It is also readable from Wren as a Node property.
  • tags is 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_layers value has no effect whatsoever on which bodies collide in Jolt.

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:

  1. Moving vs moving (1 vs 1) 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.
  2. 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.
  3. 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 as 0.

Example game.toml:

[Game]
name = "My Game"
version = "0.0.1"
[Window]
width = 800
height = 600
resizable = true
[Physics]
tag_layers = ["Default", "Interactable"]
collision_layers = ["Default", "Interactable"]
# collision_matrix omitted on purpose: everything collides.
  • Raycast: Raycast with a layer mask tests only nodes whose collision_layers bitmask 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_layers play no part in it.

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.

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 server
tick_rate = 60 # server Game.update(dt) rate, in Hz
max_clients = 32 # hard cap on simultaneous client connections
KeyTypeDefault (when [Network] is present)Meaning
portint7777UDP port --server binds. Values ≤ 0 fall back to 7777.
tick_rateint60Fixed Game.update(dt) rate in Hz. Values ≤ 0 fall back to 60.
max_clientsint32Maximum 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.

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.0
mute = false
KeyTypeMeaning
masterNumMaster group linear gain (default 1.0).
[[Mixer.group]].nameStringGroup name (matched to a Mixer.group).
[[Mixer.group]].volumeNumLinear gain, ≥ 0 (default 1.0).
[[Mixer.group]].muteBoolStart muted (default false).

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.