Spawning Godot scenes with NodeTreeView
GodotScene is a Bevy Component that lets us attach and instantiate Godot scene files (.tscn) to Bevy entities.
When we add a GodotScene to an entity, it spawns that scene in Godot's scene tree and links it to our Bevy entity,
letting us combine Godot's visual editor with Bevy's ECS architecture.
When we spawn scenes, we almost always need to reach into that scene’s node tree to:
- grab child nodes like sprites, notifiers, or UI controls
- connect signals
- drive animations or physics bodies
Doing this manually with raw GodotNodeHandle lookups quickly becomes repetitive and fragile.
The #[derive(NodeTreeView)] macro gives us a typed, ergonomic view of a scene’s node tree, driven by node paths.
This page explains how to:
- Define a
NodeTreeViewfor a scene - Spawn the scene via
GodotScene - Use the generated view to access nodes and connect signals
1. Spawn Godot scenes with GodotScene
To spawn a Godot scene from Bevy, insert GodotScene into our entity:
use bevy::prelude::*;
use godot_bevy::prelude::{GodotResource, GodotScene};
use bevy::state::app::StatesPlugin;
use bevy_asset_loader::asset_collection::AssetCollection;
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, States)]
enum GameState {
#[default]
Loading,
InGame,
}
fn plugin(app: &mut App) {
app.add_plugins(StatesPlugin)
.init_state::<GameState>()
.add_loading_state(
LoadingState::new(GameState::Loading).continue_to_state(GameState::InGame),
);
app.configure_loading_state(
LoadingStateConfig::new(GameState::Loading).load_collection::<PickupAssets>(),
);
app.add_message::<PickupBodyEntered>();
app.add_systems(Update, pickup_system);
}
/// This example uses `bevy_asset_loader` to load the
/// scene file as a packed scene at startup.
#[derive(AssetCollection, Resource)]
pub struct CharacterAssets {
#[asset(path = "scenes/character.tscn")]
pub character_scene: Handle<GodotResource>,
}
fn spawn_character(mut commands: Commands, assets: Res<CharacterAssets>) {
commands
.spawn_empty()
// Add additional Bevy components here (e.g. position, gameplay data, etc.)
.insert(Transform::default())
.insert(
// Attach the Godot scene to this Bevy entity
GodotScene::from_handle(assets.character_scene.clone())
// Optionally, connect signals here with the
// `with_signal_connection` builder method discussed below.
);
}
Scene instantiation happens later in PostUpdate, once the asset is loaded.
from_handle retries each frame while the handle is loading, preserving setups
and signal connections until then. Failed loads and missing assets return a spawn error.
GodotNodeHandle is added to the entity after instantiation, so trying to modify
it immediately after commands.spawn(...) will find no handle.
Configure the scene root with with_setup
Use with_setup to configure the root before it enters the scene tree. With
GodotAssetsPlugin and GodotPackedScenePlugin enabled, register this plugin
on your app; it spawns a scene whose root is an Area2D and sets its collision
mask to layer 3 (4):
#![allow(unused)] fn main() { use bevy::prelude::*; use godot::classes::Area2D; use godot_bevy::prelude::*; pub fn bullet_plugin(app: &mut App) { app.add_systems(Startup, spawn_bullet); } fn spawn_bullet(mut commands: Commands, assets: Res<AssetServer>) { commands.spawn( GodotScene::from_handle(assets.load("scenes/bullet.tscn")).with_setup::<Area2D, _>( |root| { root.set_collision_mask(4); }, ), ); } }
The closure runs once after the entity's Transform is applied and before the
root enters the tree via add_child, so _ready and scene-tree mirroring see
the configured values. Chained setups run in the order added. The root must
inherit the class you specify; a mismatch returns a spawn error naming the
scene, entity, expected class, and actual class, and removes
GodotScene to cancel the spawn.
Configure nodes with ECS data
Use an On<Add<GodotNodeHandle>> observer when setup needs ECS queries or
resources, or when configuring nodes other than the root. The observer runs
after the root has entered the tree and _ready has run. This example reads
the collision mask from a component:
#![allow(unused)] fn main() { use bevy::prelude::*; use godot::classes::Area2D; use godot_bevy::prelude::*; #[derive(Component)] pub struct Bullet { pub collision_mask: u32, } pub fn bullet_plugin(app: &mut App) { app.add_observer(configure_bullet) .add_systems(Startup, spawn_bullet); } fn spawn_bullet(mut commands: Commands, assets: Res<AssetServer>) { commands.spawn(( Bullet { collision_mask: 4 }, GodotScene::from_handle(assets.load("scenes/bullet.tscn")), )); } fn configure_bullet( event: On<Add<GodotNodeHandle>>, bullets: Query<(&Bullet, &GodotNodeHandle)>, mut godot: GodotAccess, ) { if let Ok((bullet, handle)) = bullets.get(event.entity) { let mut root = godot.get::<Area2D>(*handle); root.set_collision_mask(bullet.collision_mask); } } }
The Bullet query limits the observer to entities with that component.
GodotAccess resolves the handle to the scene's root node; use NodeTreeView
below to reach its children.
2. Access scene children with NodeTreeView
Let's assume we have a Godot scene with the following node structure:
Node2D(the “character”)AnimatedSprite2DVisibleOnScreenNotifier2D
We can describe the nodes of the scene we want to access by their path like so:
use godot_bevy::interop::GodotNodeHandle;
use godot_bevy::prelude::NodeTreeView;
#[derive(NodeTreeView)]
pub struct CharacterNodes {
#[node("AnimatedSprite2D")]
pub animated_sprite: GodotNodeHandle,
#[node("VisibleOnScreenNotifier2D")]
pub visibility_notifier: GodotNodeHandle,
}
The NodeTreeView field types can be GodotNodeHandle or Option<GodotNodeHandle>.
The #[node("<node_path>")] attribute supports wildcards (*).
See below or the NodeTreeView docs for more details.
Then we can access the tree view in our system like this:
fn new_character_initialize(
entities: Query<&GodotNodeHandle, Added<Character>>,
mut godot: GodotAccess,
) {
for handle in &entities {
let character = godot.get::<RigidBody2D>(*handle);
let character_nodes = CharacterNodes::from_node(character).unwrap();
}
}
Path patterns
Node paths support simple patterns to avoid hard-coding full names:
/root/*/HUD/CurrentLevel- matches any single node name where * appears/root/Level*/HUD/CurrentLevel- matches node names starting with "Level"*/HUD/CurrentLevel- matches relative to the base node
Generated path constants
For each #[node("<node_path>")] field, NodeTreeView generates a public string constant named
<UPPERCASE_FIELD_NAME>_PATH inside our struct’s impl.
Given the CharacterNodes example above, the macro generates an impl like:
impl CharacterNodes {
pub const ANIMATED_SPRITE_PATH: &'static str = "AnimatedSprite2D";
pub const VISIBILITY_NOTIFIER_PATH: &'static str = "VisibleOnScreenNotifier2D";
}
These constants are very convenient when we need to refer to the same path in multiple places, especially when connecting signals from a spawned scene (covered below).
3. Connect signals to scene children using GodotScene::with_signal_connection
When spawning scenes, we often want to connect signals to child nodes.
There are three useful resources when connecting signals:
GodotScene'swith_signal_connectionbuilder method.NodeTreeView's generated path constants.godot_bevy::interop::<GODOT_NODE_TYPE>Signalstypes which contain string constants for all signals of a given Godot node type.
Here is an example using the CharacterNodes NodeTreeView from above and
the VisibleOnScreenNotifier2DSignals::SCREEN_EXITED string constant to
connect the VisibleOnScreenNotifier2D's screen_exited signal to a Bevy message.
use godot_bevy::interop::VisibleOnScreenNotifier2DSignals;
use godot_bevy::prelude::GodotScene;
use bevy::ecs::entity::Entity;
use bevy::prelude::Message;
#[derive(Message, Debug, Clone, Copy)]
pub struct CharacterScreenExited {
pub entity: Entity,
}
fn spawn_character_with_signals(mut commands: Commands, assets: Res<CharacterAssets>) {
commands
.spawn_empty()
.insert(Transform::default())
.insert(
GodotScene::from_handle(assets.character_scene.clone())
.with_signal_connection(
// Use the NodeTreeView-generated path constant:
CharacterNodes::VISIBILITY_NOTIFIER_PATH,
// The Godot signal we want to connect:
VisibleOnScreenNotifier2DSignals::SCREEN_EXITED,
// Closure to turn a Godot signal into a Bevy message:
|_args, _node_handle, entity| {
Some(CharacterScreenExited {
entity: entity.expect("entity was provided"),
})
},
),
);
}