Bevy 0.20 UI 迁移指南:Button 与 Interaction 的弃用及 Hovered/Pressed 替代方案
本篇围绕 Bevy 迁移文档 _release-content/migration-guides/deprecate_button_and_interaction.md(对应 PR #25197)展开:ui::widgets::Button 和 ui::Interaction 已被弃用,官方推荐分别改用 ui_widgets::Button 和 picking::hover::Hovered + ui::Pressed 组件。读完后你将了解这两个旧 API 的源码级弃用机制、新无头(headless)按钮组件的事件模型,以及如何按官方 button.rs 示例把旧代码迁移到新的交互状态方案。
弃用的范围:旧 Button 与 Interaction 是什么
ui::widgets::Button:旧版 UI 按钮组件
ui::widgets::Button 原本可通过 bevy::ui::prelude 直接引入,是一个"按钮标记组件",通常配合轮询 Interaction 组件来驱动按钮外观。在当前仓库源码中,它已被改造为一个带弃用标注的类型别名:
- crates/bevy_ui/src/widget/button.rs:实际实现是私有的
DeprecatedButton结构体(#[require(Node, FocusPolicy::Block)]),而对外暴露的pub type Button = DeprecatedButton上挂着#[deprecated(since = "0.20.0", note = "Use ui_widgets::Button.")](见 button.rs 第 24 行)。 - crates/bevy_ui/src/lib.rs:
ui的 prelude 仍然再导出widget::Button与Interaction,但整块被#[expect(deprecated, ...)]包裹,注释明确写着 "Should be removed after 0.20 is released when Button & Interaction are removed",说明这两个别名属于过渡期兼容层。
代码里采用"type alias 弃用"这一手法的原因在源码注释中写得很清楚:直接给 DeprecatedButton 结构体加 deprecated 会触发无法用 #[expect] 压制的内部 lint,所以用别名来承载弃用属性。这意味着旧代码在弃用期内仍可编译(只是产生 deprecation 警告),属于平滑过渡,而非立即删除。
另一个值得注意的细节:DeprecatedButton 通过 #[require(InteractionTemp)] 仍然要求实体上存在 Interaction 组件——这体现了旧按钮与旧交互状态系统之间的耦合,也是本次迁移中两者被一起替换的原因。
ui::Interaction:旧版交互状态枚举
Interaction 原本是一个枚举组件,表示 UI 元素与指针的交互状态,旧版示例普遍用 Changed<Interaction> 过滤器来检测状态跳变。当前仓库中它同样以类型别名形式保留:
- crates/bevy_ui/src/focus.rs:
pub(crate) enum DeprecatedInteraction定义枚举本身,pub type Interaction = DeprecatedInteraction;作为对外别名。从源码中引用的变体看,它包含None、Hovered、Pressed三个状态。 - 同一文件中还保留了负责"根据鼠标光标活动为所有 UI 元素设置
Interaction"的旧系统(focus.rs 第 165 行起 附近的set_interaction_states逻辑),它会按点击/悬停把实体上的Interaction在None/Hovered/Pressed之间切换。这说明弃用期内旧行为仍然生效,迁移不会导致旧 UI 突然失效。
替代方案一:ui_widgets::Button 无头按钮组件
bevy_ui_widgets 是仓库中独立的组件 crate(见 crates/bevy_ui_widgets/src/lib.rs),提供一套"只管行为、不管外观"(behavior-only)的 widget 组件。新版 Button 正是其中的核心成员:
- 组件定义在 crates/bevy_ui_widgets/src/button.rs:一个"无头按钮 widget,维护自身的 pressed 状态,并在按钮释放(un-press)时发出
Activate事件"。它#[require]了一个accesskit::Node::new(Role::Button)的AccessibilityNode,即无障碍支持是组件自带的。 - 状态维护完全基于 ECS 观察者(observer)而非每帧轮询,见 button.rs 第 141-153 行 的
ButtonPlugin,它注册了 6 个 observer:button_on_pointer_down:监听PointerPress,未禁用且未处于按下状态时插入Pressed组件;button_on_pointer_up/button_on_pointer_drag_end/button_on_pointer_cancel:监听PointerRelease、PointerDragEnd、PointerCancel,移除Pressed组件;button_on_pointer_click:监听PointerClick(一次完整点击),仅在"按下过且未禁用且未开启 press 激活"时触发Activate { entity }事件;button_on_key_event:监听焦点实体的键盘输入,Enter或Space(非重复)按键同样触发Activate。
- 事件定义
pub struct Activate位于 crates/bevy_ui_widgets/src/lib.rs。 - 可选标记组件
ActivateOnPress(button.rs 第 37 行):让按钮在按下瞬间(pointer down)而非释放时激活,适合菜单按钮这类"即按即发"的场景。 - 禁用支持:插入
bevy::ui::InteractionDisabled标记组件即可让按钮停止响应激活(该组件定义见 crates/bevy_ui/src/interaction_states.rs,它还会同步把对应的AccessibilityNode标记为 disabled)。
DefaultPlugins 已包含 bevy_ui_widgets 的插件,因此按钮行为开箱即用,无需额外注册。
替代方案二:用 Hovered + Pressed 组件替代 Interaction
弃用 Interaction 后,交互状态被拆分为两个独立的、基于 ECS 变更检测的组件:
picking::hover::Hovered:定义在 crates/bevy_picking/src/hover.rs,形如Hovered(pub bool),由 picking 后端负责维护。源码文档说明:- 语义与 CSS
:hover一致——指针悬停在实体或其任意后代上时均为 true; - 布尔值只在指针进入/离开时变化,因此可以高效配合 Bevy 的 change detection 使用(对比每帧都会变化的
HoverMap资源); - 提供
hovered.get()访问器。 - 使用方式是把
Hovered::default()插入到关心悬停状态的实体上。
- 语义与 CSS
bevy::ui::Pressed:定义在 crates/bevy_ui/src/interaction_states.rs,是一个标记组件(marker component),表示按钮或 widget 当前处于"被按住"状态。查询时用Has<Pressed>即可判断是否存在。
两者的语义正好覆盖旧枚举:Interaction::Hovered → hovered.get() == true;Interaction::Pressed → Has<Pressed> 为 true;Interaction::None → 两者皆否。原来"一个枚举 + Changed<Interaction> 过滤器"的轮询模式,被替换为"两个组件 + 每帧读取(或 observer 观察组件变化)"的模式。
官方示例:迁移后的完整写法
迁移文档指引读者"查看更新后的 button.rs 示例",即 examples/ui/widgets/button.rs。该示例完整演示了新 API 的组合用法,可直接作为迁移模板:
use bevy::{
color::palettes::basic::*,
picking::hover::Hovered,
prelude::*,
ui::Pressed,
ui_widgets::{observe, Activate, Button},
};
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn((
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
button(&assets),
// 按钮完成一次点击时触发 Activate 事件,用 observer 直接挂在按钮实体上
observe(|_activate: On<Activate>| {
info!("Button clicked!");
}),
)],
));
}
fn button(asset_server: &AssetServer) -> impl Bundle {
(
Button, // 无头按钮 widget:处理输入与按下状态
Hovered::default(), // 由 picking 后端跟踪指针是否悬停
Node {
width: px(150),
height: px(65),
border: UiRect::all(px(5)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::MAX,
..default()
},
BorderColor::all(Color::BLACK),
BackgroundColor(NORMAL_BUTTON),
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
font_size: FontSize::Px(33.0),
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
)],
)
}
/// 每帧按当前状态重绘外观:
/// Button 在被按住时维护 Pressed 组件,picking 后端负责更新 Hovered。
fn update_button_appearance(
mut buttons: Query<
(&Hovered, Has<Pressed>, &mut BackgroundColor, &mut BorderColor, &Children),
With<Button>,
>,
mut text_query: Query<&mut Text>,
) {
for (hovered, pressed, mut color, mut border_color, children) in &mut buttons {
let Ok(mut text) = text_query.get_mut(children[0]) else {
continue;
};
match (hovered.get(), pressed) {
(_, true) => {
**text = "Press".to_string();
*color = PRESSED_BUTTON.into();
border_color.set_all(RED);
}
(true, false) => {
**text = "Hover".to_string();
*color = HOVERED_BUTTON.into();
border_color.set_all(WHITE);
}
(false, false) => {
**text = "Button".to_string();
*color = NORMAL_BUTTON.into();
border_color.set_all(BLACK);
}
}
}
}
这段示例覆盖了迁移的四个关键点:
- 事件替代轮询判断点击:不再需要自己比较
Interaction的前后值来推断"完成了一次点击",直接observe(On<Activate>)即可,且 observer 可以精确挂到按钮实体上。 Hovered与Has<Pressed>组合出三态外观:match (hovered.get(), pressed)的三分支与旧Interaction枚举的三态一一对应。- 行为与样式解耦:
Button只负责输入处理和按下状态,样式(Node、BackgroundColor、BorderColor、文字)全部由开发者自行提供——这正是旧ui::widgets::Button与外观代码常常混在一起的模式所不同之处。 - 无障碍与禁用:
Button自带AccessibilityNode(Role::Button);如需禁用,插入InteractionDisabled并依据Has<InteractionDisabled>调整外观(示例文档注释还指引参考standard_widgets与standard_widgets_observers两个 UI 示例)。
示例文件头部的文档注释还提示了两个进阶方向:加入 bevy::input_focus::tab_navigation::TabNavigationPlugin、TabGroup 与 TabIndex 后可用 Tab 聚焦并以 Enter/Space 激活(触发同一个 Activate 事件);若偏好观察者而非每帧轮询,可用 On<Insert, Pressed> 这类组件变更 observer 来驱动外观更新。
迁移清单与兼容性注意
结合迁移文档与源码中的"0.20 发布后移除"注释,旧代码迁移可按以下清单进行:
| 旧写法 | 新写法 | 依据 |
|---|---|---|
use bevy::ui::prelude::* 引入的 Button |
use bevy::ui_widgets::Button |
bevy_ui/src/widget/button.rs 的弃用 note |
查询/过滤 &Interaction / Changed<Interaction> |
&Hovered + Has<Pressed>(或 observer 观察 Pressed 的插入/移除) |
bevy_picking/src/hover.rs、bevy_ui/src/interaction_states.rs |
| 自行判断"一次完整点击" | 观察 On<Activate> 事件 |
bevy_ui_widgets/src/button.rs |
| 按钮按下即触发 | 加 ActivateOnPress 标记 |
bevy_ui_widgets/src/button.rs |
| 禁用按钮 | 插入 InteractionDisabled |
bevy_ui/src/interaction_states.rs |
适用前提与限制:
- 弃用不等于删除:弃用期内
ui::prelude中的Button、Interaction仍可编译(见 bevy_ui/src/lib.rs),但会产生 deprecation 警告;源码注释表明这些别名计划在 0.20 发布后的后续版本中移除,因此不建议在新代码中继续依赖。 - 行为差异:旧
Interaction由bevy_ui内部的单一系统统一驱动(见 focus.rs 附近的设置逻辑);新方案中Pressed由bevy_ui_widgets的各 observer 在 pointer 事件上直接插入/移除,Hovered则依赖 picking 后端——若你的项目自行实现了自定义 picking 后端,需要确认Hovered的更新链路仍然成立。 Hovered的后代语义:与 CSS:hover一致,指针悬停在按钮内部的子节点(如文字)上时,父按钮的Hovered也为 true,这在迁移时通常与旧Interaction的体感一致,但若有依赖"精确到单个实体"的旧逻辑,建议参考 hover.rs 中提及的不含后代的替代组件。
总体而言,这次迁移把"交互状态"从一个引擎内部统一维护的枚举,拆分成了可被 ECS 变更检测与 observer 直接消费的独立组件,并与 bevy_ui_widgets 的无头 widget 体系对齐:状态驱动事件(Activate),事件驱动逻辑,组件驱动外观。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00