Signal Handling

Godot signals are a core communication mechanism in the Godot engine. godot-bevy bridges those signals into Bevy events so your ECS systems can react to UI, gameplay, and scene-tree events in a type-safe way.

This page focuses on the typed signals API (recommended). A legacy API remains available but is deprecated; see the Legacy section below.

Outline

Quick Start

  1. Define a Bevy message for your case:
#![allow(unused)]
fn main() {
use bevy::prelude::*;
use godot_bevy::prelude::*;

#[derive(Message, Debug, Clone)]
struct StartGameRequested;
}
  1. Register the typed plugin for your message type:
#![allow(unused)]
fn main() {
fn build_app(app: &mut App) {
    app.add_plugins(GodotTypedSignalsPlugin::<StartGameRequested>::default());
}
}
  1. Connect a Godot signal and map it to your message:
#![allow(unused)]
fn main() {
fn connect_button(
    mut buttons: Query<&mut GodotNodeHandle, With<Button>>, 
    typed: TypedGodotSignals<StartGameRequested>,
) {
    for mut handle in &mut buttons {
        typed.connect_map(&mut handle, "pressed", None, |_args, _node, _ent| Some(StartGameRequested));
    }
}
}
  1. Listen for the message anywhere:
#![allow(unused)]
fn main() {
fn on_start(mut ev: MessageReader<StartGameRequested>) {
    for _ in ev.read() {
        // Start the game!
    }
}
}

Multiple Typed Events

Use one plugin per event type. You can map the same Godot signal to multiple typed events if you like:

#![allow(unused)]
fn main() {
#[derive(Message, Debug, Clone)] struct ToggleFullscreen;
#[derive(Message, Debug, Clone)] struct QuitRequested { source: GodotNodeHandle }

fn setup(app: &mut App) {
    app.add_plugins(GodotTypedSignalsPlugin::<ToggleFullscreen>::default())
       .add_plugins(GodotTypedSignalsPlugin::<QuitRequested>::default());
}

fn connect_menu(
    mut menu: Query<(&mut GodotNodeHandle, &MenuTag)>,
    toggle: TypedGodotSignals<ToggleFullscreen>,
    quit: TypedGodotSignals<QuitRequested>,
) {
    for (mut button, tag) in &mut menu {
        match tag {
            MenuTag::Fullscreen => {
                toggle.connect_map(&mut button, "pressed", None, |_a, _n, _e| Some(ToggleFullscreen));
            }
            MenuTag::Quit => {
                quit.connect_map(&mut button, "pressed", None, |_a, n, _e| Some(QuitRequested { source: n.clone() }));
            }
        }
    }
}
}

Passing Context (Node, Entity, Arguments)

The mapper closure receives:

  • args: &[Variant]: raw Godot arguments (clone if you need detailed parsing)
  • node: &GodotNodeHandle: emitting node; clone into your event if useful
  • entity: Option<Entity>: Bevy entity if you passed Some(entity) to connect_map

Example adding the entity:

#![allow(unused)]
fn main() {
#[derive(Message, Debug, Clone, Copy)]
struct AreaExited(Entity);

fn connect_area(
    mut q: Query<(Entity, &mut GodotNodeHandle), With<Area2D>>, 
    typed: TypedGodotSignals<AreaExited>,
) {
    for (entity, mut area) in &mut q {
        typed.connect_map(&mut area, "body_exited", Some(entity), |_a, _n, e| Some(AreaExited(e.unwrap())));
    }
}
}

Deferred Connections

When spawning entities before their GodotNodeHandle is ready, you can defer connections. Add TypedDeferredSignalConnections<T> with a signal-to-event mapper; the GodotTypedSignalsPlugin<T> wires it once the handle appears.

#![allow(unused)]
fn main() {
#[derive(Component)] struct MyArea;
#[derive(Message, Debug, Clone, Copy)] struct BodyEntered(Entity);

fn setup(app: &mut App) {
    app.add_plugins(GodotTypedSignalsPlugin::<BodyEntered>::default());
}

fn spawn_area(mut commands: Commands) {
    commands.spawn((
        MyArea,
        // Defer until GodotNodeHandle is available on this entity
        TypedDeferredSignalConnections::<BodyEntered>::with_connection("body_entered", |_a, _n, e| Some(BodyEntered(e.unwrap()))),
    ));
}
}

Attaching signals to Godot scenes

When spawning an entity associated with a Godot scene, you can schedule signals to be connected to children of the scene once the scene is spawned. When inserting a GodotScene resource, use the with_signal_connection builder method to schedule connections.

The method arguments are similar to other typed signal constructors such as connect_map:

  • node_path - Path relative to the scene root (e.g., "VBox/MyButton" or "." for root node). Argument supports the same syntax as Node.get_node.
  • signal_name - Name of the Godot signal to connect (e.g., "pressed").
  • mapper - Closure that maps signal arguments to your typed message.
    • The closure receives three arguments: args, node_handle, and entity:
      • args: &[Variant]: raw Godot arguments (clone if you need detailed parsing).
      • node_handle: &GodotNodeHandle: emitting node; clone into your event if useful.
      • entity: Option<Entity>: Bevy entity the GodotScene component is attached to (Always Some).
    • The closure returns an optional Bevy Message, or None to not send the message.
impl Command for SpawnPickup {
    fn apply(self, world: &mut World) -> () {
        let assets = world.get_resource::<PickupAssets>().cloned();

        let mut pickup = world.spawn_empty();
        pickup
            .insert(Name::new("Pickup"))
            .insert(Transform::from_xyz(200.0, 200.0, 0.0));

        // Only insert GodotScene if Godot engine is running; useful when running tests without Godot.
        if let Some(assets) = assets {
            pickup.insert(
                GodotScene::from_handle(assets.scene.clone())
                
                    // Schedule the "area_entered" signal on the Area2D child
                    // to be connected to PickupAreaEntered message
                    .with_signal_connection(
                        "Area2D",
                        "area_entered",
                        |_args, _handle, _entity| {
                            // Pickup "area_entered" signal mapped
                            Some(PickupAreaEntered)
                        },
                ),
            );
        }
    }
}

Untyped Legacy API (Deprecated)

The legacy API (GodotSignals, GodotSignal, connect_godot_signal) remains available but is deprecated. Prefer the typed API above. Minimal usage for migration:

#![allow(unused)]
fn main() {
fn connect_legacy(mut q: Query<&mut GodotNodeHandle, With<Button>>, legacy: GodotSignals) {
    for mut handle in &mut q { legacy.connect(&mut handle, "pressed"); }
}

fn read_legacy(mut ev: MessageReader<GodotSignal>) {
    for s in ev.read() {
        if s.name == "pressed" { /* ... */ }
    }
}
}

For physics signals (collisions), use the collisions plugin/events instead of raw signals when possible.