renzora
Game Engine

Visual Blueprints

Wire up entity logic with a node graph instead of code — saved as a .blueprint file and interpreted directly by the engine at runtime.

What a blueprint is

A blueprint is a visual node graph stored as a BlueprintGraph. On disk it is a .blueprint file (the .bp extension is an accepted alias) containing JSON — a list of nodes and the wires (connections) between their pins. The same graph type is also a regular ECS component, so blueprints serialize straight into a scene's .ron alongside everything else on the entity.

The system lives in the renzora_blueprint crate. Its BlueprintPlugin registers itself with renzora::add!(BlueprintPlugin) at runtime scope, so blueprints run in both the editor (play mode) and exported games — there is nothing extra to enable.

Every frame that scripts are running, the interpreter (interpreter::run_blueprints) walks each entity that has a BlueprintGraph, starting from its event nodes, and emits engine actions — ScriptActions, transform writes, and character commands — that the same physics/audio/UI/animation systems consume from Lua and Rhai scripts.

Blueprints are interpreted directly — they are not compiled to Lua (or to any bytecode) before they run. The graph is walked live each tick. The old claim that "blueprints compile to the same internal representation as scripts with no performance difference" is wrong: they are a distinct, interpreted execution path.

Blueprints vs. text scripts

Blueprints and text scripts (Lua / Rhai) are separate systems that happen to share the same downstream action plumbing:

  • A blueprint is not a 1:1 visual mirror of the scripting API. It exposes its own, smaller node palette (listed below). Anything outside that palette has to be done in Lua or Rhai.
  • A single entity can carry both a BlueprintGraph component and a ScriptComponent — they run side by side and write to the same world.
  • Blueprint event nodes mirror only a subset of the script lifecycle hooks; there is no blueprint equivalent of, for example, on_rpc or on_http.

Editor-only "bake to Lua". The editor can export a graph to a Lua file (apply_blueprint_to_luascripts/bp_<name>.lua) and attach it as a script. This is a one-way convenience for reading/extending the generated code — it is not how blueprints normally execute. The shipped runtime always interprets the BlueprintGraph directly.

The Blueprint Editor

Blueprints are edited in the Blueprints workspace (one of the editor's ribbon workspaces) using the Blueprint Editor panel. It works in two modes:

  • Scene mode (default) — the editor edits the BlueprintGraph component on the currently selected entity. The graph follows your selection and is saved as part of the scene.
  • Asset mode — a standalone .blueprint file is open in a document tab; edits are written back to that file. Open one by double-clicking a .blueprint in the Assets browser.

To create a new blueprint, use the Assets browser's New → Blueprint entry (it creates NewBlueprint.blueprint), then either open it in Asset mode or add a BlueprintGraph to an entity and author it in Scene mode.

The toolbar has Add Node, Auto Layout (re-arranges the graph into tidy dependency-ordered columns), and Apply (compile to Lua — scene mode only).

Adding nodes (search palette)

Nodes are added through a searchable palette grouped by category:

  • Right-click empty graph, or press Spacebar with the cursor over the graph, to open the palette at the cursor — the new node spawns where you clicked.
  • The toolbar Add Node button opens the same palette under the button.
  • Drag a cable off a pin and release on empty graph to open the palette — the node you pick is auto-wired to that pin (to its best-matching compatible pin).
  • Type to filter; Enter places the first match. Click a category in the sidebar to jump to it.

Comments / groups

Select one or more nodes and press C to wrap them in a comment box (the engine's group construct). Drag the box's body to move it and every node it encloses together; drag the bottom-right grip to resize it; edit the title in its header; click to delete it (the nodes inside are kept). Comments are purely visual — never compiled — and are saved with the graph.

Attaching a blueprint to an entity

A blueprint runs when its entity has a BlueprintGraph component (the equivalent of a script's ScriptComponent). Three ways to get one onto an entity:

  • Drag a .blueprint file from the Assets browser onto the entity in the viewport — the graph loads from the file and attaches to the entity under the cursor (its nearest named ancestor). This is the quickest way to run a saved blueprint.
  • Inspector → Add Component → Blueprint — inserts an empty BlueprintGraph to author in Scene mode.
  • Author directly in Scene mode (selecting the entity, then Add Node creates the component).

Pins and wires

Every node has typed pins. Connections come in two flavours:

  • Execution pins (Exec) — the white "flow" wires. An output exec pin can fan out to several targets; flow runs left to right from an event node.
  • Data pins — carry a value. A data input accepts exactly one wire; if nothing is connected it falls back to the node's inline constant, then to the pin's default.

Data pins use the PinType enum:

Pin typeNotes
ExecExecution flow (not a value)
Float32-bit float
Int32-bit signed integer
Booltrue / false
StringUTF-8 text
Vec22-component vector
Vec33-component vector
ColorRGBA (4 floats)
Entityreference to an entity (resolved by name at runtime)
Anywildcard — accepts any non-exec type

The editor allows these implicit conversions when wiring mismatched data pins (PinType::compatible):

  • Int → Float
  • Float → Vec2 / Vec3 / Color
  • Vec3 ↔ Color
  • Bool → Int / Float
  • any non-exec type ↔ Any

Node categories

The palette is organised into these categories (in editor display order). Node-type strings are namespaced (category/name, e.g. transform/set_position).

CategoryWhat's in it (examples)
EventEntry points — on_ready, on_update, on_collision_enter
Flowbranch, sequence, do_once, flip_flop, gate, delay, counter, start_timer, for_loop, while_loop, switch_int, switch_string, call_event, send_message
Matharithmetic (adddivide, modulo, pow, square), full trig (sin/cos/tan, asin/acos/atan/atan2), sqrt/exp/ln/log10, abs/sign/floor/ceil/round/fract/trunc, clamp/saturate/lerp/inverse_lerp/map_range, min/max, step/smoothstep, move_toward, wrap_angle, deg2rad/rad2deg, pi/tau, select (data-side ternary), logic (and/or/not/compare), Vec3 distance/dot/cross/normalize, combine_vec3/split_vec3
Vectormake_vec2/break_vec2, vec2_length/vec2_scale/vec2_add/vec2_normalize/vec2_dot, vec3_add/vec3_sub/vec3_scale/vec3_length/vec3_lerp
Actioncall, call_on — fire any named ScriptAction (generic escape hatch, like scripting's action())
Stringconcat, format, to_float, to_int
Convertto_string, to_float, to_int, to_bool
Transformget_position/set_position, translate, get_rotation/set_rotation, rotate, look_at, set_scale, get_forward
Inputget_movement, is_key_pressed, is_key_just_pressed, get_mouse_position, is_mouse_pressed, get_gamepad, is_action_pressed, get_action_axis/get_action_axis2d
Entityget_self, get_entity, spawn, despawn, despawn_self
Componentget_field, set_field (reflection-based, any registered component)
Physicsapply_force, apply_impulse, set_velocity, raycast, kinematic_slide, is_grounded, get_velocity
Audioplay_sound, play_music, stop_music
UIshow/hide/toggle, set_text, set_progress, set_health, set_slider, set_checkbox, set_toggle, set_visible, set_theme, set_color
Sceneload
Variableget, set (per-instance graph variables)
Renderingset_visibility, set_material_color
Animationplay, crossfade, stop/pause/resume, set_speed, set_param/set_bool_param/trigger, set_layer_weight, tween_position, plus reads (get_time, get_length, is_playing)
Networkis_server, is_connected, send_message, spawn
Lifecycleon_scene_loaded, global_get, global_set
Navigationset_destination, clear_destination, has_path, has_target, is_at_destination, distance_to_destination
Debuglog, draw_line

Loops (for_loop, while_loop) run their body to completion within one frame (capped at 100k iterations as a hang guard); use flow/counter instead when you want one step per frame. Selection is flow/branch (bool) or flow/switch_int / flow/switch_string. Reusable subgraphs are event/custom + flow/call_event. There is still no array/list pin type, so there is no ForEach-over-collection node.

The full per-node pin reference lives in the Blueprint Node API. To add your own node types, see Custom Blueprint Nodes.

Event nodes

Event nodes are the graph's entry points — they have an exec output but no exec input. The interpreter starts a flow at each of them when its trigger condition is met.

Node typeDisplay nameFires whenOutputs
event/on_readyOn ReadyThe entity is first initialized (once per play session)
event/on_updateOn UpdateEvery framedelta, elapsed
event/on_collision_enterOn Collision EnterThis entity starts colliding with anotherother (Entity)
event/on_collision_exitOn Collision ExitA collision endsother (Entity)
event/on_timerOn TimerA named timer completes
event/on_messageOn MessageA named message arrives (UI, scripts, other blueprints)
animation/on_finishedOn Animation FinishedA non-looping clip finishesname (Clip Name)
network/on_messageOn MessageA named network message is receiveddata, sender (Sender ID)
lifecycle/on_scene_loadedOn Scene LoadedA scene finishes loadingscene (path)

Runtime status. The interpreter drives on_ready, on_update, on_timer, on_message, on_collision_enter/on_collision_exit, animation/on_finished, and lifecycle/on_scene_loaded. on_ready re-fires whenever play mode (re)starts. Collision events come from CollisionReadState (populated by renzora_physics from Avian contact pairs) and report one other entity per frame. network/on_message is still palette-only.

Variables

Blueprint variables are read and written with the Variable nodes (variable/get, variable/set). They are stored per blueprint instance in the interpreter's runtime state and reset when play mode restarts, so a freshly started entity always begins from a clean slate. Reference a variable by its string name from any node in the graph.

Data and execution flow

Two things happen as the interpreter walks a graph:

  • Execution flows forward along exec wires (exec/then/true/false/...). Action nodes (Set Position, Play Sound, Apply Force, ...) emit their engine action when reached.
  • Data is pulled: when an action node needs an input value, the interpreter evaluates the upstream data node feeding that pin (and caches the result for the rest of the tick). Pure data nodes (Math, Get Position, Is Grounded, ...) have no exec pins and only run when something downstream asks for their output.

File format

A .blueprint is JSON. The top level is the BlueprintGraph (nodes, connections, next_id); each node carries its id, node_type, editor position, and any inline input_values; each connection links one node's output pin to another's input pin:

{
  "nodes": [
    { "id": 1, "node_type": "event/on_update", "position": [0.0, 0.0], "input_values": {} },
    {
      "id": 2,
      "node_type": "transform/rotate",
      "position": [240.0, 0.0],
      "input_values": { "degrees": { "Vec3": [0.0, 90.0, 0.0] } }
    }
  ],
  "connections": [
    { "from_node": 1, "from_pin": "exec", "to_node": 2, "to_pin": "exec" }
  ],
  "next_id": 3
}

You normally never hand-edit this — the Blueprint Editor reads and writes it for you — but because it is plain JSON it diffs and merges in version control like any other text asset.

See also