Editor Scripting API¶
The editor scripting API is for editor tools – scripts under an editor/ folder of your project.
Tools run alongside your game script and talk to the editor itself: read and change the current
selection, query and drive the editor camera, group several changes into a single undo step, and put
their own widgets next to the editor’s. require engine.editor brings the whole API in.
Two annotations attach a function to a component’s inspector section. The function takes the
component it operates on as its single argument – the argument’s type picks the section, and
comp.nodeId is the node the component belongs to:
[menu_item] def my_tool(comp : Mesh?)– an entry in that component’s context menu;[tool_button] def my_tool(comp : Mesh?)– a button above the component’s fields (below– below them,new_line– start a new button row).
For a plain button that is not tied to a component, use the tool_button widget function (see the
widgets below).
Two more annotations hook a function into the editor loop (the function name is free):
[editor_update] def f()– runs every editor frame; the usual place to declare tool UI widgets (see below);[editor_node_changed] def f(node : NodeId)– runs when the editor changed a node: an inspector edit, a gizmo drag, an undo or a redo.
Whether the game is running: game_state() returns Stopped, Paused or Playing.
Every change made through this API is registered in the editor’s undo history, exactly like an edit made by hand in the editor UI.
The prefab currently open in the editor is reachable too: prefab_name() returns its file name,
prefab_save() saves it like Ctrl+S, and prefab_open("prefabs/level1.prefab") queues switching
to another prefab by its project-relative path.
Tools draw their own UI without any window management. Inside an [editor_update] hook, every
tool_* call (tool_slider, tool_button, tool_label, …) declares a widget for this
frame and returns its current value; keep calling it every frame to keep the widget on screen. When
the call stops running the widget disappears after about a second, but its value is kept and comes
back with the widget. Values of input widgets (sliders, float3, color, checkbox, text input, enum combo,
resource picker) also persist in the project settings, surviving an editor restart – in
.project/settings.blk next to the project sources, so they are shared with the team. Per-node
values of component_tool widgets persist too, keyed by the node:
[editor_update]
def scatter_ui() {
tool_window("Scatter") {
let radius = tool_slider("radius", 5., 0.1, 50.)
tool_hint("Scatter radius in meters")
if (tool_button("scatter")) {
scatter(get_selected_nodes(), radius)
}
}
}
The widgets live in floating tool windows over the viewport, so the viewport itself never shrinks or jumps:
the default
Toolsoverlay – translucent, draggable, collapsible. Widgets declared outside anytool_windowblock land here; it cannot be created explicitly;a normal window per
tool_window("name") { ... }block – draggable, resizable, closable, with the widgets declared inside the block. Blocks nest, andtool_window(name, bg_alpha) { ... }overrides any window’s background opacity (including the default overlay via"");a
component_tool("Mesh") { ... }block draws its widgets in that component’s inspector section instead, per inspected node.
All tool windows are listed in the editor’s top Window menu, where a closed one can be reopened.
A widget is identified by its name and its call site. The name is the visible label and pins the tuned value across code edits; the call site tells apart unnamed widgets (labels, separators, line breaks) and repeated declarations – one call site hit N times in a loop yields N widgets, in order. Widgets draw in declaration order.
Enumerations¶
- EditorTab¶
The editor’s main view tabs.
- Values:
Welcome = 0 - The start page
Scene = 1 - The game scene view
Game = 2 - The running game view
Prefab = 3 - The prefab editor
- GameState¶
The game as the editor’s Play/Pause buttons see it; returned by game_state(). A stopped game with a leftover pause flag reads as Stopped.
- Values:
Stopped = 0 - The game is not started
Paused = 1 - The game is started but frozen - manually, by compilation errors or by the user menu (holds in the prefab tab too)
Playing = 2 - The game is started and running
- GizmoOperation¶
The viewport gizmo mode, as picked by the toolbar buttons or the W/E/R/T/Y hotkeys.
- Values:
Translate = 0 - Move handles (W)
Rotate = 1 - Rotate handles (E)
Scale = 2 - Scale handles (R)
Bounds = 3 - The 2D rect tool (T)
Universal = 4 - Translate, rotate and scale handles at once - the Transform toolbar button (Y)
- ShadeMode¶
The viewport shading mode from the editor’s Options menu.
- Values:
Shaded = 0 - Normal shading
Wireframe = 1 - Wireframe only
ShadedWireframe = 2 - Wireframe drawn over the shaded view
Selection¶
- clear_selection()¶
Clears the editor selection. Same as set_selection with an empty array (undoable).
- get_first_selected_node(): NodeId¶
Returns the first selected node, or an invalid NodeId when nothing is selected or no editor is running.
- get_selected_nodes(): array<NodeId>¶
Returns a copy of the currently selected nodes, in selection order. Empty when nothing is selected or no editor is running.
- set_selection(nodes: array<NodeId>)¶
Replaces the current editor selection with nodes (empty array = clear selection). Registered as an undoable action; inside a batch_actions() block it merges into the batch’s single undo step.
- Arguments:
nodes : array< NodeId>
Camera¶
- enter_camera_2d_mode()¶
Switches the editor camera of the active view into 2D (UI) mode, same as clicking the “UI” button in the camera toolbar: it stores the current 3D camera state and smoothly frames the scene’s UI canvases in orthographic projection. No-op when no editor is running.
- exit_camera_2d_mode()¶
Switches the editor camera of the active view back to its last 3D state, same as clicking the “UI” button again. No-op when no editor is running.
- get_camera_forward(): float3¶
Returns the world forward vector of the editor camera.
- get_camera_fov(): float¶
Returns the editor camera’s field of view in degrees (perspective mode). Returns 0 when no editor is running.
- get_camera_orthographic_scale(): float¶
Returns the editor camera’s orthographic zoom (half-height of the view volume, in world units). Returns 0 when no editor is running.
- get_camera_right(): float3¶
Returns the world right vector of the editor camera.
- get_camera_transform(): float3x4¶
Returns the world transform of the editor camera of the active view (columns: right, up, forward, position). Identity when no editor is running.
- get_camera_up(): float3¶
Returns the world up vector of the editor camera.
- get_camera_zfar(): float¶
Returns the editor camera’s far clip distance. Returns 0 when no editor is running.
- get_camera_znear(): float¶
Returns the editor camera’s near clip distance. Returns 0 when no editor is running.
- is_camera_2d(): bool¶
Returns true if the editor camera of the active view is in 2D (UI) mode. Returns false when no editor is running.
- is_camera_orthographic(): bool¶
Returns true if the editor camera of the active view is in orthographic projection mode. Returns false when no editor is running.
- look_at(center: float3; radius: float)¶
Smoothly frames the sphere at center with the given radius, keeping the camera orientation.
- Arguments:
center : float3
radius : float
- look_at(node: NodeId)
Smoothly frames node in the editor viewport, exactly like pressing F on a selected
node: the camera keeps its orientation and moves back until the node’s bounds fit the view.
- Arguments:
node : NodeId
- look_at_instant(center: float3; radius: float)¶
Same as look_at, but the camera jumps immediately.
- Arguments:
center : float3
radius : float
- look_at_instant(node: NodeId)
Same as look_at, but the camera jumps immediately - a tool reading the camera right after the call sees the final state.
- Arguments:
node : NodeId
- set_camera_clip(znear: float; zfar: float)¶
Sets the editor camera’s near and far clip distances instantly (validated the same way as the camera settings popup: znear is clamped to a minimum, and zfar falls back to its maximum if it would end up too close to or below znear). No-op when no editor is running.
- Arguments:
znear : float
zfar : float
- set_camera_fov(deg: float)¶
Sets the editor camera’s field of view in degrees, instantly (clamped to the same range as the camera settings popup). No-op when no editor is running.
- Arguments:
deg : float
- set_camera_orthographic()¶
Switches the editor camera of the active view to orthographic projection, instantly. No-op when no editor is running.
- set_camera_orthographic_scale(scale: float)¶
Sets the editor camera’s orthographic zoom instantly (clamped to the same range as the camera settings popup). Takes effect immediately, whether or not the camera is currently in orthographic mode. No-op when no editor is running.
- Arguments:
scale : float
- set_camera_perspective()¶
Switches the editor camera of the active view to perspective projection, instantly. No-op when no editor is running.
- set_camera_position(pos: float3)¶
Moves the editor camera to pos instantly. A tool reading the camera right after the call sees the new state.
- Arguments:
pos : float3
- set_camera_rotation(rot: quat4)¶
Rotates the editor camera to rot instantly. The camera pitch is clamped to +-90 degrees, same as when rotating it with the mouse.
- Arguments:
rot : quat4
- set_camera_transform(tm: float3x4)¶
Sets the editor camera position and rotation from a world transform instantly. The scale part is ignored.
- Arguments:
tm : float3x4
Editor actions¶
- batch_actions(cb: block<():void>)¶
Groups editor actions that are each ALREADY undoable on their own (set_selection, snap_to_grid, snap_to_surface, …) into a single undo step: one Ctrl+Z reverts the whole block instead of one action at a time. It does not make anything undoable by itself – raw game-API scene mutations (create_node, set_parent, component writes) register no actions and are NOT captured; wrap those in undoable instead.
Rule of thumb: everything inside is already undoable one-by-one -> batch_actions; the block mutates the scene through the game API -> undoable. batch_actions is free (two markers cross into the editor, no snapshots):
batch_actions() {
for (node in get_selected_nodes()) {
snap_to_surface(node)
}
}
- Arguments:
cb : block<void>
- snap_to_grid(node: NodeId): bool¶
Snaps the node’s world position to the translation grid step from the editor preferences: each axis rounds to the nearest multiple of the step. A step of 0 (grid disabled) leaves the position untouched. Undoable. Returns false when the node is dead:
snap_to_grid(node)
- Arguments:
node : NodeId
- snap_to_surface(node: NodeId): bool¶
Drops the node straight down onto the first surface below it - other nodes’ geometry; the node itself and its children are ignored. Undoable. Returns false when there is nothing below the node:
if (!snap_to_surface(node)) {
print("nothing under the node")
}
- Arguments:
node : NodeId
- undoable(name: string; blk: block<():void>)¶
Makes every scene mutation done inside the block - create_node, set_parent, component and field changes, anything from the game API - ONE undoable editor step. Game-API mutations are not undoable on their own; this is what makes them so: the open prefab is snapshotted before and after the block, and undo/redo restore the snapshots, so nodes created inside the block get their ids back on redo. Needs an open prefab. A block that changes nothing registers nothing.
Rule of thumb: the block mutates the scene through the game API -> undoable; everything inside is already undoable one-by-one (set_selection, snap_to_grid, …) -> batch_actions. The two snapshots serialize the whole prefab, which is not free on a big one - so do not wrap a couple of already-undoable editor actions in undoable, batch_actions does that at no cost:
undoable("scatter") {
for (i in range(10)) {
let node = create_node(NodeData(name = "tree_{i}", parent = parent))
snap_to_surface(node)
}
}
- Arguments:
name : string
blk : block<void>
Tool UI widgets¶
tool_button (name: string; icon: string = “”; selected: bool = false) : bool
tool_float3 (name: string; def_value: float3; speed: float = 0.1f) : float3
tool_slider (name: string; def_value: float; min_v: float; max_v: float) : float
tool_slider (name: string; def_value: int; min_v: int; max_v: int) : int
tool_window (name: string; bg_alpha: float; blk: block<():void>)
- component_tool(component: string; blk: block<():void>)¶
Shows the tool widgets declared inside the block in component’s inspector section, next to the component’s own fields, for the node being inspected. The widgets are per-node: values tuned for one node do not leak into another’s section. component_tool_node() returns the node the section belongs to:
component_tool("Mesh") {
let intensity = tool_slider("intensity", 1., 0., 2.)
if (tool_button("rebuild")) {
rebuild_mesh(component_tool_node(), intensity)
}
}
- Arguments:
component : string
blk : block<void>
- component_tool_node(): NodeId¶
The node the widgets of a component_tool block are shown for: the one currently open in the editor’s inspector. Stays at the last inspected node when the inspector is empty; invalid NodeId until anything was inspected - check with valid() before acting on it.
- tool_button(name: string; icon: string = ""; selected: bool = false): bool¶
Declares a button in the current tool window; returns true once per click. icon is an svg drawn on the button next to the name; start the name with ## for an icon-only button (the name stays the widget’s id). selected draws the button in the pressed style - together with icon-only buttons in a loop that is a tileset picker:
for (i in range(length(tiles))) {
if (tool_button("##tile{i}", tiles[i].icon, g_currentTile == i)) {
g_currentTile = i
}
}
- Arguments:
name : string
icon : string
selected : bool
- tool_checkbox(name: string; def_value: bool): bool¶
Declares a checkbox in the current tool window and returns its current state:
let mirror = tool_checkbox("mirror", false)
- Arguments:
name : string
def_value : bool
- tool_color(name: string; def_value: float3): float3¶
Declares an RGB color field in the current tool window and returns its current value (0..1 per channel). Clicking the color swatch opens the full picker:
let tint = tool_color("tint", float3(1, 0.5, 0.2))
- Arguments:
name : string
def_value : float3
- tool_combo(name: string; def_value: auto(TT)): TT¶
Declares a dropdown over all values of an enum type in the current tool window and returns the chosen value; def_value picks both the enum and the initial selection. The choice persists in the project settings by value name:
enum ScatterMode {
Grid
Random
Poisson
}
let mode = tool_combo("mode", ScatterMode.Random)
- Arguments:
name : string
def_value : auto(TT)
- tool_float3(name: string; def_value: float3; speed: float = 0.1f): float3¶
Declares a float3 drag field (x/y/z, drag any of them to change; speed = value change per pixel dragged) in the current tool window and returns its current value:
let offset = tool_float3("offset", float3(0, 1, 0))
- Arguments:
name : string
def_value : float3
speed : float
- tool_hint(text: string)¶
Attaches a hover tooltip to the widget declared just before this call (any tool_* widget, window or component scope):
let radius = tool_slider("radius", 5., 0.1, 50.)
tool_hint("Scatter radius in meters")
- Arguments:
text : string
- tool_input(name: string; def_value: string): string¶
Declares a text input in the current tool window and returns its current text:
let prefix = tool_input("prefix", "scattered_")
- Arguments:
name : string
def_value : string
- tool_label(name: string; text: string)¶
Shows a line of changing text in the current tool window: the widget is keyed by name, so text may be different every frame. Use it for dynamic readouts (the single-argument overload is for fixed captions):
tool_label("state", "selected={length(get_selected_nodes())}")
- Arguments:
name : string
text : string
- tool_label(text: string)
Shows a line of fixed text in the current tool window:
tool_label("Scatter tool")
- Arguments:
text : string
- tool_new_line()¶
Breaks the current widget row: the following widgets start on a new line.
- tool_progress(name: string; fraction: float)¶
Shows a progress bar in the current tool window. fraction (0..1) is supplied by the tool each frame - the value flows from the code, not from the UI:
tool_progress("baking", float(done) / float(total))
- Arguments:
name : string
fraction : float
- tool_resource(name: string; type_: auto(TT)): TT¶
Declares a resource picker in the current tool window: a field showing the chosen resource plus a Select button opening the editor’s resource popup, filtered to the requested type. The type<…> argument picks both the filter and the return type. The choice persists across sessions (stored as the asset’s guid):
let mesh = tool_resource("mesh", type<MeshId>)
let decal = tool_resource("decal", type<MaterialId>)
- Arguments:
name : string
type_ : auto(TT)
- tool_separator()¶
Inserts a separator between the surrounding widgets (a vertical rule within a row).
- tool_slider(name: string; def_value: float; min_v: float; max_v: float): float¶
Declares a float slider in the current tool window and returns its current value – def_value until first edited; the tuned value persists in the project settings:
let radius = tool_slider("radius", 5., 0.1, 50.)
- Arguments:
name : string
def_value : float
min_v : float
max_v : float
- tool_slider(name: string; def_value: int; min_v: int; max_v: int): int
Declares an int slider in the current tool window and returns its current value:
let count = tool_slider("count", 10, 1, 100)
- Arguments:
name : string
def_value : int
min_v : int
max_v : int
- tool_window(name: string; bg_alpha: float; blk: block<():void>)¶
Like tool_window(name, block), additionally setting the window’s background opacity: bg_alpha in 0..1, where 1 is fully opaque. Also works for “” - the default Tools overlay:
tool_window("Scatter", 0.9) {
...
}
- Arguments:
name : string
bg_alpha : float
blk : block<void>
- tool_window(name: string; blk: block<():void>)
Shows the tool widgets declared inside the block in a floating tool window with this title – a normal draggable, resizable, closable window, listed in the editor’s top Window menu. Widgets declared outside any tool_window block land in the default translucent Tools overlay (”” routes there explicitly). Blocks nest. Example:
tool_window("Scatter") {
let radius = tool_slider("radius", 5., 0.1, 50.)
if (tool_button("scatter")) {
scatter(get_selected_nodes(), radius)
}
}
- Arguments:
name : string
blk : block<void>
Active prefab¶
- prefab_close()¶
Queues closing the prefab currently open in the editor - the prefab tab goes away on the next editor tick, like the play flow closing it before the game starts. Does nothing when no prefab is open:
prefab_close()
- prefab_name(): string¶
The file name of the prefab currently open in the editor, “” when none is open:
tool_label("prefab", prefab_name())
- prefab_open(path: string): bool¶
Queues opening a prefab by its project-relative path; the prefab must exist and have a .meta file. Opening happens on the next editor tick, not immediately. Returns false (and logs why) when the prefab is not found or is a read-only model prefab:
if (prefab_open("prefabs/level1.prefab")) {
print("switching...")
}
- Arguments:
path : string
- prefab_save(): bool¶
Saves the prefab currently open in the editor, exactly like pressing Ctrl+S. Returns false when nothing was saved: no prefab is open, or compilation errors block the save (the editor shows a notification).
Editor and game state¶
- focus_tab(tab: EditorTab): bool¶
Brings the tab to front, like clicking its header. Returns false when the tab is not open (a tab cannot be opened this way, only focused):
if (!focus_tab(EditorTab.Game)) {
print("the game tab is not open")
}
- Arguments:
tab : EditorTab
- game_next_frame(frames: int = 1)¶
Advances the paused game by frames frames (clamped to 1..1024). A stopped game is started paused first - together with game_pause this gives deterministic stepping.
- Arguments:
frames : int
- game_pause(pause: bool)¶
Sets or clears the manual pause flag. It is just a flag: it does not start or stop the game by itself.
- Arguments:
pause : bool
- game_play(play: bool = true): bool¶
Starts (true) or stops (false) the game, like the editor’s Play button. Returns false when starting is blocked by compilation errors (the reason is logged):
if (tool_button("run")) {
game_play()
}
- Arguments:
play : bool
- game_restart(): bool¶
Stops the game (if running) and starts it again - picks up code changes, resets the scene. Returns false when blocked by compilation errors (the reason is logged).
- game_state(): GameState¶
The game as the editor’s Play/Pause buttons see it: Stopped (not started), Paused (started but frozen - manually, by compilation errors or by the user menu; holds in the prefab tab too), or Playing. A stopped game with a leftover pause flag reads as Stopped. Editor tools branch on it; game code should not (and cannot ship with it - this module does not exist outside the editor):
tool_label("state", "{game_state()}")
- gizmo_operation(): GizmoOperation¶
The current viewport gizmo mode.
- is_fullscreen(): bool¶
True while the game view fills the window and the editor UI is hidden (like after F10).
- is_tab_open(tab: EditorTab): bool¶
True when the tab currently exists in the editor (not necessarily focused).
- Arguments:
tab : EditorTab
- set_fullscreen(enable: bool)¶
Hides the editor UI leaving the fullscreen game view (true), or brings the editor back (false).
- Arguments:
enable : bool
- set_gizmo_operation(operation: GizmoOperation)¶
Switches the viewport gizmo mode, like the toolbar buttons or the W/E/R/T/Y hotkeys:
set_gizmo_operation(GizmoOperation.Rotate)
- Arguments:
operation : GizmoOperation
- set_shade_mode(mode: ShadeMode)¶
Switches the viewport shading mode, like the editor’s Options menu. Undoable; the mode persists in the project settings:
set_shade_mode(ShadeMode.Wireframe)
- Arguments:
mode : ShadeMode
- shade_mode(): ShadeMode¶
The current viewport shading mode.
- take_screenshot()¶
Saves a screenshot of the game view to the screenshots folder, like pressing F11.
Work with resources and assets¶
- find_assets_in_folder(folder: string): array<string>¶
Returns the project-relative paths of every recognized asset file (any type) directly inside folder (not descending into subfolders). .meta files and files with an unrecognized extension are skipped. Pass an empty string for the project root:
let levelAssets = find_assets_in_folder("levels/level1")
- Arguments:
folder : string
- find_assets_in_folder_recursive(folder: string): array<string>¶
Same as find_assets_in_folder, but also descends into every subfolder of folder:
let levelAssets = find_assets_in_folder_recursive("levels")
- Arguments:
folder : string
- find_resources(res_type: auto(TT)): array<TT>¶
Returns every alive resource of the requested type - loaded from assets and procedural alike - as an array of that exact typed id. The type<…> argument picks which resources are collected and the element type of the result; it must be a resource id type (MeshId, TextureId, …). Empty when none are alive:
for (mesh in find_resources(type<MeshId>)) {
print("mesh at {get_asset_path(mesh)}")
}
- Arguments:
res_type : auto(TT)
- get_asset_path(id: ResourceId|MeshId|MaterialId|TextureId|SpriteId|SoundId|ModelId|AnimationId|SkeletonId|TextId|FontId|PrefabId|CollisionId|ShaderId|AnimationMaskId): string¶
Returns the project-relative path of the asset file backing the typed resource id (e.g. MeshId, TextureId, PrefabId, …). Returns an empty string if id is invalid or is a procedural resource with no backing asset file:
let path = get_asset_path(meshId)
- Arguments:
id : option< ResourceId| MeshId| MaterialId| TextureId| SpriteId| SoundId| ModelId| AnimationId| SkeletonId| TextId| FontId| PrefabId| CollisionId| ShaderId| AnimationMaskId>
- get_resources_in_asset(path: string): array<ResourceId>¶
Returns every ResourceId currently resolved from the asset at project-relative path. A single asset can produce more than one resource - e.g. a model asset also builds its meshes and materials. Returns an empty array if path does not resolve to a known asset:
let resources = get_resources_in_asset(get_asset_path(meshId))
- Arguments:
path : string