diff --git a/framework_crates/bones_bevy_renderer/src/debug.rs b/framework_crates/bones_bevy_renderer/src/debug.rs index 7cdb0d4726..ac21ee714e 100644 --- a/framework_crates/bones_bevy_renderer/src/debug.rs +++ b/framework_crates/bones_bevy_renderer/src/debug.rs @@ -6,7 +6,7 @@ use bevy::{ }; use bones_framework::debug::FrameDiagState; -use crate::BonesData; +use crate::BonesGame; /// Plugin for debug tools that hook into Bevy, such as [`bevy::diagnostic::FrameTimeDiagnosticsPlugin`]. pub struct BevyDebugPlugin; @@ -18,8 +18,7 @@ impl Plugin for BevyDebugPlugin { } } -fn sync_frame_time(mut bones_data: ResMut, diagnostics: Res) { - let game = &mut bones_data.game; +fn sync_frame_time(mut game: ResMut, diagnostics: Res) { let mut state = game.init_shared_resource::(); let fps = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS).unwrap(); diff --git a/framework_crates/bones_bevy_renderer/src/input.rs b/framework_crates/bones_bevy_renderer/src/input.rs new file mode 100644 index 0000000000..ce0719fce1 --- /dev/null +++ b/framework_crates/bones_bevy_renderer/src/input.rs @@ -0,0 +1,97 @@ +use super::*; + +use bevy::input::{ + gamepad::GamepadEvent, + keyboard::KeyboardInput, + mouse::{MouseButtonInput, MouseMotion, MouseWheel}, +}; + +pub fn insert_bones_input( + In((mouse_inputs, keyboard_inputs, gamepad_inputs)): In<( + bones::MouseInputs, + bones::KeyboardInputs, + bones::GamepadInputs, + )>, + mut game: ResMut, +) { + // Add the game inputs + game.insert_shared_resource(mouse_inputs); + game.insert_shared_resource(keyboard_inputs); + game.insert_shared_resource(gamepad_inputs); +} + +pub fn get_bones_input( + mut mouse_button_input_events: EventReader, + mut mouse_motion_events: EventReader, + mut mouse_wheel_events: EventReader, + mut keyboard_events: EventReader, + mut gamepad_events: EventReader, +) -> ( + bones::MouseInputs, + bones::KeyboardInputs, + bones::GamepadInputs, +) { + // TODO: investigate possible ways to avoid allocating vectors every frame for event lists. + ( + bones::MouseInputs { + movement: mouse_motion_events + .iter() + .last() + .map(|x| x.delta) + .unwrap_or_default(), + wheel_events: mouse_wheel_events + .iter() + .map(|event| bones::MouseScrollEvent { + unit: event.unit.into_bones(), + movement: Vec2::new(event.x, event.y), + }) + .collect(), + button_events: mouse_button_input_events + .iter() + .map(|event| bones::MouseButtonEvent { + button: event.button.into_bones(), + state: event.state.into_bones(), + }) + .collect(), + }, + bones::KeyboardInputs { + key_events: keyboard_events + .iter() + .map(|event| bones::KeyboardEvent { + scan_code: event.scan_code, + key_code: event.key_code.map(|x| x.into_bones()).into(), + button_state: event.state.into_bones(), + }) + .collect(), + }, + bones::GamepadInputs { + gamepad_events: gamepad_events + .iter() + .map(|event| match event { + GamepadEvent::Connection(c) => { + bones::GamepadEvent::Connection(bones::GamepadConnectionEvent { + gamepad: c.gamepad.id as u32, + event: if c.connected() { + bones::GamepadConnectionEventKind::Connected + } else { + bones::GamepadConnectionEventKind::Disconnected + }, + }) + } + GamepadEvent::Button(b) => { + bones::GamepadEvent::Button(bones::GamepadButtonEvent { + gamepad: b.gamepad.id as u32, + button: b.button_type.into_bones(), + value: b.value, + }) + } + GamepadEvent::Axis(a) => bones::GamepadEvent::Axis(bones::GamepadAxisEvent { + gamepad: a.gamepad.id as u32, + axis: a.axis_type.into_bones(), + value: a.value, + }), + }) + .collect(), + }, + ) +} diff --git a/framework_crates/bones_bevy_renderer/src/lib.rs b/framework_crates/bones_bevy_renderer/src/lib.rs index 0cc730b3f0..91eb8efa34 100644 --- a/framework_crates/bones_bevy_renderer/src/lib.rs +++ b/framework_crates/bones_bevy_renderer/src/lib.rs @@ -5,53 +5,44 @@ #![cfg_attr(doc, allow(unknown_lints))] #![deny(rustdoc::all)] -use std::path::PathBuf; - pub use bevy; -use bevy::{ - input::{ - gamepad::GamepadEvent, - keyboard::KeyboardInput, - mouse::{MouseButtonInput, MouseMotion, MouseWheel}, - InputSystem, - }, - prelude::*, - render::{camera::ScalingMode, Extract, RenderApp}, - sprite::{extract_sprites, Anchor, ExtractedSprite, ExtractedSprites, SpriteSystem}, - tasks::IoTaskPool, - utils::{HashMap, Instant}, - window::WindowMode, -}; -use bevy_egui::EguiContext; -use glam::*; - -use bevy_prototype_lyon::prelude as lyon; -use bones_framework::prelude::{ - self as bones, BitSet, ComponentIterBitset, EguiCtx, SchemaBox, SCHEMA_REGISTRY, -}; -use prelude::convert::{IntoBevy, IntoBones}; -use serde::{de::Visitor, Deserialize, Serialize}; - /// The prelude pub mod prelude { pub use crate::*; } -mod convert; mod debug; +mod storage; -/// Marker component for entities that are rendered in Bevy for bones. -#[derive(Component)] -pub struct BevyBonesEntity; +mod convert; +use convert::*; +mod input; +use input::*; +mod render; +use render::*; +mod ui; +use ui::*; + +use bevy::prelude::*; +use bones_framework::prelude as bones; + +use bevy::{ + input::InputSystem, + render::RenderApp, + sprite::{extract_sprites, SpriteSystem}, + tasks::IoTaskPool, + utils::Instant, +}; +use std::path::{Path, PathBuf}; /// Renderer for [`bones_framework`] [`Game`][bones::Game]s using Bevy. pub struct BonesBevyRenderer { - /// Skip the default loading screen and run the bones game immediately, so that you can - /// implement your own loading screen. - pub custom_load_progress: Option< - Box, - >, + /// Whether or not to load all assets on startup with a loading screen, + /// or skip straight to running the bones game immedietally. + pub preload: bool, + /// Optional field to implement your own loading screen. Does nothing if [`Self::preload`] = false + pub custom_load_progress: Option, /// Whether or not to use nearest-neighbor sampling for textures. pub pixel_art: bool, /// The bones game to run. @@ -69,127 +60,20 @@ pub struct BonesBevyRenderer { pub packs_dir: PathBuf, } -/// Resource containing the entity spawned for all of the bones game renderables. -#[derive(Resource)] -pub struct BonesGameEntity(pub Entity); -impl FromWorld for BonesGameEntity { - fn from_world(world: &mut World) -> Self { - Self(world.spawn(VisibilityBundle::default()).id()) - } -} - -/// Resource mapping bones image IDs to their bevy handles. -#[derive(Resource, Debug, Deref, DerefMut)] -pub struct BonesImageIds { - #[deref] - map: HashMap>, - next_id: u32, -} -impl Default for BonesImageIds { - fn default() -> Self { - Self { - map: Default::default(), - next_id: 1, - } - } -} - -impl BonesImageIds { - /// Load all bones images into bevy. - pub fn load_bones_images( - &mut self, - bones_assets: &bones::AssetServer, - bones_egui_textures: &mut bones::EguiTextures, - bevy_images: &mut Assets, - bevy_egui_textures: &mut bevy_egui::EguiUserTextures, - ) { - for entry in bones_assets.store.asset_ids.iter() { - let handle = entry.key(); - let cid = entry.value(); - let mut asset = bones_assets.store.assets.get_mut(cid).unwrap(); - if let Ok(image) = asset.data.try_cast_mut::() { - self.load_bones_image( - handle.typed(), - image, - bones_egui_textures, - bevy_images, - bevy_egui_textures, - ) - } - } - } - - /// Load a bones image into bevy. - pub fn load_bones_image( - &mut self, - bones_handle: bones::Handle, - image: &mut bones::Image, - bones_egui_textures: &mut bones::EguiTextures, - bevy_images: &mut Assets, - bevy_egui_textures: &mut bevy_egui::EguiUserTextures, - ) { - let Self { map, next_id } = self; - let mut taken_image = bones::Image::External(0); // Dummy value temporarily - std::mem::swap(image, &mut taken_image); - if let bones::Image::Data(data) = taken_image { - let handle = bevy_images.add(Image::from_dynamic(data, true)); - let egui_texture = bevy_egui_textures.add_image(handle.clone()); - bones_egui_textures.insert(bones_handle, egui_texture); - map.insert(*next_id, handle); - *image = bones::Image::External(*next_id); - *next_id += 1; - - // The image has already been loaded. This may happen if multiple asset handles use the same - // image data. We will end up visiting the same data twice. - } else { - // Swap the image back to it's previous value. - std::mem::swap(image, &mut taken_image); - } - } -} - -fn update_egui_fonts(ctx: &bevy_egui::egui::Context, bones_assets: &bones::AssetServer) { - use bevy_egui::egui; - let mut fonts = egui::FontDefinitions::default(); - - for entry in bones_assets.store.assets.iter() { - let asset = entry.value(); - if let Ok(font) = asset.try_cast_ref::() { - let previous = fonts - .font_data - .insert(font.family_name.to_string(), font.data.clone()); - if previous.is_some() { - warn!( - name=%font.family_name, - "Found two fonts with the same family name, using \ - only the latest one" - ); - } - fonts - .families - .entry(egui::FontFamily::Name(font.family_name.clone())) - .or_default() - .push(font.family_name.to_string()); - } +/// Bevy resource containing the [`bones::Game`] +#[derive(Resource, Deref, DerefMut)] +pub struct BonesGame(pub bones::Game); +impl BonesGame { + /// Shorthand for [`bones::AssetServer`] typed access to the shared resource + pub fn asset_server(&self) -> Option> { + self.0.shared_resource() } - - ctx.set_fonts(fonts); } -/// Bevy resource that contains the info for the bones game that is being rendered. -#[derive(Resource)] -pub struct BonesData { - /// The bones game. - pub game: bones::Game, - /// The bones asset server cell. - pub asset_server: Option, - /// The bones egui texture resource. - pub bones_egui_textures: bones::AtomicResource, - /// The custom load progress indicator. - pub custom_load_progress: Option< - Box, - >, -} +#[derive(Resource, Deref, DerefMut)] +struct LoadingContext(pub Option); +type LoadingFunction = + Box; impl BonesBevyRenderer { // TODO: Create a better builder pattern struct for `BonesBevyRenderer`. @@ -198,15 +82,53 @@ impl BonesBevyRenderer { /// Create a new [`BonesBevyRenderer`] for the provided game. pub fn new(game: bones::Game) -> Self { BonesBevyRenderer { + preload: true, pixel_art: true, custom_load_progress: None, game, game_version: bones::Version::new(0, 1, 0), - app_namespace: ("org".into(), "fishfolk".into(), "bones_demo_game".into()), + app_namespace: ("local".into(), "developer".into(), "bones_demo_game".into()), asset_dir: PathBuf::from("assets"), packs_dir: PathBuf::from("packs"), } } + /// Whether or not to load all assets on startup with a loading screen, + /// or skip straight to running the bones game immedietally. + pub fn preload(self, preload: bool) -> Self { + Self { preload, ..self } + } + /// Insert a custom loading screen function that will be used in place of the default + pub fn loading_screen(mut self, function: LoadingFunction) -> Self { + self.custom_load_progress = Some(function); + self + } + /// Whether or not to use nearest-neighbor sampling for textures. + pub fn pixel_art(self, pixel_art: bool) -> Self { + Self { pixel_art, ..self } + } + /// The (qualifier, organization, application) that will be used to pick a persistent storage + /// location for the game. + /// + /// For example: `("org", "fishfolk", "jumpy")` + pub fn namespace(mut self, (qualifier, organization, application): (&str, &str, &str)) -> Self { + self.app_namespace = (qualifier.into(), organization.into(), application.into()); + self + } + /// The path to load assets from. + pub fn asset_dir(self, asset_dir: PathBuf) -> Self { + Self { asset_dir, ..self } + } + /// The path to load asset packs from. + pub fn packs_dir(self, packs_dir: PathBuf) -> Self { + Self { packs_dir, ..self } + } + /// Set the version of the game, used for the asset loader. + pub fn version(self, game_version: bones::Version) -> Self { + Self { + game_version, + ..self + } + } /// Return a bevy [`App`] configured to run the bones game. pub fn app(mut self) -> App { @@ -226,50 +148,33 @@ impl BonesBevyRenderer { plugins = plugins.set(ImagePlugin::default_nearest()); } - app.add_plugins(plugins) - .add_plugins(( - bevy_egui::EguiPlugin, - lyon::ShapePlugin, - debug::BevyDebugPlugin, - )) - .insert_resource({ + app.add_plugins(plugins).add_plugins(( + bevy_egui::EguiPlugin, + bevy_prototype_lyon::plugin::ShapePlugin, + debug::BevyDebugPlugin, + )); + if self.pixel_art { + app.insert_resource({ let mut egui_settings = bevy_egui::EguiSettings::default(); - if self.pixel_art { - egui_settings.use_nearest_descriptor(); - } + egui_settings.use_nearest_descriptor(); egui_settings - }) - .init_resource::(); + }); + } + app.init_resource::(); - 'asset_load: { - let Some(mut asset_server) = self.game.shared_resource_mut::() - else { - break 'asset_load; - }; + if let Some(mut asset_server) = self.game.shared_resource_mut::() { asset_server.set_game_version(self.game_version); - - // Configure the AssetIO implementation - #[cfg(not(target_arch = "wasm32"))] - { - let io = bones::FileAssetIo::new(&self.asset_dir, &self.packs_dir); - asset_server.set_io(io); + asset_server.set_io(asset_io(&self.asset_dir, &self.packs_dir)); + + if self.preload { + // Spawn the task to load game assets + let s = asset_server.clone(); + IoTaskPool::get() + .spawn(async move { + s.load_assets().await.unwrap(); + }) + .detach(); } - #[cfg(target_arch = "wasm32")] - { - let window = web_sys::window().unwrap(); - let path = window.location().pathname().unwrap(); - let base = path.rsplit_once('/').map(|x| x.0).unwrap_or(&path); - let io = bones::WebAssetIo::new(&format!("{base}/assets")); - asset_server.set_io(io); - } - - // Spawn the task to load game assets - let s = asset_server.clone(); - IoTaskPool::get() - .spawn(async move { - s.load_assets().await.unwrap(); - }) - .detach(); // Enable asset hot reload. asset_server.watch_for_changes(); @@ -286,7 +191,6 @@ impl BonesBevyRenderer { self.game .insert_shared_resource(bones::EguiTextures::default()); - app.insert_resource(BonesImageIds::default()); // Insert empty inputs that will be updated by the `insert_bones_input` system later. self.game.init_shared_resource::(); @@ -294,35 +198,9 @@ impl BonesBevyRenderer { self.game.init_shared_resource::(); // Insert the bones data - app.insert_resource(BonesData { - asset_server: self - .game - .shared_resource::() - .map(|x| (*x).clone()), - bones_egui_textures: self - .game - .shared_resource_cell::() - .unwrap(), - game: self.game, - custom_load_progress: self.custom_load_progress, - }) - .init_resource::(); - - let assets_are_loaded = |data: Res| { - // Game is not required to have AssetServer, so default to true. - data.asset_server - .as_ref() - .map(|x| x.load_progress.is_finished()) - .unwrap_or(true) - }; - let assets_not_loaded = |data: Res| { - data.asset_server - .as_ref() - .map(|x| !x.load_progress.is_finished()) - .unwrap_or(true) - }; - let egui_ctx_initialized = - |data: Res| data.game.shared_resource::().is_some(); + app.insert_resource(BonesGame(self.game)) + .insert_resource(LoadingContext(self.custom_load_progress)) + .init_resource::(); // Add the world sync systems app.add_systems( @@ -333,15 +211,19 @@ impl BonesBevyRenderer { egui_input_hook, ) .chain() - .run_if(assets_are_loaded) + .run_if(assets_are_loaded.or_else(move || !self.preload)) .after(bevy_egui::EguiSet::ProcessInput) .before(bevy_egui::EguiSet::BeginFrame), - ) - .add_systems(Update, asset_load_status.run_if(assets_not_loaded)) - .add_systems( + ); + if self.preload { + app.add_systems(Update, asset_load_status.run_if(assets_not_loaded)); + } + app.add_systems( Update, ( load_egui_textures, + sync_bones_window, + handle_asset_changes, // Run world simulation step_bones_game, // Synchronize bones render components with the Bevy world. @@ -353,7 +235,7 @@ impl BonesBevyRenderer { ), ) .chain() - .run_if(assets_are_loaded) + .run_if(assets_are_loaded.or_else(move || !self.preload)) .run_if(egui_ctx_initialized), ); @@ -370,245 +252,54 @@ impl BonesBevyRenderer { } } -mod storage { - use super::*; - - #[cfg(target_arch = "wasm32")] - pub use wasm::StorageBackend; - #[cfg(target_arch = "wasm32")] - mod wasm { - use super::*; - pub struct StorageBackend { - storage_key: String, - } - - impl StorageBackend { - pub fn new(qualifier: &str, organization: &str, application: &str) -> Self { - Self { - storage_key: format!("{qualifier}.{organization}.{application}.storage"), - } - } - } - - impl bones::StorageApi for StorageBackend { - fn save(&mut self, data: Vec) { - let mut buffer = Vec::new(); - let mut serializer = serde_yaml::Serializer::new(&mut buffer); - LoadedStorage(data) - .serialize(&mut serializer) - .expect("Failed to serialize to storage file."); - let data = String::from_utf8(buffer).unwrap(); - let window = web_sys::window().unwrap(); - let storage = window.local_storage().unwrap().unwrap(); - storage.set_item(&self.storage_key, &data).unwrap(); - } +fn egui_ctx_initialized(game: Res) -> bool { + game.shared_resource::().is_some() +} - fn load(&mut self) -> Vec { - let window = web_sys::window().unwrap(); - let storage = window.local_storage().unwrap().unwrap(); - let Some(data) = storage.get_item(&self.storage_key).unwrap() else { - return default(); - }; +fn assets_are_loaded(game: Res) -> bool { + // Game is not required to have AssetServer, so default to true. + game.asset_server() + .as_ref() + .map(|x| x.load_progress.is_finished()) + .unwrap_or(true) +} - let Ok(loaded) = serde_yaml::from_str::(&data) else { - return default(); - }; - loaded.0 - } - } - } +fn assets_not_loaded(game: Res) -> bool { + game.asset_server() + .as_ref() + .map(|x| !x.load_progress.is_finished()) + .unwrap_or(true) +} +/// A [`bones::AssetIo`] configured for web and local file access +pub fn asset_io(asset_dir: &Path, packs_dir: &Path) -> impl bones::AssetIo + 'static { #[cfg(not(target_arch = "wasm32"))] - pub use native::StorageBackend; - #[cfg(not(target_arch = "wasm32"))] - mod native { - use super::*; - - pub struct StorageBackend { - storage_path: PathBuf, - } - - impl StorageBackend { - pub fn new(qualifier: &str, organization: &str, application: &str) -> Self { - let project_dirs = - directories::ProjectDirs::from(qualifier, organization, application) - .expect("Identify system data dir path"); - Self { - storage_path: project_dirs.data_dir().join("storage.yml"), - } - } - } - - impl bones::StorageApi for StorageBackend { - fn save(&mut self, data: Vec) { - let file = std::fs::OpenOptions::new() - .write(true) - .truncate(true) - .create(true) - .open(&self.storage_path) - .expect("Failed to open storage file"); - let mut serializer = serde_yaml::Serializer::new(file); - LoadedStorage(data) - .serialize(&mut serializer) - .expect("Failed to serialize to storage file."); - } - - fn load(&mut self) -> Vec { - use anyhow::Context; - if self.storage_path.exists() { - let result: anyhow::Result = (|| { - let file = std::fs::OpenOptions::new() - .read(true) - .open(&self.storage_path) - .context("Failed to open storage file")?; - let loaded: LoadedStorage = serde_yaml::from_reader(file) - .context("Failed to deserialize storage file")?; - - anyhow::Result::Ok(loaded) - })(); - match result { - Ok(loaded) => loaded.0, - Err(e) => { - error!( - "Error deserializing storage file, ignoring file, \ - data will be overwritten when saved: {e:?}" - ); - default() - } - } - } else { - std::fs::create_dir_all(self.storage_path.parent().unwrap()).unwrap(); - default() - } - } - } + { + bones::FileAssetIo::new(asset_dir, packs_dir) } - - struct LoadedStorage(Vec); - impl Serialize for LoadedStorage { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let data: HashMap = self - .0 - .iter() - .map(|x| (x.schema().full_name.to_string(), x.as_ref())) - .collect(); - - use serde::ser::SerializeMap; - let mut map = serializer.serialize_map(Some(data.len()))?; - - for (key, value) in data { - map.serialize_key(&key)?; - map.serialize_value(&bones::SchemaSerializer(value))?; - } - - map.end() - } - } - impl<'de> Deserialize<'de> for LoadedStorage { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_map(LoadedStorageVisitor).map(Self) - } - } - struct LoadedStorageVisitor; - impl<'de> Visitor<'de> for LoadedStorageVisitor { - type Value = Vec; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "Mapping of string type names to type data.") - } - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - let mut data = Vec::new(); - while let Some(type_name) = map.next_key::()? { - let Some(schema) = SCHEMA_REGISTRY - .schemas - .iter() - .find(|schema| schema.full_name.as_ref() == type_name) - else { - error!( - "\n\nCannot find schema registration for `{}` while loading persisted \ - storage. This means you that you need to call \ - `{}::schema()` to register your persisted storage type before \ - creating the `BonesBevyRenderer` or that there is data from an old \ - version of the app inside of the persistent storage file.\n\n", - type_name, type_name, - ); - continue; - }; - - data.push(map.next_value_seed(bones::SchemaDeserializer(schema))?); - } - - Ok(data) - } + #[cfg(target_arch = "wasm32")] + { + let _ = asset_dir; + let _ = packs_dir; + let window = web_sys::window().unwrap(); + let path = window.location().pathname().unwrap(); + let base = path.rsplit_once('/').map(|x| x.0).unwrap_or(&path); + bones::WebAssetIo::new(&format!("{base}/assets")) } } -fn default_load_progress(asset_server: &bones::AssetServer, ctx: &bevy_egui::egui::Context) { - use bevy_egui::egui; - let errored = asset_server.load_progress.errored(); - - egui::CentralPanel::default().show(ctx, |ui| { - let height = ui.available_height(); - let ctx = ui.ctx().clone(); - - let space_size = 0.03; - let spinner_size = 0.07; - let text_size = 0.034; - ui.vertical_centered(|ui| { - ui.add_space(height * 0.3); - - if errored > 0 { - ui.label( - egui::RichText::new("⚠") - .color(egui::Color32::RED) - .size(height * spinner_size), - ); - ui.add_space(height * space_size); - ui.label( - egui::RichText::new(format!( - "Error loading {errored} asset{}.", - if errored > 1 { "s" } else { "" } - )) - .color(egui::Color32::RED) - .size(height * text_size * 0.75), - ); - } else { - ui.add(egui::Spinner::new().size(height * spinner_size)); - ui.add_space(height * space_size); - ui.label(egui::RichText::new("Loading").size(height * text_size)); - } - }); - - ctx.data_mut(|d| { - d.insert_temp(ui.id(), (spinner_size, space_size, text_size)); - }) - }); -} - fn asset_load_status( - mut data: ResMut, + game: Res, + mut custom_load_context: ResMut, mut egui_query: Query<&mut bevy_egui::EguiContext, With>, ) { - let BonesData { - asset_server, - custom_load_progress, - .. - } = &mut *data; - let Some(asset_server) = &asset_server else { + let Some(asset_server) = &game.asset_server() else { return; }; let mut ctx = egui_query.single_mut(); - if let Some(load_progress) = custom_load_progress { - (load_progress)(asset_server, ctx.get_mut()); + if let Some(function) = &mut **custom_load_context { + (function)(asset_server, ctx.get_mut()); } else { default_load_progress(asset_server, ctx.get_mut()); } @@ -616,7 +307,7 @@ fn asset_load_status( fn load_egui_textures( mut has_initialized: Local, - data: ResMut, + game: ResMut, mut bones_image_ids: ResMut, mut bevy_images: ResMut>, mut bevy_egui_textures: ResMut, @@ -626,11 +317,8 @@ fn load_egui_textures( } else { return; } - if let Some(asset_server) = &data.asset_server { - let bones_egui_textures_cell = data - .game - .shared_resource_cell::() - .unwrap(); + if let Some(asset_server) = &game.asset_server() { + let bones_egui_textures_cell = game.shared_resource_cell::().unwrap(); // TODO: Avoid doing this every frame when there have been no assets loaded. // We should should be able to use the asset load progress event listener to detect newly // loaded assets that will need to be handled. @@ -645,173 +333,21 @@ fn load_egui_textures( } } -/// Startup system to load egui fonts and textures. -fn setup_egui(world: &mut World) { - world.resource_scope(|world: &mut World, mut bones_data: Mut| { - let ctx = { - let mut egui_query = world.query_filtered::<&mut EguiContext, With>(); - let mut egui_ctx = egui_query.get_single_mut(world).unwrap(); - egui_ctx.get_mut().clone() - }; - - // Insert the egui context as a shared resource - bones_data - .game - .insert_shared_resource(bones::EguiCtx(ctx.clone())); - - if let Some(bones_assets) = &bones_data.asset_server { - update_egui_fonts(&ctx, bones_assets); - - // Insert the bones egui textures - ctx.data_mut(|map| { - map.insert_temp( - bevy_egui::egui::Id::null(), - bones_data.bones_egui_textures.clone(), - ); - }); - } +/// System to step the bones simulation. +fn step_bones_game(world: &mut World) { + world.resource_scope(|world: &mut World, mut game: Mut| { + let time = world.get_resource::(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut data = Vec::new(); + while let Some(type_name) = map.next_key::()? { + let Some(schema) = SCHEMA_REGISTRY + .schemas + .iter() + .find(|schema| schema.full_name.as_ref() == type_name) + else { + error!( + "\n\nCannot find schema registration for `{}` while loading persisted \ + storage. This means you that you need to call \ + `{}::schema()` to register your persisted storage type before \ + creating the `BonesBevyRenderer` or that there is data from an old \ + version of the app inside of the persistent storage file.\n\n", + type_name, type_name, + ); + continue; + }; + + data.push(map.next_value_seed(SchemaDeserializer(schema))?); + } + + Ok(data) + } +} diff --git a/framework_crates/bones_bevy_renderer/src/ui.rs b/framework_crates/bones_bevy_renderer/src/ui.rs new file mode 100644 index 0000000000..c44d85d991 --- /dev/null +++ b/framework_crates/bones_bevy_renderer/src/ui.rs @@ -0,0 +1,123 @@ +use super::*; +use bevy_egui::EguiContext; + +/// Startup system to load egui fonts and textures. +pub fn setup_egui(world: &mut World) { + world.resource_scope(|world: &mut World, mut game: Mut| { + let ctx = { + let mut egui_query = world.query_filtered::<&mut EguiContext, With>(); + let mut egui_ctx = egui_query.get_single_mut(world).unwrap(); + egui_ctx.get_mut().clone() + }; + + // Insert the egui context as a shared resource + game.insert_shared_resource(bones::EguiCtx(ctx.clone())); + + if let Some(bones_assets) = &game.asset_server() { + update_egui_fonts(&ctx, bones_assets); + + // Insert the bones egui textures + ctx.data_mut(|map| { + map.insert_temp( + bevy_egui::egui::Id::null(), + game.shared_resource_cell::().unwrap(), + ); + }); + } + }); +} + +pub fn egui_input_hook( + mut egui_query: Query<&mut bevy_egui::EguiInput, With>, + mut game: ResMut, +) { + if let Some(hook) = game.shared_resource_cell::() { + let hook = hook.borrow().unwrap(); + let mut egui_input = egui_query.get_single_mut().unwrap(); + (hook.0)(&mut game, &mut egui_input); + } +} + +pub fn sync_egui_settings( + game: Res, + mut bevy_egui_settings: ResMut, +) { + for session_name in &game.sorted_session_keys { + let session = game.sessions.get(*session_name).unwrap(); + let world = &session.world; + + if let Some(settings) = world.get_resource::() { + bevy_egui_settings.scale_factor = settings.scale; + } + } +} + +pub fn update_egui_fonts(ctx: &bevy_egui::egui::Context, bones_assets: &bones::AssetServer) { + use bevy_egui::egui; + let mut fonts = egui::FontDefinitions::default(); + + for entry in bones_assets.store.assets.iter() { + let asset = entry.value(); + if let Ok(font) = asset.try_cast_ref::() { + let previous = fonts + .font_data + .insert(font.family_name.to_string(), font.data.clone()); + if previous.is_some() { + warn!( + name=%font.family_name, + "Found two fonts with the same family name, using \ + only the latest one" + ); + } + fonts + .families + .entry(egui::FontFamily::Name(font.family_name.clone())) + .or_default() + .push(font.family_name.to_string()); + } + } + + ctx.set_fonts(fonts); +} + +pub fn default_load_progress(asset_server: &bones::AssetServer, ctx: &bevy_egui::egui::Context) { + use bevy_egui::egui; + let errored = asset_server.load_progress.errored(); + + egui::CentralPanel::default().show(ctx, |ui| { + let height = ui.available_height(); + let ctx = ui.ctx().clone(); + + let space_size = 0.03; + let spinner_size = 0.07; + let text_size = 0.034; + ui.vertical_centered(|ui| { + ui.add_space(height * 0.3); + + if errored > 0 { + ui.label( + egui::RichText::new("⚠") + .color(egui::Color32::RED) + .size(height * spinner_size), + ); + ui.add_space(height * space_size); + ui.label( + egui::RichText::new(format!( + "Error loading {errored} asset{}.", + if errored > 1 { "s" } else { "" } + )) + .color(egui::Color32::RED) + .size(height * text_size * 0.75), + ); + } else { + ui.add(egui::Spinner::new().size(height * spinner_size)); + ui.add_space(height * space_size); + ui.label(egui::RichText::new("Loading").size(height * text_size)); + } + }); + + ctx.data_mut(|d| { + d.insert_temp(ui.id(), (spinner_size, space_size, text_size)); + }) + }); +} diff --git a/framework_crates/bones_ecs/src/system.rs b/framework_crates/bones_ecs/src/system.rs index 868b46a5f2..73b8ae927c 100644 --- a/framework_crates/bones_ecs/src/system.rs +++ b/framework_crates/bones_ecs/src/system.rs @@ -4,11 +4,6 @@ use std::sync::Arc; use crate::prelude::*; -#[derive(Deref, DerefMut)] -struct Test { - s: String, -} - /// Trait implemented by systems. pub trait System { /// Run the system. diff --git a/framework_crates/bones_framework/src/render.rs b/framework_crates/bones_framework/src/render.rs index ea55ca0642..742ef6757e 100644 --- a/framework_crates/bones_framework/src/render.rs +++ b/framework_crates/bones_framework/src/render.rs @@ -4,9 +4,7 @@ use bones_lib::prelude::*; /// Module prelude. pub mod prelude { - pub use super::{ - camera::*, color::*, line::*, sprite::*, tilemap::*, transform::*, Renderer, RendererApi, - }; + pub use super::{camera::*, color::*, line::*, sprite::*, tilemap::*, transform::*}; #[cfg(feature = "audio")] pub use super::audio::*; @@ -35,25 +33,3 @@ pub fn render_plugin(session: &mut Session) { .install_plugin(ui::ui_plugin) .install_plugin(camera::plugin); } - -/// Trait for the interface exposed by external bones renderers. -/// -/// These methods allow the game to notify the renderer when certain things happen, and to allow the -/// game to instruct the renderer to do certain things. -/// -/// -pub trait RendererApi: Sync + Send { - /// Have the renderer delete the session. - /// - /// The default implementation doesn't do anything, and that may be appropriate for some - /// renderers. Other renderers may need to clean up synchronized entities that are present in - /// the deleted session. - fn delete_session(&self, session: Session) { - let _ = session; - } -} - -/// Resource containing the [`RendererApi`] implementation provided by the bones renderer. -#[derive(HasSchema)] -#[schema(opaque, no_clone, no_default)] -pub struct Renderer(Box);