use crate::plugins::core::PrePhysicsUpdate;
use crate::plugins::{
collisions::CollisionMessageReader,
core::{PhysicsDelta, PhysicsUpdate},
input::InputEventReader,
scene_tree::SceneTreeMessageReader,
};
use crate::watchers::collision_watcher::CollisionWatcher;
use crate::watchers::input_watcher::GodotInputWatcher;
use crate::watchers::scene_tree_watcher::SceneTreeWatcher;
use bevy_app::{App, PluginsState};
use bevy_ecs::message::Messages;
use crossbeam_channel::unbounded;
use godot::prelude::*;
use std::sync::OnceLock;
pub static BEVY_INIT_FUNC: OnceLock<Box<dyn Fn(&mut App) + Send + Sync>> = OnceLock::new();
pub static BEVY_APP_CONFIG: OnceLock<BevyAppConfig> = OnceLock::new();
#[derive(Debug, Clone, Copy)]
pub struct BevyAppConfig {
pub scene_tree_auto_despawn_children: bool,
}
#[derive(GodotClass)]
#[class(base=Node)]
pub struct BevyApp {
base: Base<Node>,
app: Option<App>,
#[allow(clippy::type_complexity)]
instance_init_func: Option<Box<dyn Fn(&mut App) + Send + Sync>>,
}
impl BevyApp {
pub fn get_app(&self) -> Option<&App> {
self.app.as_ref()
}
pub fn get_app_mut(&mut self) -> Option<&mut App> {
self.app.as_mut()
}
pub fn set_instance_init_func(&mut self, func: Box<dyn Fn(&mut App) + Send + Sync>) {
self.instance_init_func = Some(func);
}
fn register_scene_tree_watcher(&mut self, app: &mut App) {
if self.base().has_node("SceneTreeWatcher") {
return;
}
let (sender, receiver) = unbounded();
let mut scene_tree_watcher = SceneTreeWatcher::new_alloc();
scene_tree_watcher.bind_mut().notification_channel = Some(sender);
scene_tree_watcher.set_name("SceneTreeWatcher");
self.base_mut().add_child(&scene_tree_watcher);
app.insert_resource(SceneTreeMessageReader::new(receiver));
}
fn register_input_event_watcher(&mut self, app: &mut App) {
let (sender, receiver) = unbounded();
let mut input_event_watcher = GodotInputWatcher::new_alloc();
input_event_watcher.bind_mut().notification_channel = Some(sender);
input_event_watcher.set_name("InputEventWatcher");
self.base_mut().add_child(&input_event_watcher);
app.insert_non_send_resource(InputEventReader(receiver));
}
fn register_collision_watcher(&mut self, app: &mut App) {
if self.base().has_node("CollisionWatcher") {
return;
}
let (sender, receiver) = unbounded();
let mut collision_watcher = CollisionWatcher::new_alloc();
collision_watcher.bind_mut().notification_channel = Some(sender);
collision_watcher.set_name("CollisionWatcher");
self.base_mut().add_child(&collision_watcher);
app.insert_resource(CollisionMessageReader::new(receiver));
}
fn register_optimized_scene_tree_watcher(&mut self) {
if self.base().has_node("OptimizedSceneTreeWatcher") {
return;
}
let path = "res://addons/godot-bevy/optimized_scene_tree_watcher.gd";
if godot::classes::FileAccess::file_exists(&godot::builtin::GString::from(path)) {
let mut resource_loader = godot::classes::ResourceLoader::singleton();
if let Some(resource) = resource_loader.load(path)
&& let Ok(mut script) = resource.try_cast::<godot::classes::GDScript>()
&& let Ok(instance) = script.try_instantiate(&[])
&& let Ok(mut node) = instance.try_to::<godot::obj::Gd<godot::classes::Node>>()
{
node.set_name("OptimizedSceneTreeWatcher");
self.base_mut().add_child(&node);
tracing::info!("Successfully registered OptimizedSceneTreeWatcher");
} else {
tracing::warn!(
"Failed to instantiate OptimizedSceneTreeWatcher - using fallback method"
);
}
} else {
tracing::debug!("OptimizedSceneTreeWatcher not available - using fallback method");
}
}
#[cfg(debug_assertions)]
fn register_optimized_bulk_operations(&mut self) {
if self.base().has_node("OptimizedBulkOperations") {
return;
}
let path = "res://addons/godot-bevy/optimized_bulk_operations.gd";
if godot::classes::FileAccess::file_exists(&godot::builtin::GString::from(path)) {
let mut resource_loader = godot::classes::ResourceLoader::singleton();
if let Some(resource) = resource_loader.load(path)
&& let Ok(mut script) = resource.try_cast::<godot::classes::GDScript>()
&& let Ok(instance) = script.try_instantiate(&[])
&& let Ok(mut node) = instance.try_to::<godot::obj::Gd<godot::classes::Node>>()
{
node.set_name("OptimizedBulkOperations");
self.base_mut().add_child(&node);
tracing::info!("Successfully registered OptimizedBulkOperations");
} else {
tracing::warn!(
"Failed to instantiate OptimizedBulkOperations - bulk operations unavailable"
);
}
} else {
tracing::debug!("OptimizedBulkOperations not available");
}
}
#[cfg(debug_assertions)]
fn cache_bulk_operations(&self, app: &mut App) {
use crate::interop::BulkOperationsCache;
if let Some(node) = self
.base()
.get_node_or_null("OptimizedBulkOperations")
.map(|n| n.upcast::<godot::classes::Object>())
{
app.insert_non_send_resource(BulkOperationsCache::new(node));
tracing::debug!("Cached OptimizedBulkOperations node reference");
} else {
app.init_non_send_resource::<BulkOperationsCache>();
}
}
}
#[godot_api]
impl INode for BevyApp {
fn init(base: Base<Node>) -> Self {
Self {
base,
app: Default::default(),
instance_init_func: None,
}
}
fn ready(&mut self) {
if godot::classes::Engine::singleton().is_editor_hint() {
return;
}
#[cfg(debug_assertions)]
self.register_optimized_bulk_operations();
let has_init = self.instance_init_func.is_some() || BEVY_INIT_FUNC.get().is_some();
if !has_init {
return;
}
let mut app = App::new();
let config = BEVY_APP_CONFIG.get().copied().unwrap_or(BevyAppConfig {
scene_tree_auto_despawn_children: true,
});
app.add_plugins(crate::plugins::core::GodotBaseCorePlugin)
.add_plugins(crate::plugins::scene_tree::GodotSceneTreePlugin {
auto_despawn_children: config.scene_tree_auto_despawn_children,
});
if let Some(ref instance_func) = self.instance_init_func {
instance_func(&mut app);
} else if let Some(app_builder_func) = BEVY_INIT_FUNC.get() {
app_builder_func(&mut app);
}
use crate::plugins::scene_tree::SceneTreeMessage;
if app
.world()
.contains_resource::<Messages<SceneTreeMessage>>()
{
self.register_scene_tree_watcher(&mut app);
self.register_optimized_scene_tree_watcher();
}
use crate::plugins::collisions::CollisionStarted;
if app
.world()
.contains_resource::<Messages<CollisionStarted>>()
{
self.register_collision_watcher(&mut app);
}
use crate::plugins::input::KeyboardInput;
if app.world().contains_resource::<Messages<KeyboardInput>>() {
self.register_input_event_watcher(&mut app);
}
#[cfg(debug_assertions)]
self.cache_bulk_operations(&mut app);
if app.plugins_state() != PluginsState::Cleaned {
while app.plugins_state() == PluginsState::Adding {
#[cfg(not(target_arch = "wasm32"))]
bevy_tasks::tick_global_task_pools_on_main_thread();
}
app.finish();
app.cleanup();
}
app.init_resource::<PhysicsDelta>();
self.app = Some(app);
}
fn process(&mut self, _delta: f64) {
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
if godot::classes::Engine::singleton().is_editor_hint() {
return;
}
if let Some(app) = self.app.as_mut()
&& let Err(e) = catch_unwind(AssertUnwindSafe(|| {
app.update();
crate::profiling::frame_mark();
}))
{
self.app = None;
eprintln!("bevy app update panicked");
resume_unwind(e);
}
}
fn physics_process(&mut self, delta: f32) {
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
if godot::classes::Engine::singleton().is_editor_hint() {
return;
}
if let Some(app) = self.app.as_mut()
&& let Err(e) = catch_unwind(AssertUnwindSafe(|| {
app.world_mut().resource_mut::<PhysicsDelta>().delta_seconds = delta;
app.world_mut().run_schedule(PrePhysicsUpdate);
app.world_mut().run_schedule(PhysicsUpdate);
crate::profiling::secondary_frame_mark("physics");
}))
{
self.app = None;
eprintln!("bevy app physics update panicked");
resume_unwind(e);
}
}
}