godot-bevy 0.10.0

Bridge between Bevy ECS and Godot 4 for Rust-powered game development
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
use crate::interop::GodotNodeHandle;
use bevy_app::{App, First, Plugin};
use bevy_ecs::{
    component::Component,
    entity::Entity,
    message::{Message, MessageWriter, message_update_system},
    schedule::IntoScheduleConfigs,
    system::{Commands, NonSend, NonSendMut, Query, SystemParam},
};
use godot::{
    classes::{Node, Object},
    obj::{Gd, InstanceId},
    prelude::{Callable, Variant},
};
use std::fmt::Debug;
use std::sync::Arc;
use std::sync::mpsc::Sender;
use tracing::error;

#[derive(Default)]
pub struct GodotSignalsPlugin;

impl Plugin for GodotSignalsPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(
            First,
            write_godot_signal_messages.before(message_update_system),
        )
        .add_message::<GodotSignal>();
    }
}

#[derive(Debug, Clone)]
pub struct GodotSignalArgument {
    pub type_name: String,
    pub value: String,
    pub instance_id: Option<InstanceId>,
}

#[derive(Debug, Message)]
pub struct GodotSignal {
    pub name: String,
    pub origin: GodotNodeHandle,
    pub target: GodotNodeHandle,
    pub arguments: Vec<GodotSignalArgument>,
}

#[doc(hidden)]
pub struct GodotSignalReader(pub std::sync::mpsc::Receiver<GodotSignal>);

#[doc(hidden)]
pub struct GodotSignalSender(pub std::sync::mpsc::Sender<GodotSignal>);

/// Global, type-erased dispatch for typed signal messages
pub(crate) trait TypedDispatch: Send {
    fn write_into_world(self: Box<Self>, world: &mut bevy_ecs::world::World);
}

struct TypedEnvelope<T: Message + Send + 'static>(T);

impl<T: Message + Send + 'static> TypedDispatch for TypedEnvelope<T> {
    fn write_into_world(self: Box<Self>, world: &mut bevy_ecs::world::World) {
        if let Some(mut messages) = world.get_resource_mut::<bevy_ecs::message::Messages<T>>() {
            messages.write(self.0);
        }
    }
}

#[doc(hidden)]
pub(crate) struct GlobalTypedSignalReceiver(pub std::sync::mpsc::Receiver<Box<dyn TypedDispatch>>);

#[doc(hidden)]
pub(crate) struct GlobalTypedSignalSender(pub std::sync::mpsc::Sender<Box<dyn TypedDispatch>>);

/// System parameter for connecting Godot signals to Bevy's message system
/// Legacy SystemParam (deprecated) wrapped in a narrow module-level allow
mod legacy_signals_param {
    #![allow(deprecated)]
    use super::*;

    /// Clean API for connecting Godot signals - hides implementation details from users
    #[derive(SystemParam)]
    #[deprecated(
        note = "Legacy signal bus. Prefer TypedGodotSignals<T> with GodotTypedSignalsPlugin<T>."
    )]
    pub struct GodotSignals<'w> {
        pub(super) signal_sender: NonSendMut<'w, GodotSignalSender>,
    }

    impl<'w> GodotSignals<'w> {
        /// Connect a Godot signal to be forwarded to Bevy's message system
        pub fn connect(&self, node: &mut GodotNodeHandle, signal_name: &str) {
            connect_godot_signal(node, signal_name, self.signal_sender.0.clone());
        }
    }
}

#[allow(deprecated)]
pub use legacy_signals_param::GodotSignals;

fn write_godot_signal_messages(
    events: NonSendMut<GodotSignalReader>,
    mut message_writer: MessageWriter<GodotSignal>,
) {
    message_writer.write_batch(events.0.try_iter());
}

pub fn connect_godot_signal(
    node: &mut GodotNodeHandle,
    signal_name: &str,
    signal_sender: Sender<GodotSignal>,
) {
    let mut node = node.get::<Node>();
    let node_clone = node.clone();
    let signal_name_copy = signal_name.to_string();
    let node_id = node_clone.instance_id();

    let closure = move |args: &[&Variant]| -> Variant {
        // Use captured sender directly - no global state needed!
        let arguments: Vec<GodotSignalArgument> = args
            .iter()
            .map(|&arg| variant_to_signal_argument(arg))
            .collect();

        let origin_handle = GodotNodeHandle::from_instance_id(node_id);

        let _ = signal_sender.send(GodotSignal {
            name: signal_name_copy.clone(),
            origin: origin_handle.clone(),
            target: origin_handle,
            arguments,
        });

        Variant::nil()
    };

    // Create callable from our universal closure
    let callable = Callable::from_fn("universal_signal_handler", closure);

    // Connect the signal - this will work with ANY number of arguments!
    node.connect(signal_name, &callable);
}

pub fn variant_to_signal_argument(variant: &Variant) -> GodotSignalArgument {
    let type_name = match variant.get_type() {
        godot::prelude::VariantType::NIL => "Nil",
        godot::prelude::VariantType::BOOL => "Bool",
        godot::prelude::VariantType::INT => "Int",
        godot::prelude::VariantType::FLOAT => "Float",
        godot::prelude::VariantType::STRING => "String",
        godot::prelude::VariantType::VECTOR2 => "Vector2",
        godot::prelude::VariantType::VECTOR3 => "Vector3",
        godot::prelude::VariantType::OBJECT => "Object",
        _ => "Unknown",
    }
    .to_string();

    let value = variant.stringify().to_string();

    // Extract instance ID for objects
    let instance_id = if variant.get_type() == godot::prelude::VariantType::OBJECT {
        variant
            .try_to::<Gd<Object>>()
            .ok()
            .map(|obj| obj.instance_id())
    } else {
        None
    };

    GodotSignalArgument {
        type_name,
        value,
        instance_id,
    }
}

/// Generic plugin to enable typed Godot-signal-to-Bevy-message routing for `T`
pub struct GodotTypedSignalsPlugin<T: Message + Send + 'static> {
    _phantom: std::marker::PhantomData<T>,
}

impl<T: Message + Send + 'static> Default for GodotTypedSignalsPlugin<T> {
    fn default() -> Self {
        Self {
            _phantom: Default::default(),
        }
    }
}

impl<T: Message + Send + 'static> Plugin for GodotTypedSignalsPlugin<T> {
    fn build(&self, app: &mut App) {
        // Ensure the Bevy message type exists
        app.add_message::<T>();

        // Install global typed signal channel and consolidated drain once
        if !app.world().contains_non_send::<GlobalTypedSignalSender>() {
            let (sender, receiver) = std::sync::mpsc::channel::<Box<dyn TypedDispatch>>();
            app.world_mut()
                .insert_non_send_resource(GlobalTypedSignalSender(sender));
            app.world_mut()
                .insert_non_send_resource(GlobalTypedSignalReceiver(receiver));

            // One consolidated drain for all typed messages
            app.add_systems(
                First,
                drain_global_typed_signals.before(message_update_system),
            );
        }

        // Per-T deferred connection processor
        app.add_systems(First, process_typed_deferred_signal_connections::<T>);
    }
}

// Exclusive system to drain type-erased global queue into the correct Messages<T> resources
fn drain_global_typed_signals(world: &mut bevy_ecs::world::World) {
    // Collect first to avoid overlapping mutable borrows of `world`
    let mut pending: Vec<Box<dyn TypedDispatch>> = Vec::new();
    if let Some(receiver) = world.get_non_send_resource_mut::<GlobalTypedSignalReceiver>() {
        pending.extend(receiver.0.try_iter());
    }
    for dispatch in pending.drain(..) {
        dispatch.write_into_world(world);
    }
}

/// SystemParam providing typed connect helpers for a specific Bevy `Message` T
#[derive(SystemParam)]
pub struct TypedGodotSignals<'w, T: Message + Send + 'static> {
    /// Global type-erased sender. Provided by first `GodotTypedSignalsPlugin` added.
    typed_sender: NonSend<'w, GlobalTypedSignalSender>,
    _marker: std::marker::PhantomData<T>,
}

impl<'w, T: Message + Send + 'static> TypedGodotSignals<'w, T> {
    /// Connect a Godot signal and map it to a typed Bevy Message `T` via `mapper`.
    /// Multiple connections are supported; each connection sends a `T` when fired.
    pub fn connect_map<F>(
        &self,
        node: &mut GodotNodeHandle,
        signal_name: &str,
        source_entity: Option<Entity>,
        mut mapper: F,
    ) where
        F: FnMut(&[Variant], &GodotNodeHandle, Option<Entity>) -> Option<T> + Send + 'static,
    {
        let mut node_ref = node.get::<Node>();
        let signal_name_copy = signal_name.to_string();
        let source_node = node.clone();
        let sender_t = self.typed_sender.0.clone();

        let closure = move |args: &[&Variant]| -> Variant {
            // Clone variants to owned values we can inspect
            let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
            let event = mapper(&owned, &source_node, source_entity);
            if let Some(event) = event {
                let _ = sender_t.send(Box::new(TypedEnvelope::<T>(event)));
            }
            Variant::nil()
        };

        let callable =
            Callable::from_fn(&format!("signal_handler_typed_{signal_name_copy}"), closure);
        node_ref.connect(signal_name, &callable);
    }
}

/// Process typed deferred signal connections for entities that now have GodotNodeHandles
fn process_typed_deferred_signal_connections<T: Message + Send + 'static>(
    mut commands: Commands,
    mut query: Query<(
        Entity,
        &mut GodotNodeHandle,
        &mut TypedDeferredSignalConnections<T>,
    )>,
    typed: TypedGodotSignals<T>,
) {
    for (entity, mut handle, mut deferred) in query.iter_mut() {
        for conn in deferred.connections.drain(..) {
            let signal = conn.signal_name;
            let mapper = conn.mapper;
            typed.connect_map(
                &mut handle,
                &signal,
                Some(entity),
                move |args, node, ent| (mapper)(args, node, ent),
            );
        }
        // Remove marker after wiring all deferred connections
        commands
            .entity(entity)
            .remove::<TypedDeferredSignalConnections<T>>();
    }
}

// ====================
// Typed Deferred Connections
// ====================

/// A single typed deferred connection item for `T` messages
pub struct TypedDeferredConnection<T: Message + Send + 'static> {
    pub signal_name: String,
    pub mapper: Arc<
        dyn Fn(&[Variant], &GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
    >,
}

impl<T: Message + Send + 'static> Debug for TypedDeferredConnection<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "TypedDeferredConnection {{ signal_name: {:?} }}",
            self.signal_name
        )
    }
}

/// Component to defer Godot signal connections until a `GodotNodeHandle` exists on the entity
#[derive(Component, Debug)]
pub struct TypedDeferredSignalConnections<T: Message + Send + 'static> {
    pub connections: Vec<TypedDeferredConnection<T>>,
}

impl<T: Message + Send + 'static> Default for TypedDeferredSignalConnections<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Message + Send + 'static> TypedDeferredSignalConnections<T> {
    pub fn new() -> Self {
        Self {
            connections: Vec::new(),
        }
    }

    pub fn with_connection<F>(signal_name: impl Into<String>, mapper: F) -> Self
    where
        F: Fn(&[Variant], &GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
    {
        Self {
            connections: vec![TypedDeferredConnection {
                signal_name: signal_name.into(),
                mapper: Arc::new(mapper),
            }],
        }
    }

    pub fn push<F>(&mut self, signal_name: impl Into<String>, mapper: F)
    where
        F: Fn(&[Variant], &GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
    {
        self.connections.push(TypedDeferredConnection {
            signal_name: signal_name.into(),
            mapper: Arc::new(mapper),
        });
    }
}

/// Type-erased deferred connections. Allows deferred connections of any Bevy Message type
/// to be processed after a GodotNodeHandle exists.
#[doc(hidden)]
pub(crate) trait DeferredSignalConnection: Send + Sync + Debug {
    /// Connect the deferred signal to the given Godot node.
    fn connect(&self, root_node: &Gd<Node>, entity: Entity, typed_sender: &GlobalTypedSignalSender);
}

/// Deferred connection information for a specific `T` message type.
#[doc(hidden)]
#[derive(Debug)]
pub(crate) struct SignalConnectionSpec<T: Message + Send + 'static> {
    pub(crate) node_path: String,
    pub(crate) signal_name: String,
    pub(crate) connections: TypedDeferredSignalConnections<T>,
}

#[doc(hidden)]
impl<T: Message + Send + Debug + 'static> DeferredSignalConnection for SignalConnectionSpec<T> {
    fn connect(
        &self,
        root_node: &Gd<Node>,
        source_entity: Entity,
        typed_sender: &GlobalTypedSignalSender,
    ) {
        let Some(mut target_node) = root_node.get_node_or_null(self.node_path.as_str()) else {
            error!(
                "Failed to find node at path '{}' for signal connection",
                self.node_path
            );
            return;
        };

        for connection in self.connections.connections.iter() {
            let source_node_handle = GodotNodeHandle::new(target_node.clone());
            let typed_sender_copy = typed_sender.0.clone();
            let mapper = connection.mapper.clone();
            let signal_name = self.signal_name.clone();

            let closure = move |args: &[&Variant]| -> Variant {
                let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
                if let Some(event) = mapper(&owned, &source_node_handle, Some(source_entity)) {
                    let _ = typed_sender_copy.send(Box::new(TypedEnvelope::<T>(event)));
                }
                Variant::nil()
            };

            target_node.connect(
                &signal_name,
                &Callable::from_fn(&format!("signal_handler_typed_{signal_name}"), closure),
            );
        }
    }
}