├── .gitignore ├── rust-toolchain.toml ├── LICENSE.md ├── deny.toml ├── examples ├── centered_style.rs ├── render_layer.rs ├── simple_builtins.rs ├── runtime_configure.rs └── custom_diagnostic.rs ├── Cargo.toml ├── .github └── workflows │ └── dependencies.yml ├── README.md ├── .vscode └── tasks.json └── src ├── extras.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | trace-*.json 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | bevy_screen_diagnostics is dual-licensed under either 2 | 3 | - MIT License (http://opensource.org/licenses/MIT) 4 | - Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 5 | 6 | at your option. 7 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | vulnerability = "deny" 3 | unmaintained = "deny" 4 | notice = "deny" 5 | unsound = "deny" 6 | ignore = ["RUSTSEC-2020-0056"] 7 | 8 | [licenses] 9 | unlicensed = "deny" 10 | copyleft = "deny" 11 | allow-osi-fsf-free = "either" 12 | default = "deny" 13 | 14 | [[licenses.clarify]] 15 | name = "stretch" 16 | expression = "MIT" 17 | license-files = [] 18 | 19 | [bans] 20 | multiple-versions = "allow" 21 | wildcards = "deny" 22 | 23 | [sources] 24 | unknown-registry = "deny" 25 | unknown-git = "deny" 26 | -------------------------------------------------------------------------------- /examples/centered_style.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use bevy_screen_diagnostics::{ScreenDiagnosticsPlugin, ScreenFrameDiagnosticsPlugin}; 4 | 5 | fn main() { 6 | App::new() 7 | .insert_resource(ClearColor(Color::BLACK)) 8 | .add_plugins(DefaultPlugins) 9 | .add_plugins(ScreenDiagnosticsPlugin { 10 | style: Node { 11 | margin: UiRect::all(Val::Auto), 12 | align_self: AlignSelf::Center, 13 | ..default() 14 | }, 15 | ..default() 16 | }) 17 | .add_plugins(ScreenFrameDiagnosticsPlugin) 18 | .add_systems(Startup, setup_camera) 19 | .run(); 20 | } 21 | 22 | // need a camera to display the UI 23 | fn setup_camera(mut commands: Commands) { 24 | commands.spawn(Camera2d); 25 | } 26 | -------------------------------------------------------------------------------- /examples/render_layer.rs: -------------------------------------------------------------------------------- 1 | /// Show frametimes and framerate 2 | use bevy::{prelude::*, render::view::RenderLayers}; 3 | 4 | use bevy_screen_diagnostics::{ScreenDiagnosticsPlugin, ScreenFrameDiagnosticsPlugin}; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins(DefaultPlugins) 9 | .add_plugins(ScreenDiagnosticsPlugin { 10 | render_layer: RenderLayers::from_layers(&[1]), // render diagnostics to different layer 11 | ..default() 12 | }) 13 | .add_plugins(ScreenFrameDiagnosticsPlugin) 14 | .add_systems(Startup, setup_camera) 15 | .run(); 16 | } 17 | 18 | // need a camera to display the UI 19 | fn setup_camera(mut commands: Commands) { 20 | // spawn camera on different layer 21 | commands.spawn((Camera2d, RenderLayers::from_layers(&[1]))); 22 | // could be useful for rendre-to-texture or editor-like applications 23 | } 24 | -------------------------------------------------------------------------------- /examples/simple_builtins.rs: -------------------------------------------------------------------------------- 1 | /// Add and show all pre-defined simple plugins. Run with feature sysinfo_plugin to get system info like CPU usage. 2 | use bevy::prelude::*; 3 | 4 | use bevy_screen_diagnostics::{ 5 | ScreenDiagnosticsPlugin, ScreenEntityDiagnosticsPlugin, ScreenFrameDiagnosticsPlugin, 6 | }; 7 | 8 | #[cfg(feature = "sysinfo_plugin")] 9 | use bevy_screen_diagnostics::ScreenSystemInformationDiagnosticsPlugin; 10 | 11 | fn main() { 12 | let mut app = App::new(); 13 | app.add_plugins(DefaultPlugins) 14 | .add_plugins(ScreenDiagnosticsPlugin::default()) 15 | .add_plugins(ScreenFrameDiagnosticsPlugin) 16 | .add_plugins(ScreenEntityDiagnosticsPlugin) 17 | .add_systems(Startup, setup_camera); 18 | 19 | #[cfg(feature = "sysinfo_plugin")] 20 | app.add_plugins(ScreenSystemInformationDiagnosticsPlugin); 21 | 22 | app.run(); 23 | } 24 | 25 | // need a camera to display the UI 26 | fn setup_camera(mut commands: Commands) { 27 | commands.spawn(Camera2d); 28 | } 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_screen_diagnostics" 3 | description = "Bevy plugin for displaying diagnostics on screen." 4 | documentation = "https://docs.rs/bevy_screen_diagnostics" 5 | version = "0.8.1" 6 | license = "MIT OR Apache-2.0" 7 | edition = "2024" 8 | keywords = ["gamedev", "bevy", "diagnostics", "debug"] 9 | repository = "https://github.com/laundmo/bevy_screen_diagnostics" 10 | readme = "README.md" 11 | authors = ["laundmo"] 12 | 13 | [features] 14 | default = ["builtin-font"] 15 | builtin-font = ["bevy/default_font"] 16 | sysinfo_plugin = ["bevy/sysinfo_plugin"] 17 | 18 | [dependencies] 19 | bevy = { version = "0.16.0", default-features = false, features = [ 20 | "bevy_text", 21 | "bevy_ui", 22 | "bevy_asset", 23 | "bevy_render", 24 | ] } 25 | 26 | [dev-dependencies] 27 | bevy = { version = "0.16.0", default-features = true } 28 | 29 | [profile.dev] 30 | opt-level = 1 31 | 32 | [profile.dev.package."*"] 33 | opt-level = 3 34 | 35 | [package.metadata.docs.rs] 36 | all-features = true 37 | rustdoc-args = ["--cfg", "docsrs"] 38 | -------------------------------------------------------------------------------- /examples/runtime_configure.rs: -------------------------------------------------------------------------------- 1 | // Example of configuring 2 | 3 | use bevy::prelude::*; 4 | 5 | use bevy_screen_diagnostics::{ 6 | Aggregate, ScreenDiagnostics, ScreenDiagnosticsPlugin, ScreenFrameDiagnosticsPlugin, 7 | }; 8 | 9 | fn main() { 10 | App::new() 11 | .insert_resource(ClearColor(Color::BLACK)) 12 | .add_plugins(DefaultPlugins) 13 | .add_plugins(ScreenDiagnosticsPlugin::default()) 14 | .add_plugins(ScreenFrameDiagnosticsPlugin) 15 | .add_systems(Startup, setup_camera) 16 | .add_systems(Update, (rainbow, mouse)) 17 | .run(); 18 | } 19 | 20 | // need a camera to display the UI 21 | fn setup_camera(mut commands: Commands) { 22 | commands.spawn(Camera2d); 23 | } 24 | 25 | fn rainbow(mut diags: ResMut, mut hue: Local) { 26 | diags 27 | .modify("fps") 28 | .diagnostic_color(Color::hsl(*hue, 1., 0.5)) 29 | .name_color(Color::hsl(*hue, 0.5, 0.5)); 30 | *hue = (*hue + 1.) % 360.; 31 | } 32 | 33 | fn mouse( 34 | buttons: Res>, 35 | mut diags: ResMut, 36 | mut aggregate_toggle: Local, 37 | ) { 38 | if buttons.just_pressed(MouseButton::Left) { 39 | diags.modify("ms/frame").toggle(); 40 | } 41 | if buttons.just_pressed(MouseButton::Right) { 42 | if *aggregate_toggle { 43 | diags 44 | .modify("fps") 45 | .aggregate(Aggregate::Value) 46 | .format(|v| format!("{v:.0}")); 47 | } else { 48 | diags 49 | .modify("fps") 50 | .aggregate(Aggregate::Average) 51 | .format(|v| format!("{v:.3}")); 52 | } 53 | *aggregate_toggle = !*aggregate_toggle; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/custom_diagnostic.rs: -------------------------------------------------------------------------------- 1 | /// Add a custom diagnostic to bevy and to your screen diagnostics 2 | use bevy::{ 3 | diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic}, 4 | prelude::*, 5 | }; 6 | 7 | use bevy_screen_diagnostics::{Aggregate, ScreenDiagnostics, ScreenDiagnosticsPlugin}; 8 | 9 | fn main() { 10 | App::new() 11 | .add_plugins(DefaultPlugins) 12 | .register_diagnostic(Diagnostic::new(BOX_COUNT)) 13 | .add_plugins(ScreenDiagnosticsPlugin::default()) 14 | .add_systems(Startup, (setup, setup_diagnostic)) 15 | .add_systems(Update, thing_count) 16 | .run(); 17 | } 18 | 19 | #[derive(Component)] 20 | struct Thing; 21 | 22 | fn setup(mut commands: Commands) { 23 | // need a camera to display the UI 24 | commands.spawn(Camera2d); 25 | // spawn 10 things 26 | for i in 0..10 { 27 | commands 28 | .spawn(( 29 | Sprite { 30 | color: Color::WHITE, 31 | custom_size: Some(Vec2::new(5.0, 5.0)), 32 | ..default() 33 | }, 34 | Transform::from_translation(Vec3::new(i as f32 * 10.0, 0.0, i as f32 * 10.0)), 35 | )) 36 | .insert(Thing); 37 | } 38 | } 39 | 40 | // For a full explanation on adding custom diagnostics, see: https://github.com/bevyengine/bevy/blob/main/examples/diagnostics/custom_diagnostic.rs 41 | const BOX_COUNT: DiagnosticPath = DiagnosticPath::const_new("box_count"); 42 | 43 | fn setup_diagnostic(mut onscreen: ResMut) { 44 | onscreen 45 | .add("things".to_string(), BOX_COUNT) 46 | .aggregate(Aggregate::Value) 47 | .format(|v| format!("{v:.0}")); 48 | } 49 | 50 | fn thing_count(mut diagnostics: Diagnostics, parts: Query<&Thing>) { 51 | diagnostics.add_measurement(&BOX_COUNT, || parts.iter().len() as f64); 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Dependencies 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**/Cargo.toml' 7 | - 'deny.toml' 8 | push: 9 | paths: 10 | - '**/Cargo.toml' 11 | - 'deny.toml' 12 | branches-ignore: 13 | - 'dependabot/**' 14 | - staging-squash-merge.tmp 15 | schedule: 16 | - cron: "0 0 * * 0" 17 | 18 | env: 19 | CARGO_TERM_COLOR: always 20 | 21 | jobs: 22 | check-advisories: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: nightly 29 | override: true 30 | - name: Install cargo-deny 31 | run: cargo install cargo-deny 32 | - name: Check for security advisories and unmaintained crates 33 | run: cargo deny check advisories 34 | 35 | check-bans: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | toolchain: nightly 42 | override: true 43 | - name: Install cargo-deny 44 | run: cargo install cargo-deny 45 | - name: Check for banned and duplicated dependencies 46 | run: cargo deny check bans 47 | 48 | check-licenses: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v3 52 | - uses: actions-rs/toolchain@v1 53 | with: 54 | toolchain: nightly 55 | override: true 56 | - name: Install cargo-deny 57 | run: cargo install cargo-deny 58 | - name: Check for unauthorized licenses 59 | run: cargo deny check licenses 60 | 61 | check-sources: 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v3 65 | - uses: actions-rs/toolchain@v1 66 | with: 67 | toolchain: nightly 68 | override: true 69 | - name: Install cargo-deny 70 | run: cargo install cargo-deny 71 | - name: Checked for unauthorized crate sources 72 | run: cargo deny check sources 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_screen_diagnostics 2 | 3 | Display bevy diagnostics on the window without any hassle. 4 | 5 |

6 | 7 |

8 | 9 | What this can do: 10 | 11 | - easy frame and entity dignostics 12 | - add custom diagnostics 13 | - change display of diagnostics on the fly 14 | - toggle diagnostics easily 15 | 16 | see the [examples](./examples/) on how to do this. 17 | 18 | ## Quickstart 19 | 20 | This adds the framerate and frametime diagnostics to your window. 21 | 22 | ```rs 23 | use bevy::prelude::*; 24 | 25 | use bevy_screen_diagnostics::{ScreenDiagnosticsPlugin, ScreenFrameDiagnosticsPlugin}; 26 | 27 | fn main() { 28 | App::new() 29 | .add_plugins(DefaultPlugins) 30 | .add_plugins(ScreenDiagnosticsPlugin::default()) 31 | .add_plugins(ScreenFrameDiagnosticsPlugin) 32 | .add_systems(Startup, setup_camera) 33 | .run(); 34 | } 35 | 36 | fn setup_camera(mut commands: Commands) { 37 | commands.spawn(Camera2d); 38 | } 39 | ``` 40 | 41 | The ScreenFrameDiagnosticsPlugin is a [very simple plugin](./src/extras.rs) 42 | 43 | ## Plugins 44 | 45 | bevy_screen_diagnostics provides the following bevy plugins: 46 | - [`ScreenDiagnostics`] which offers the basic functionality of displaying diagnostics. 47 | - [`ScreenFrameDiagnosticsPlugin`] display the framerate and frametime (also adds the corresponding bevy diagnostic plugin) 48 | - [`ScreenEntityDiagnosticsPlugin`] display the amount of entities (also adds the corresponding bevy diagnostic plugin) 49 | - [`ScreenSystemInformationDiagnosticsPlugin`] feature `sysinfo_plugin` only, display the memory and CPU usage of this process and the entire system (also adds the corresponding bevy diagnostic plugin) 50 | 51 | ## Font 52 | 53 | This crate uses bevy's default font (a stripped version of FiraCode) through the `builtin-font` default feature. 54 | You can provide your own font while initialising the `ScreenDiagnosticsPlugin` by passing it a asset file path. 55 | 56 | ## compatible bevy versions 57 | 58 | 59 | | bevy | bevy_screen_diagnostics | 60 | | ---- | ----------------------- | 61 | | 0.16 | 0.8.1 | 62 | | 0.15 | 0.7 | 63 | | 0.14 | 0.6 | 64 | | 0.13 | 0.5 | 65 | | 0.12 | 0.4 | 66 | | 0.11 | 0.3 | 67 | | 0.10 | 0.2 | 68 | | 0.9 | 0.1 | 69 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Test (fast compile)", 8 | "detail": "Runs all tests", 9 | "type": "shell", 10 | "group": "test", 11 | "command": "cargo t --workspace --features bevy/dynamic", 12 | "options": { 13 | "env": { 14 | "RUST_LOG": "warn,aabbsplosions=trace" 15 | } 16 | }, 17 | "problemMatcher": ["$rustc"], 18 | "presentation": { 19 | "echo": true, 20 | "reveal": "always", 21 | "panel": "shared", 22 | "showReuseMessage": true, 23 | "clear": true, 24 | "focus": true 25 | } 26 | }, 27 | { 28 | "label": "Run release (fast compile)", 29 | "detail": "Runs binary in release mode", 30 | "type": "shell", 31 | "group": "build", 32 | "command": "cargo run --features bevy/dynamic --release", 33 | "problemMatcher": ["$rustc"], 34 | "presentation": { 35 | "echo": true, 36 | "reveal": "always", 37 | "panel": "shared", 38 | "showReuseMessage": true, 39 | "clear": true, 40 | "focus": true 41 | } 42 | }, 43 | { 44 | "label": "Run debug (fast compile)", 45 | "detail": "Runs binary in debug mode", 46 | "type": "shell", 47 | "group": "build", 48 | "command": "cargo run --features bevy/dynamic", 49 | "problemMatcher": ["$rustc"], 50 | "presentation": { 51 | "echo": true, 52 | "reveal": "always", 53 | "panel": "shared", 54 | "showReuseMessage": true, 55 | "clear": true, 56 | "focus": true 57 | } 58 | }, 59 | { 60 | "label": "Update", 61 | "detail": "Update dependencies (Cargo.lock)", 62 | "type": "shell", 63 | "command": "cargo update", 64 | "problemMatcher": [], 65 | "presentation": { 66 | "echo": true, 67 | "reveal": "always", 68 | "panel": "shared", 69 | "showReuseMessage": true, 70 | "clear": true, 71 | "focus": true 72 | } 73 | }, 74 | { 75 | "label": "Clippy", 76 | "detail": "Look for Clippy errors", 77 | "type": "shell", 78 | "command": "cargo clippy --all-features --all-targets -- -D warnings", 79 | "problemMatcher": [], 80 | "presentation": { 81 | "echo": true, 82 | "reveal": "always", 83 | "panel": "shared", 84 | "showReuseMessage": true, 85 | "clear": true, 86 | "focus": true 87 | } 88 | }, 89 | { 90 | "label": "Format", 91 | "detail": "Format all source", 92 | "type": "shell", 93 | "command": "cargo fmt --all --quiet", 94 | "problemMatcher": [], 95 | "presentation": { 96 | "echo": false, 97 | "reveal": "never", 98 | "panel": "shared", 99 | "showReuseMessage": false 100 | } 101 | }, 102 | { 103 | "label": "Clean", 104 | "detail": "Clean build artifacts", 105 | "type": "shell", 106 | "command": "cargo clean", 107 | "problemMatcher": [], 108 | "presentation": { 109 | "echo": false, 110 | "reveal": "never", 111 | "panel": "shared", 112 | "showReuseMessage": false 113 | } 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /src/extras.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | diagnostic::{EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin}, 3 | prelude::*, 4 | }; 5 | 6 | use crate::{Aggregate, ScreenDiagnostics}; 7 | 8 | /// Plugin which adds the bevy [`FrameTimeDiagnosticsPlugin`] and adds its diagnostics to [DiagnosticsText] 9 | /// 10 | /// Example: ``16.6 ms/frame 60 fps`` 11 | pub struct ScreenFrameDiagnosticsPlugin; 12 | 13 | impl Plugin for ScreenFrameDiagnosticsPlugin { 14 | fn build(&self, app: &mut App) { 15 | if !app.is_plugin_added::() { 16 | app.add_plugins(FrameTimeDiagnosticsPlugin::default()); 17 | } 18 | app.add_systems(Startup, setup_frame_diagnostics); 19 | } 20 | } 21 | 22 | fn setup_frame_diagnostics(mut diags: ResMut) { 23 | diags 24 | .add("fps".to_string(), FrameTimeDiagnosticsPlugin::FPS) 25 | .aggregate(Aggregate::Value) 26 | .format(|v| format!("{v:.0}")); 27 | 28 | diags 29 | .add( 30 | "ms/frame".to_string(), 31 | FrameTimeDiagnosticsPlugin::FRAME_TIME, 32 | ) 33 | .aggregate(Aggregate::MovingAverage(5)) 34 | .format(|v| format!("{v:.2}")); 35 | } 36 | 37 | /// Plugin which adds the bevy [`EntityCountDiagnosticsPlugin`] and adds its diagnostics to [DiagnosticsText] 38 | /// 39 | /// Example: ``15 entities`` 40 | pub struct ScreenEntityDiagnosticsPlugin; 41 | 42 | impl Plugin for ScreenEntityDiagnosticsPlugin { 43 | fn build(&self, app: &mut App) { 44 | if !app.is_plugin_added::() { 45 | app.add_plugins(EntityCountDiagnosticsPlugin); 46 | } 47 | app.add_systems(Startup, setup_entity_diagnostics); 48 | } 49 | } 50 | 51 | fn setup_entity_diagnostics(mut diags: ResMut) { 52 | diags 53 | .add( 54 | "entities".to_string(), 55 | EntityCountDiagnosticsPlugin::ENTITY_COUNT, 56 | ) 57 | .aggregate(Aggregate::Value) 58 | .format(|v| format!("{v:.0}")); 59 | } 60 | #[cfg(feature = "sysinfo_plugin")] 61 | pub(crate) mod sysinfo_plugin { 62 | use bevy::{diagnostic::SystemInformationDiagnosticsPlugin, prelude::*}; 63 | 64 | use crate::{Aggregate, ScreenDiagnostics}; 65 | /// Plugin which adds the bevy [`SystemInformationDiagnosticsPlugin`] and adds its diagnostics to [DiagnosticsText]. 66 | /// "Total" is the value of the entire machine. 67 | /// 68 | /// Example: ``09.8% Memory Total 01.1% Memory 12.5% CPU Total 01.5% CPU`` 69 | pub struct ScreenSystemInformationDiagnosticsPlugin; 70 | 71 | impl Plugin for ScreenSystemInformationDiagnosticsPlugin { 72 | fn build(&self, app: &mut App) { 73 | if !app.is_plugin_added::() { 74 | app.add_plugins(SystemInformationDiagnosticsPlugin); 75 | } 76 | app.add_systems(Startup, setup_systeminfo_diagnostics); 77 | } 78 | } 79 | 80 | fn setup_systeminfo_diagnostics(mut diags: ResMut) { 81 | diags 82 | .add( 83 | "CPU".to_string(), 84 | SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE, 85 | ) 86 | .aggregate(Aggregate::Value) 87 | .format(|v| format!("{v:0>4.1}%")); 88 | diags 89 | .add( 90 | "Memory".to_string(), 91 | SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE, 92 | ) 93 | .aggregate(Aggregate::Value) 94 | .format(|v| format!("{v:0>4.1}%")); 95 | diags 96 | .add( 97 | "CPU Total".to_string(), 98 | SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE, 99 | ) 100 | .aggregate(Aggregate::Value) 101 | .format(|v| format!("{v:0>4.1}%")); 102 | diags 103 | .add( 104 | "Memory Total".to_string(), 105 | SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE, 106 | ) 107 | .aggregate(Aggregate::Value) 108 | .format(|v| format!("{v:0>4.1}%")); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] 2 | #![warn(missing_docs)] 3 | #![warn(rustdoc::missing_doc_code_examples)] 4 | 5 | use std::{collections::BTreeMap, time::Duration}; 6 | 7 | use bevy::color::palettes::css; 8 | use bevy::{ 9 | diagnostic::{DiagnosticPath, DiagnosticsStore}, 10 | prelude::*, 11 | render::view::RenderLayers, 12 | text::LineBreak, 13 | time::common_conditions::on_timer, 14 | }; 15 | 16 | mod extras; 17 | 18 | #[cfg(feature = "sysinfo_plugin")] 19 | pub use self::extras::sysinfo_plugin::ScreenSystemInformationDiagnosticsPlugin; 20 | pub use self::extras::{ScreenEntityDiagnosticsPlugin, ScreenFrameDiagnosticsPlugin}; 21 | 22 | const TIMESTEP_10_PER_SECOND: f64 = 1.0 / 10.0; 23 | 24 | /// Plugin for displaying Diagnostics on screen. 25 | pub struct ScreenDiagnosticsPlugin { 26 | /// The rate at which the diagnostics on screen are updated. Default: 1.0/10.0 (10 times per second). 27 | pub timestep: f64, 28 | /// The Style used to position the Text. 29 | /// 30 | /// By default this is in the bottom right corner of the window: 31 | /// ```rust 32 | ///# use bevy::prelude::*; 33 | ///# use bevy_screen_diagnostics::ScreenDiagnosticsPlugin; 34 | /// 35 | ///# fn main() { 36 | ///# App::new() 37 | ///# .add_plugins(DefaultPlugins) 38 | ///# .add_plugins(ScreenDiagnosticsPlugin { 39 | ///# style: 40 | /// Node { 41 | /// align_self: AlignSelf::FlexEnd, 42 | /// position_type: PositionType::Absolute, 43 | /// bottom: Val::Px(5.0), 44 | /// right: Val::Px(15.0), 45 | /// ..default() 46 | /// }, 47 | ///# ..default() 48 | ///# }); 49 | ///# } 50 | /// ``` 51 | pub style: Node, 52 | /// The font used for the text. By default [FiraCodeBold](https://github.com/tonsky/FiraCode) is used. 53 | pub font: Option<&'static str>, 54 | /// The render layer for the UI 55 | pub render_layer: RenderLayers, 56 | } 57 | 58 | const DEFAULT_COLORS: (Srgba, Srgba) = (css::RED, css::WHITE); 59 | 60 | impl Default for ScreenDiagnosticsPlugin { 61 | fn default() -> Self { 62 | Self { 63 | timestep: TIMESTEP_10_PER_SECOND, 64 | style: Node { 65 | align_self: AlignSelf::FlexEnd, 66 | position_type: PositionType::Absolute, 67 | bottom: Val::Px(5.0), 68 | right: Val::Px(15.0), 69 | ..default() 70 | }, 71 | font: None, 72 | render_layer: RenderLayers::default(), 73 | } 74 | } 75 | } 76 | 77 | #[derive(Resource)] 78 | struct FontOption(Option<&'static str>); 79 | 80 | #[derive(Resource, Reflect)] 81 | struct DiagnosticsStyle(Node); 82 | 83 | #[derive(Resource, Deref, Reflect)] 84 | struct DiagnosticsLayer(RenderLayers); 85 | 86 | impl Plugin for ScreenDiagnosticsPlugin { 87 | fn build(&self, app: &mut App) { 88 | app.init_resource::() 89 | .insert_resource(FontOption(self.font)) 90 | .init_resource::() 91 | .insert_resource(DiagnosticsStyle(self.style.clone())) 92 | .insert_resource(DiagnosticsLayer(self.render_layer.clone())) 93 | .add_systems(Startup, spawn_ui) 94 | .add_systems( 95 | Update, 96 | (update_onscreen_diags_layout, update_diags) 97 | .chain() 98 | .run_if(on_timer(Duration::from_secs_f64(self.timestep))), 99 | ); 100 | } 101 | } 102 | 103 | #[derive(Resource, Reflect)] 104 | struct ScreenDiagnosticsFont(Handle); 105 | 106 | impl FromWorld for ScreenDiagnosticsFont { 107 | fn from_world(world: &mut World) -> Self { 108 | let font = world.get_resource::().unwrap(); 109 | let assets = world.get_resource::().unwrap(); 110 | let font = match font.0 { 111 | Some(font) => assets.load(font), 112 | #[cfg(not(feature = "builtin-font"))] 113 | None => panic!( 114 | "No default font supplied, please either set the `builtin-font` \ 115 | feature or provide your own font file by setting the `font` field of \ 116 | `ScreenDiagnosticsPlugin` to `Some(\"font_asset_path\")`" 117 | ), 118 | #[cfg(feature = "builtin-font")] 119 | None => Default::default(), 120 | }; 121 | Self(font) 122 | } 123 | } 124 | 125 | #[derive(Component, Reflect)] 126 | #[require(Text)] 127 | struct DiagnosticsTextMarker; 128 | 129 | /// Aggregaes which can be used for displaying Diagnostics. 130 | #[derive(Copy, Clone, Default, Debug, Reflect)] 131 | pub enum Aggregate { 132 | /// The latest [Diagnostic::value] 133 | #[default] 134 | Value, 135 | /// The [Diagnostic::average] of all recorded diagnostic measurements. 136 | #[allow(dead_code)] 137 | Average, 138 | /// A moving average over n last diagnostic measurements. 139 | /// 140 | /// If this is larger than the amount of diagnostic measurement stored for that diagnostic, no update will happen. 141 | MovingAverage(usize), 142 | } 143 | 144 | /// Type alias for the fuction used to format a diagnostic value to a string. 145 | /// 146 | /// Useful especially for applying some operations to the value before formatting. 147 | /// 148 | /// Example: ``|v| format!("{:.2}", v);`` which limits the decimal places to 1. 149 | pub type FormatFn = fn(f64) -> String; 150 | 151 | /// Resource which maps the name to the [DiagnosticPath], [Aggregate] and [ConvertFn] 152 | #[derive(Resource, Reflect)] 153 | #[reflect(from_reflect = false)] 154 | pub struct ScreenDiagnostics { 155 | text_alignment: JustifyText, 156 | diagnostics: BTreeMap, 157 | layout_changed: bool, 158 | } 159 | 160 | impl Default for ScreenDiagnostics { 161 | fn default() -> Self { 162 | Self { 163 | text_alignment: JustifyText::Left, 164 | diagnostics: Default::default(), 165 | layout_changed: Default::default(), 166 | } 167 | } 168 | } 169 | 170 | // ngl, i still haven't fully grasped the various parts of bevy_reflect 171 | // so i don't know if a placeholder like this as a default is useful 172 | // hell, i dont even know if deriving Reflect on this at all will be useful... 173 | const PLACEHOLDER_DIAGNOSTIC_PATH: DiagnosticPath = 174 | DiagnosticPath::const_new("bevy_screen_diagnostics/placeholder"); 175 | fn placeholder_path() -> DiagnosticPath { 176 | PLACEHOLDER_DIAGNOSTIC_PATH 177 | } 178 | fn placeholder_format() -> FormatFn { 179 | |v| format!("{v:.2}") 180 | } 181 | 182 | #[derive(Reflect)] 183 | struct DiagnosticsText { 184 | name: String, 185 | // might not be useful to have reflect here at all, but i needed this to make it not complain 186 | #[reflect(ignore, default = "placeholder_path")] 187 | path: DiagnosticPath, 188 | agg: Aggregate, 189 | // might not be useful to have reflect here at all, but i needed this to make it not complain 190 | #[reflect(ignore, default = "placeholder_format")] 191 | format: FormatFn, 192 | show: bool, 193 | show_name: bool, 194 | colors: (Color, Color), 195 | edit: bool, 196 | rebuild: bool, 197 | index: Option, 198 | } 199 | 200 | impl DiagnosticsText { 201 | fn format(&self, v: f64) -> String { 202 | let formatter = self.format; 203 | formatter(v) 204 | } 205 | 206 | fn get_name(&self) -> String { 207 | match self.show_name { 208 | true => format!(" {} ", self.name), 209 | false => " ".to_string(), 210 | } 211 | } 212 | } 213 | 214 | /// Builder-like interface for a [DiagnosticsText]. 215 | pub struct DiagnosticsTextBuilder<'a> { 216 | m: &'a mut BTreeMap, 217 | k: String, 218 | } 219 | 220 | impl DiagnosticsTextBuilder<'_> { 221 | /// Set the Aggregate function for this [DiagnosticsText] 222 | pub fn aggregate(self, agg: Aggregate) -> Self { 223 | self.m.entry(self.k.clone()).and_modify(|e| { 224 | e.agg = agg; 225 | e.rebuild = true; 226 | }); 227 | self 228 | } 229 | 230 | /// Set the formatting function for this [DiagnosticsText] 231 | pub fn format(self, format: FormatFn) -> Self { 232 | self.m.entry(self.k.clone()).and_modify(|e| { 233 | e.format = format; 234 | e.rebuild = true; 235 | }); 236 | self 237 | } 238 | 239 | /// Set the text color for the diagnostic value 240 | pub fn diagnostic_color(self, color: Color) -> Self { 241 | self.m.entry(self.k.clone()).and_modify(|e| { 242 | e.colors.0 = color; 243 | e.edit = true; 244 | }); 245 | self 246 | } 247 | 248 | /// Set the text color for the diagnostic name 249 | pub fn name_color(self, color: Color) -> Self { 250 | self.m.entry(self.k.clone()).and_modify(|e| { 251 | e.colors.1 = color; 252 | e.edit = true; 253 | }); 254 | self 255 | } 256 | 257 | /// Toggle whether the diagnostic name is displayed. 258 | pub fn toggle_name(self) -> Self { 259 | self.m.entry(self.k.clone()).and_modify(|e| { 260 | e.show_name = !e.show_name; 261 | e.edit = true; 262 | }); 263 | self 264 | } 265 | 266 | /// Toggle whether the diagnostic is displayed at all. 267 | pub fn toggle(self) -> Self { 268 | self.m.entry(self.k.clone()).and_modify(|e| { 269 | e.show = !e.show; 270 | e.rebuild = true; 271 | }); 272 | self 273 | } 274 | } 275 | 276 | impl ScreenDiagnostics { 277 | /// Add a diagnostic to be displayed. 278 | /// 279 | /// * `name` - The name displayed on-screen. Also used as a key. 280 | /// * `path` - The [DiagnosticPath] which is displayed. 281 | /// 282 | /// ```rust 283 | ///# use bevy::{diagnostic::FrameTimeDiagnosticsPlugin, prelude::*}; 284 | ///# use bevy_screen_diagnostics::{Aggregate, ScreenDiagnosticsPlugin,ScreenDiagnostics}; 285 | /// 286 | ///# fn main() { 287 | ///# App::new() 288 | ///# .add_plugins(DefaultPlugins) 289 | ///# .add_plugins(ScreenDiagnosticsPlugin::default()) 290 | ///# .add_systems(Startup, setup); 291 | ///# } 292 | /// fn setup(mut screen_diagnostics: ResMut) { 293 | /// screen_diagnostics 294 | /// .add( 295 | /// "ms/frame".to_string(), 296 | /// FrameTimeDiagnosticsPlugin::FRAME_TIME, 297 | /// ) 298 | /// .aggregate(Aggregate::Value) 299 | /// .format(|v| format!("{:.0}", v)); 300 | /// } 301 | /// ``` 302 | pub fn add(&mut self, name: S, path: DiagnosticPath) -> DiagnosticsTextBuilder 303 | where 304 | S: Into, 305 | { 306 | let name: String = name.into(); 307 | 308 | let text = DiagnosticsText { 309 | name: name.clone(), 310 | path, 311 | agg: Aggregate::Value, 312 | format: |v| format!("{v:.2}"), 313 | show: true, 314 | show_name: true, 315 | colors: (DEFAULT_COLORS.0.into(), DEFAULT_COLORS.1.into()), 316 | edit: false, 317 | rebuild: true, 318 | index: None, 319 | }; 320 | 321 | self.diagnostics.insert(name.clone(), text); 322 | 323 | DiagnosticsTextBuilder { 324 | m: &mut self.diagnostics, 325 | k: name, 326 | } 327 | } 328 | 329 | /// Modify a [DiagnosticsText] by name. 330 | /// 331 | /// Uses the same syntax as [ScreenDiagnostics::add] 332 | pub fn modify(&mut self, name: S) -> DiagnosticsTextBuilder 333 | where 334 | S: Into, 335 | { 336 | DiagnosticsTextBuilder { 337 | m: &mut self.diagnostics, 338 | k: name.into(), 339 | } 340 | } 341 | 342 | /// Remove a diagnostic by name. 343 | #[allow(dead_code)] 344 | pub fn remove(&mut self, name: String) { 345 | self.diagnostics.remove(&name); 346 | } 347 | 348 | /// Set the [JustifyText] and trigger a rebuild 349 | pub fn set_alignment(&mut self, align: JustifyText) { 350 | self.text_alignment = align; 351 | self.layout_changed = true; 352 | } 353 | } 354 | 355 | fn spawn_ui( 356 | mut commands: Commands, 357 | diag_style: Res, 358 | diag_layer: Res, 359 | ) { 360 | commands.spawn(( 361 | Text::default(), 362 | diag_style.0.clone(), 363 | diag_layer.clone(), 364 | DiagnosticsTextMarker, 365 | )); 366 | } 367 | 368 | fn update_onscreen_diags_layout( 369 | mut diags: ResMut, 370 | font: Res, 371 | mut text_layout: Single<(Entity, &mut TextLayout), With>, 372 | mut commands: Commands, 373 | ) { 374 | if diags.layout_changed { 375 | commands.entity(text_layout.0).remove::(); 376 | 377 | for (i, text) in diags 378 | .diagnostics 379 | .values_mut() 380 | .rev() 381 | .filter(|t| t.show) 382 | .enumerate() 383 | { 384 | text.index = Some(i * 2 + 1); 385 | commands.entity(text_layout.0).with_children(|c| { 386 | c.spawn(( 387 | TextSpan::new("test_val"), 388 | TextFont::from_font(font.0.clone()).with_font_size(20.0), 389 | TextColor(text.colors.0), 390 | )); 391 | c.spawn(( 392 | TextSpan::new(text.get_name()), 393 | TextFont::from_font(font.0.clone()).with_font_size(20.0), 394 | TextColor(text.colors.1), 395 | )); 396 | }); 397 | } 398 | 399 | *text_layout.1 = TextLayout { 400 | justify: diags.text_alignment, 401 | linebreak: LineBreak::WordBoundary, 402 | }; 403 | 404 | diags.layout_changed = false; 405 | } 406 | } 407 | 408 | fn update_diags( 409 | mut diag: ResMut, 410 | diagnostics: Res, 411 | root_text: Single>, 412 | mut writer: TextUiWriter, 413 | ) -> Result { 414 | if diag.layout_changed { 415 | return Ok(()); 416 | } 417 | let mut layout_changed = false; 418 | for text_diag in diag.diagnostics.values_mut().rev() { 419 | if text_diag.rebuild { 420 | layout_changed = true; 421 | text_diag.rebuild = false; 422 | continue; 423 | } 424 | // needs to be checked here otherwise this tries to edit bad texts 425 | if !text_diag.show { 426 | continue; 427 | } 428 | 429 | if let Some(index) = text_diag.index { 430 | if text_diag.edit { 431 | // set the value color 432 | *writer.color(root_text.entity(), index) = text_diag.colors.0.into(); 433 | // set the name color 434 | *writer.color(root_text.entity(), index + 1) = text_diag.colors.1.into(); 435 | 436 | // toggle the name visibility 437 | *writer.text(root_text.entity(), index + 1) = text_diag.get_name(); 438 | 439 | text_diag.edit = false; 440 | } 441 | } 442 | 443 | if let Some(diag_val) = diagnostics.get(&text_diag.path) { 444 | let diag_val = match text_diag.agg { 445 | Aggregate::Value => diag_val.value(), 446 | Aggregate::Average => diag_val.average(), 447 | Aggregate::MovingAverage(count) => { 448 | let skip_maybe = diag_val.history_len().checked_sub(count); 449 | skip_maybe.map(|skip| diag_val.values().skip(skip).sum::() / count as f64) 450 | } 451 | }; 452 | 453 | if let Some(val) = diag_val { 454 | if let Some(index) = text_diag.index { 455 | *writer.text(root_text.entity(), index) = text_diag.format(val); 456 | } 457 | } 458 | } 459 | } 460 | diag.layout_changed = layout_changed; 461 | Ok(()) 462 | } 463 | --------------------------------------------------------------------------------