| Header | Link |
|---|---|
| Purpose | Purpose |
| Use Cases | Use Cases |
| Reference | Reference |
Worked, copy-ready .scn fragments for setups that wire several nodes together: scene roots and parenting, live camera and webcam streams, per-placement script vars, animation bindings, render-layer filtering, and matching 2D/3D physics bodies. Reach for these when a single node template is not enough and you want a known-good arrangement to adapt. For node trees a script builds at runtime, use Node Collections.
Use these scene files as the source of truth for fixed topology. Do not move a
known parent/child layout into Rust: .scn composition stays reusable,
reviewable, editor-visible, and available to build tooling. Rust should load
the scene and handle behavior or dynamic decisions around its returned root.
- Understand scene parenting and the root key: Parent And Root (
parent = $root,parent = @Key). - Feed a live in-world camera view onto a surface (CCTV monitor, portal, rear-view mirror):
CameraStream2D/CameraStream3D, see Security Camera Stream. - Show a webcam feed as a texture: Webcam Stream.
- Override per-placement script values from the scene:
script_vars, see Script Vars. - Bind animation clips and players to a node: Animation Bindings.
- Control what each camera sees:
render_layers/render_mask, see Render Layers. - Match equivalent 2D and 3D physics body setups: Physics Parity Templates.
Start from the smallest example that matches the relationship you need, then replace names and injected values with project data. Keep fixed dependencies in script_vars, keep structural dependencies in parent/child links, and use queries only for changing sets. Do not combine unrelated examples into one node merely because their fields parse together.
The runtime boundary stays explicit:
.scn -> create authored subtree + inject fixed refs
Rust -> load/reparent subtree + run state/lifecycle/method logic
Every scene needs one root node.
Set it with $root = @NodeKey.
Every non-root node needs parent.
Use $root for root children or @OtherNode for any other parent.
$root = @Level
[Level]
[Node2D/]
[/Level]
[Player]
parent = $root
script = "res://scripts/player.rs"
[Node2D]
position = (0, 0)
[/Node2D]
[/Player]
[Muzzle]
parent = @Player
[Node2D]
position = (12, 0)
[/Node2D]
[/Muzzle]
Parent sets transform inheritance.
Muzzle moves with Player.
[SecurityCamera]
parent = $root
[Camera3D]
active = false
post_processing = [
{ type = "bloom", intensity = 0.15 }
]
[Node3D]
position = (0, 3, -6)
rotation_deg = (-15, 0, 0)
[/Node3D]
[/Camera3D]
[/SecurityCamera]
[Monitor]
parent = $root
[CameraStream3D]
camera = @SecurityCamera
resolution = (640, 360)
aspect_mode = "fit"
size = (1.6, 0.9)
post_processing = [
{ type = "crt", scanline_strength = 0.35, vignette = 0.2 }
]
[Node3D]
position = (2, 1.2, -2)
[/Node3D]
[/CameraStream3D]
[/Monitor]
Camera post-processing runs first. Stream post-processing runs after.
[FaceCam]
parent = $root
[Webcam]
slot = ""
resolution = (640, 480)
fps = 30
mirror = true
cpu_frames = false
enabled = true
[/Webcam]
[/FaceCam]
[FaceCamView]
parent = $root
[UiCameraStream]
camera = @FaceCam
aspect_mode = "fit"
[UiNode]
anchor = "bottom_right"
size_ratio = (0.25, 0.25)
pivot_ratio = (1, 1)
[/UiNode]
[/UiCameraStream]
[/FaceCamView]
UiCameraStream.camera accepts Camera2D, Camera3D, or Webcam.
When it points at an enabled visible Webcam, capture opens automatically.
script_vars seeds the attached script state when the node is created.
Keys must match pub fields in the script #[State] struct.
Scene:
[Player]
parent = $root
script = "res://scripts/player.rs"
script_vars = {
speed = 8.0
health = 120
target = @Enemy
}
[Node2D/]
[/Player]
[Enemy]
parent = $root
[Node2D/]
[/Enemy]
Script:
use perro_api::prelude::*;
#[State]
pub struct PlayerState {
#[default = 6.0]
pub speed: f32,
#[default = 100]
pub health: i32,
pub target: Option<NodeID>,
}
lifecycle!({
fn on_init(&self, ctx: &mut ScriptContext<'_, API>) {
with_state!(ctx.run, PlayerState, ctx.id, |state| {
// scene script_vars already applied here
let _speed = state.speed;
let _target = state.target;
}).unwrap_or_default();
}
});Values omitted from script_vars use #[default = ...] or type default.
Node refs use @NodeKey.
.panim files store object names.
Scene bindings map those object names to scene nodes.
If animation tracks target object Hero, bind Hero = @PlayerRoot.
[PlayerRoot]
parent = $root
[Node2D/]
[/PlayerRoot]
[PlayerAnim]
parent = @PlayerRoot
[AnimationPlayer]
animation = "res://animations/player_idle.panim"
bindings = { Hero = @PlayerRoot }
speed = 1.0
paused = false
playback = loop
[/AnimationPlayer]
[/PlayerAnim]
AnimationTree uses per-clip bindings.
Each entry can bind same object name to same or different nodes.
[PlayerTree]
parent = @PlayerRoot
[AnimationTree]
tree = "res://animations/player.panimtree"
animations = [
{ animation = "res://animations/idle.panim", bindings = { Hero = @PlayerRoot }, playback = loop, speed = 1.0, paused = false },
{ animation = "res://animations/run.panim", bindings = { Hero = @PlayerRoot }, playback = loop, speed = 1.0, paused = false },
{ animation = "res://animations/aim.panim", bindings = { Hero = @PlayerRoot }, playback = boomerang, speed = 1.0, paused = false }
]
speed = 1.0
paused = false
[/AnimationTree]
[/PlayerTree]
Binding side rule:
left side = animation object name
right side = scene node ref
render_mask belongs to cameras.
render_layers belongs to renderable nodes.
Camera mask hides node layers when they intersect.
Default camera mask is no layers.
Default node render layers is all layers.
Add layers to a camera mask to hide them.
2D example:
$root = @Scene2D
[Scene2D]
[Node2D/]
[/Scene2D]
[GameplayCamera]
parent = $root
[Camera2D]
active = true
render_mask = [2]
[/Camera2D]
[/GameplayCamera]
[PlayerSprite]
parent = $root
[Sprite2D]
texture = "res://textures/player.png"
[Node2D]
render_layers = [1]
[/Node2D]
[/Sprite2D]
[/PlayerSprite]
[EditorOnlySprite]
parent = $root
[Sprite2D]
texture = "res://textures/gizmo.png"
[Node2D]
render_layers = [2]
[/Node2D]
[/Sprite2D]
[/EditorOnlySprite]
GameplayCamera sees PlayerSprite.
It skips EditorOnlySprite.
3D example:
$root = @Scene3D
[Scene3D]
[Node3D/]
[/Scene3D]
[MainCamera]
parent = $root
[Camera3D]
active = true
render_mask = [2]
[/Camera3D]
[/MainCamera]
[LevelMesh]
parent = $root
[MeshInstance3D]
mesh = "res://models/level.glb:mesh[0]"
material = "res://materials/level.pmat"
[Node3D]
render_layers = [1]
[/Node3D]
[/MeshInstance3D]
[/LevelMesh]
[ReflectionOnlyMesh]
parent = $root
[MeshInstance3D]
mesh = "res://models/reflection_proxy.glb:mesh[0]"
material = "res://materials/proxy.pmat"
[Node3D]
render_layers = [2]
[/Node3D]
[/MeshInstance3D]
[/ReflectionOnlyMesh]
MainCamera sees layer 1 and 3.
It skips layer 2.
Current body layer/mask fields:
collision_layers tags a body/area.
collision_mask says which tagged layers it ignores.
Default body/area layers is all layers.
Default body/area mask is no layers.
Collision requires neither side to ignore the other.
Empty mask ([]) means ignore nothing.
$root = @Physics2D
[Physics2D]
[Node2D/]
[/Physics2D]
[Body]
parent = $root
[RigidBody2D]
collision_layers = [1]
collision_mask = []
[/RigidBody2D]
[/Body]
[BodyShape]
parent = @Body
[CollisionShape2D]
shape = { type = "quad" width = 1 height = 1 }
[/CollisionShape2D]
[/BodyShape]
2D joints:
[AnchorBody]
parent = $root
[StaticBody2D]
collision_layers = [1]
collision_mask = []
[/StaticBody2D]
[/AnchorBody]
[AnchorBodyShape]
parent = @AnchorBody
[CollisionShape2D]
shape = { type = "quad" width = 1 height = 1 }
[/CollisionShape2D]
[/AnchorBodyShape]
[SwingBody]
parent = $root
[RigidBody2D]
collision_layers = [1]
collision_mask = []
[/RigidBody2D]
[/SwingBody]
[SwingBodyShape]
parent = @SwingBody
[CollisionShape2D]
shape = { type = "quad" width = 1 height = 1 }
[/CollisionShape2D]
[/SwingBodyShape]
[rope_pin]
parent = $root
[PinJoint2D]
body_a = @AnchorBody
body_b = @SwingBody
anchor_a = (0, 0)
anchor_b = (0, 0.5)
enabled = true
collide_connected = false
[/PinJoint2D]
[/rope_pin]
[distance_link]
parent = $root
[DistanceJoint2D]
body_a = @AnchorBody
body_b = @SwingBody
anchor_a = (0, 0)
anchor_b = (0, 0)
min_distance = 0
max_distance = 2
enabled = true
collide_connected = false
[/DistanceJoint2D]
[/distance_link]
[fixed_link_2d]
parent = $root
[FixedJoint2D]
body_a = @AnchorBody
body_b = @SwingBody
anchor_a = (0, 0)
anchor_b = (0, 0)
enabled = true
collide_connected = false
[/FixedJoint2D]
[/fixed_link_2d]
3D joints:
$root = @Physics3D
[Physics3D]
[Node3D/]
[/Physics3D]
[FrameBody]
parent = $root
[StaticBody3D]
collision_layers = [1]
collision_mask = []
[/StaticBody3D]
[/FrameBody]
[FrameBodyShape]
parent = @FrameBody
[CollisionShape3D]
shape = { type = cube, size = (1, 1, 1) }
[/CollisionShape3D]
[/FrameBodyShape]
[DoorBody]
parent = $root
[RigidBody3D]
collision_layers = [1]
collision_mask = []
[/RigidBody3D]
[/DoorBody]
[DoorBodyShape]
parent = @DoorBody
[CollisionShape3D]
shape = { type = cube, size = (1, 2, 0.2) }
[/CollisionShape3D]
[/DoorBodyShape]
[ball_socket]
parent = $root
[BallJoint3D]
body_a = @FrameBody
body_b = @DoorBody
anchor_a = (0, 1, 0)
anchor_b = (-0.5, 1, 0)
enabled = true
collide_connected = false
[/BallJoint3D]
[/ball_socket]
[door_hinge]
parent = $root
[HingeJoint3D]
body_a = @FrameBody
body_b = @DoorBody
anchor_a = (0, 1, 0)
anchor_b = (-0.5, 1, 0)
axis = (0, 1, 0)
enabled = true
collide_connected = false
[/HingeJoint3D]
[/door_hinge]
[fixed_link_3d]
parent = $root
[FixedJoint3D]
body_a = @FrameBody
body_b = @DoorBody
anchor_a = (0, 0, 0)
anchor_b = (0, 0, 0)
enabled = true
collide_connected = false
[/FixedJoint3D]
[/fixed_link_3d]