Signal Handling
Godot signals are a core communication mechanism in the Godot engine, allowing nodes to notify other parts of the game when events occur. godot-bevy bridges Godot signals into Bevy's event system, enabling ECS systems to respond to UI interactions, collision events, and other Godot-specific events.
How Signal Bridging Works
When you connect a Godot signal through godot-bevy, the signal is automatically converted into a GodotSignal
event that can be read by Bevy systems using EventReader<GodotSignal>
. This includes support for signals with arguments - the signal arguments are preserved and passed along with the event.
Basic Signal Connection
To connect to a Godot signal, use the GodotSignals
resource to connect to any node's signal:
#![allow(unused)] fn main() { use bevy::prelude::*; use godot_bevy::prelude::*; fn connect_signals( mut scene_tree: SceneTreeRef, signals: GodotSignals, ) { if let Some(root) = scene_tree.get().get_root() { if let Some(button) = root.try_get_node_as::<Button>("UI/MyButton") { let mut handle = GodotNodeHandle::from_instance_id(button.instance_id()); signals.connect(&mut handle, "pressed"); } } } }
Reading Signal Events
Once connected, signals become GodotSignal
events that you can read in any Bevy system:
#![allow(unused)] fn main() { fn handle_signals(mut signal_events: EventReader<GodotSignal>) { for signal in signal_events.read() { match signal.name.as_str() { "pressed" => { println!("Button was pressed!"); } "toggled" => { println!("Toggle button changed state"); } _ => {} } } } }
Signals with Arguments
Many Godot signals carry arguments that provide additional context about the event. godot-bevy preserves these arguments and makes them available through the arguments
field:
#![allow(unused)] fn main() { fn handle_input_signals(mut signal_events: EventReader<GodotSignal>) { for signal in signal_events.read() { if signal.name == "input_event" { println!("Received input_event signal with {} arguments", signal.arguments.len()); // CollisionObject2D.input_event has 3 arguments: viewport, event, shape_idx if signal.arguments.len() >= 2 { // The second argument is the InputEvent let event_arg = &signal.arguments[1]; // Parse the event argument to determine event type if event_arg.value.contains("InputEventMouseButton") { println!("Mouse button event detected!"); if event_arg.value.contains("pressed=true") { if event_arg.value.contains("button_index=1") { println!("Left mouse button clicked!"); } else if event_arg.value.contains("button_index=2") { println!("Right mouse button clicked!"); } } } else if event_arg.value.contains("InputEventMouseMotion") { println!("Mouse motion over area"); } } } } } }
Signal Arguments Structure
Signal arguments are provided as a Vec<SignalArgument>
where each SignalArgument
has:
value
: AString
representation of the argument's value- Additional metadata about the argument type (implementation details may vary)
For complex signal arguments like InputEvent
, you'll typically need to parse the value
string to extract the information you need, as shown in the examples above.
Common Signal Patterns
UI Signals
#![allow(unused)] fn main() { // Button pressed if signal.name == "pressed" { println!("Button clicked!"); } // CheckBox toggled if signal.name == "toggled" && signal.arguments.len() > 0 { let pressed = signal.arguments[0].value.contains("true"); println!("Checkbox is now: {}", if pressed { "checked" } else { "unchecked" }); } // LineEdit text changed if signal.name == "text_changed" && signal.arguments.len() > 0 { println!("Text changed to: {}", signal.arguments[0].value); } }
Physics Signals
For physics-related events like collisions, godot-bevy provides dedicated resources that are more efficient than signals. Instead of connecting to physics signals, use the Collisions
resource:
#![allow(unused)] fn main() { // Instead of using signals for collision detection, use the Collisions resource fn check_player_death( mut player: Query<(&mut GodotNodeHandle, &Collisions), With<Player>>, mut next_state: ResMut<NextState<GameState>>, ) { if let Ok((mut player, collisions)) = player.single_mut() { if collisions.colliding().is_empty() { return; } player.get::<Node2D>().set_visible(false); next_state.set(GameState::GameOver); } } }
The Collisions
resource provides direct access to collision state without the overhead of signal processing, making it ideal for gameplay-critical physics events.
For non-gameplay physics events that need custom data, signals are still appropriate:
#![allow(unused)] fn main() { // Custom physics events that carry additional data if signal.name == "projectile_hit" && signal.arguments.len() > 0 { let damage = signal.arguments[0].value.parse::<f32>().unwrap_or(0.0); println!("Projectile hit for {} damage", damage); } }
Best Practices
1. One-time Connection Setup
Use a resource or local state to ensure signals are connected only once:
#![allow(unused)] fn main() { #[derive(Resource, Default)] struct SignalConnectionState { connected: bool, } fn setup_signals( mut state: ResMut<SignalConnectionState>, // ... other parameters ) { if !state.connected { // Connect signals state.connected = true; } } }
2. Signal Name Matching
Use string matching or consider creating an enum for frequently used signals:
#![allow(unused)] fn main() { #[derive(Debug, PartialEq)] enum GameSignal { ButtonPressed, PlayerHit, AreaEntered, Unknown(String), } impl From<&str> for GameSignal { fn from(name: &str) -> Self { match name { "pressed" => Self::ButtonPressed, "player_hit" => Self::PlayerHit, "body_entered" => Self::AreaEntered, other => Self::Unknown(other.to_string()), } } } }