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
use super::scene_tree::SceneTreeRef;
use crate::plugins::assets::GodotResource;
use crate::plugins::signals::{
DeferredSignalConnection, GlobalTypedSignalSender, SignalConnectionSpec,
TypedDeferredSignalConnections,
};
use crate::plugins::transforms::IntoGodotTransform2D;
use crate::prelude::main_thread_system;
use crate::{interop::GodotNodeHandle, plugins::transforms::IntoGodotTransform};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_asset::{Assets, Handle};
use bevy_ecs::message::Message;
use bevy_ecs::system::NonSend;
use bevy_ecs::{
component::Component,
entity::Entity,
query::Without,
system::{Commands, Query, ResMut},
};
use bevy_transform::components::Transform;
use godot::prelude::Variant;
use godot::{
builtin::GString,
classes::{Node, Node2D, Node3D, PackedScene, ResourceLoader},
obj::Singleton,
};
use std::str::FromStr;
use tracing::error;
#[derive(Default)]
pub struct GodotPackedScenePlugin;
impl Plugin for GodotPackedScenePlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, spawn_scene);
}
}
// silence warning about the following docs referring to private `spawn_scene`
#[allow(rustdoc::private_intra_doc_links)]
/// A to-be-instanced-and-spawned Godot scene.
///
/// [`GodotScene`]s that are spawned/inserted into the bevy world will be instanced from the provided
/// handle/path and the instance will be added as an [`GodotNodeHandle`] in the next PostUpdateFlush set.
/// (see [`spawn_scene`])
#[derive(Debug, Component)]
pub struct GodotScene {
resource: GodotSceneResource,
parent: Option<GodotNodeHandle>,
deferred_signal_connections: Vec<Box<dyn DeferredSignalConnection>>,
}
#[derive(Debug)]
enum GodotSceneResource {
Handle(Handle<GodotResource>),
Path(String),
}
impl GodotScene {
/// Instantiate the godot scene from a Bevy `Handle<GodotResource>` and add it to the
/// scene tree root. This is the preferred method when using Bevy's asset system.
pub fn from_handle(handle: Handle<GodotResource>) -> Self {
Self {
resource: GodotSceneResource::Handle(handle),
parent: None,
deferred_signal_connections: Vec::new(),
}
}
/// Instantiate the godot scene from the given path and add it to the scene tree root.
///
/// Note that this will call [`ResourceLoader`].load() - which is a blocking load.
/// If you want async loading, you should load your resources through Bevy's AssetServer
/// and use from_handle().
pub fn from_path(path: &str) -> Self {
Self {
resource: GodotSceneResource::Path(path.to_string()),
parent: None,
deferred_signal_connections: Vec::new(),
}
}
/// Set the parent node for this scene when spawned.
pub fn with_parent(mut self, parent: GodotNodeHandle) -> Self {
self.parent = Some(parent);
self
}
/// Connect a typed Godot signal from a child node to a Bevy `Message`.
/// The signal will be connected when the scene is spawned.
///
/// # Requirements
/// This method requires [`GodotTypedSignalsPlugin<T>`] to be added to your app for the
/// message type `T` you're using. If the plugin is not added, the signal connections
/// will be ignored and an error will be logged at runtime.
///
/// # Arguments
/// * `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](https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-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.
///
/// # Example
/// ```ignore
/// let scene: Handle<GodotResource> = ...;
/// let entity = world.spawn_empty();
/// entity.insert(
/// GodotScene::from_handle(scene).with_signal_connection::<MyMessage>(
/// "VBox/MyButton",
/// "pressed",
/// |args, _node, _entity| {
/// Some(MyMessage::from_args(args))
/// }
/// )
/// );
/// ```
pub fn with_signal_connection<T, F>(
mut self,
node_path: &str,
signal_name: &str,
mapper: F,
) -> Self
where
T: Message + Send + std::fmt::Debug + 'static,
F: Fn(&[Variant], &GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
{
self.deferred_signal_connections
.push(Box::new(SignalConnectionSpec {
node_path: node_path.to_string(),
signal_name: signal_name.to_string(),
connections: TypedDeferredSignalConnections::<T>::with_connection(
signal_name,
mapper,
),
}));
self
}
}
#[main_thread_system]
fn spawn_scene(
mut commands: Commands,
mut new_scenes: Query<(&mut GodotScene, Entity, Option<&Transform>), Without<GodotNodeHandle>>,
mut scene_tree: SceneTreeRef,
mut assets: ResMut<Assets<GodotResource>>,
typed_message_sender: Option<NonSend<GlobalTypedSignalSender>>,
) {
for (mut scene, ent, transform) in new_scenes.iter_mut() {
let packed_scene = match &scene.resource {
GodotSceneResource::Handle(handle) => assets
.get_mut(handle)
.expect("packed scene to exist in assets")
.get()
.clone(),
GodotSceneResource::Path(path) => ResourceLoader::singleton()
.load(&GString::from_str(path).expect("path to be a valid GString"))
.expect("packed scene to load"),
};
let packed_scene_cast = packed_scene.clone().try_cast::<PackedScene>();
if packed_scene_cast.is_err() {
error!("Resource is not a PackedScene: {:?}", packed_scene);
continue;
}
let packed_scene = packed_scene_cast.unwrap();
let instance = match packed_scene.instantiate() {
Some(instance) => instance,
None => {
error!("Failed to instantiate PackedScene");
continue;
}
};
if let Some(transform) = transform {
if let Ok(mut node) = instance.clone().try_cast::<Node3D>() {
node.set_global_transform(transform.to_godot_transform());
} else if let Ok(mut node) = instance.clone().try_cast::<Node2D>() {
node.set_global_transform(transform.to_godot_transform_2d());
} else {
error!(
"attempted to spawn a scene with a transform on Node that did not inherit from Node, the transform was not set"
)
}
}
// Connect signals (only if typed signals plugin is available)
if !scene.deferred_signal_connections.is_empty() {
if let Some(ref sender) = typed_message_sender {
for deferred_connection in scene.deferred_signal_connections.drain(..) {
deferred_connection.connect(&instance, ent, sender);
}
} else {
error!(
"GodotScene has signal connections but GodotTypedSignalsPlugin is not added. \
Add GodotTypedSignalsPlugin<YourMessageType> to enable signal connections."
);
scene.deferred_signal_connections.clear();
}
}
match &mut scene.parent {
Some(parent) => {
let mut parent = parent.get::<Node>();
parent.add_child(&instance);
}
None => {
scene_tree.get().get_root().unwrap().add_child(&instance);
}
}
commands.entity(ent).insert(GodotNodeHandle::new(instance));
}
}