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:

  1. Define a NodeTreeView for a scene
  2. Spawn the scene via GodotScene
  3. 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”)
    • AnimatedSprite2D
    • VisibleOnScreenNotifier2D

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's with_signal_connection builder method.
  • NodeTreeView's generated path constants.
  • godot_bevy::interop::<GODOT_NODE_TYPE>Signals types 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"),
                        })
                    },
                ),
        );
}