BedderScript Manual
What the language and runtime do today — every promise here is verified by an
automated proof battery (87 tests: 86 green + 1 [Ignore]d known gap).
No roadmap material here; if it's in this manual, it runs.
1. Language
Variables & types
var speed: number = 8 // typed
var name = "player_one" // inferred
var thing = null // null-initialized ⇒ type `any` (assign anything later)
const MAX: int = 99 // const — assignment is a compile error
Types: number, int, float, bool, string, any, Vector2/3/4, Quaternion, Color,
Rect, GameObject. Type annotations are checked; warnings are non-fatal, errors stop compile.
Control flow
if (x > 0) { } else { }
while (running) { }
do { } while (again)
for i from 0 to 10 { } // range loop
for item in list { } // arrays; for maps use: for k in keys(m) { }
switch (state) { case 1: ... default: ... }
break / continue
var v = cond ? a : b // ternary
Word operators: and, or, not. Comparison via == != < <= > >=. x ?? fallback
null-coalesce. a is Type, a as Type casts.
Functions & lambdas
function damage(base: number, crit: bool) -> number {
return crit ? base * 2 : base
}
var d = damage(10, true) // script functions are callable from anywhere
var cmp = (x, y) => y - x // lambdas: (a, b) => expr or x => expr
sort(list, cmp)
Known gap (honestly): lambdas do not capture enclosing locals yet — a lambda only sees
its own parameters and globals. The battery carries the spec test, [Ignore]d until built.
Events
event scored(points: number) // declare
on scored(points) { // handle (any number of handlers)
total = total + points
}
emit scored(15) // fire, with args
The BedderScriptRunner emits: awake (after top-level run), start, update(dt),
fixed_update(dt), late_update(dt), enable / disable / destroy, and the physics set —
trigger_enter / trigger_exit / trigger_stay and collision_enter / collision_exit /
collision_stay (3D and 2D colliders, same names). Physics events arrive name-first:
on trigger_enter(other_name, other) — the other object's name, then its GameObject; declare
one parameter or two. Your game can broadcast anything else via the embedding door.
Waits (latency is native)
on start() {
wait 2 // seconds — wait(2) also parses
wait_frames 10
wait_until door_open
log("all three waited")
}
Both surface forms are legal: wait 2 and wait(2) compile to the same opcode.
The VM suspends and resumes across ticks — no coroutine plumbing in your script.
Errors
try {
throw "boom"
} catch (err) {
log(err)
} finally { }
Classes (basic)
class Greeter {
function shout(x: number) -> number { return x * 2 }
}
Classes parse and compile (methods, self, new); treat deep inheritance/super chains as
lightly-proven — battery coverage is basic.
Modules
import mathx // loads module "mathx" ONCE, inline, flat namespace
The embedding application must install a resolver: engine.ModuleResolver = name => source.
Security: module names are attacker-chosen strings — never map them to file paths without
canonicalization (see SECURITY.md).
No resolver ⇒ compile error IMP001 (imports are never silently ignored). Missing module ⇒
IMP002. Circular imports ⇒ IMP003.
2. Standard library
Function-style calls: push(arr, x), not arr.push(x). .length works on arrays/strings as a
property; len(x) as a function.
Math — abs floor ceil round sign sqrt pow exp ln log sin cos tan asin acos min max clamp lerp smooth_step ping_pong move_towards repeat random random_range set_random_seed
Strings — len substring(s, start[, len]) split(s, sep) replace(s, a, b) trim to_upper to_lower starts_with ends_with contains index_of find
Arrays — push pop len contains index_of insert(a, i, v) remove_at(a, i) reverse join(a, sep) clear sort(a[, comparator]) — comparator is any function/lambda returning
negative/zero/positive.
Maps — keys(m) values(m) has_key(m, k) remove_key(m, k) clear(m) — iterate with
for k in keys(m).
Conversions — str int float number bool typeof (note: typeof(x) in call position has a
parse quirk — pending fix; is works for type tests).
Vectors — vec2(x, y) vec3(x, y, z) vec_length vec_norm vec_lerp(a, b, t) dot distance
(vectors are maps with x/y/z fields — index them: p["x"]).
Diagnostics — print log warn error assert(cond, msg).
3. Engine API (default Unity host)
Self & objects — self() (the GameObject the script is bound to), spawn(name),
destroy(go), find(name), find_tag(tag), set_active(go, on), get_name(go) /
set_name(go, s), get_tag(go) / set_tag(go, s).
Transform — get_position(go) → vec3, set_position(go, x, y, z),
translate(go, x, y, z), get_rotation(go) → euler vec3, set_rotation(go, x, y, z),
set_scale(go, x, y, z), look_at(go, x, y, z),
transform_direction(go, v) → vec3 (local direction to world space — replaces the
sin/cos yaw projection dance in movement scripts).
set_position, translate, look_at, set_rotation, set_scale, set_velocity and
add_force also accept a vec3 in place of the three numbers:
set_position(self(), target) works when target came from vec3()/get_position().
Physics — add_force(go, x, y, z), set_velocity(go, x, y, z),
raycast(origin_vec3, dir_vec3[, max_dist]) → hit info or null,
move(go, dx, dy, dz) (collision-aware displacement — CharacterController.Move when one is
present, translate otherwise), is_grounded(go) (CharacterController), check_sphere(x, y, z, r).
The Runner also forwards OnControllerColliderHit as the event
controller_hit(name, obj, dir_x, dir_y, dir_z).
Events between scripts — send_event(go, "name", args…) fires on name(...)
handlers on the target object's scripts only (returns true when a running script
received it). raise_signal remains the broadcast. Event chains are depth-capped by
the sandbox, so two objects ping-ponging events cannot hang the game.
Components — add_component(go, "Rigidbody"), get_component, has_component,
remove_component. Allow-list: Transform, Rigidbody, Rigidbody2D, Collider, Collider2D,
Animator, AudioSource, Light, Camera, MeshRenderer, SpriteRenderer.
Input — input_axis("Horizontal"), input_down("Jump"), input_pressed, input_up —
Unity Input Manager axis/button names first, key names as fallback.
AV & UI — play_sound stop_sound play_animation set_animation_param set_ui_text set_ui_visible.
Persistence — save_set(key, value) save_get save_has save_delete.
Time — now() get_time() delta_time().
4. Running scripts
The component door
BedderScriptRunner (add to a GameObject): scriptAsset (a .bedder TextAsset) or inline
source; useDesktopLimits for editor-grade budgets. Lifecycle: Awake = compile + run top level +
awake event; Start = start; Update = tick (drives on update and waits); FixedUpdate /
LateUpdate emit fixed_update(dt) / late_update(dt); all trigger/collision messages (3D and
2D) forward as events, name-first. self() is the GameObject the runner sits on.
The embedding door
For a game that treats scripts as content it owns (levels, saves, mods), the
BedderScript.Embedding layer plugs the engine into your game: BedderScriptDirector
attaches a script to a GameObject, ticks them all each frame, broadcasts game events, and
reports per-script errors; ExtensibleEngineHost.RegisterApi is the custom-API seam —
anything you register becomes a free function in the language for scripts run under that
host (unknown names fail loudly). This is the host-integration surface, in the
BedderScript.Embedding namespace.
Sandbox limits
SandboxLimits (per engine): MaxInstructionsPerFrame/PerCall, MaxLoopIterations,
MaxRecursionDepth, MaxCallCount, MaxEventChainDepth, MaxSpawnedObjects, MaxCollectionSize,
MaxScriptMemoryBytes, MaxCoroutineCount, MaxStackDepth, MaxExecutionSeconds. Presets:
ForIos, ForDesktop. All settable.
5. Files & tooling
.bedderinside Unity (ScriptedImporter → TextAsset)..bsexternally (the language is BS — never BC). Legacy.bcfiles still open; Unity's native importer owns that extension (LLVM bitcode), so never use it underAssets/.- Edit-mode IDE:
Tools → BedderScript → Open Bedder IDE— or just double-click any.bedderin the Project window. A dockable editor window with the full IDE (explorer, tabs, completion, click-to-jump problems/outline, quick-fix, find/replace, format) — no Play mode needed. Save writes to disk and reimports the asset; ✦ Ask AI sends the open script as reference to an AI assistant with your question (Unity AI wiring lands when connected — today the door explains itself). Hot-swapping a RUNNING game is the in-game F1 IDE's job (▶ APPLY).Tools → BedderScript → Play In-Game IDEis the other door: a throwaway playground scene in Play mode with the F1 overlay up. - Putting a script on an object (edit mode): right-click the GameObject →
BedderScript → Assign Script…— a picker lists every.bedderin the project; it reuses the object's existing runner (swapping the script) or adds one, with Undo. (The runner'sScript Assetslot also accepts a dragged.bedderdirectly.) - VS Code: extension
beddereder.bedderscript— highlighting for both extensions, Copilot-completes. - Validation without running:
ScriptEngine.Validate(source)→ diagnostics; also what the in-game IDE's problems panel uses. - The proof battery: every language promise is an EditMode test. The rule of the house: a feature isn't in the language until its battery test is green.
- Scripts in builds (
BsScriptStore+BsShipScriptsOnBuild): the included build hook runs automatically before every build and mirrors your scripts folder (Assets/ArTchie Studios/BedderScript) intoStreamingAssets/BedderScripts— scripts written in the editor ship with the game. First launch seeds the player's editable copies intoDocuments/<product>/Scripts(persistentDataPath on mobile) with a hash manifest, and a game update refreshes ONLY files the player never edited. On boot, an edited copy outranks the TextAsset baked into the build — in-game IDE edits survive restarting a shipped game. The IDE's Save status always shows the full folder path, so players can find their work; a host can relocate the folder by settingBsScriptStore.UserScriptsRootOverridebefore first script access. - Snapshots (
Hist ▾in both the IDE and Blox): user-controlled checkpoints, never automatic. Take = copies the script's files into.snapshots/<name>/<time>_<label>/; restore = copies content back over the live files (asset GUIDs never move) and reloads the open tab/canvas. - ONE SCRIPT (design law): the
.beddertext is the only script there is — blocks are clothes on that body. F2 always DERIVES blocks from the current text (the IDE's live buffer first, disk second) through the real parser; unmappable lines and comments ride as rawBS »blocks. Every block edit regenerates the text and pushes it into the IDE's open tab live; Save writes the.bedder(nothing else —.bloxproject files are retired). If the text changes, F2 re-derives. There is no second copy and therefore nothing to drift. Overwriting content the blocks were never derived from keeps a one-time<name>_orig.bedderbackup.
6. Scene edits that survive — the scene-op spine
The same philosophy as scripts, one level up: the shipped scene is the untouchable floor, the user's edits are a small replayable layer on top, and applying is deliberate.
The four pieces (all in Runtime/Components/):
BsId— the sticker. A permanent unique code on a GameObject. Scene ops address objects ONLY by sticker — never by name-path or sibling index — so edits survive renames, moves, and game updates.BsSceneDelta— the booklet. An ordered list of small JSON ops:create(Empty + primitives),create_probuilder(Cube/Stair/Arch/Pipe/… — needs the optionalcom.unity.probuilderpackage, skips politely without it),create_terrain(native Unity Terrain + TerrainData; kind isres,width,height,lengthe.g.257,200,50,200),set_terrain_heights(deflated 16-bit heightmap invalue, survives ▶ LOAD / boot replay),set_terrain_alphamaps(deflated 8-bit splat weights + layer list — tint/name/tile — invalue),set_terrain_details(deflated 8-bit grass density + prototype tint/name invalue),import_asset,create_from_asset,set_texture,destroy,rename,add_component,remove_component,set_property,assign_script,remove_script,reparent(child BsId → parent BsId, or""for scene root). The booklet IS the user's mod — untouched objects always come from the shipped scene..bsmesh— save as prefab. Any placed object with a readable mesh (a ProBuilder shape, an imported OBJ, a resized primitive) can be frozen from the F3 inspector ("💾 Save as prefab") into a.bsmeshfile in the asset catalog — final mesh + material colors, never the gestures — and re-placed forever after from Project ▸ Imports like any imported model.BsSceneApply— the robot. One function replays a booklet onto the loaded scene. The SAME function runs for an in-session ▶ LOAD and for boot-time reload — never fork it. Replay is idempotent (acreatewhose sticker already exists does nothing); a missing sticker skips politely and is reported, never fatal. Components resolve by type name across all loaded assemblies (ALL components, not a curated subset); properties set by reflection ("1,2,3"vectors,"r,g,b,a"colors, euler"x,y,z"rotations).assign_scriptattaches aBedderScriptRunnerreading the stem from the user Scripts folder and hot-boots it viaReloadSource.BsSceneModLoader— the shelf reader. On scene Start, loadsDocuments/<product>/SceneMods/<scene>.jsonand hands it to the robot.
One-time wiring, per scene:
- Open the scene → Tools → BedderScript → Add BsIds To Open Scene → save. Why: every existing object gets its sticker baked into the scene file, so shipped scenes are always addressable. Safe to re-run any time — existing stickers are never touched; only new objects get new ones. Re-run it after adding objects you want editable, before shipping.
- Add Component →
BsSceneModLoaderon any object that always exists (convention: the same object that holds the IDE/Blox loaders) → save. Why: this is what makes user scene edits reload on every launch. It has no settings; it does nothing when no mod file exists.
Smoke test (no UI needed): write a booklet by hand to
Documents/<product>/SceneMods/<scene>.json —
{ "ops": [ { "op": "create", "id": "smoke1", "kind": "Sphere", "name": "RobotWasHere",
"value": "", "parent": "" },
{ "op": "set_property", "id": "smoke1", "kind": "UnityEngine.Transform",
"name": "position", "value": "0,5,0" } ] }
Press Play → a sphere appears at (0,5,0). Delete the file to undo.
Status: the spine is battery-proven, and the F3 Scene panel (§7) writes booklets from clicks — hierarchy, inspector, gizmos, Project window, asset placement all stage ops through this one spine.
7. The in-game editor — the four faces
Host games wire these with small loaders (the showcase Bs*Loaders are the
reference). F1–F3 are tabs (opening one closes the others); F4 is an overlay
that opens over anything and gates all other input while open. Whenever any face is
open, the player script yields (ide_shown/ide_hidden events) and the cursor unlocks.
| Key | Face |
|---|---|
| F1 | IDE (BedderEder) — edit .bedder scripts, APPLY hot-swaps into the running game |
| F2 | Blox — block view of the SAME script (ONE SCRIPT: §5), edits regenerate the text |
| F3 | Scene panel — hierarchy, inspector, viewport, Project window |
| F4 | Bug reporter — name / description / repro / screenshot → Documents/<product>/Bugs/ |
F1 IDE (BedderEder)
The CODE face is EderCodeView (showcase loader: BsIdeLoader). Explorer + file tabs +
editor well + problems/outline. ▶ APPLY hot-swaps the active tab into the matching
running BedderScriptRunner. Save writes every open tab to disk (the player-build
folder is Documents/<product>/Scripts). Completions, verify, go-to-definition, and
quick-fix call the existing IdeSession / TextDocument APIs — they are not a second
editor brain. Scene / Project Open in CODE uses the same door:
BsIdeLoader.OpenLive → EderCodeView.OpenProjectFile → IdeSession.OpenDocument.
If that file is already an open tab, the live buffer is reused (disk is not re-read),
so unsaved typing survives F3 → F1. Hiding CODE flushes the editor widget into the
document first.
Problems — click a row to jump the caret to that diagnostic (line + column). Rows
carry an E / W / i badge and a severity color bar; fix on the row applies that
diagnostic's first available QuickFix when IdeSession attached one.
Outline — click a symbol to jump to its declaration. If analysis has no symbols
(broken parse), the list falls back to surface function / on / let / const
declarations in the current buffer.
Shortcuts while F1 is visible. The F4 bug card is modal and owns all input while open. Name prompts and context menus do the same. F3 is next-find only while the find card is open, so it does not steal the Scene face key.
| Input | Action |
|---|---|
| Ctrl/Cmd+S | Save open tabs to disk |
| Ctrl/Cmd+Z | Undo (TextDocument). Status flashes when the stack is empty. |
| Ctrl/Cmd+Shift+Z or Ctrl+Y | Redo. Status flashes when the stack is empty. |
| F5 | Verify / Analyze |
| Ctrl/Cmd+Space | Open completions (Tab still commits the selected item) |
| typing | Completions also open while typing an identifier or after . (not inside strings/comments) |
| Ctrl/Cmd+. | Quick Fix — first available diagnostic fix at the caret, else safe auto-correct, else Verify |
| F12 | Go to definition of the identifier at the caret |
| Ctrl/Cmd+F | Find / replace card |
| F3 / Shift+F3 | Next / previous match (only while Find is open) |
| Esc | Close Find, or dismiss the completion list |
| Tab | Complete, or commit the selected suggestion |
| Fix (toolbar) | Same as Ctrl+. |
F3 Scene panel
Layout — panels dock left/right/bottom; drag a panel header to move it (ghost chip
shows the target), drag the 12px splitters to resize. Layout ▾ saves named layouts
(Documents/<product>/Layout/), lists them, and restores default.
Scene camera (the imposter view — the main camera stops rendering while F3 is open).
Gameplay input is blocked for every Bedder face; while F3 is open, BedderScript
on update / fixed_update / late_update also pause, so WASD flies the scene
cam instead of walking the player (gravity, FollowCam, and move() all stand down).
A ground grid is drawn on XZ at y=0 (or XY at z=0 in 2D), spaced to the
current snap increment.
| Input | Action |
|---|---|
| RMB hold + mouse | Look around (fly mode). In 2D, RMB pans |
| W A S D | Move · Q / E down / up · Shift ×4 sprint. In 2D, WASD pans in the XY plane |
| MMB drag | Pan in the camera plane (screen-space at the orbit pivot) |
| Alt+LMB drag | Orbit around the selection (else last F / view center). In 2D, pans |
| Scroll (while flying) | Fly speed ×1.15 per notch — Shift+scroll fine-tune ×1.03 (toast shows the multiplier) |
| Scroll (over the viewport, not flying) | Dolly. In Ortho / 2D, zooms orthographic size |
| Ortho button | Perspective ↔ orthographic (keeps the current heading) |
| 2D button | Ortho + look down +Z at the XY plane (Unity 2D scene view). Toggle off restores the previous 3D heading |
| F | Frame the selected object (primary / last click) |
| F1 | Open the related .bedder / .bs in CODE as a live buffer (Project script preview, else BedderScript on the selection). Same as Open in CODE. Already-open tabs keep unsaved typing. No related file = show the last CODE tab. |
| Shift+click / Ctrl+click | Range-select through the visible hierarchy, or toggle (viewport uses the same modifiers) |
| Drag a hierarchy row | Reparent onto the drop-target row (all selected; skip cycles). Drop on empty tree space = unparent to scene root |
| Ctrl+D | Duplicate the selection (new BsId, offset, staged as booklet create + imposter) |
| Ctrl+Shift+P (also Ctrl+P) | Command palette. Type to filter, ↑↓ highlight, Enter runs, Esc or click-outside closes. Commands: Create Empty (and Create Empty Child when a stickered object is selected), Duplicate, Focus, Open in CODE, Import (the Project Import button / folder picker), Toggle Ortho, Toggle 2D. Grey rows cannot run (nothing selected, or no .bedder on the selection). |
| Delete | Stage destroy for every selected stickered object — the mesh hides immediately (gone from the viewport and hierarchy). Ctrl+Z un-hides it. ▶ LOAD is the real delete |
| Ctrl/Cmd+Z | Undo the last pending booklet op group (create / delete / transform / reparent this session). Status flashes when the stack is empty. |
| Ctrl/Cmd+Shift+Z or Ctrl+Y | Redo. Status flashes when the stack is empty. |
| Esc | Clear the selection (Forge Esc still exits geometry edit; a command palette Esc closes the overlay instead) |
| Empty viewport click | Clear the selection (a 3D pick miss — hierarchy chrome is not a miss) |
Gizmos (on the selected object). A tool strip at the top-left of the viewport mirrors the keys: W / E / R, Local/Global, Pivot/Center, Snap (+ increment field), V, Ortho, 2D.
| Input | Action |
|---|---|
| W / E / R | Move / Rotate / Scale (buttons stay in sync). Also leave Terrain sculpt. |
| Terr | Terrain sculpt/paint on the selected Terrain (second strip: Raise / Lower / Smooth / Flatten / Paint + layers) |
| X or Local/Global | Toggle world vs local axes |
| Pivot/Center | Handles at the transform, or at renderer/collider bounds center |
| Snap button | Sticky snap on/off. Shift+click cycles the move increment (0.125 / 0.25 / 0.5 / 1 / 2 / 4) |
| Snap increment field | Type a move step (0.01–100). Rotate stays 15°, scale 0.1 |
| Ctrl (while dragging) | Snap even when the button is off — uses the current increment |
| V (hold or the V button) | Vertex snap while translating. Readable MeshFilter / skinned / mesh-collider verts if mesh.isReadable; otherwise the renderer/collider AABB's 8 corners. Does not snap while rotating or scaling. Built-in Unity primitives are often non-readable, so those fall back to corners |
| Drag axis / plane quad / ring / cube | The usual Unity handles; center square = screen-plane move |
Inspector — selected objects open with a GameObject header (active checkbox,
editable name, tag, layer) above Transform and the other components. Rename
stages the booklet rename op (Ctrl+Z undoes it). Active / tag / layer apply live
on the object; they have no booklet channel yet, so the status line says so.
Every component is discovered by reflection: checkboxes, sliders,
enum menus, X/Y/Z scrub grips, HSV color picker. Drag any value label to scrub it
(Shift = ×0.1 fine). Edits apply LIVE and stage a set_property op. A BedderScript
component adds Open in CODE (F1) (the script body is not an inspector field —
inline scripts with no file stay here). The section header folds from the arrow /
title only — clicking the enable checkbox or ✗ never folds it, exactly like
Unity. Multi-select keeps showing the last-clicked object's inspector, and an edit
there applies to every selected object that has that component — property,
Transform vector, component enabled, material tint — all in one Ctrl+Z group.
Objects missing that component (and pending imposters) sit the edit out. Where the
selection disagrees on a value you get a mixed readout instead of the primary's
number: — for fields, vectors (per axis), sliders and toggles, Mixed on enum
menus, an empty swatch for colors. Setting a value clears the mix; an axis still
showing — is left alone, so a typed Y never flattens everyone's X. ✗ Remove
component stays on the last-clicked object only — a stray ✗ can't strip a component
off the whole set. Actions:
+ Add Component ▾, + Script ▾ (from the user Scripts folder), 🎨 Texture ▾
(from imported images), 💾 Save as prefab (any readable mesh → .bsmesh in the
catalog), 🗑 Delete (hides immediately; ▶ LOAD commits).
Create ▾ — Empty + primitives, Terrain (200×200, 257 heightmap — see
Terrain below), and ProBuilder ▸ shapes (Cube, Stair,
CurvedStair, Arch, Door, Pipe, Cylinder, Prism, Torus, Cone) when the optional package
is installed. New objects appear as imposters (solid look, no colliders, no
gameplay) exactly where the real ones will be. Terrain keeps its collider so you can
sculpt it before ▶ LOAD. The same model covers every
create and delete: Duplicate, Create, Project Place, and a model dropped on
the viewport all stage a booklet op and show an imposter immediately. Delete
hides the live mesh immediately (it leaves the hierarchy too). Nothing is committed
until ▶ LOAD, which runs the booklet through the robot and saves it for boot
replay. There is no second “apply now” path — a drag-drop is not a silent FinishLoad.
Right-click hierarchy rows for per-object
menus (Open in CODE (F1) when a BedderScript file is on the object, Create / Rename / Duplicate / Delete); right-click empty tree space to
create at root. Drag a selected row onto another row to reparent (Unity-like);
multi-select reparents the whole set under the drop target, skipping any object that
would parent under its own descendant. Drop on empty hierarchy space unparents to
the scene root (same as Create at root). The drop target row highlights with the
selection color. Click replaces the selection; Ctrl/Cmd+click toggles;
Shift+click ranges through the visible tree (same modifiers in the viewport).
The last click is the primary — inspector and gizmos follow it; Delete and Duplicate
act on the whole set. Duplicate clones stickered objects (or the primary if the list
is empty), assigns a new BsId, offsets slightly, and stages booklet create ops
matching the existing create path. Objects without a BsId are skipped, same as Delete.
Esc or an empty viewport click (no object under the cursor) clears the selection;
clicking hierarchy chrome that isn't a row does not.
Filter… — the hierarchy has a filter field at the top of the tree (below the dock
⠿ grip, not on the W/E/R gizmo bar). Typing filters the rows live by GameObject
name, case-insensitive contains; the query is trimmed, so an all-whitespace filter
lists everything and cube still finds Cube. Nothing is a wildcard — *, . and
[ match themselves. A matching child still shows its ancestors (they open for the
search only; clearing the query restores the folds you left), and non-matching siblings
stay hidden while the filter is on. An empty tree reads No objects yet; a query
that matches nothing reads No matching objects. Esc in the field clears the
query and keeps the caret there, a second Esc leaves the field, and after that
Esc deselects as usual — a command palette Esc still just closes the overlay.
Row chrome (type chip / lock / eye) — every hierarchy row carries three widgets
on its right, in that order: a type chip (pending, Terrain, Camera, Light,
Canvas, Audio, Mesh, Empty — first match on the object itself wins), then a
lock, then an eye. The name to their left stays the selectable label, and the
chip is display-only. The eye toggles the GameObject's active state — the same
switch as the inspector header checkbox, and the two stay in sync either way you flip
it. Inactive objects stay listed in the tree and just dim (name and chip both), so
nothing vanishes on you. The lock takes an object out of the viewport's reach:
clicks in the 3D view pick straight past it and a locked object gets no gizmo handles,
while a hierarchy click still selects it and the inspector still edits it. Both are
per-object — locking a parent does not lock its children — and both are also on the
row's right-click menu as Lock / Unlock and Hide / Unhide. Clicking either
widget never changes the selection and never starts a reparent drag. Eye and lock are
session-local: neither stages a booklet op, so ▶ LOAD does not persist them
and they reset when Bedder restarts.
Terrain — Create ▾ → Terrain drops a native Unity Terrain (200×200 m,
50 m height range, 257×257 heightmap) in front of the scene camera, surface at y=0.
It shows in the hierarchy (amber · pending until ▶ LOAD). Select it and a
second tool strip appears: Raise / Lower / Smooth / Flatten / Paint / Grass, plus Size
(world metres, default 24) and Opacity (0–1, default 0.35). A second row holds
layer chips (1–4) with + / −. New terrains start with one grass-green
layer; + adds a generated 64×64 albedo tint (Dirt / Rock / Sand — no extra
packages). Grass scatters 1–2 runtime billboard tufts (tinted blade textures,
detail map 256 / patch 8) with the same Size/Opacity brush; Shift erases
density to 0. LMB stamps under the cursor with a soft falloff; a cyan ring tracks
the brush on the surface. Shift inverts Raise/Lower, and on Paint erases the
active layer toward the others (usually Grass). W / E / R leave sculpt/paint
and restore object handles (terrain pivot is the min-XZ corner, Unity-style).
Terr on the gizmo strip toggles the tools back on. Flatten samples the height
at mouse-down and levels the stroke to it. One Ctrl+Z undoes a whole mouse-up
stroke or a layer add/remove (redo with Ctrl+Y). Sculpt stages set_terrain_heights;
paint and layer edits stage set_terrain_alphamaps; grass strokes stage
set_terrain_details — ▶ LOAD writes them into
Documents/<product>/SceneMods/<scene>.json so a reload keeps the hills, splats,
and grass. Not this pass: trees, holes, neighbor stitch, heightmap import.
Undo is over pending booklet ops staged this session — not Hist. Hist ▾
snapshots remain checkpoints of the saved scene-mod file. Ctrl/Cmd+Z pops the last
op group: undoing a create removes the imposter and the staged op; undoing a
staged delete un-hides the object (it was hidden the moment Delete staged, never
destroyed until ▶ LOAD); undoing a gizmo
or inspector transform restores the previous TRS and the pending set_property;
undoing a terrain stroke restores the heightmap and the pending
set_terrain_heights; undoing a paint stroke or layer add/remove restores
the alphamaps / layer list and the pending set_terrain_alphamaps;
undoing a grass stroke restores the detail density and the pending
set_terrain_details;
undoing a hierarchy reparent restores the previous parent (one undo for the
whole multi-select drop) and pops the pending reparent op.
▶ LOAD (or Clear pending) commits or discards the session stack — Clear pending
also un-hides any staged deletes. Shortcuts fire
only while F3 is open and focus is not in an InputField, name prompt, popup, or
the F4 bug card — F1 IDE Ctrl+Z stays with TextDocument.
Project window — roots: Scripts, Imports (the asset Library), SceneMods,
Assets (editor only), and any folder you add with + Add source… (the
system folder dialog; sources persist per product and a ✕ on the row forgets them).
Rows carry real icons: texture files show a thumbnail of the image (cached by
path + mtime); folders, model-bundle folders, prefabs, scripts, meshes, audio, and
packages each get a distinct chip. File rows also show a dim type/size on the right.
A Filter… box at the top of the file list matches names (substring); Icons
toggles a 64px thumb grid for the current folder (List is the default).
Selection works like a file explorer: click, shift+click for a range,
ctrl+click to toggle — then Import (N), Move ▾ (N), or Delete (N)
act on the whole set. With nothing selected, Import all takes the folder. A
selected .unitypackage / .abspack is Open package… (not a competing Import).
A selected .bedder / .bs offers Open in CODE; double-click the row does
the same (live buffer in F1, not a stale copy). A selected .prefab is Place in
scene (or drag onto the viewport): the prefab is copied/registered into
Imports/Prefabs and staged as booklet create_from_asset — ghost + pending
Hierarchy row immediately, status Staged: place <Name> — ▶ LOAD makes it permanent. Ctrl+Z undoes the pending place. ▶ LOAD writes it into the scene mod
so a reload keeps the object. Player builds place catalog-backed prefabs only;
AssetDatabase is editor-only for the copy/bake, never on the replay path.
Import stays in the folder you were browsing — it does not jump the Project
root to Imports. A status line reports Imported 3 → Imports/Textures (click
show in Imports on that row to jump when you want). Re-importing bytes that
are already in the catalog says already in library, not a failure. If you are
already inside Imports, the current folder just refreshes.
Imports land sorted: Models/ (each model as a self-contained bundle folder
with its textures), Textures/, Audio/ — make your own folders with + New
folder and file things with Move ▾. Click an import to preview it in the
inspector and pre-configure components/scripts before placing; drag a model into
the viewport to place it (imposter now, booklet pending, same as Create / Duplicate —
▶ LOAD makes it real); drag a .prefab the same way (it registers into
Imports/Prefabs first, then stages create_from_asset); drag a texture onto an
object to apply it live and stage the paint (textures never place as objects);
scripts drop onto the object under the cursor and stage assign_script (▶ LOAD
attaches it).
Supported: images (.png .jpg .jpeg .tga .bmp .gif .tif/.tiff .psd), audio (.wav .ogg .mp3 .aiff/.aif), models — .obj with runtime MTL + textures, .fbx (in-house importer: binary 7.x, the older big-endian 6.x, and ASCII — static meshes, submesh materials, diffuse textures incl. .tga/.bmp/.gif/.tif; rigs and animation are not imported), .glb/.gltf (via the optional glTFast package), and .bsmesh. Files are COPIED into the catalog (sha-deduped, 50 MB cap) — imports never depend on the source folder afterward.
Image notes (honest): .gif is the first frame only (LoadImage if the host
codec accepts it, else an in-package LZW decoder). .tif/.tiff are 8-bit
gray/RGB/RGBA, uncompressed or PackBits — LZW/JPEG/tiled TIFF will copy into
Imports but will not decode at Place; export PNG. .psd loads Photoshop's own
flattened composite (8-bit RGB/gray, raw or PackBits), not a layer-stack
replay — ZIP or 16/32-bit PSDs will not decode; export PNG or TGA. .exr .hdr
.webp stay blocked (no runtime decoder). Status on an unsupported type names
what is supported in one line.
Selecting a .cs file in a source folder and hitting Import converts it with
BsCsToBedder into Scripts/<stem>.bedder (_2 if the name is taken) and
reports the TODO(bs) count. C# is never copied into Imports.
Import whole Unity packages. Select a .unitypackage (or .abspack) in any
source folder → ⬇ Open package… → the familiar dialog: a collapsible folder
tree with per-item and per-folder ticks. Everything lands at the SAME paths the
tree showed, under Models/ Textures/ Audio/ Prefabs/. The importer reads
the package's own .mat and .prefab wiring, so prefabs assemble as placeable
units with the right texture on the right submesh — atlas-textured packs
included. .bedder files in a pack go straight to the scripts folder. C# scripts
convert to BedderScript by default (untick to skip) and are marked honestly:
the converter is a translator assistant that makes mistakes; everything it cannot
map is commented and tagged TODO(bs) for a human to review. The finish line
reports how many TODO(bs) flags need a look.
Author your own packs. Tools → BedderScript → Export ABS Pack… (Unity
editor) exports a targeted .abspack — models, textures, audio, .bedder
scripts, and prefabs (baked to .bsmesh, every mesh combined, with each
material's albedo texture shipped beside the mesh and rewired on import) — that
players import through the same dialog, manifest name and author shown in its
title.
You are responsible for having the right to redistribute everything you
include; the export gate requires that confirmation.
F4 bug reporter
Fields for name / description / how-to-reproduce, plus 📷 Screenshot (the card
hides itself for the capture). Saves report.md + screenshot.png per report under
Documents/<product>/Bugs/<time>_<name>/. While open it is modal: camera, gizmos,
panel hotkeys, and the player script all yield.
Appendix — full API reference
Every built-in function, generated from the engine's own census (the same table
that drives typechecking and IDE completion), so this list is always complete.
Regenerate after engine changes: Tools → BedderScript → Regenerate API Reference.
143 functions.
Core
print(value)— Write a value to the log.assert(condition, message)— Stop with message when condition is false.len(value) -> int— Length of a string, array, or map.str(value) -> string— Convert any value to a string.int(value) -> int— Convert to a whole number.float(value) -> float— Convert to a decimal number.number(value) -> number— Convert to a number.bool(value) -> bool— Convert to true/false.typeof(value) -> string— Name of a value's type.log(value)— Log a message.warn(value)— Log a warning.error(value)— Log an error.
Math
min(a, b) -> number— Smaller of two numbers.max(a, b) -> number— Larger of two numbers.abs(n) -> number— Absolute value.floor(n) -> int— Round down.ceil(n) -> int— Round up.round(n) -> int— Round to nearest whole number.clamp(n, lo, hi) -> number— Limit a number to a range.lerp(a, b, t) -> number— Blend from a to b by t (0..1).sin(radians) -> float— Sine.cos(radians) -> float— Cosine.sqrt(n) -> float— Square root.pow(n, e) -> float— n raised to e.random() -> float— Random number 0..1.random_range(lo, hi) -> float— Random number in a range.tan(radians) -> number— Tangent.asin(n) -> number— Arc sine (radians).acos(n) -> number— Arc cosine (radians).atan2(y, x) -> number— Angle of a vector (radians).exp(n) -> number— e raised to n.ln(n) -> number— Natural logarithm.sign(n) -> number— -1, 0, or 1.smooth_step(a, b, t) -> number— Eased blend from a to b.ping_pong(t, length) -> number— Bounce t between 0 and length.move_towards(cur, target, maxDelta) -> number— Step toward target without overshooting.repeat(t, length) -> number— Wrap t into 0..length.set_random_seed(n)— Make random() deterministic.
Time
now() -> float— Seconds since play started.delta_time() -> float— Seconds since last frame.get_time() -> float— Seconds since play started.set_timeout(seconds, fn) -> id— Run a function after a delay.
Components
add_component(go, "Rigidbody") -> comp— Add an allow-listed Unity component.get_component(go, "Light") -> comp— Get a component, or 0 when missing.has_component(go, kind) -> bool— True when the component exists.remove_component(go, kind)— Remove a component.
Transform
get_position(go) -> vec3— World position as a vec3 map.set_position(go, x, y, z | go, v)— Move to a world position (numbers or a vec3).translate(go, dx, dy, dz | go, v)— Move relative to current position.look_at(go, x, y, z | go, v)— Face a world point.set_rotation(go, x, y, z | go, v)— Set rotation from euler angles.set_scale(go, x, y, z | go, v)— Set local scale.transform_direction(go, v) -> vec3— Local direction to world space.get_rotation(go) -> vec3— Euler angles as a vec3 map.
Physics
add_force(go, x, y, z | go, v)— Push a rigidbody.set_velocity(go, x, y, z | go, v)— Set rigidbody velocity.get_velocity(go) -> vec3— Rigidbody velocity as a vec3 map.move(go, dx, dy, dz) -> bool— Collision-aware move (CharacterController when present).is_grounded(go) -> bool— CharacterController grounded check.check_sphere(x, y, z, r) -> bool— Anything solid inside a sphere?raycast(origin, dir [, maxDist]) -> hit | null— Cast a ray, get hit info.
Objects
set_active(go, on)— Show or hide an object.get_name(go) -> string— Object name.set_name(go, name)— Rename an object.get_tag(go) -> string— Object tag.set_tag(go, tag)— Set an object's tag.spawn(name) -> go— Create an object from the catalog.destroy(go)— Remove an object from the scene.find(name) -> go | find(s, sub) -> int— Find an object by name (object form wins the typecheck).find_tag(tag) -> array— All objects with a tag.spawn_entity(name, x, y [, w, h]) -> go— Spawn a catalog entity at a position.self() -> go— The object this script runs on.
Vector
vec3(x, y, z) -> vec3— Make a vector map with x/y/z.vec_lerp(a, b, t) -> vec3— Blend between two vectors.vec_length(v) -> float— Vector magnitude.vec_norm(v) -> vec3— Vector scaled to length 1.dot(a, b) -> float— Dot product.distance(a, b) -> float— Distance between two points/objects.vec2(x, y) -> vec2— Make a 2D vector map.color(r, g, b [, a]) -> color— Make a color map.
Events
send_event(go, name, args...) -> bool— Fireon name(...)on the target's scripts.raise_signal(name)— Broadcast a named signal.
Collections
push(arr, value)— Append to an array.pop(arr) -> value— Remove and return the last element.contains(coll, value) -> bool— String/array/map contains a value.index_of(coll, value) -> number— Position of a value, -1 when absent.insert(arr, index, value)— Insert into an array.remove_at(arr, index)— Remove an array element by position.reverse(arr)— Reverse in place.join(arr, sep) -> string— Join elements into a string.clear(coll)— Empty an array or map.sort(arr [, compare])— Sort in place.keys(map) -> array— Map keys.values(map) -> array— Map values.has_key(map, key) -> bool— Map contains a key.remove_key(map, key) -> bool— Remove a map entry.
Input
input_up(action) -> bool— Input released this frame.input_down(action) -> bool— Pressed this frame (GetButtonDown) — the demo scripts' jump.input_axis(axis) -> float— Analog axis (-1..1).input_pressed(action) -> bool— Held (GetButton) — sprint, mining.
Av
set_animation_param(name, param, value)— Set an Animator parameter.stop_sound(name)— Stop a playing sound.play_sound(name)— Play a sound.play_animation(name)— Play an animation state.
Ui
set_ui_text(name, text)— Set a UI text element.set_ui_visible(name, on)— Show or hide a UI element.
String
substring(s, start, len) -> string— Slice of a string.split(s, sep) -> array— Split a string into parts.replace(s, from, to) -> string— Replace every occurrence.trim(s) -> string— Strip surrounding whitespace.to_lower(s) -> string— Lowercase.to_upper(s) -> string— Uppercase.starts_with(s, prefix) -> bool— String begins with prefix.ends_with(s, suffix) -> bool— String ends with suffix.
Persistence
save_set(key, value)— Persist a value.save_get(key) -> value— Read a persisted value.save_has(key) -> bool— Persisted key exists.save_delete(key)— Delete a persisted value.
Session
set_var(key, value)— Set a session variable.get_var(key) -> number— Read a session variable.add_var(key, delta) -> number— Add to a session variable.move_piece(dx, dy)— Nudge the bound piece (2D host).set_ambient(level)— Ambient light level.set_solid(on)— Make the bound object solid or pass-through.set_color(color)— Tint the bound object.set_tuning(key, value)— Override a tuning knob.revert_tuning(key)— Restore one tuning knob.reset_tuning()— Restore all tuning knobs.hide_self()— Hide the bound object.destroy_self()— Destroy the bound object.
Player
teleport_player(x, y)— Move the player instantly.impulse_player(x, y)— Push the player.grant_ability(name)— Give the player an ability.revoke_ability(name)— Take an ability away.kill_player()— Kill and respawn the player.win_level()— Finish the level.player_speed() -> number— Player's current speed.distance_to_player() -> number— Distance from the bound object to the player.set_player_scale(x, y)— Resize the player.get_player() -> go— The player object.player_overlaps_self() -> bool— Player touching the bound object?player_hit_underside() -> bool— Player bumped this from below?contact_normal_y() -> number— Y of the player's last contact normal.
Runner events (on name(args) { ... })
- awake() — after the script's top level runs
- start() — first frame
- update(dt) — every frame
- fixed_update(dt) — physics step
- late_update(dt) — after update, for cameras
- enable() / disable() — object toggled on/off
- destroy() — object is being destroyed
- trigger_enter(name, obj) / trigger_exit(name, obj) / trigger_stay(name, obj) — trigger volumes (3D and 2D)
- collision_enter(name, obj) / collision_exit(name, obj) / collision_stay(name, obj) — collisions (3D and 2D)
- controller_hit(name, obj, dx, dy, dz) — CharacterController hit something
Source in the box: Assets/ArTchie Studios/ArTchie-Bedder-Suite/com.bedder.script/MANUAL.md