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
#![deny(missing_docs)]
//! Add a diagnostics overlay in Bevy.
//!
//! This crate provides a Bevy plugin, [ScreenDiagsPlugin] to add a resource, [ScreenDiagsState], containing the information
//! diagnostic, and the more comprehensive [ScreenDiagsTextPlugin] to create and update the diagnostics text overlay.
//!
//! Currently the only diagnostic show is FPS (frames per second).
use std::fmt::Write;
use bevy::{
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
prelude::*,
utils::Duration,
};
/// Font size used by [ScreenDiagsTextPlugin].
const FONT_SIZE: f32 = 32.0;
/// Font color used by [ScreenDiagsTextPlugin].
const FONT_COLOR: Color = Color::RED;
/// The update interval used.
const UPDATE_INTERVAL: Duration = Duration::from_secs(1);
/// The prefix of the string to display the FPS.
const STRING_FORMAT: &str = "FPS: ";
/// The string used when the FPS is unavailable.
const STRING_INITIAL: &str = "FPS: ...";
/// A plugin that collect diagnostics and updates any `Text` marked as [ScreenDiagsText].
/// Currently only the FPS is displayed.
///
/// Use the [resource](ScreenDiagsState) to control its behaviour.
pub struct ScreenDiagsPlugin;
impl Plugin for ScreenDiagsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(FrameTimeDiagnosticsPlugin::default())
.add_systems(Update, update_frame_rate)
.init_resource::<ScreenDiagsState>()
.init_resource::<FrameRate>()
.add_systems(Update, update_text);
}
}
/// A plugin that draws diagnostics on-screen with Bevy UI.
/// Currently only the FPS is displayed.
///
/// This plugin builds on [ScreenDiagsPlugin] and adds a default [ScreenDiagsText] to display
/// the diagnostics using the font defined in `assets/fonts/screen-diags-font.ttf`,
/// [FONT_SIZE] and [FONT_COLOR].
pub struct ScreenDiagsTextPlugin;
impl Plugin for ScreenDiagsTextPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ScreenDiagsPlugin)
.add_systems(Startup, spawn_text);
}
}
/// The diagnostics state resource.
///
/// To disable the FPS rate, get a [ResMut](bevy::prelude::ResMut) reference to this struct and
/// pause the timer. Unpause the timer to re-enable the rate.
#[derive(Resource)]
pub struct ScreenDiagsState {
/// The timer that triggers a diagnostics reading.
///
/// Disabling the timer disables the collection of the diagnostics and stops the display.
///
/// This is public, to allow flexible use, but in general you should use the methods
/// [enable] and [disable] to interact with it.
pub timer: Timer,
/// A flag to indicate to update the display, even if the timer has not popped.
///
/// This is public, to allow flexible use, but in general you should use the methods
/// [enable] and [disable] to interact with it.
pub update_now: bool,
}
impl Default for ScreenDiagsState {
fn default() -> Self {
Self {
timer: Timer::new(UPDATE_INTERVAL, TimerMode::Repeating),
update_now: true,
}
}
}
impl ScreenDiagsState {
/// Enable the FPS collection and display.
pub fn enable(&mut self) {
self.timer.unpause();
self.update_now = true;
}
/// Disable the FPS collection and display.
pub fn disable(&mut self) {
self.timer.pause();
self.update_now = true;
}
/// Whether the FPS collection and display enabled.
pub fn enabled(&self) -> bool {
!self.timer.paused()
}
}
/// Resource containing the FPS (frames per second) diagnostic.
#[derive(Resource, Default)]
pub struct FrameRate(pub f64);
// Updates the frame_rate measure in the resource.
fn update_frame_rate(
time: Res<Time>,
diagnostics: Res<DiagnosticsStore>,
state_resource: Option<ResMut<ScreenDiagsState>>,
mut frame_rate: ResMut<FrameRate>,
) {
if let Some(mut state) = state_resource {
if state.update_now || state.timer.tick(time.delta()).just_finished() {
if state.timer.paused() {
return;
} else {
let fps_diags = extract_fps(&diagnostics);
if let Some(fps) = fps_diags {
frame_rate.0 = fps;
} else {
frame_rate.0 = 0.0;
}
}
}
}
}
/// The marker on the text to be updated.
#[derive(Component)]
pub struct ScreenDiagsText;
/// The Bevy system to update the text marked with [ScreenDiagsText].
fn update_text(
time: Res<Time>,
state_resource: Option<ResMut<ScreenDiagsState>>,
mut text_query: Query<&mut Text, With<ScreenDiagsText>>,
frame_rate: Res<FrameRate>,
) {
if let Some(mut state) = state_resource {
if state.update_now || state.timer.tick(time.delta()).just_finished() {
if state.timer.paused() {
// Time is paused so remove text
for mut text in text_query.iter_mut() {
let value = &mut text.sections[0].value;
value.clear();
}
} else {
for mut text in text_query.iter_mut() {
let value = &mut text.sections[0].value;
value.clear();
write!(value, "{}{:.0}", STRING_FORMAT, frame_rate.0).unwrap();
}
}
}
}
}
/// Utility function to get the current fps from the FrameTimeDiagnosticsPlugin
fn extract_fps(diagnostics: &DiagnosticsStore) -> Option<f64> {
diagnostics
.get(FrameTimeDiagnosticsPlugin::FPS)
.and_then(|fps| fps.average())
}
/// Function to spawn the text that will be updated to the current FPS.
fn spawn_text(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/screen-diags-font.ttf");
commands
.spawn(TextBundle {
text: Text {
sections: vec![TextSection {
value: STRING_INITIAL.to_string(),
style: TextStyle {
font,
font_size: FONT_SIZE,
color: FONT_COLOR,
},
}],
..Default::default()
},
..Default::default()
})
.insert(ScreenDiagsText);
}