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);
}