mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
12952486ef
# Objective We include several run condition combinators, such as `and`, `or`, etc., which short-circuit depending on the output of the first condition in the combinator. This is incredibly error-prone due to the subtle way that short-circuiting interacts with change detection -- rather than reacting to changes frame-by-frame, the second condition in short-circuiting combinator will react to _the last time that the first condition did not short circuit_. This can easily lead to confusing bugs if the user does not expect this, and I suspect that most users will not expect this. For this reason, when combining multiple run conditions added via `.run_if()`, all run conditions are intentionally eagerly evaluated. ## Solution Add new run condition combinators `and_then`, `and_eager`, `or_else`, `or_eager`, etc., for clarity, and deprecate the previous methods, pointing users to the new ones. After the previous combinators have been removed for a few release cycles, we should consider renaming combinators such as `and_eager` to simply `and`. # Migration Guide Bevy supports run condition combinators (`and`, `or`, `nan`, `nor`), which have historically short-circuited. While familiar, short-circuiting interacts with Bevy’s change detection in a subtle way: when the left-hand condition short-circuits, the right-hand condition is not evaluated and therefore does not observe changes on that frame. Instead, it reacts based on the last frame it ran, which can lead to confusing and non-local bugs. By contrast, Bevy's scheduler combines multiple .run_if(...) conditions using eager evaluation, which avoids this known pitfall. To make intent explicit and reduce footguns, short-circuiting combinators have been renamed and eagerly-evaluated variants have been added. ## Examples Most users should use eager evaluation, which ensures all conditions participate in change detection every frame: ```rust // Before (deprecated) cond_a.and(cond_b) cond_a.or(cond_b) cond_a.nand(cond_b) cond_a.nor(cond_b) // After (recommended default) cond_a.and_eager(cond_b) cond_a.or_eager(cond_b) cond_a.nand_eager(cond_b) cond_a.nor_eager(cond_b) ``` If you *intentionally rely on short-circuiting* for correctness, use the explicit short-circuiting variants: ```rust // Explicit short-circuiting cond_a.and_then(cond_b) cond_a.or_else(cond_b) cond_a.nand_then(cond_b) cond_a.nor_else(cond_b) ``` `xor` and `xnor` are unchanged, as they cannot short-circuit by nature. ## Future naming note The `_eager` suffix exists to ease migration without changing the behavior of existing code that relied on short-circuiting. After the deprecated combinators have been removed for a few release cycles, we expect to revisit naming and likely remove the _eager suffix, keeping `and_then` / `or_else` as the explicit short-circuiting forms. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Mike <mike.hsu@gmail.com>
308 lines
10 KiB
Rust
308 lines
10 KiB
Rust
//! Shows different built-in plugins that logs diagnostics, like frames per second (FPS), to the console.
|
|
|
|
use bevy::{
|
|
color::palettes,
|
|
diagnostic::{
|
|
DiagnosticPath, EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin,
|
|
LogDiagnosticsPlugin, LogDiagnosticsState, SystemInformationDiagnosticsPlugin,
|
|
},
|
|
prelude::*,
|
|
};
|
|
|
|
const FRAME_TIME_DIAGNOSTICS: [DiagnosticPath; 3] = [
|
|
FrameTimeDiagnosticsPlugin::FPS,
|
|
FrameTimeDiagnosticsPlugin::FRAME_COUNT,
|
|
FrameTimeDiagnosticsPlugin::FRAME_TIME,
|
|
];
|
|
const ENTITY_COUNT_DIAGNOSTICS: [DiagnosticPath; 1] = [EntityCountDiagnosticsPlugin::ENTITY_COUNT];
|
|
const SYSTEM_INFO_DIAGNOSTICS: [DiagnosticPath; 4] = [
|
|
SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE,
|
|
SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE,
|
|
SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE,
|
|
SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE,
|
|
];
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins((
|
|
// The diagnostics plugins need to be added after DefaultPlugins as they use e.g. the time plugin for timestamps.
|
|
DefaultPlugins,
|
|
// Adds a system that prints diagnostics to the console.
|
|
// The other diagnostics plugins can still be used without this if you want to use them in an ingame overlay for example.
|
|
LogDiagnosticsPlugin::default(),
|
|
// Adds frame time, FPS and frame count diagnostics.
|
|
FrameTimeDiagnosticsPlugin::default(),
|
|
// Adds an entity count diagnostic.
|
|
EntityCountDiagnosticsPlugin::default(),
|
|
// Adds cpu and memory usage diagnostics for systems and the entire game process.
|
|
SystemInformationDiagnosticsPlugin,
|
|
// Forwards various diagnostics from the render app to the main app.
|
|
// These are pretty verbose but can be useful to pinpoint performance issues.
|
|
bevy::render::diagnostic::RenderDiagnosticsPlugin,
|
|
))
|
|
// No rendering diagnostics are emitted unless something is drawn to the screen,
|
|
// so we spawn a small scene.
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, filters_inputs)
|
|
.add_systems(
|
|
Update,
|
|
update_commands.run_if(
|
|
resource_exists_and_changed::<LogDiagnosticsStatus>
|
|
.or_eager(resource_exists_and_changed::<LogDiagnosticsFilters>),
|
|
),
|
|
)
|
|
.run();
|
|
}
|
|
|
|
/// set up a simple 3D scene
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
// circular base
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Circle::new(4.0))),
|
|
MeshMaterial3d(materials.add(Color::WHITE)),
|
|
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
|
));
|
|
// cube
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
|
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
|
|
Transform::from_xyz(0.0, 0.5, 0.0),
|
|
));
|
|
// light
|
|
commands.spawn((
|
|
PointLight {
|
|
shadow_maps_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 8.0, 4.0),
|
|
));
|
|
// camera
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
|
|
commands.init_resource::<LogDiagnosticsFilters>();
|
|
commands.init_resource::<LogDiagnosticsStatus>();
|
|
|
|
commands.spawn((
|
|
LogDiagnosticsCommands,
|
|
Node {
|
|
top: px(5),
|
|
left: px(5),
|
|
flex_direction: FlexDirection::Column,
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn filters_inputs(
|
|
keys: Res<ButtonInput<KeyCode>>,
|
|
mut status: ResMut<LogDiagnosticsStatus>,
|
|
mut filters: ResMut<LogDiagnosticsFilters>,
|
|
mut log_state: ResMut<LogDiagnosticsState>,
|
|
) {
|
|
if keys.just_pressed(KeyCode::KeyQ) {
|
|
*status = match *status {
|
|
LogDiagnosticsStatus::Enabled => {
|
|
log_state.disable_filtering();
|
|
LogDiagnosticsStatus::Disabled
|
|
}
|
|
LogDiagnosticsStatus::Disabled => {
|
|
log_state.enable_filtering();
|
|
if filters.frame_time {
|
|
enable_filters(&mut log_state, FRAME_TIME_DIAGNOSTICS);
|
|
}
|
|
if filters.entity_count {
|
|
enable_filters(&mut log_state, ENTITY_COUNT_DIAGNOSTICS);
|
|
}
|
|
if filters.system_info {
|
|
enable_filters(&mut log_state, SYSTEM_INFO_DIAGNOSTICS);
|
|
}
|
|
LogDiagnosticsStatus::Enabled
|
|
}
|
|
};
|
|
}
|
|
|
|
let enabled = *status == LogDiagnosticsStatus::Enabled;
|
|
if keys.just_pressed(KeyCode::Digit1) {
|
|
filters.frame_time = !filters.frame_time;
|
|
if enabled {
|
|
if filters.frame_time {
|
|
enable_filters(&mut log_state, FRAME_TIME_DIAGNOSTICS);
|
|
} else {
|
|
disable_filters(&mut log_state, FRAME_TIME_DIAGNOSTICS);
|
|
}
|
|
}
|
|
}
|
|
if keys.just_pressed(KeyCode::Digit2) {
|
|
filters.entity_count = !filters.entity_count;
|
|
if enabled {
|
|
if filters.entity_count {
|
|
enable_filters(&mut log_state, ENTITY_COUNT_DIAGNOSTICS);
|
|
} else {
|
|
disable_filters(&mut log_state, ENTITY_COUNT_DIAGNOSTICS);
|
|
}
|
|
}
|
|
}
|
|
if keys.just_pressed(KeyCode::Digit3) {
|
|
filters.system_info = !filters.system_info;
|
|
if enabled {
|
|
if filters.system_info {
|
|
enable_filters(&mut log_state, SYSTEM_INFO_DIAGNOSTICS);
|
|
} else {
|
|
disable_filters(&mut log_state, SYSTEM_INFO_DIAGNOSTICS);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn enable_filters(
|
|
log_state: &mut LogDiagnosticsState,
|
|
diagnostics: impl IntoIterator<Item = DiagnosticPath>,
|
|
) {
|
|
log_state.extend_filter(diagnostics);
|
|
}
|
|
|
|
fn disable_filters(
|
|
log_state: &mut LogDiagnosticsState,
|
|
diagnostics: impl IntoIterator<Item = DiagnosticPath>,
|
|
) {
|
|
for diagnostic in diagnostics {
|
|
log_state.remove_filter(&diagnostic);
|
|
}
|
|
}
|
|
|
|
fn update_commands(
|
|
mut commands: Commands,
|
|
log_commands: Single<Entity, With<LogDiagnosticsCommands>>,
|
|
status: Res<LogDiagnosticsStatus>,
|
|
filters: Res<LogDiagnosticsFilters>,
|
|
) {
|
|
let enabled = *status == LogDiagnosticsStatus::Enabled;
|
|
let alpha = if enabled { 1. } else { 0.25 };
|
|
let enabled_color = |enabled| {
|
|
if enabled {
|
|
Color::from(palettes::tailwind::GREEN_400)
|
|
} else {
|
|
Color::from(palettes::tailwind::RED_400)
|
|
}
|
|
};
|
|
commands
|
|
.entity(*log_commands)
|
|
.despawn_related::<Children>()
|
|
.insert(children![
|
|
(
|
|
Node {
|
|
flex_direction: FlexDirection::Row,
|
|
column_gap: px(5),
|
|
..default()
|
|
},
|
|
children![
|
|
Text::new("[Q] Toggle filtering:"),
|
|
(
|
|
Text::new(format!("{:?}", *status)),
|
|
TextColor(enabled_color(enabled))
|
|
)
|
|
]
|
|
),
|
|
(
|
|
Node {
|
|
flex_direction: FlexDirection::Row,
|
|
column_gap: px(5),
|
|
..default()
|
|
},
|
|
children![
|
|
(
|
|
Text::new("[1] Frame times:"),
|
|
TextColor(Color::WHITE.with_alpha(alpha))
|
|
),
|
|
(
|
|
Text::new(format!("{:?}", filters.frame_time)),
|
|
TextColor(enabled_color(filters.frame_time).with_alpha(alpha))
|
|
)
|
|
]
|
|
),
|
|
(
|
|
Node {
|
|
flex_direction: FlexDirection::Row,
|
|
column_gap: px(5),
|
|
..default()
|
|
},
|
|
children![
|
|
(
|
|
Text::new("[2] Entity count:"),
|
|
TextColor(Color::WHITE.with_alpha(alpha))
|
|
),
|
|
(
|
|
Text::new(format!("{:?}", filters.entity_count)),
|
|
TextColor(enabled_color(filters.entity_count).with_alpha(alpha))
|
|
)
|
|
]
|
|
),
|
|
(
|
|
Node {
|
|
flex_direction: FlexDirection::Row,
|
|
column_gap: px(5),
|
|
..default()
|
|
},
|
|
children![
|
|
(
|
|
Text::new("[3] System info:"),
|
|
TextColor(Color::WHITE.with_alpha(alpha))
|
|
),
|
|
(
|
|
Text::new(format!("{:?}", filters.system_info)),
|
|
TextColor(enabled_color(filters.system_info).with_alpha(alpha))
|
|
)
|
|
]
|
|
),
|
|
(
|
|
Node {
|
|
flex_direction: FlexDirection::Row,
|
|
column_gap: px(5),
|
|
..default()
|
|
},
|
|
children![
|
|
(
|
|
Text::new("[4] Render diagnostics:"),
|
|
TextColor(Color::WHITE.with_alpha(alpha))
|
|
),
|
|
(
|
|
Text::new("Private"),
|
|
TextColor(enabled_color(false).with_alpha(alpha))
|
|
)
|
|
]
|
|
),
|
|
]);
|
|
}
|
|
|
|
#[derive(Debug, Default, PartialEq, Eq, Resource)]
|
|
enum LogDiagnosticsStatus {
|
|
/// No filtering, showing all logs
|
|
#[default]
|
|
Disabled,
|
|
/// Filtering enabled, showing only subset of logs
|
|
Enabled,
|
|
}
|
|
|
|
#[derive(Default, Resource)]
|
|
struct LogDiagnosticsFilters {
|
|
frame_time: bool,
|
|
entity_count: bool,
|
|
system_info: bool,
|
|
#[expect(
|
|
dead_code,
|
|
reason = "Currently the diagnostic paths referent to RenderDiagnosticPlugin are private"
|
|
)]
|
|
render_diagnostics: bool,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
/// Marks the UI node that has instructions on how to change the filtering
|
|
struct LogDiagnosticsCommands;
|