diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 7be2541583148..290772f150c7f 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -25,6 +25,7 @@ bevy_text = { path = "../bevy_text", version = "0.14.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index 51b8e7886dcf9..4092a228f41ab 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -1,5 +1,7 @@ //! Module containing logic for FPS overlay. +use std::any::TypeId; + use bevy_app::{Plugin, Startup, Update}; use bevy_asset::Handle; use bevy_color::Color; @@ -8,10 +10,15 @@ use bevy_ecs::{ component::Component, query::With, schedule::{common_conditions::resource_changed, IntoSystemConfigs}, - system::{Commands, Query, Res, Resource}, + system::{Commands, Query, Res}, }; +use bevy_reflect::Reflect; +use bevy_render::view::Visibility; use bevy_text::{Font, Text, TextSection, TextStyle}; use bevy_ui::node_bundles::TextBundle; +use bevy_utils::warn_once; + +use crate::{DevTool, DevToolApp, DevToolsStore}; /// A plugin that adds an FPS overlay to the Bevy application. /// @@ -22,7 +29,7 @@ use bevy_ui::node_bundles::TextBundle; /// - **Metal**: setting env variable `MTL_HUD_ENABLED=1` #[derive(Default)] pub struct FpsOverlayPlugin { - /// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource. + /// Starting configuration of overlay, this can be later be changed through [`DevToolsStore`] resource. pub config: FpsOverlayConfig, } @@ -32,25 +39,31 @@ impl Plugin for FpsOverlayPlugin { if !app.is_plugin_added::() { app.add_plugins(FrameTimeDiagnosticsPlugin); } - app.insert_resource(self.config.clone()) + app.insert_dev_tool(self.config.clone()) .add_systems(Startup, setup) .add_systems( Update, ( - customize_text.run_if(resource_changed::), - update_text, + change_visibility.run_if(resource_changed::), + (customize_text, update_text).run_if(|dev_tools: Res| { + dev_tools + .get(&TypeId::of::()) + .is_some_and(|dev_tool| dev_tool.is_enabled) + }), ), ); } } /// Configuration options for the FPS overlay. -#[derive(Resource, Clone)] +#[derive(Clone, Debug, Reflect)] pub struct FpsOverlayConfig { /// Configuration of text in the overlay. pub text_config: TextStyle, } +impl DevTool for FpsOverlayConfig {} + impl Default for FpsOverlayConfig { fn default() -> Self { FpsOverlayConfig { @@ -66,11 +79,19 @@ impl Default for FpsOverlayConfig { #[derive(Component)] struct FpsText; -fn setup(mut commands: Commands, overlay_config: Res) { +fn setup(mut commands: Commands, dev_tools: Res) { + let Some(dev_tool) = dev_tools.get(&TypeId::of::()) else { + warn_once!("Dev tool with TypeId of FpsOverlayConfig does not exist in DevToolsStore. Fps overlay won't be created"); + return; + }; + let Some(tool_config) = dev_tool.get_tool_config::() else { + warn_once!("Failed to get tool config from dev tool. Fps overlay won't be created"); + return; + }; commands.spawn(( TextBundle::from_sections([ - TextSection::new("FPS: ", overlay_config.text_config.clone()), - TextSection::from_style(overlay_config.text_config.clone()), + TextSection::new("FPS: ", tool_config.text_config.clone()), + TextSection::from_style(tool_config.text_config.clone()), ]), FpsText, )); @@ -86,13 +107,36 @@ fn update_text(diagnostic: Res, mut query: Query<&mut Text, Wi } } -fn customize_text( - overlay_config: Res, - mut query: Query<&mut Text, With>, -) { +fn customize_text(dev_tools: Res, mut query: Query<&mut Text, With>) { for mut text in &mut query { for section in text.sections.iter_mut() { - section.style = overlay_config.text_config.clone(); + let Some(dev_tool) = dev_tools.get(&TypeId::of::()) else { + warn_once!("Dev tool with TypeId of FpsOverlayConfig does not exist in DevToolsStore. You won't be able to customize the overlay"); + return; + }; + let Some(tool_config) = dev_tool.get_tool_config::() else { + warn_once!("Failed to get tool config from dev tool. Fps overlay won't be created. You won't be able to customize the overlay"); + return; + }; + section.style = tool_config.text_config.clone(); + } + } +} + +fn change_visibility( + mut query: Query<&mut Visibility, With>, + dev_tools: Res, +) { + if dev_tools + .get(&TypeId::of::()) + .is_some_and(|dev_tool| dev_tool.is_enabled) + { + for mut visibility in query.iter_mut() { + *visibility = Visibility::Visible; + } + } else { + for mut visibility in query.iter_mut() { + *visibility = Visibility::Hidden; } } } diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index adad8cec9030b..bb1af17d0177f 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -3,6 +3,10 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] use bevy_app::prelude::*; +use bevy_ecs::system::Resource; +use bevy_reflect::Reflect; +use bevy_utils::HashMap; +use std::{any::TypeId, fmt::Debug}; #[cfg(feature = "bevy_ci_testing")] pub mod ci_testing; @@ -45,3 +49,128 @@ impl Plugin for DevToolsPlugin { } } } + +/// Trait implemented for every dev tool. +pub trait DevTool: Sync + Send + Debug + Reflect + 'static {} + +/// Information about dev tool. +#[derive(Debug)] +pub struct DevToolConfig { + /// Identifier of a dev tool. + pub id: TypeId, + /// Tool specific configuration. + tool_config: Box, + is_enabled: bool, +} + +impl DevToolConfig { + /// Returns true if [`DevTool`] is enabled. + pub fn is_enabled(&self) -> bool { + self.is_enabled + } + + /// Enables [`DevTool`]. + pub fn enable(&mut self) { + self.is_enabled = true; + } + + /// Disables + pub fn disable(&mut self) { + self.is_enabled = false; + } + + /// Toggles [`DevTool`]. + pub fn toggle(&mut self) { + self.is_enabled = !self.is_enabled; + } +} + +impl DevToolConfig { + /// Creates a new [`DevTool`] from a specified [`TypeId`]. + /// New tool is enabled by default. + pub fn new(id: TypeId, tool_config: impl DevTool) -> DevToolConfig { + DevToolConfig { + id, + tool_config: Box::new(tool_config), + is_enabled: true, + } + } + + /// Returns a tool specific configuration. + pub fn get_tool_config(&self) -> Option<&D> { + self.tool_config.as_any().downcast_ref::() + } + + /// Returns a mutable tool specific configuration. + pub fn get_tool_config_mut(&mut self) -> Option<&mut D> { + self.tool_config.as_any_mut().downcast_mut::() + } +} + +/// A collection of [`DevTool`]s. +#[derive(Resource, Default, Debug)] +pub struct DevToolsStore { + dev_tools: HashMap, +} + +impl DevToolsStore { + /// Adds a new [`DevTool`]. + /// + /// If possible, prefer calling [`App::init_dev_tool`] or [`App::insert_dev_tool`]. + pub fn add(&mut self, dev_tool: DevToolConfig) { + self.dev_tools.insert(dev_tool.id, dev_tool); + } + + /// Removes a [`DevTool`]. + pub fn remove(&mut self, id: &TypeId) { + self.dev_tools.remove(id); + } + + /// Returns a reference to the given [`DevTool`] if present. + pub fn get(&self, id: &TypeId) -> Option<&DevToolConfig> { + self.dev_tools.get(id) + } + + /// Returns a mutable reference to the given [`DevTool`] if present. + pub fn get_mut(&mut self, id: &TypeId) -> Option<&mut DevToolConfig> { + self.dev_tools.get_mut(id) + } + + /// Returns an iterator over all [`DevTool`]s, by reference. + pub fn iter(&self) -> impl Iterator { + self.dev_tools.values() + } + + /// Returns an iterator over all [`DevTool`]s, by mutable reference. + pub fn iter_mut(&mut self) -> impl Iterator { + self.dev_tools.values_mut() + } +} + +/// Extends [`App`] with new `init_dev_tool` and `insert_dev_tool` functions. +pub trait DevToolApp { + /// Initialize a new [`DevTool`]. + fn init_dev_tool(&mut self) -> &mut Self; + /// Insert a new [`DevTool`] with configuration. + fn insert_dev_tool(&mut self, value: D) -> &mut Self; +} + +impl DevToolApp for App { + fn init_dev_tool(&mut self) -> &mut Self { + let dev_tool = DevToolConfig::new(TypeId::of::(), D::default()); + let mut dev_tools = self + .world + .get_resource_or_insert_with::(Default::default); + dev_tools.add(dev_tool); + self + } + + fn insert_dev_tool(&mut self, value: D) -> &mut Self { + let dev_tool = DevToolConfig::new(TypeId::of::(), value); + let mut dev_tools = self + .world + .get_resource_or_insert_with::(Default::default); + dev_tools.add(dev_tool); + self + } +} diff --git a/examples/dev_tools/fps_overlay.rs b/examples/dev_tools/fps_overlay.rs index e7f4cada95462..29cfb82946614 100644 --- a/examples/dev_tools/fps_overlay.rs +++ b/examples/dev_tools/fps_overlay.rs @@ -1,7 +1,12 @@ //! Showcase how to use and configure FPS overlay. +use std::any::TypeId; + use bevy::{ - dev_tools::fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin}, + dev_tools::{ + fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin}, + DevToolsStore, + }, prelude::*, }; @@ -54,12 +59,22 @@ fn setup(mut commands: Commands) { ); } -fn customize_config(input: Res>, mut overlay: ResMut) { +fn customize_config(input: Res>, mut dev_tools: ResMut) { + // We try to get mutable reference to fps overlay dev tool. Otherwise we don't do anything + let Some(dev_tool) = dev_tools.get_mut(&TypeId::of::()) else { + return; + }; + + // We try to access configuration struct that is specific to this dev tool. + let Some(tool_config) = dev_tool.get_tool_config_mut::() else { + return; + }; + if input.just_pressed(KeyCode::Digit1) { - // Changing resource will affect overlay - overlay.text_config.color = Color::srgb(1.0, 0.0, 0.0); + // Changing tool_config will affect overlay + tool_config.text_config.color = Color::srgb(1.0, 0.0, 0.0); } if input.just_pressed(KeyCode::Digit2) { - overlay.text_config.font_size -= 2.0; + tool_config.text_config.font_size -= 2.0; } }