Skip to main content

godot_bevy/plugins/
signals.rs

1use crate::interop::{GodotAccess, GodotNodeHandle};
2use bevy_app::{App, First, Last, Plugin};
3use bevy_ecs::{
4    component::Component,
5    entity::Entity,
6    event::Event,
7    prelude::Resource,
8    system::{Commands, Query, Res, SystemParam},
9};
10use crossbeam_channel::Sender;
11use godot::{
12    classes::Node,
13    obj::Gd,
14    prelude::{Callable, Variant},
15};
16use parking_lot::Mutex;
17use std::fmt::Debug;
18use std::sync::Arc;
19use tracing::error;
20
21/// Trait for type-erased signal dispatch that triggers observers
22pub(crate) trait SignalDispatch: Send {
23    fn trigger_in_world(self: Box<Self>, world: &mut bevy_ecs::world::World);
24}
25
26/// Envelope that carries a signal event for observer triggering
27struct SignalEnvelope<T: Event + Clone + Send + 'static> {
28    event: T,
29}
30
31impl<T: Event + Clone + Send + 'static> SignalDispatch for SignalEnvelope<T>
32where
33    for<'a> T::Trigger<'a>: Default,
34{
35    fn trigger_in_world(self: Box<Self>, world: &mut bevy_ecs::world::World) {
36        world.trigger(self.event);
37    }
38}
39
40/// Resource for receiving signal dispatches from Godot callbacks.
41/// Wrapped in Mutex to be Send+Sync, allowing it to be a regular Bevy Resource.
42#[derive(Resource)]
43pub(crate) struct SignalReceiver(pub Mutex<crossbeam_channel::Receiver<Box<dyn SignalDispatch>>>);
44
45impl SignalReceiver {
46    pub fn new(receiver: crossbeam_channel::Receiver<Box<dyn SignalDispatch>>) -> Self {
47        Self(Mutex::new(receiver))
48    }
49}
50
51#[doc(hidden)]
52#[derive(Resource)]
53pub(crate) struct SignalSender(pub crossbeam_channel::Sender<Box<dyn SignalDispatch>>);
54
55#[derive(Resource, Default)]
56struct PendingSignalConnections {
57    queue: Mutex<Vec<Box<dyn PendingSignalConnection>>>,
58}
59
60trait PendingSignalConnection: Send {
61    fn connect(self: Box<Self>, godot: &mut GodotAccess);
62}
63
64impl PendingSignalConnections {
65    fn push(&self, connection: Box<dyn PendingSignalConnection>) {
66        self.queue.lock().push(connection);
67    }
68
69    fn drain(&self) -> Vec<Box<dyn PendingSignalConnection>> {
70        self.queue.lock().drain(..).collect()
71    }
72}
73
74fn ensure_signal_connection_queue(app: &mut App) {
75    if !app.world().contains_resource::<PendingSignalConnections>() {
76        app.init_resource::<PendingSignalConnections>()
77            // Process pending connections at end of frame so connections made
78            // during Update are applied same-frame (ready for next frame's signals)
79            .add_systems(Last, process_pending_signal_connections);
80    }
81}
82
83fn process_pending_signal_connections(
84    pending: Res<PendingSignalConnections>,
85    mut godot: GodotAccess,
86) {
87    for connection in pending.drain() {
88        connection.connect(&mut godot);
89    }
90}
91
92fn connect_signal<T>(
93    godot: &mut GodotAccess,
94    node: GodotNodeHandle,
95    signal_name: &str,
96    source_entity: Option<Entity>,
97    mapper: Box<
98        dyn FnMut(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + 'static,
99    >,
100    sender: Sender<Box<dyn SignalDispatch>>,
101) where
102    T: Event + Clone + Send + 'static,
103    for<'a> T::Trigger<'a>: Default,
104{
105    let mut node_ref = godot.get::<Node>(node);
106    let signal_name_copy = signal_name.to_string();
107    let source_node_handle = node;
108    let mut mapper = mapper;
109
110    let closure = move |args: &[&Variant]| -> Variant {
111        // Clone variants to owned values we can inspect
112        let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
113        let event = mapper(&owned, source_node_handle, source_entity);
114        if let Some(event) = event {
115            let _ = sender.send(Box::new(SignalEnvelope { event }));
116        }
117        Variant::nil()
118    };
119
120    let callable = Callable::from_fn(&format!("signal_handler_{signal_name_copy}"), closure);
121    node_ref.connect(signal_name, &callable);
122}
123
124/// Plugin to enable Godot signal to Bevy observer routing for event type `T`.
125///
126/// When a Godot signal is connected via [`GodotSignals`], it triggers Bevy observers
127/// for the event type `T`. This provides a reactive, entity-targeted way to handle
128/// Godot signals.
129///
130/// # Example
131///
132/// ```ignore
133/// use bevy::prelude::*;
134/// use godot_bevy::prelude::*;
135///
136/// #[derive(Event, Clone)]
137/// struct ButtonPressed;
138///
139/// fn setup_app(app: &mut App) {
140///     app.add_plugins(GodotSignalsPlugin::<ButtonPressed>::default());
141///
142///     // React to button presses with a global observer
143///     app.add_observer(|_trigger: Trigger<ButtonPressed>| {
144///         println!("A button was pressed!");
145///     });
146/// }
147///
148/// fn connect_button(
149///     button_handle: GodotNodeHandle,
150///     signals: GodotSignals<ButtonPressed>,
151/// ) {
152///     signals.connect(button_handle, "pressed", None, |_, _, _| {
153///         Some(ButtonPressed)
154///     });
155/// }
156/// ```
157pub struct GodotSignalsPlugin<T>
158where
159    T: Event + Clone + Send + 'static,
160    for<'a> T::Trigger<'a>: Default,
161{
162    _phantom: std::marker::PhantomData<T>,
163}
164
165impl<T> Default for GodotSignalsPlugin<T>
166where
167    T: Event + Clone + Send + 'static,
168    for<'a> T::Trigger<'a>: Default,
169{
170    fn default() -> Self {
171        Self {
172            _phantom: Default::default(),
173        }
174    }
175}
176
177impl<T> Plugin for GodotSignalsPlugin<T>
178where
179    T: Event + Clone + Send + 'static,
180    for<'a> T::Trigger<'a>: Default,
181{
182    fn build(&self, app: &mut App) {
183        ensure_signal_connection_queue(app);
184
185        // Install global signal channel and drain system once
186        if !app.world().contains_resource::<SignalSender>() {
187            let (sender, receiver) = crossbeam_channel::unbounded::<Box<dyn SignalDispatch>>();
188            app.world_mut().insert_resource(SignalSender(sender));
189            app.world_mut()
190                .insert_resource(SignalReceiver::new(receiver));
191
192            // Drain signals and trigger observers
193            app.add_systems(First, drain_and_trigger_signals);
194        }
195
196        // Per-T deferred connection processor
197        app.add_systems(First, process_deferred_signal_connections::<T>);
198    }
199}
200
201/// Exclusive system to drain signal queue and trigger observers
202fn drain_and_trigger_signals(world: &mut bevy_ecs::world::World) {
203    // Collect first to avoid overlapping mutable borrows of `world`
204    let mut pending: Vec<Box<dyn SignalDispatch>> = Vec::new();
205    if let Some(receiver) = world.get_resource::<SignalReceiver>() {
206        let guard = receiver.0.lock();
207        pending.extend(guard.try_iter());
208    }
209    for dispatch in pending.drain(..) {
210        dispatch.trigger_in_world(world);
211    }
212}
213
214/// SystemParam for connecting Godot signals to Bevy observers.
215///
216/// Use this to connect a Godot node's signal to trigger a Bevy event `T`.
217/// The event will be delivered to observers registered with `app.add_observer()`
218/// or entity-specific observers added with `commands.entity(e).observe()`.
219///
220/// # Example
221///
222/// ```ignore
223/// fn connect_signals(
224///     button: Query<&GodotNodeHandle, With<MyButton>>,
225///     signals: GodotSignals<ButtonPressed>,
226/// ) {
227///     if let Ok(handle) = button.single() {
228///         // Connect the Godot "pressed" signal to trigger ButtonPressed event
229///         signals.connect(*handle, "pressed", None, |_, _, _| {
230///             Some(ButtonPressed)
231///         });
232///     }
233/// }
234/// ```
235#[derive(SystemParam)]
236pub struct GodotSignals<'w, T>
237where
238    T: Event + Clone + Send + 'static,
239    for<'a> T::Trigger<'a>: Default,
240{
241    sender: Res<'w, SignalSender>,
242    pending: Res<'w, PendingSignalConnections>,
243    _marker: std::marker::PhantomData<T>,
244}
245
246impl<'w, T> GodotSignals<'w, T>
247where
248    T: Event + Clone + Send + 'static,
249    for<'a> T::Trigger<'a>: Default,
250{
251    /// Connect a Godot signal to trigger a Bevy event `T`.
252    ///
253    /// When the signal fires, the `mapper` function is called with the signal arguments.
254    /// If it returns `Some(event)`, that event is triggered for observers.
255    ///
256    /// # Arguments
257    ///
258    /// * `node` - The Godot node to connect the signal from
259    /// * `signal_name` - The name of the Godot signal (e.g., "pressed", "body_entered")
260    /// * `source_entity` - Optional entity to include in the mapper callback
261    /// * `mapper` - Function to convert signal arguments to the event type
262    pub fn connect<F>(
263        &self,
264        node: GodotNodeHandle,
265        signal_name: &str,
266        source_entity: Option<Entity>,
267        mapper: F,
268    ) where
269        F: FnMut(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + 'static,
270    {
271        self.pending.push(Box::new(PendingSignalConnectionImpl {
272            node,
273            signal_name: signal_name.to_string(),
274            source_entity,
275            mapper: Box::new(mapper),
276            sender: self.sender.0.clone(),
277            _marker: std::marker::PhantomData,
278        }));
279    }
280
281    /// Connect a signal from any Godot object directly.
282    ///
283    /// This is useful for connecting to signals from objects that aren't tracked
284    /// as ECS entities, such as the SceneTree or other non-node objects.
285    ///
286    /// When the signal fires, the `mapper` function is called with the signal arguments.
287    /// If it returns `Some(event)`, that event is triggered for observers.
288    ///
289    /// # Arguments
290    ///
291    /// * `object` - A `Gd<T>` reference to any Godot object
292    /// * `signal_name` - The name of the Godot signal (e.g., "scene_changed", "timeout")
293    /// * `mapper` - Function to convert signal arguments to the event type
294    ///
295    /// # Example
296    ///
297    /// ```ignore
298    /// use bevy::prelude::*;
299    /// use godot_bevy::prelude::*;
300    /// use godot_bevy::interop::signal_names::SceneTreeSignals;
301    ///
302    /// #[derive(Event, Clone)]
303    /// struct SceneChanged;
304    ///
305    /// fn connect_scene_tree(
306    ///     signals: GodotSignals<SceneChanged>,
307    ///     mut scene_tree: SceneTreeRef,
308    /// ) {
309    ///     let tree = scene_tree.get().clone();
310    ///     signals.connect_object(tree, SceneTreeSignals::SCENE_CHANGED, |_args| {
311    ///         Some(SceneChanged)
312    ///     });
313    /// }
314    /// ```
315    pub fn connect_object<O, F>(&self, object: Gd<O>, signal_name: &str, mapper: F)
316    where
317        O: godot::obj::Inherits<godot::classes::Object> + godot::obj::GodotClass,
318        F: FnMut(&[Variant]) -> Option<T> + Send + 'static,
319    {
320        self.pending.push(Box::new(PendingDirectNodeConnection {
321            instance_id: object.instance_id(),
322            signal_name: signal_name.to_string(),
323            mapper: Box::new(mapper),
324            sender: self.sender.0.clone(),
325            _marker: std::marker::PhantomData,
326        }));
327    }
328}
329
330/// Backwards compatibility alias
331#[deprecated(note = "Use GodotSignals instead")]
332pub type TypedGodotSignals<'w, T> = GodotSignals<'w, T>;
333
334struct PendingSignalConnectionImpl<T>
335where
336    T: Event + Clone + Send + 'static,
337    for<'a> T::Trigger<'a>: Default,
338{
339    node: GodotNodeHandle,
340    signal_name: String,
341    source_entity: Option<Entity>,
342    mapper:
343        Box<dyn FnMut(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + 'static>,
344    sender: Sender<Box<dyn SignalDispatch>>,
345    _marker: std::marker::PhantomData<T>,
346}
347
348impl<T> PendingSignalConnection for PendingSignalConnectionImpl<T>
349where
350    T: Event + Clone + Send + 'static,
351    for<'a> T::Trigger<'a>: Default,
352{
353    fn connect(self: Box<Self>, godot: &mut GodotAccess) {
354        let PendingSignalConnectionImpl {
355            node,
356            signal_name,
357            source_entity,
358            mapper,
359            sender,
360            _marker: _,
361        } = *self;
362        connect_signal(godot, node, &signal_name, source_entity, mapper, sender);
363    }
364}
365
366/// Pending connection for direct object references (singletons, Object instances, etc.)
367struct PendingDirectNodeConnection<T>
368where
369    T: Event + Clone + Send + 'static,
370    for<'a> T::Trigger<'a>: Default,
371{
372    instance_id: godot::obj::InstanceId,
373    signal_name: String,
374    mapper: Box<dyn FnMut(&[Variant]) -> Option<T> + Send + 'static>,
375    sender: Sender<Box<dyn SignalDispatch>>,
376    _marker: std::marker::PhantomData<T>,
377}
378
379impl<T> PendingSignalConnection for PendingDirectNodeConnection<T>
380where
381    T: Event + Clone + Send + 'static,
382    for<'a> T::Trigger<'a>: Default,
383{
384    fn connect(self: Box<Self>, _godot: &mut GodotAccess) {
385        // GodotAccess is unused here: direct object connections resolve the target
386        // via InstanceId rather than through GodotAccess node lookups.
387        let PendingDirectNodeConnection {
388            instance_id,
389            signal_name,
390            mut mapper,
391            sender,
392            _marker: _,
393        } = *self;
394
395        let Ok(mut node) = Gd::<godot::classes::Object>::try_from_instance_id(instance_id) else {
396            error!(
397                "Failed to get object with instance_id {:?} for signal connection",
398                instance_id
399            );
400            return;
401        };
402
403        let signal_name_copy = signal_name.clone();
404
405        let closure = move |args: &[&Variant]| -> Variant {
406            let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
407            if let Some(event) = mapper(&owned) {
408                let _ = sender.send(Box::new(SignalEnvelope { event }));
409            }
410            Variant::nil()
411        };
412
413        let callable = Callable::from_fn(&format!("signal_handler_{signal_name_copy}"), closure);
414        node.connect(&signal_name, &callable);
415    }
416}
417
418/// Process deferred signal connections for entities that now have GodotNodeHandles
419fn process_deferred_signal_connections<T>(
420    mut commands: Commands,
421    mut query: Query<(Entity, &GodotNodeHandle, &mut DeferredSignalConnections<T>)>,
422    signals: GodotSignals<T>,
423) where
424    T: Event + Clone + Send + 'static,
425    for<'a> T::Trigger<'a>: Default,
426{
427    for (entity, handle, mut deferred) in query.iter_mut() {
428        for conn in deferred.connections.drain(..) {
429            let signal = conn.signal_name;
430            let mapper = conn.mapper;
431            signals.connect(
432                *handle,
433                &signal,
434                Some(entity),
435                move |args, node_handle, ent| (mapper)(args, node_handle, ent),
436            );
437        }
438        // Remove marker after wiring all deferred connections
439        commands
440            .entity(entity)
441            .remove::<DeferredSignalConnections<T>>();
442    }
443}
444
445// ====================
446// Deferred Connections
447// ====================
448
449/// A single deferred signal connection for event type `T`
450pub struct DeferredConnection<T: Event + Clone + Send + 'static> {
451    /// The signal name to connect to
452    pub signal_name: String,
453    /// Mapper function to convert signal arguments to the event
454    pub mapper: Arc<
455        dyn Fn(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
456    >,
457}
458
459impl<T: Event + Clone + Send + 'static> Debug for DeferredConnection<T> {
460    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461        write!(
462            f,
463            "DeferredConnection {{ signal_name: {:?} }}",
464            self.signal_name
465        )
466    }
467}
468
469/// Component to defer Godot signal connections until a `GodotNodeHandle` exists on the entity.
470///
471/// Add this component to an entity when you want to connect signals but the entity
472/// doesn't have a `GodotNodeHandle` yet. Once the handle is available, the connections
473/// will be automatically established.
474#[derive(Component, Debug)]
475pub struct DeferredSignalConnections<T: Event + Clone + Send + 'static> {
476    /// The pending connections to establish
477    pub connections: Vec<DeferredConnection<T>>,
478}
479
480impl<T: Event + Clone + Send + 'static> Default for DeferredSignalConnections<T> {
481    fn default() -> Self {
482        Self::new()
483    }
484}
485
486impl<T: Event + Clone + Send + 'static> DeferredSignalConnections<T> {
487    /// Create an empty deferred connections component
488    pub fn new() -> Self {
489        Self {
490            connections: Vec::new(),
491        }
492    }
493
494    /// Create with a single connection
495    pub fn with_connection<F>(signal_name: impl Into<String>, mapper: F) -> Self
496    where
497        F: Fn(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
498    {
499        Self {
500            connections: vec![DeferredConnection {
501                signal_name: signal_name.into(),
502                mapper: Arc::new(mapper),
503            }],
504        }
505    }
506
507    /// Add a connection to establish once the node handle is available
508    pub fn push<F>(&mut self, signal_name: impl Into<String>, mapper: F)
509    where
510        F: Fn(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
511    {
512        self.connections.push(DeferredConnection {
513            signal_name: signal_name.into(),
514            mapper: Arc::new(mapper),
515        });
516    }
517}
518
519/// Backwards compatibility alias
520#[deprecated(note = "Use DeferredSignalConnections instead")]
521pub type TypedDeferredSignalConnections<T> = DeferredSignalConnections<T>;
522
523/// Backwards compatibility alias
524#[deprecated(note = "Use DeferredConnection instead")]
525pub type TypedDeferredConnection<T> = DeferredConnection<T>;
526
527/// Type-erased deferred connections for internal use
528#[doc(hidden)]
529pub(crate) trait DeferredSignalConnectionTrait: Send + Sync + Debug {
530    fn connect(&self, root_node: &Gd<Node>, entity: Entity, sender: &SignalSender);
531}
532
533/// Deferred connection specification for packed scenes
534#[doc(hidden)]
535#[derive(Debug)]
536pub(crate) struct SignalConnectionSpec<T>
537where
538    T: Event + Clone + Send + 'static,
539    for<'a> T::Trigger<'a>: Default,
540{
541    pub(crate) node_path: String,
542    pub(crate) signal_name: String,
543    pub(crate) connections: DeferredSignalConnections<T>,
544}
545
546#[doc(hidden)]
547impl<T> DeferredSignalConnectionTrait for SignalConnectionSpec<T>
548where
549    T: Event + Clone + Send + Debug + 'static,
550    for<'a> T::Trigger<'a>: Default,
551{
552    fn connect(&self, root_node: &Gd<Node>, source_entity: Entity, sender: &SignalSender) {
553        let Some(mut target_node) = root_node.get_node_or_null(self.node_path.as_str()) else {
554            error!(
555                "Failed to find node at path '{}' for signal connection",
556                self.node_path
557            );
558            return;
559        };
560
561        for connection in self.connections.connections.iter() {
562            let source_node_id = GodotNodeHandle::from(target_node.instance_id());
563            let sender_copy = sender.0.clone();
564            let mapper = connection.mapper.clone();
565            let signal_name = self.signal_name.clone();
566
567            let closure = move |args: &[&Variant]| -> Variant {
568                let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
569                if let Some(event) = mapper(&owned, source_node_id, Some(source_entity)) {
570                    let _ = sender_copy.send(Box::new(SignalEnvelope { event }));
571                }
572                Variant::nil()
573            };
574
575            target_node.connect(
576                &signal_name,
577                &Callable::from_fn(&format!("signal_handler_{signal_name}"), closure),
578            );
579        }
580    }
581}