bevy_fps_counter/
lib.rs

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
//! FPS counter for Bevy game engine

use crate::config::{STRING_FORMAT, UPDATE_INTERVAL};
use bevy::{
    diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
    prelude::*,
};
use config::{STRING_INITIAL, STRING_MISSING};

mod config {
    use bevy::prelude::*;
    use std::time::Duration;

    pub const FONT_SIZE: f32 = 32.;
    pub const FONT_COLOR: Color = Color::WHITE;
    pub const UPDATE_INTERVAL: Duration = Duration::from_secs(1);

    pub const STRING_FORMAT: &str = "FPS: ";
    pub const STRING_INITIAL: &str = "FPS: ...";
    pub const STRING_MISSING: &str = "FPS: ???";
}

/// FPS counter plugin
pub struct FpsCounterPlugin;

impl Plugin for FpsCounterPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(FrameTimeDiagnosticsPlugin)
            .add_systems(Startup, spawn_text)
            .add_systems(Update, update)
            .init_resource::<FpsCounter>();
    }
}

#[derive(Resource)]
pub struct FpsCounter {
    pub timer: Timer,
    pub update_now: bool,
}

impl Default for FpsCounter {
    fn default() -> Self {
        Self {
            timer: Timer::new(UPDATE_INTERVAL, TimerMode::Repeating),
            update_now: true,
        }
    }
}

impl FpsCounter {
    /// Enable FPS counter
    pub fn enable(&mut self) {
        self.timer.unpause();
        self.update_now = true;
    }

    /// Disable FPS counter
    pub fn disable(&mut self) {
        self.timer.pause();
        self.update_now = true;
    }

    /// Check if FPS counter is enabled
    pub fn is_enabled(&self) -> bool {
        !self.timer.paused()
    }
}

/// The marker on the text to be updated
#[derive(Component)]
pub struct FpsCounterText;

fn update(
    time: Res<Time>,
    diagnostics: Res<DiagnosticsStore>,
    state_resources: Option<ResMut<FpsCounter>>,
    mut query: Query<Entity, With<FpsCounterText>>,
    mut writer: TextUiWriter,
) {
    let Some(mut state) = state_resources else {
        return;
    };
    if !(state.update_now || state.timer.tick(time.delta()).just_finished()) {
        return;
    }
    if state.timer.paused() {
        for entity in query.iter_mut() {
            writer.text(entity, 0).clear();
        }
    } else {
        let fps_dialog: Option<f64> = extract_fps(&diagnostics);

        for entity in query.iter_mut() {
            if let Some(fps) = fps_dialog {
                *writer.text(entity, 0) = format!("{}{:.0}", STRING_FORMAT, fps);
            } else {
                *writer.text(entity, 0) = STRING_MISSING.to_string();
            }
        }
    }
}

fn extract_fps(diagnostics: &Res<DiagnosticsStore>) -> Option<f64> {
    diagnostics
        .get(&FrameTimeDiagnosticsPlugin::FPS)
        .and_then(|fps| fps.average())
}

fn spawn_text(mut commands: Commands) {
    commands
        .spawn((
            Text::new(STRING_INITIAL),
            TextFont {
                font_size: config::FONT_SIZE,
                ..Default::default()
            },
            TextColor(config::FONT_COLOR),
        ))
        .insert(FpsCounterText);
}