bevy 防抖机制研究
use bevy::prelude::*;
use bevy::window::WindowResized;
use std::time::Duration;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(UIScalingPlugin)
.add_systems(Startup, ui_demo_setup)
.run();
}
#[derive(Resource)]
struct CanvasConfig {
width: f32,
height: f32,
}
impl Default for CanvasConfig {
fn default() -> Self {
Self {
width: 1920.0,
height: 1080.0,
}
}
}
#[derive(Resource)]
struct VirtualScreenScale {
scale: f32,
offset_x: f32,
offset_y: f32,
}
impl Default for VirtualScreenScale {
fn default() -> Self {
Self {
scale: 1.0,
offset_x: 0.0,
offset_y: 0.0,
}
}
}
#[derive(Resource)]
struct ResizeDebounce {
last_resize_time: f64,
pending_resize: Option<(f32, f32)>,
debounce_duration: f64,
}
impl Default for ResizeDebounce {
fn default() -> Self {
Self {
last_resize_time: 0.0,
pending_resize: None,
debounce_duration: 0.016,
}
}
}
#[derive(Component)]
struct ScalableUI {
original_width: f32,
original_height: f32,
original_font_size: Option<f32>,
original_margin: UiRect,
original_padding: UiRect,
original_border: UiRect,
original_left: Option<f32>,
original_right: Option<f32>,
original_top: Option<f32>,
original_bottom: Option<f32>,
}
impl ScalableUI {
fn new() -> Self {
Self {
original_width: 0.0,
original_height: 0.0,
original_font_size: None,
original_margin: UiRect::all(Val::Px(0.0)),
original_padding: UiRect::all(Val::Px(0.0)),
original_border: UiRect::all(Val::Px(0.0)),
original_left: None,
original_right: None,
original_top: None,
original_bottom: None,
}
}
fn with_size(mut self, width: f32, height: f32) -> Self {
self.original_width = width;
self.original_height = height;
self
}
fn with_font_size(mut self, font_size: f32) -> Self {
self.original_font_size = Some(font_size);
self
}
fn with_margin(mut self, margin: UiRect) -> Self {
self.original_margin = margin;
self
}
fn with_padding(mut self, padding: UiRect) -> Self {
self.original_padding = padding;
self
}
fn with_position(mut self, left: Option<f32>, right: Option<f32>, top: Option<f32>, bottom: Option<f32>) -> Self {
self.original_left = left;
self.original_right = right;
self.original_top = top;
self.original_bottom = bottom;
self
}
fn with_left(mut self, left: f32) -> Self {
self.original_left = Some(left);
self
}
fn with_right(mut self, right: f32) -> Self {
self.original_right = Some(right);
self
}
fn with_top(mut self, top: f32) -> Self {
self.original_top = Some(top);
self
}
fn with_bottom(mut self, bottom: f32) -> Self {
self.original_bottom = Some(bottom);
self
}
}
struct UIScalingPlugin;
impl Plugin for UIScalingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<CanvasConfig>()
.init_resource::<VirtualScreenScale>()
.init_resource::<ResizeDebounce>()
.add_systems(Update, (
window_resize_debounce_system,
apply_resize_system,
).chain());
}
}
fn window_resize_debounce_system(
mut resize_events: MessageReader<WindowResized>,
mut debounce: ResMut<ResizeDebounce>,
time: Res<Time>,
) {
for event in resize_events.read() {
debounce.pending_resize = Some((event.width, event.height));
debounce.last_resize_time = time.elapsed_secs_f64();
}
}
fn apply_resize_system(
mut ui_query: Query<(&mut Node, &ScalableUI), Without<Text>>,
mut text_query: Query<(&mut TextFont, &ScalableUI), With<Text>>,
canvas_config: Res<CanvasConfig>,
mut virtual_scale: ResMut<VirtualScreenScale>,
mut debounce: ResMut<ResizeDebounce>,
time: Res<Time>,
) {
if let Some((width, height)) = debounce.pending_resize {
let current_time = time.elapsed_secs_f64();
let time_since_resize = current_time - debounce.last_resize_time;
if time_since_resize >= debounce.debounce_duration {
update_ui_scale(
width, height,
&mut ui_query,
&mut text_query,
&canvas_config,
&mut virtual_scale
);
debounce.pending_resize = None;
println!("窗口缩放: {}x{}, 缩放比例: {:.2}", width, height, virtual_scale.scale);
}
}
}
fn update_ui_scale(
window_width: f32,
window_height: f32,
ui_query: &mut Query<(&mut Node, &ScalableUI), Without<Text>>,
text_query: &mut Query<(&mut TextFont, &ScalableUI), With<Text>>,
canvas_config: &CanvasConfig,
virtual_scale: &mut VirtualScreenScale,
) {
let virtual_width = canvas_config.width;
let virtual_height = canvas_config.height;
let scale_x = window_width / virtual_width;
let scale_y = window_height / virtual_height;
let scale = scale_x.min(scale_y);
let scaled_width = virtual_width * scale;
let scaled_height = virtual_height * scale;
let offset_x = (window_width - scaled_width) / 2.0;
let offset_y = (window_height - scaled_height) / 2.0;
virtual_scale.scale = scale;
virtual_scale.offset_x = offset_x;
virtual_scale.offset_y = offset_y;
for (mut node, scalable_ui) in ui_query.iter_mut() {
update_node_scale(&mut node, scalable_ui, scale, offset_x, offset_y, virtual_width, virtual_height);
}
for (mut text_font, scalable_ui) in text_query.iter_mut() {
if let Some(original_font_size) = scalable_ui.original_font_size {
text_font.font_size = original_font_size * scale;
}
}
}
fn update_node_scale(
node: &mut Node,
scalable_ui: &ScalableUI,
scale: f32,
offset_x: f32,
offset_y: f32,
virtual_width: f32,
virtual_height: f32,
) {
if scalable_ui.original_width > 0.0 {
node.width = Val::Px(scalable_ui.original_width * scale);
}
if scalable_ui.original_height > 0.0 {
node.height = Val::Px(scalable_ui.original_height * scale);
}
node.margin = scale_ui_rect(&scalable_ui.original_margin, scale);
node.padding = scale_ui_rect(&scalable_ui.original_padding, scale);
node.border = scale_ui_rect(&scalable_ui.original_border, scale);
if let Some(left) = scalable_ui.original_left {
node.left = Val::Px(left * scale);
}
if let Some(right) = scalable_ui.original_right {
node.right = Val::Px(right * scale);
}
if let Some(top) = scalable_ui.original_top {
node.top = Val::Px(top * scale);
}
if let Some(bottom) = scalable_ui.original_bottom {
node.bottom = Val::Px(bottom * scale);
}
if scalable_ui.original_width == virtual_width && scalable_ui.original_height == virtual_height {
node.left = Val::Px(offset_x);
node.top = Val::Px(offset_y);
}
}
fn scale_ui_rect(rect: &UiRect, scale: f32) -> UiRect {
UiRect {
left: scale_val(&rect.left, scale),
right: scale_val(&rect.right, scale),
top: scale_val(&rect.top, scale),
bottom: scale_val(&rect.bottom, scale),
}
}
fn scale_val(val: &Val, scale: f32) -> Val {
match val {
Val::Px(px) => Val::Px(*px * scale),
Val::Percent(percent) => Val::Percent(*percent),
_ => *val,
}
}
fn ui_demo_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let font: Handle<Font> = asset_server.load("fonts/SarasaFixedHC-Regular.ttf");
commands.spawn(Camera2d);
commands
.spawn((
Node {
width: Val::Px(1920.0),
height: Val::Px(1080.0),
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::srgba(0.2, 0.2, 0.2, 0.8)),
ScalableUI::new().with_size(1920.0, 1080.0),
))
.with_children(|parent| {
parent
.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(200.0),
top: Val::Px(400.0),
width: Val::Px(800.0),
height: Val::Px(200.0),
padding: UiRect::all(Val::Px(20.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.9)),
ScalableUI::new()
.with_size(800.0, 200.0)
.with_padding(UiRect::all(Val::Px(20.0)))
.with_left(200.0)
.with_top(400.0),
))
.with_children(|parent| {
parent.spawn((
Text::new("UI缩放系统演示"),
TextFont {
font: font.clone(),
font_size: 36.0,
..default()
},
TextColor(Color::WHITE),
ScalableUI::new().with_font_size(36.0),
));
parent.spawn((
Text::new("改变窗口大小来查看UI缩放效果"),
TextFont {
font: font.clone(),
font_size: 24.0,
..default()
},
TextColor(Color::srgb(0.8, 0.8, 0.8)),
Node {
margin: UiRect::top(Val::Px(10.0)),
..default()
},
ScalableUI::new()
.with_font_size(24.0)
.with_margin(UiRect::top(Val::Px(10.0))),
));
parent.spawn((
Text::new("×"),
TextFont {
font: font.clone(),
font_size: 30.0,
..default()
},
TextColor(Color::srgb(1.0, 0.5, 0.5)),
Node {
position_type: PositionType::Absolute,
right: Val::Px(15.0),
top: Val::Px(10.0),
..default()
},
ScalableUI::new()
.with_font_size(30.0)
.with_right(15.0)
.with_top(10.0),
));
});
parent.spawn((
Node {
width: Val::Px(300.0),
height: Val::Px(100.0),
position_type: PositionType::Absolute,
right: Val::Px(20.0),
top: Val::Px(20.0),
padding: UiRect::all(Val::Px(10.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.7)),
ScalableUI::new()
.with_size(300.0, 100.0)
.with_padding(UiRect::all(Val::Px(10.0)))
.with_right(20.0)
.with_top(20.0),
))
.with_children(|parent| {
parent.spawn((
Text::new("虚拟分辨率: 1920x1080"),
TextFont {
font: font.clone(),
font_size: 20.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
ScalableUI::new().with_font_size(20.0),
));
});
parent.spawn((
Node {
width: Val::Px(150.0),
height: Val::Px(50.0),
position_type: PositionType::Absolute,
left: Val::Px(20.0),
bottom: Val::Px(20.0),
padding: UiRect::all(Val::Px(10.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::srgba(0.1, 0.3, 0.1, 0.7)),
ScalableUI::new()
.with_size(150.0, 50.0)
.with_padding(UiRect::all(Val::Px(10.0)))
.with_left(20.0)
.with_bottom(20.0),
))
.with_children(|parent| {
parent.spawn((
Text::new("版本 v1.0"),
TextFont {
font: font.clone(),
font_size: 16.0,
..default()
},
TextColor(Color::srgb(0.9, 1.0, 0.9)),
ScalableUI::new().with_font_size(16.0),
));
});
parent.spawn((
Node {
width: Val::Px(200.0),
height: Val::Px(60.0),
position_type: PositionType::Absolute,
left: Val::Px(1200.0),
top: Val::Px(700.0),
padding: UiRect::all(Val::Px(15.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::srgba(0.2, 0.4, 0.8, 0.9)),
ScalableUI::new()
.with_size(200.0, 60.0)
.with_padding(UiRect::all(Val::Px(15.0)))
.with_left(1200.0)
.with_top(700.0),
))
.with_children(|parent| {
parent.spawn((
Text::new("按钮示例"),
TextFont {
font: font.clone(),
font_size: 18.0,
..default()
},
TextColor(Color::WHITE),
ScalableUI::new().with_font_size(18.0),
));
});
});
}