| 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.
- 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.
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]